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

Loading…
Cancel
Save