|
|
|
|
@ -2,29 +2,10 @@ |
|
|
|
|
// Built by: bun build src/browser.ts --target browser --outfile public/mono-display.js
|
|
|
|
|
|
|
|
|
|
import m from "mithril"; |
|
|
|
|
import { |
|
|
|
|
ElementType, |
|
|
|
|
ElementTypeToString, |
|
|
|
|
MonoDisplayDriver, |
|
|
|
|
MonoDisplayFile, |
|
|
|
|
MonoDisplayParser, |
|
|
|
|
MonoDisplayRenderer, |
|
|
|
|
SectionType, |
|
|
|
|
SectionTypeToString, |
|
|
|
|
StringToElementType, |
|
|
|
|
StringToSectionType, |
|
|
|
|
cycleTheme, |
|
|
|
|
type ElementTypeName, |
|
|
|
|
type MonoFormatAnimation, |
|
|
|
|
type MonoFormatElement, |
|
|
|
|
type MonoFormatElementsAlways, |
|
|
|
|
type MonoFormatElementsTimespan, |
|
|
|
|
type MonoFormatImage2D, |
|
|
|
|
type MonoFormatPixelImage, |
|
|
|
|
type MonoFormatSection, |
|
|
|
|
type SectionTypeName |
|
|
|
|
} from "libmonoformat"; |
|
|
|
|
import * as MonoDisplay from "libmonoformat"; |
|
|
|
|
import { MonoDisplayFile, cycleTheme } from "libmonoformat"; |
|
|
|
|
|
|
|
|
|
(globalThis as typeof globalThis & { MonoDisplay: typeof MonoDisplay }).MonoDisplay = MonoDisplay; |
|
|
|
|
|
|
|
|
|
const W = 120, H = 60; |
|
|
|
|
|
|
|
|
|
@ -39,7 +20,7 @@ let isDirty = false; |
|
|
|
|
type ElFieldMeta = { key: string; label: string; type: string; default: any; full?: boolean }; |
|
|
|
|
type ElFlagMeta = { key: string; label: string; default?: boolean }; |
|
|
|
|
|
|
|
|
|
const EL_FIELDS: Partial<Record<ElementTypeName, ElFieldMeta[]>> = { |
|
|
|
|
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 }, |
|
|
|
|
@ -97,7 +78,7 @@ const EL_FIELDS: Partial<Record<ElementTypeName, ElFieldMeta[]>> = { |
|
|
|
|
], |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const EL_FLAGS: Partial<Record<ElementTypeName, ElFlagMeta[]>> = { |
|
|
|
|
const EL_FLAGS: Partial<Record<MonoDisplay.ElementTypeName, ElFlagMeta[]>> = { |
|
|
|
|
HScrollText: [ |
|
|
|
|
{ key: "endless", label: "Endless" }, |
|
|
|
|
{ key: "invertDirection", label: "Invert direction" }, |
|
|
|
|
@ -111,11 +92,11 @@ const EL_FLAGS: Partial<Record<ElementTypeName, ElFlagMeta[]>> = { |
|
|
|
|
], |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const EL_TYPES: ElementType[] = Object.keys(EL_FIELDS).map( |
|
|
|
|
(x) => StringToElementType[x as ElementTypeName] |
|
|
|
|
const EL_TYPES: MonoDisplay.ElementType[] = Object.keys(EL_FIELDS).map( |
|
|
|
|
(x) => MonoDisplay.StringToElementType[x as MonoDisplay.ElementTypeName] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const DEFAULT_FONTS = Object.keys(MonoDisplayRenderer.builtinFonts); |
|
|
|
|
const DEFAULT_FONTS = Object.keys(MonoDisplay.MonoDisplayRenderer.builtinFonts); |
|
|
|
|
|
|
|
|
|
// --- Confirm dialog ----------------------------------------------------------
|
|
|
|
|
function showConfirm( |
|
|
|
|
@ -156,7 +137,7 @@ function loadFromStorage() { |
|
|
|
|
if (!raw) return; |
|
|
|
|
const { filename, data } = JSON.parse(raw); |
|
|
|
|
const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0)); |
|
|
|
|
const parsed = new MonoDisplayParser().parse(bytes.buffer); |
|
|
|
|
const parsed = new MonoDisplay.MonoDisplayParser().parse(bytes.buffer); |
|
|
|
|
file = new MonoDisplayFile(parsed.sections); |
|
|
|
|
currentFilename = filename; |
|
|
|
|
const filenameEl = document.getElementById("filename"); |
|
|
|
|
@ -186,9 +167,9 @@ async function guardDirty(): Promise<boolean> { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// --- Helpers -----------------------------------------------------------------
|
|
|
|
|
function newSec(): MonoFormatElementsAlways { |
|
|
|
|
function newSec(): MonoDisplay.MonoFormatElementsAlways { |
|
|
|
|
return { |
|
|
|
|
sectionType: SectionType.ElementsAlways, elements: [], flags: { |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, elements: [], flags: { |
|
|
|
|
clearBuffer: true, |
|
|
|
|
drawBack: true, |
|
|
|
|
drawFront: true, |
|
|
|
|
@ -196,47 +177,47 @@ function newSec(): MonoFormatElementsAlways { |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function createNewElement(type: ElementType): MonoFormatElement { |
|
|
|
|
function createNewElement(type: MonoDisplay.ElementType): MonoDisplay.MonoFormatElement { |
|
|
|
|
const fields: Record<string, any> = {}; |
|
|
|
|
(EL_FIELDS[ElementTypeToString[type]] as ElFieldMeta[] || []) |
|
|
|
|
(EL_FIELDS[MonoDisplay.ElementTypeToString[type]] as ElFieldMeta[] || []) |
|
|
|
|
.forEach(f => (fields[f.key] = f.default)); |
|
|
|
|
const flags: Record<string, any> = {}; |
|
|
|
|
(EL_FLAGS[ElementTypeToString[type]] as ElFlagMeta[] || []) |
|
|
|
|
(EL_FLAGS[MonoDisplay.ElementTypeToString[type]] as ElFlagMeta[] || []) |
|
|
|
|
.forEach(f => (flags[f.key] = f.default ?? false)); |
|
|
|
|
const el: any = { type, ...fields, flags }; |
|
|
|
|
if (type === ElementType.Image2D) { |
|
|
|
|
if (type === MonoDisplay.ElementType.Image2D) { |
|
|
|
|
el.image = { pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height }; |
|
|
|
|
} |
|
|
|
|
if (type === ElementType.Animation) { |
|
|
|
|
if (type === MonoDisplay.ElementType.Animation) { |
|
|
|
|
el.frames = [{ pixels: new Uint8Array(el.width * el.height), width: el.width, height: el.height }]; |
|
|
|
|
} |
|
|
|
|
return el as MonoFormatElement; |
|
|
|
|
return el as MonoDisplay.MonoFormatElement; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSectionByIndex(i: number | null): MonoFormatSection | undefined { |
|
|
|
|
function getSectionByIndex(i: number | null): MonoDisplay.MonoFormatSection | undefined { |
|
|
|
|
return i !== null ? file.sections[i] : undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getElementByIndex(si: number, ei: number): MonoFormatElement | undefined { |
|
|
|
|
function getElementByIndex(si: number, ei: number): MonoDisplay.MonoFormatElement | undefined { |
|
|
|
|
const s = getSectionByIndex(si); |
|
|
|
|
return s && "elements" in s ? s.elements[ei] : undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getCustomFonts(): { fontname: string; index: number }[] { |
|
|
|
|
return file.sections |
|
|
|
|
.filter(s => s.sectionType === SectionType.CustomFont) |
|
|
|
|
.filter(s => s.sectionType === MonoDisplay.SectionType.CustomFont) |
|
|
|
|
.map((_, i) => ({ fontname: `CustomFont ${i}`, index: 0x8000 + i })); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function elSummary(el: MonoFormatElement): string { |
|
|
|
|
function elSummary(el: MonoDisplay.MonoFormatElement): string { |
|
|
|
|
switch (el.type) { |
|
|
|
|
case ElementType.ClippedText: |
|
|
|
|
case ElementType.HScrollText: |
|
|
|
|
case MonoDisplay.ElementType.ClippedText: |
|
|
|
|
case MonoDisplay.ElementType.HScrollText: |
|
|
|
|
return el.text.slice(0, 24) + (el.text.length > 24 ? "..." : ""); |
|
|
|
|
case ElementType.CurrentTime: |
|
|
|
|
case ElementType.Image2D: |
|
|
|
|
case ElementType.HorizontalScroll: |
|
|
|
|
case ElementType.VerticalScroll: |
|
|
|
|
case MonoDisplay.ElementType.CurrentTime: |
|
|
|
|
case MonoDisplay.ElementType.Image2D: |
|
|
|
|
case MonoDisplay.ElementType.HorizontalScroll: |
|
|
|
|
case MonoDisplay.ElementType.VerticalScroll: |
|
|
|
|
return `${(el as any).xOffset ?? 0}, ${(el as any).yOffset ?? 0}`; |
|
|
|
|
default: |
|
|
|
|
return el.type.toString(); |
|
|
|
|
@ -282,9 +263,9 @@ function setSectionField(si: number, key: string, val: any) { |
|
|
|
|
function setSectionType(si: number, sectionType: string) { |
|
|
|
|
const sec = getSectionByIndex(si); |
|
|
|
|
if (!sec) return; |
|
|
|
|
const wasElementsSection = sec.sectionType !== SectionType.CustomFont; |
|
|
|
|
(sec as any).sectionType = StringToSectionType[sectionType as SectionTypeName]; |
|
|
|
|
if ((sec as any).sectionType === SectionType.CustomFont) { |
|
|
|
|
const wasElementsSection = sec.sectionType !== MonoDisplay.SectionType.CustomFont; |
|
|
|
|
(sec as any).sectionType = MonoDisplay.StringToSectionType[sectionType as MonoDisplay.SectionTypeName]; |
|
|
|
|
if ((sec as any).sectionType === MonoDisplay.SectionType.CustomFont) { |
|
|
|
|
(sec as any).fontData = new Uint8Array(); |
|
|
|
|
delete (sec as any).elements; |
|
|
|
|
delete (sec as any).flags; |
|
|
|
|
@ -294,7 +275,7 @@ function setSectionType(si: number, sectionType: string) { |
|
|
|
|
(sec as any).flags = {}; |
|
|
|
|
} |
|
|
|
|
delete (sec as any).fontData; |
|
|
|
|
if ((sec as any).sectionType === SectionType.ElementsTimespan) { |
|
|
|
|
if ((sec as any).sectionType === MonoDisplay.SectionType.ElementsTimespan) { |
|
|
|
|
const now = BigInt(Math.floor(Date.now() / 1000)); |
|
|
|
|
(sec as any).startTimestamp = now; |
|
|
|
|
(sec as any).endTimestamp = now + 3600n; |
|
|
|
|
@ -309,7 +290,7 @@ function setSectionType(si: number, sectionType: string) { |
|
|
|
|
function addElement(si: number) { |
|
|
|
|
const s = getSectionByIndex(si); |
|
|
|
|
if (!s || !("elements" in s)) return; |
|
|
|
|
s.elements.push(createNewElement(ElementType.HScrollText)); |
|
|
|
|
s.elements.push(createNewElement(MonoDisplay.ElementType.HScrollText)); |
|
|
|
|
activeElIndex = s.elements.length - 1; |
|
|
|
|
activeSecIndex = si; |
|
|
|
|
markDirty(); triggerPreview(); |
|
|
|
|
@ -332,10 +313,10 @@ function selectElement(si: number, ei: number) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function changeElType(si: number, ei: number, typeString: ElementTypeName) { |
|
|
|
|
function changeElType(si: number, ei: number, typeString: MonoDisplay.ElementTypeName) { |
|
|
|
|
const el = getElementByIndex(si, ei); |
|
|
|
|
if (!el) return; |
|
|
|
|
const fresh = createNewElement(StringToElementType[typeString]); |
|
|
|
|
const fresh = createNewElement(MonoDisplay.StringToElementType[typeString]); |
|
|
|
|
Object.assign(el, fresh); |
|
|
|
|
markDirty(); triggerPreview(); |
|
|
|
|
} |
|
|
|
|
@ -360,16 +341,16 @@ const DEMO: Record<string, () => MonoDisplayFile> = { |
|
|
|
|
const pixels = new Uint8Array(W * H); |
|
|
|
|
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) pixels[y * W + x] = (x + y) % 2; |
|
|
|
|
return new MonoDisplayFile([{ |
|
|
|
|
sectionType: SectionType.ElementsAlways, |
|
|
|
|
elements: [{ type: ElementType.Image2D, image: { pixels, height: H, width: W }, xOffset: 0, yOffset: 0 }], |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, |
|
|
|
|
elements: [{ type: MonoDisplay.ElementType.Image2D, image: { pixels, height: H, width: W }, xOffset: 0, yOffset: 0 }], |
|
|
|
|
flags: { clearBuffer: true, drawFront: true, drawBack: true }, |
|
|
|
|
}]); |
|
|
|
|
}, |
|
|
|
|
Blink() { |
|
|
|
|
return new MonoDisplayFile([{ |
|
|
|
|
sectionType: SectionType.ElementsAlways, |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, |
|
|
|
|
elements: [{ |
|
|
|
|
type: ElementType.Animation, width: W, height: H, updateInterval: 12, xOffset: 0, yOffset: 0, |
|
|
|
|
type: MonoDisplay.ElementType.Animation, width: W, height: H, updateInterval: 12, xOffset: 0, yOffset: 0, |
|
|
|
|
frames: [ |
|
|
|
|
{ pixels: new Uint8Array(W * H).fill(1), width: 0, height: 0 }, |
|
|
|
|
{ pixels: new Uint8Array(W * H).fill(0), width: 0, height: 0 }, |
|
|
|
|
@ -380,16 +361,16 @@ const DEMO: Record<string, () => MonoDisplayFile> = { |
|
|
|
|
}, |
|
|
|
|
Text() { |
|
|
|
|
return new MonoDisplayFile([{ |
|
|
|
|
sectionType: SectionType.ElementsAlways, |
|
|
|
|
elements: [{ type: ElementType.ClippedText, fontIndex: 0, text: "Hello, World!", xOffset: 0, yOffset: 16, width: 60, height: 10 }], |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, |
|
|
|
|
elements: [{ type: MonoDisplay.ElementType.ClippedText, fontIndex: 0, text: "Hello, World!", xOffset: 0, yOffset: 16, width: 60, height: 10 }], |
|
|
|
|
flags: { clearBuffer: true, drawFront: true, drawBack: true }, |
|
|
|
|
}]); |
|
|
|
|
}, |
|
|
|
|
Scrolltext() { |
|
|
|
|
return new MonoDisplayFile([{ |
|
|
|
|
sectionType: SectionType.ElementsAlways, |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, |
|
|
|
|
elements: [{ |
|
|
|
|
type: ElementType.HScrollText, fontIndex: 0, |
|
|
|
|
type: MonoDisplay.ElementType.HScrollText, fontIndex: 0, |
|
|
|
|
text: "MONO DISPLAY - scrolling ticker - 🚀 ", |
|
|
|
|
xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50, |
|
|
|
|
flags: { endless: true, invertDirection: false, padStart: false, padEnd: false }, |
|
|
|
|
@ -399,13 +380,13 @@ const DEMO: Record<string, () => MonoDisplayFile> = { |
|
|
|
|
}, |
|
|
|
|
Time() { |
|
|
|
|
return new MonoDisplayFile([{ |
|
|
|
|
sectionType: SectionType.ElementsAlways, |
|
|
|
|
sectionType: MonoDisplay.SectionType.ElementsAlways, |
|
|
|
|
elements: [ |
|
|
|
|
{ type: ElementType.CurrentTime, fontIndex: 0, flags: {}, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 }, |
|
|
|
|
{ type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true }, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 }, |
|
|
|
|
{ type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: true }, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 }, |
|
|
|
|
{ type: ElementType.CurrentTime, fontIndex: 0, flags: { clock12h: true, showHours: false }, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 }, |
|
|
|
|
{ type: 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: {}, 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 }, |
|
|
|
|
], |
|
|
|
|
flags: { clearBuffer: true, drawFront: true, drawBack: true }, |
|
|
|
|
}]); |
|
|
|
|
@ -423,9 +404,10 @@ function triggerPreview() { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function buildPreview() { |
|
|
|
|
if (!(window as any)._mdDriver) |
|
|
|
|
(window as any)._mdDriver = new MonoDisplayDriver("canvas_root", { onColor: "#EC0", offColor: "#000", fps: 25 }); |
|
|
|
|
(window as any)._mdDriver.load(() => Promise.resolve(file.toBuffer())); |
|
|
|
|
if (!window.MonoDisplay) return; |
|
|
|
|
if (!window._mdDriver) |
|
|
|
|
window._mdDriver = new MonoDisplay.MonoDisplayDriver("canvas_root", { onColor: "#EC0", offColor: "#000", fps: 25 }); |
|
|
|
|
window._mdDriver.load(() => Promise.resolve(file.toBuffer())); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// --- Load / Export -----------------------------------------------------------
|
|
|
|
|
@ -438,7 +420,7 @@ function loadBin(input: HTMLInputElement) { |
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
reader.onload = e => { |
|
|
|
|
try { |
|
|
|
|
const parsed = new MonoDisplayParser().parse(e.target!.result as ArrayBuffer); |
|
|
|
|
const parsed = new window.MonoDisplay.MonoDisplayParser().parse(e.target!.result as ArrayBuffer); |
|
|
|
|
file = new MonoDisplayFile(parsed.sections); |
|
|
|
|
activeSecIndex = null; |
|
|
|
|
activeElIndex = null; |
|
|
|
|
@ -454,7 +436,7 @@ function loadBin(input: HTMLInputElement) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function exportBin() { |
|
|
|
|
// if (!window.MonoDisplay) { alert("MonoDisplay library not loaded."); return; }
|
|
|
|
|
if (!window.MonoDisplay) { alert("MonoDisplay library not loaded."); return; } |
|
|
|
|
const blob = new Blob([file.toBuffer() as any], { type: "application/octet-stream" }); |
|
|
|
|
const a = document.createElement("a"); |
|
|
|
|
a.href = URL.createObjectURL(blob); |
|
|
|
|
@ -465,7 +447,7 @@ function exportBin() { |
|
|
|
|
|
|
|
|
|
// --- Mithril Components -------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoFormatElement): m.Vnode { |
|
|
|
|
function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoDisplay.MonoFormatElement): m.Vnode { |
|
|
|
|
const val = (el as any)[field.key] ?? field.default; |
|
|
|
|
switch (field.type) { |
|
|
|
|
case "text": |
|
|
|
|
@ -493,11 +475,11 @@ function FieldInput(si: number, ei: number, field: ElFieldMeta, el: MonoFormatEl |
|
|
|
|
|
|
|
|
|
const PIXEL_SCALE = 4; |
|
|
|
|
|
|
|
|
|
function getPixelIndex(img: MonoFormatPixelImage, x: number, y: number): number { |
|
|
|
|
function getPixelIndex(img: MonoDisplay.MonoFormatPixelImage, x: number, y: number): number { |
|
|
|
|
return y * img.width + x; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function drawPixelCanvas(canvas: HTMLCanvasElement, img: MonoFormatPixelImage) { |
|
|
|
|
function drawPixelCanvas(canvas: HTMLCanvasElement, img: MonoDisplay.MonoFormatPixelImage) { |
|
|
|
|
const ctx = canvas.getContext("2d")!; |
|
|
|
|
ctx.fillStyle = "#000"; |
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
@ -513,11 +495,10 @@ function drawPixelCanvas(canvas: HTMLCanvasElement, img: MonoFormatPixelImage) { |
|
|
|
|
|
|
|
|
|
interface PixelCanvasState { drawing: boolean; drawValue: number; } |
|
|
|
|
|
|
|
|
|
const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void }, PixelCanvasState> = { |
|
|
|
|
oninit(vnode) { |
|
|
|
|
vnode.state.drawing = false; |
|
|
|
|
vnode.state.drawValue = 1; |
|
|
|
|
}, |
|
|
|
|
const PixelCanvas: m.Component<{ img: MonoDisplay.MonoFormatPixelImage; onpaint: () => void }, PixelCanvasState> = { |
|
|
|
|
drawing: false, |
|
|
|
|
drawValue: 1, |
|
|
|
|
|
|
|
|
|
view({ attrs: { img, onpaint }, state }) { |
|
|
|
|
function pixelFromEvent(e: MouseEvent): { x: number; y: number } | null { |
|
|
|
|
const canvas = e.currentTarget as HTMLCanvasElement; |
|
|
|
|
@ -586,10 +567,10 @@ const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void |
|
|
|
|
img.height = h; |
|
|
|
|
// then read imageData from the scaled offscreen canvas as before
|
|
|
|
|
img.pixels = new Uint8Array(w * h).map((_, i) => { |
|
|
|
|
const r = imageData.data[i * 4] || 0; |
|
|
|
|
const g = imageData.data[i * 4 + 1] || 0; |
|
|
|
|
const b = imageData.data[i * 4 + 2] || 0; |
|
|
|
|
const a = imageData.data[i * 4 + 3] || 0; |
|
|
|
|
const r = imageData.data[i * 4]; |
|
|
|
|
const g = imageData.data[i * 4 + 1]; |
|
|
|
|
const b = imageData.data[i * 4 + 2]; |
|
|
|
|
const a = imageData.data[i * 4 + 3]; |
|
|
|
|
// transparent = 0, dark pixels = 1, light pixels = 0
|
|
|
|
|
return a > 128 && (r * 299 + g * 587 + b * 114) < 128_000 ? 0 : 1; |
|
|
|
|
}); |
|
|
|
|
@ -605,7 +586,7 @@ const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void |
|
|
|
|
|
|
|
|
|
// --- Image2D editor -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoFormatImage2D }> = { |
|
|
|
|
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; |
|
|
|
|
@ -623,10 +604,9 @@ const Image2DEditor: m.Component<{ si: number; ei: number; el: MonoFormatImage2D |
|
|
|
|
|
|
|
|
|
interface AnimationEditorState { activeFrame: number; } |
|
|
|
|
|
|
|
|
|
const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoFormatAnimation }, AnimationEditorState> = { |
|
|
|
|
oninit(vnode) { |
|
|
|
|
vnode.state.activeFrame = 0; |
|
|
|
|
}, |
|
|
|
|
const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatAnimation }, AnimationEditorState> = { |
|
|
|
|
activeFrame: 0, |
|
|
|
|
|
|
|
|
|
view({ attrs: { si, ei, el }, state }) { |
|
|
|
|
const w = el.width || W; |
|
|
|
|
const h = el.height || H; |
|
|
|
|
@ -669,7 +649,7 @@ const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoFormatAnima |
|
|
|
|
m("button.add-el", { onclick: addFrame }, "+ frame"), |
|
|
|
|
), |
|
|
|
|
m("div", { key: state.activeFrame }, |
|
|
|
|
m(PixelCanvas as any, { |
|
|
|
|
m(PixelCanvas, { |
|
|
|
|
img: frame, |
|
|
|
|
onpaint: () => { markDirty(); triggerPreview(); }, |
|
|
|
|
}), |
|
|
|
|
@ -678,10 +658,10 @@ const AnimationEditor: m.Component<{ si: number; ei: number; el: MonoFormatAnima |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const ElementItem: m.Component<{ si: number; ei: number; el: MonoFormatElement }> = { |
|
|
|
|
const ElementItem: m.Component<{ si: number; ei: number; el: MonoDisplay.MonoFormatElement }> = { |
|
|
|
|
view({ attrs: { si, ei, el } }) { |
|
|
|
|
const isActive = activeSecIndex === si && activeElIndex === ei; |
|
|
|
|
const typeStr = ElementTypeToString[el.type]; |
|
|
|
|
const typeStr = MonoDisplay.ElementTypeToString[el.type]; |
|
|
|
|
const fields = EL_FIELDS[typeStr] || []; |
|
|
|
|
const flags = EL_FLAGS[typeStr] || []; |
|
|
|
|
|
|
|
|
|
@ -698,10 +678,10 @@ const ElementItem: m.Component<{ si: number; ei: number; el: MonoFormatElement } |
|
|
|
|
m(".field.full", |
|
|
|
|
m("label", "Type"), |
|
|
|
|
m("select", { |
|
|
|
|
onchange: (e: Event) => changeElType(si, ei, (e.target as HTMLSelectElement).value as ElementTypeName), |
|
|
|
|
onchange: (e: Event) => changeElType(si, ei, (e.target as HTMLSelectElement).value as MonoDisplay.ElementTypeName), |
|
|
|
|
}, |
|
|
|
|
EL_TYPES.map(t => |
|
|
|
|
m("option", { value: ElementTypeToString[t], selected: t === el.type }, ElementTypeToString[t]) |
|
|
|
|
m("option", { value: MonoDisplay.ElementTypeToString[t], selected: t === el.type }, MonoDisplay.ElementTypeToString[t]) |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
@ -727,15 +707,15 @@ const ElementItem: m.Component<{ si: number; ei: number; el: MonoFormatElement } |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
) : null, |
|
|
|
|
el.type === ElementType.Image2D |
|
|
|
|
el.type === MonoDisplay.ElementType.Image2D |
|
|
|
|
? m(".field.full", |
|
|
|
|
m("label", "Pixels"), |
|
|
|
|
m(Image2DEditor, { si, ei, el: el as MonoFormatImage2D }), |
|
|
|
|
m(Image2DEditor, { si, ei, el: el as MonoDisplay.MonoFormatImage2D }), |
|
|
|
|
) |
|
|
|
|
: el.type === ElementType.Animation |
|
|
|
|
: el.type === MonoDisplay.ElementType.Animation |
|
|
|
|
? m(".field.full", |
|
|
|
|
m("label", "Frames"), |
|
|
|
|
m(AnimationEditor, { si, ei, el: el as MonoFormatAnimation }), |
|
|
|
|
m(AnimationEditor, { si, ei, el: el as MonoDisplay.MonoFormatAnimation }), |
|
|
|
|
) |
|
|
|
|
: null, |
|
|
|
|
), |
|
|
|
|
@ -748,7 +728,7 @@ const SectionCard: m.Component<{ si: number }> = { |
|
|
|
|
const section = file.sections[si]; |
|
|
|
|
if (!section) return null; |
|
|
|
|
const isActive = activeSecIndex === si; |
|
|
|
|
const typeStr = SectionTypeToString[section.sectionType]; |
|
|
|
|
const typeStr = MonoDisplay.SectionTypeToString[section.sectionType]; |
|
|
|
|
|
|
|
|
|
const hdr = m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) }, |
|
|
|
|
m("span.sec-arrow", "▶"), |
|
|
|
|
@ -759,7 +739,7 @@ const SectionCard: m.Component<{ si: number }> = { |
|
|
|
|
}, "X"), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (section.sectionType === SectionType.CustomFont) { |
|
|
|
|
if (section.sectionType === MonoDisplay.SectionType.CustomFont) { |
|
|
|
|
return m(`.sec-card.open${isActive ? ".active" : ""}`, |
|
|
|
|
m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) }, |
|
|
|
|
m("span.sec-arrow", "▶"), |
|
|
|
|
@ -774,7 +754,7 @@ const SectionCard: m.Component<{ si: number }> = { |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const elSection = section as MonoFormatElementsAlways | MonoFormatElementsTimespan; |
|
|
|
|
const elSection = section as MonoDisplay.MonoFormatElementsAlways | MonoDisplay.MonoFormatElementsTimespan; |
|
|
|
|
|
|
|
|
|
return m(`.sec-card.open${isActive ? ".active" : ""}`, |
|
|
|
|
m(`.sec-hdr${isActive ? ".active" : ""}`, { onclick: () => toggleSection(si) }, |
|
|
|
|
@ -786,7 +766,7 @@ const SectionCard: m.Component<{ si: number }> = { |
|
|
|
|
onclick: (e: Event) => { e.stopPropagation(); removeSection(si); }, |
|
|
|
|
}, "X"), |
|
|
|
|
), |
|
|
|
|
section.sectionType === SectionType.ElementsTimespan |
|
|
|
|
section.sectionType === MonoDisplay.SectionType.ElementsTimespan |
|
|
|
|
? m(".el-list", |
|
|
|
|
([["Start Time", "startTimestamp"], ["Stop Time", "endTimestamp"]] as [string, string][]) |
|
|
|
|
.map(([label, key]) => { |
|
|
|
|
@ -834,12 +814,12 @@ const MetaPanel: m.Component = { |
|
|
|
|
m("label", "Selected section"), |
|
|
|
|
section |
|
|
|
|
? m("select.meta-val.green", { |
|
|
|
|
value: SectionTypeToString[section.sectionType], |
|
|
|
|
value: MonoDisplay.SectionTypeToString[section.sectionType], |
|
|
|
|
onchange: (e: Event) => |
|
|
|
|
activeSecIndex !== null && setSectionType(activeSecIndex, (e.target as HTMLSelectElement).value), |
|
|
|
|
}, |
|
|
|
|
Object.values(SectionTypeToString).map(name => |
|
|
|
|
m("option", { value: name, selected: name === SectionTypeToString[section.sectionType] }, name) |
|
|
|
|
Object.values(MonoDisplay.SectionTypeToString).map(name => |
|
|
|
|
m("option", { value: name, selected: name === MonoDisplay.SectionTypeToString[section.sectionType] }, name) |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
: m("span.meta-val.green", "-"), |
|
|
|
|
|