feat: update line support

typescript_changes
flop 2 weeks ago
parent 9a3e435cfc
commit 213ead59b2
  1. 220
      ts/src/browser.ts

@ -25,64 +25,71 @@ const EL_FIELDS: Partial<Record<MonoDisplay.ElementTypeName, ElFieldMeta[]>> = {
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<Record<MonoDisplay.ElementTypeName, ElFlagMeta[]>> = {
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<boolean> {
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<string, () => 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) {

Loading…
Cancel
Save