diff --git a/ts/src/browser.ts b/ts/src/browser.ts index 050df59..de713f6 100644 --- a/ts/src/browser.ts +++ b/ts/src/browser.ts @@ -25,64 +25,71 @@ const EL_FIELDS: Partial> = { Image2D: [ { key: "xOffset", label: "X offset", type: "number", default: 0 }, { key: "yOffset", label: "Y offset", type: "number", default: 0 }, - { key: "width", label: "Width", type: "number", default: W }, - { key: "height", label: "Height", type: "number", default: H }, + { key: "width", label: "Width", type: "number", default: W }, + { key: "height", label: "Height", type: "number", default: H }, ], Animation: [ - { key: "xOffset", label: "X offset", type: "number", default: 0 }, - { key: "yOffset", label: "Y offset", type: "number", default: 0 }, - { key: "width", label: "Width", type: "number", default: W }, - { key: "height", label: "Height", type: "number", default: H }, - { key: "updateInterval", label: "Update interval", type: "number", default: 12 }, - { key: "frames", label: "Frames", type: "list", default: [] }, + { key: "xOffset", label: "X offset", type: "number", default: 0 }, + { key: "yOffset", label: "Y offset", type: "number", default: 0 }, + { key: "width", label: "Width", type: "number", default: W }, + { key: "height", label: "Height", type: "number", default: H }, + { key: "updateInterval", label: "Update interval", type: "number", default: 12 }, + { key: "frames", label: "Frames", type: "list", default: [] }, ], ClippedText: [ - { key: "text", label: "Text", type: "text", default: "Hello, World!", full: true }, - { key: "xOffset", label: "X offset", type: "number", default: 0 }, - { key: "yOffset", label: "Y offset", type: "number", default: 10 }, - { key: "width", label: "Width", type: "number", default: 30 }, - { key: "height", label: "Height", type: "number", default: 10 }, - { key: "fontIndex", label: "Font", type: "font", default: 0 }, + { key: "text", label: "Text", type: "text", default: "Hello, World!", full: true }, + { key: "xOffset", label: "X offset", type: "number", default: 0 }, + { key: "yOffset", label: "Y offset", type: "number", default: 10 }, + { key: "width", label: "Width", type: "number", default: 30 }, + { key: "height", label: "Height", type: "number", default: 10 }, + { key: "fontIndex", label: "Font", type: "font", default: 0 }, ], HScrollText: [ - { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true }, - { key: "xOffset", label: "X offset", type: "number", default: 0 }, - { key: "yOffset", label: "Y offset", type: "number", default: 10 }, - { key: "width", label: "Width", type: "number", default: 30 }, - { key: "height", label: "Height", type: "number", default: 10 }, + { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true }, + { key: "xOffset", label: "X offset", type: "number", default: 0 }, + { key: "yOffset", label: "Y offset", type: "number", default: 10 }, + { key: "width", label: "Width", type: "number", default: 30 }, + { key: "height", label: "Height", type: "number", default: 10 }, { key: "scrollSpeed", label: "Scroll speed", type: "number", default: 50 }, - { key: "fontIndex", label: "Font", type: "font", default: 0 }, + { key: "fontIndex", label: "Font", type: "font", default: 0 }, ], VScrollText: [ - { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true }, - { key: "xOffset", label: "X offset", type: "number", default: 0 }, - { key: "yOffset", label: "Y offset", type: "number", default: 32 }, - { key: "width", label: "Width", type: "number", default: 30 }, - { key: "height", label: "Height", type: "number", default: 10}, + { key: "text", label: "Text", type: "text", default: "Scrolling text - ", full: true }, + { key: "xOffset", label: "X offset", type: "number", default: 0 }, + { key: "yOffset", label: "Y offset", type: "number", default: 32 }, + { key: "width", label: "Width", type: "number", default: 30 }, + { key: "height", label: "Height", type: "number", default: 10 }, { key: "scrollSpeed", label: "Scroll speed", type: "number", default: 50 }, - { key: "fontIndex", label: "Font", type: "font", default: 0 }, + { key: "fontIndex", label: "Font", type: "font", default: 0 }, + ], + Line: [ + { key: "xOrigin", label: "X offset", type: "number", default: 0 }, + { key: "yOrigin", label: "Y offset", type: "number", default: 32 }, + { key: "xTarget", label: "X offset", type: "number", default: 0 }, + { key: "yTarget", label: "Y offset", type: "number", default: 32 }, + { key: "linestyle", label: "Lintylee S", type: "number", default: 0 }, ], CurrentTime: [ - { key: "xOffset", label: "X offset", type: "number", default: 0 }, - { key: "yOffset", label: "Y offset", type: "number", default: 8 }, - { key: "width", label: "Width", type: "number", default: W }, - { key: "height", label: "Height", type: "number", default: H }, + { key: "xOffset", label: "X offset", type: "number", default: 0 }, + { key: "yOffset", label: "Y offset", type: "number", default: 8 }, + { key: "width", label: "Width", type: "number", default: W }, + { key: "height", label: "Height", type: "number", default: H }, { key: "utcOffsetMinutes", label: "UTC offset (min)", type: "number", default: 120 }, - { key: "fontIndex", label: "Font", type: "font", default: 0 }, + { key: "fontIndex", label: "Font", type: "font", default: 0 }, ], }; const EL_FLAGS: Partial> = { HScrollText: [ - { key: "endless", label: "Endless" }, + { key: "endless", label: "Endless" }, { key: "invertDirection", label: "Invert direction" }, - { key: "padBefore", label: "Pad Before" }, - { key: "padAfter", label: "Pad After" }, + { key: "padBefore", label: "Pad Before" }, + { key: "padAfter", label: "Pad After" }, ], CurrentTime: [ - { key: "clock12h", label: "12h mode", default: false }, - { key: "showHours", label: "Show hours", default: true }, - { key: "showSeconds", label: "Show seconds", default: true }, + { key: "clock12h", label: "12h mode", default: false }, + { key: "showHours", label: "Show hours", default: true }, + { key: "showSeconds", label: "Show seconds", default: true }, ], }; @@ -99,10 +106,10 @@ function showConfirm( ): Promise { return new Promise(resolve => { const overlay = document.getElementById("confirm-overlay"); - const msgEl = document.getElementById("confirm-msg"); - const btnsEl = document.getElementById("confirm-btns"); - if (msgEl) msgEl.textContent = msg; - if (btnsEl) btnsEl.innerHTML = ""; + const msgEl = document.getElementById("confirm-msg"); + const btnsEl = document.getElementById("confirm-btns"); + if (msgEl) msgEl.textContent = msg; + if (btnsEl) btnsEl.innerHTML = ""; buttons.forEach(b => { const btn = document.createElement("button"); btn.className = "cb" + (b.primary ? " primary" : ""); @@ -122,7 +129,7 @@ function saveToStorage() { const buf = file.toBuffer(); const b64 = btoa(String.fromCharCode(...buf)); localStorage.setItem(STORAGE_KEY, JSON.stringify({ filename: currentFilename, data: b64 })); - } catch {} + } catch { } } function loadFromStorage() { @@ -139,7 +146,7 @@ function loadFromStorage() { isDirty = true; document.getElementById("filename")?.classList.add("dirty"); m.redraw(); - } catch {} + } catch { } } // --- Dirty tracking ---------------------------------------------------------- @@ -167,7 +174,8 @@ function newSec(): MonoDisplay.MonoFormatElementsAlways { clearBuffer: true, drawBack: true, drawFront: true, - } }; + } + }; } function createNewElement(type: MonoDisplay.ElementType): MonoDisplay.MonoFormatElement { @@ -375,8 +383,8 @@ const DEMO: Record MonoDisplayFile> = { return new MonoDisplayFile([{ sectionType: MonoDisplay.SectionType.ElementsAlways, elements: [ - { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: {}, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 }, - { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true }, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 }, + { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: {}, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 }, + { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true }, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 }, { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: true }, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 }, { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: false }, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 }, { type: MonoDisplay.ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showSeconds: true }, xOffset: 80, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 }, @@ -496,10 +504,10 @@ const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: function pixelFromEvent(e: MouseEvent): { x: number; y: number } | null { const canvas = e.currentTarget as HTMLCanvasElement; const rect = canvas.getBoundingClientRect(); - const scaleX = canvas.width / rect.width; + const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = Math.floor((e.clientX - rect.left) * scaleX / PIXEL_SCALE); - const y = Math.floor((e.clientY - rect.top) * scaleY / PIXEL_SCALE); + const y = Math.floor((e.clientY - rect.top) * scaleY / PIXEL_SCALE); if (x < 0 || x >= img.width || y < 0 || y >= img.height) return null; return { x, y }; } @@ -513,7 +521,7 @@ const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: } return m("canvas.pixel-editor", { - width: img.width * PIXEL_SCALE, + width: img.width * PIXEL_SCALE, height: img.height * PIXEL_SCALE, oncreate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img), onupdate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img), @@ -524,7 +532,7 @@ const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: paint(e); }, onmousemove: (e: MouseEvent) => { if (state.drawing) paint(e); }, - onmouseup: () => { state.drawing = false; }, + onmouseup: () => { state.drawing = false; }, onmouseleave: () => { state.drawing = false; }, }); }, @@ -535,7 +543,7 @@ const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatImage2D }> = { view({ attrs: { si, ei, el } }) { if (!el.image) { - const w = (el as any).width || W; + const w = (el as any).width || W; const h = (el as any).height || H; el.image = { pixels: new Uint8Array(w * h), width: w, height: h }; } @@ -554,7 +562,7 @@ const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.Mon activeFrame: 0, view({ attrs: { si, ei, el }, state }) { - const w = el.width || W; + const w = el.width || W; const h = el.height || H; if (!el.frames) el.frames = []; if (!el.frames.length) { @@ -587,8 +595,8 @@ const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.Mon m("span", fi + 1), el.frames.length > 1 ? m("button.x-btn-sm", { - onclick: (e: Event) => { e.stopPropagation(); removeFrame(fi); }, - }, "×") + onclick: (e: Event) => { e.stopPropagation(); removeFrame(fi); }, + }, "×") : null, ) ), @@ -609,7 +617,7 @@ const ElementItem: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFor const isActive = activeSecIndex === si && activeElIndex === ei; const typeStr = MonoDisplay.ElementTypeToString[el.type]; const fields = EL_FIELDS[typeStr] || []; - const flags = EL_FLAGS[typeStr] || []; + const flags = EL_FLAGS[typeStr] || []; return m(".el-item", { class: isActive ? "active" : "" }, m(".el-item-hdr", { onclick: () => selectElement(si, ei) }, @@ -655,15 +663,15 @@ const ElementItem: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFor ) : null, el.type === MonoDisplay.ElementType.Image2D ? m(".field.full", - m("label", "Pixels"), - m(Image2DEditor, { si, ei, el: el as MonoDisplay.MonoFormatImage2D }), - ) + m("label", "Pixels"), + m(Image2DEditor, { si, ei, el: el as MonoDisplay.MonoFormatImage2D }), + ) : el.type === MonoDisplay.ElementType.Animation - ? m(".field.full", + ? m(".field.full", m("label", "Frames"), m(AnimationEditor, { si, ei, el: el as MonoDisplay.MonoFormatAnimation }), ) - : null, + : null, ), ); }, @@ -674,7 +682,7 @@ const SectionCard: m.Component<{ si: number }> = { const section = file.sections[si]; if (!section) return null; const isActive = activeSecIndex === si; - const typeStr = MonoDisplay.SectionTypeToString[section.sectionType]; + const typeStr = MonoDisplay.SectionTypeToString[section.sectionType]; const hdr = m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) }, m("span.sec-arrow", "▶"), @@ -714,26 +722,26 @@ const SectionCard: m.Component<{ si: number }> = { ), section.sectionType === MonoDisplay.SectionType.ElementsTimespan ? m(".el-list", - ([ ["Start Time", "startTimestamp"], ["Stop Time", "endTimestamp"] ] as [string, string][]) - .map(([label, key]) => { - let posix: bigint = (section as any)[key]; - if (!posix) { - posix = BigInt(Math.floor(Date.now() / 1000)); - setSectionField(si, key, posix); - } - const dtLocal = new Date(Number(posix) * 1000).toISOString().slice(0, 16); - return m(".field", - m("label", label), - m("input[type=datetime-local]", { - value: dtLocal, - onchange: (e: Event) => { - const ms = new Date((e.target as HTMLInputElement).value).getTime(); - setSectionField(si, key, BigInt(Math.floor(ms / 1000))); - }, - }), - ); - }) - ) + ([["Start Time", "startTimestamp"], ["Stop Time", "endTimestamp"]] as [string, string][]) + .map(([label, key]) => { + let posix: bigint = (section as any)[key]; + if (!posix) { + posix = BigInt(Math.floor(Date.now() / 1000)); + setSectionField(si, key, posix); + } + const dtLocal = new Date(Number(posix) * 1000).toISOString().slice(0, 16); + return m(".field", + m("label", label), + m("input[type=datetime-local]", { + value: dtLocal, + onchange: (e: Event) => { + const ms = new Date((e.target as HTMLInputElement).value).getTime(); + setSectionField(si, key, BigInt(Math.floor(ms / 1000))); + }, + }), + ); + }) + ) : null, m(".el-list", elSection.elements.map((el, ei) => m(ElementItem, { key: String(ei), si, ei, el })), @@ -760,14 +768,14 @@ const MetaPanel: m.Component = { m("label", "Selected section"), section ? m("select.meta-val.green", { - value: MonoDisplay.SectionTypeToString[section.sectionType], - onchange: (e: Event) => - activeSecIndex !== null && setSectionType(activeSecIndex, (e.target as HTMLSelectElement).value), - }, - Object.values(MonoDisplay.SectionTypeToString).map(name => - m("option", { value: name, selected: name === MonoDisplay.SectionTypeToString[section.sectionType] }, name) - ) + value: MonoDisplay.SectionTypeToString[section.sectionType], + onchange: (e: Event) => + activeSecIndex !== null && setSectionType(activeSecIndex, (e.target as HTMLSelectElement).value), + }, + Object.values(MonoDisplay.SectionTypeToString).map(name => + m("option", { value: name, selected: name === MonoDisplay.SectionTypeToString[section.sectionType] }, name) ) + ) : m("span.meta-val.green", "-"), ), m(".meta-row", @@ -796,30 +804,30 @@ const DemoButtons: m.Component = { view() { return Object.entries(DEMO).map(([name, fn]) => m("button.tb-btn", { - style: activeDemoName === name ? "color:#33ff66" : "", - onclick: async () => { - const ok = await guardDirty(); - if (!ok) return; - file = fn(); - activeSecIndex = null; - activeElIndex = null; - currentFilename = name.toLowerCase(); - const filenameEl = document.getElementById("filename"); - if (filenameEl) filenameEl.textContent = currentFilename; - activeDemoName = name; - markClean(); - triggerPreview(); - m.redraw(); - }, - }, name) + style: activeDemoName === name ? "color:#33ff66" : "", + onclick: async () => { + const ok = await guardDirty(); + if (!ok) return; + file = fn(); + activeSecIndex = null; + activeElIndex = null; + currentFilename = name.toLowerCase(); + const filenameEl = document.getElementById("filename"); + if (filenameEl) filenameEl.textContent = currentFilename; + activeDemoName = name; + markClean(); + triggerPreview(); + m.redraw(); + }, + }, name) ); }, }; // --- Mount -------------------------------------------------------------------- m.mount(document.getElementById("sections-wrap")!, SectionsList); -m.mount(document.getElementById("demo-btns")!, DemoButtons); -m.mount(document.getElementById("sec-meta")!, MetaPanel); +m.mount(document.getElementById("demo-btns")!, DemoButtons); +m.mount(document.getElementById("sec-meta")!, MetaPanel); async function clearAll() { if (isDirty && file.sections.length) {