feat: current Time support

pull/1/head
flop 4 weeks ago
parent c8a2b03a54
commit 6fb4b390ad
  1. 16
      ts/index.html
  2. 7
      ts/src/driver.ts
  3. 25
      ts/src/file.ts
  4. 25
      ts/src/parser.ts
  5. 23
      ts/src/renderer.ts
  6. 6
      ts/src/types.ts

@ -184,6 +184,22 @@
}).toBuffer(); }).toBuffer();
}, },
}, },
{
label: "Time",
make() {
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.CurrentTime,
xOffset: 0, yOffset: 32,
width: W, height: 16,
utcOffsetMinutes: 120,
}],
},
}).toBuffer();
},
},
]; ];
const controls = document.getElementById("controls"); const controls = document.getElementById("controls");

@ -30,6 +30,12 @@ export interface MonoDisplayDriverOptions {
loop?: boolean; loop?: boolean;
/** Error handler. Default: throw. */ /** Error handler. Default: throw. */
onError?: (err: Error) => void; onError?: (err: Error) => void;
/**
* Clock source for CurrentTime elements. Called once per render tick.
* Default: `() => new Date()` (system time).
* Override to freeze or mock the clock, e.g. `() => new Date('2024-01-01T12:00:00Z')`.
*/
now?: () => Date;
} }
@ -66,6 +72,7 @@ export class MonoDisplayDriver {
fps: options.fps ?? 25, fps: options.fps ?? 25,
loop: options.loop ?? true, loop: options.loop ?? true,
onError: options.onError ?? ((e: Error) => { throw e; }), onError: options.onError ?? ((e: Error) => { throw e; }),
now: options.now ?? (() => new Date()),
}; };
this.parser = new MonoDisplayParser(); this.parser = new MonoDisplayParser();
} }

@ -4,7 +4,7 @@
import { packedSize, packPixels, pad32 } from "./helper"; import { packedSize, packPixels, pad32 } from "./helper";
import { MONOFORMAT_MAGIC_HEADER } from "./parser"; import { MONOFORMAT_MAGIC_HEADER } from "./parser";
import { ElementType, SectionType, type AnimationElement, type ClippedTextElement, type DrawElement, type ElementsAlwaysDescriptor, type HScrollElement, type HScrollTextElement, type Image2DElement, type LineElement, type MonoDisplayFileDescriptor, type ScrollElementFlags, type SectionFlags, type VScrollElement } from "./types"; import { ElementType, SectionType, type AnimationElement, type ClippedTextElement, type CurrentTimeElement, type DrawElement, type ElementsAlwaysDescriptor, type HScrollElement, type HScrollTextElement, type Image2DElement, type LineElement, type MonoDisplayFileDescriptor, type ScrollElementFlags, type SectionFlags, type VScrollElement } from "./types";
/** /**
* Encodes a MonoDisplayFileDescriptor to a valid .bin ArrayBuffer. * Encodes a MonoDisplayFileDescriptor to a valid .bin ArrayBuffer.
@ -85,6 +85,7 @@ export class MonoDisplayFile {
case ElementType.Line: return this.#encodeLine(el); case ElementType.Line: return this.#encodeLine(el);
case ElementType.ClippedText: return this.#encodeClippedText(el); case ElementType.ClippedText: return this.#encodeClippedText(el);
case ElementType.HScrollText: return this.#encodeHScrollText(el); case ElementType.HScrollText: return this.#encodeHScrollText(el);
case ElementType.CurrentTime: return this.#encodeCurrentTime(el);
default: return new Uint8Array(); default: return new Uint8Array();
} }
} }
@ -230,6 +231,28 @@ export class MonoDisplayFile {
return out; return out;
} }
#encodeCurrentTime(el: CurrentTimeElement): Uint8Array {
// Fixed 16 bytes, already 32-bit aligned
const out = new Uint8Array(16);
const v = new DataView(out.buffer);
const f = el.flags;
const flagsByte =
(f?.clock12h ? 0x01 : 0) |
(f?.showHours ?? true ? 0x02 : 0) |
(f?.showMinutes ?? true ? 0x04 : 0) |
(f?.showSeconds ? 0x08 : 0);
v.setUint16( 0, ElementType.CurrentTime, true);
v.setUint16( 2, el.xOffset ?? 0, true);
v.setUint16( 4, el.yOffset ?? 0, true);
v.setUint16( 6, el.width, true);
v.setUint16( 8, el.height, true);
v.setUint16(10, el.fontIndex ?? 0, true);
v.setInt16( 12, el.utcOffsetMinutes ?? 0, true);
v.setUint8( 14, flagsByte);
v.setUint8( 15, 0); // reserved
return out;
}
// --- utilities --- // --- utilities ---
#concat(...parts: Uint8Array[]): Uint8Array { #concat(...parts: Uint8Array[]): Uint8Array {

@ -5,7 +5,7 @@
import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper"; import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper";
import { type MonoFormatFile } from "./types"; import { type MonoFormatFile } from "./types";
import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types"; import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCurrentTime, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types";
// Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE // Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE
export const MONOFORMAT_MAGIC_HEADER = 0x632B7EAF; export const MONOFORMAT_MAGIC_HEADER = 0x632B7EAF;
@ -116,6 +116,7 @@ export class MonoDisplayParser {
case ElementType.Line: return this.#parseLine(r); case ElementType.Line: return this.#parseLine(r);
case ElementType.ClippedText: return this.#parseClippedText(r, elementStart); case ElementType.ClippedText: return this.#parseClippedText(r, elementStart);
case ElementType.HScrollText: return this.#parseHScrollText(r, elementStart); case ElementType.HScrollText: return this.#parseHScrollText(r, elementStart);
case ElementType.CurrentTime: return this.#parseCurrentTime(r);
default: default:
// Unknown element type - cannot safely skip without knowing size; stop parsing section elements // Unknown element type - cannot safely skip without knowing size; stop parsing section elements
return null; return null;
@ -238,6 +239,28 @@ export class MonoDisplayParser {
return { type: ElementType.ClippedText, xOffset, yOffset, width, height, fontIndex, text }; return { type: ElementType.ClippedText, xOffset, yOffset, width, height, fontIndex, text };
} }
#parseCurrentTime(r: BinaryReader): MonoFormatCurrentTime {
// Layout after type field (already consumed): 14 bytes, total element = 16 bytes (aligned)
const xOffset = r.readUint16();
const yOffset = r.readUint16();
const width = r.readUint16();
const height = r.readUint16();
const fontIndex = r.readUint16();
const utcOffsetMinutes = r.readInt16();
const flagsByte = r.readByte();
r.readByte(); // reserved
return {
type: ElementType.CurrentTime,
xOffset, yOffset, width, height, fontIndex, utcOffsetMinutes,
flags: {
clock12h: !!(flagsByte & 0x01),
showHours: !!(flagsByte & 0x02),
showMinutes: !!(flagsByte & 0x04),
showSeconds: !!(flagsByte & 0x08),
},
};
}
#parseHScrollText(r: BinaryReader, start: number): MonoFormatHScrollText { #parseHScrollText(r: BinaryReader, start: number): MonoFormatHScrollText {
const xOffset = r.readUint16(); const xOffset = r.readUint16();
const yOffset = r.readUint16(); const yOffset = r.readUint16();

@ -4,6 +4,7 @@ import {
SectionType, SectionType,
type MonoFormatAnimation, type MonoFormatAnimation,
type MonoFormatClippedText, type MonoFormatClippedText,
type MonoFormatCurrentTime,
type MonoFormatElement, type MonoFormatElement,
type MonoFormatElementsAlways, type MonoFormatElementsAlways,
type MonoFormatElementsTimespan, type MonoFormatElementsTimespan,
@ -82,7 +83,7 @@ export class MonoDisplayRenderer {
} }
#renderFile(file: MonoFormatFile): void { #renderFile(file: MonoFormatFile): void {
const now = BigInt(Math.floor(Date.now() / 1000)); const now = BigInt(Math.floor(this.opts.now().getTime() / 1000));
let cleared = false; let cleared = false;
let elementId = 0; let elementId = 0;
@ -124,6 +125,7 @@ export class MonoDisplayRenderer {
case ElementType.Line: this.#renderLine(el); break; case ElementType.Line: this.#renderLine(el); break;
case ElementType.ClippedText: this.#renderClippedText(el); break; case ElementType.ClippedText: this.#renderClippedText(el); break;
case ElementType.HScrollText: this.#renderHScrollText(el, id); break; case ElementType.HScrollText: this.#renderHScrollText(el, id); break;
case ElementType.CurrentTime: this.#renderCurrentTime(el); break;
} }
} }
@ -252,4 +254,23 @@ export class MonoDisplayRenderer {
} }
this.hScrollPos.set(id, pos); this.hScrollPos.set(id, pos);
} }
#renderCurrentTime(el: MonoFormatCurrentTime): void {
const d = new Date(this.opts.now().getTime() + el.utcOffsetMinutes * 60_000);
const { clock12h, showHours, showMinutes, showSeconds } = el.flags;
const parts: string[] = [];
if (showHours) {
const h = d.getUTCHours();
parts.push(String(clock12h ? (h % 12 || 12) : h).padStart(2, '0'));
}
if (showMinutes) parts.push(String(d.getUTCMinutes()).padStart(2, '0'));
if (showSeconds) parts.push(String(d.getUTCSeconds()).padStart(2, '0'));
const text = parts.join(':');
if (!text) return;
const img = this.#getTextImage(text, el.height, el.fontIndex);
this.#blitClipped(img, el.xOffset, el.yOffset, el.width, el.height, 0, 0);
}
} }

@ -31,12 +31,6 @@ export enum ElementType {
Line = 5, Line = 5,
ClippedText = 16, ClippedText = 16,
HScrollText = 17, HScrollText = 17,
/**
* Current time display.
* NOTE: the spec diagram labels this element "Type: 16", which collides with
* ClippedText. This is assumed to be a spec typo; 18 is used here as the
* probable intended value until the spec is corrected.
*/
CurrentTime = 18, CurrentTime = 18,
} }

Loading…
Cancel
Save