feat(editor-ui): now with loading / checking and add demos

pull/1/head
flop 4 weeks ago
parent 21859cc366
commit 01a032ffc8
  1. 549
      ts/index.html

@ -25,7 +25,6 @@
font-size: 12px
}
/* TOP BAR */
#topbar {
height: 36px;
background: #1a1a1a;
@ -97,14 +96,16 @@
background: #2a2a2a
}
/* MAIN */
.dirty {
color: #886622 !important
}
#main {
flex: 1;
display: flex;
min-height: 0
}
/* LEFT */
#left {
width: 48%;
border-right: 1px solid #1e1e1e;
@ -140,7 +141,13 @@
text-transform: uppercase
}
/* SECTION META */
#demo-btns {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 6px
}
#sec-meta {
background: #141414;
border: 1px solid #1e1e1e;
@ -185,7 +192,6 @@
cursor: pointer
}
/* RIGHT */
#right {
flex: 1;
display: flex;
@ -213,38 +219,6 @@
text-transform: uppercase
}
.rh-btn {
background: transparent;
color: #444;
border: 1px solid #252525;
border-radius: 3px;
padding: 2px 8px;
font-family: monospace;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap
}
.rh-btn:hover {
color: #aaa;
border-color: #3a3a3a;
background: #1a1a1a
}
.rh-btn svg {
width: 11px;
height: 11px;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round
}
/* SECTIONS SCROLL */
#sections-wrap {
flex: 1;
overflow-y: auto;
@ -259,7 +233,7 @@
line-height: 2
}
/* SECTION CARD */
/* section card */
.sec-card {
border: 1px solid #222;
border-radius: 3px;
@ -278,7 +252,8 @@
padding: 6px 8px;
cursor: pointer;
user-select: none;
background: #161616
background: #161616;
position: relative
}
.sec-hdr:hover {
@ -317,24 +292,26 @@
flex-shrink: 0
}
.sec-del {
/* red × — section & element */
.x-btn {
background: none;
border: none;
color: #2a2a2a;
cursor: pointer;
font-size: 15px;
line-height: 1;
padding: 1px 4px;
padding: 2px 5px;
border-radius: 2px;
flex-shrink: 0
flex-shrink: 0;
transition: color .1s, background .1s
}
.sec-del:hover {
color: #cc3333;
.x-btn:hover {
color: #ff4444;
background: #1e1010
}
/* ELEMENT LIST */
/* element */
.el-list {
background: #0f0f0f;
border-top: 1px solid #1c1c1c;
@ -382,25 +359,13 @@
.el-name {
flex: 1;
color: #555;
font-size: 11px
}
.el-del {
background: none;
border: none;
color: #252525;
cursor: pointer;
font-size: 13px;
line-height: 1;
padding: 2px 4px;
border-radius: 2px
}
.el-del:hover {
color: #cc3333
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px
}
/* FIELDS */
.el-fields {
padding: 6px 8px 8px;
border-top: 1px solid #1a1a1a;
@ -488,6 +453,70 @@
color: #888;
border-color: #444
}
/* confirm dialog */
#confirm-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .6);
z-index: 100;
align-items: center;
justify-content: center
}
#confirm-overlay.show {
display: flex
}
#confirm-box {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 4px;
padding: 20px 24px;
max-width: 340px;
width: 90%;
display: flex;
flex-direction: column;
gap: 14px
}
#confirm-msg {
color: #aaa;
font-size: 12px;
line-height: 1.6
}
#confirm-btns {
display: flex;
gap: 8px;
justify-content: flex-end
}
.cb {
background: #161616;
color: #888;
border: 1px solid #2d2d2d;
border-radius: 3px;
padding: 4px 14px;
font-family: monospace;
font-size: 11px;
cursor: pointer
}
.cb:hover {
background: #222;
color: #bbb
}
.cb.primary {
border-color: #2b3d2b;
color: #33ff66
}
.cb.primary:hover {
background: #182018
}
</style>
</head>
@ -525,16 +554,13 @@
</div>
<div id="main">
<!-- LEFT: preview + meta -->
<div id="left">
<span class="pv-label">preview · 160 × 80</span>
<div id="display-box">
<canvas id="canvas_root" width="160" height="80"></canvas>
</div>
<div id="demos">
<span class="pv-label">demos</span>
<div id="demo-btns" style="display:flex;flex-wrap:wrap;gap:5px;margin-top:6px"></div>
</div>
<span class="pv-label">demos</span>
<div id="demo-btns"></div>
<div id="sec-meta">
<div class="meta-row">
<label>Selected section</label>
@ -558,7 +584,6 @@
</div>
</div>
<!-- RIGHT: section + element editor -->
<div id="right">
<div id="right-hdr">
<span class="rh-title">sections</span>
@ -569,21 +594,25 @@
</div>
</div>
<!-- Assumes mono-display.js is loaded in the real page -->
<script src="./public/mono-display.js"></script>
<!-- confirm dialog -->
<div id="confirm-overlay">
<div id="confirm-box">
<div id="confirm-msg"></div>
<div id="confirm-btns"></div>
</div>
</div>
<script src="./public/mono-display.js"></script>
<script>
// ─── State ───────────────────────────────────────────────────────────────────
const W = 160, H = 80;
let sections = []; // [{id,name,open,flags:{drawFront,clearBuffer},elements:[…]}]
let activeSec = null; // id
let activeEl = null; // {secId,elId}
let sections = [];
let activeSec = null, activeEl = null;
let secCounter = 0, elCounter = 0;
let currentFilename = 'untitled';
let isDirty = false;
// ─── Element type definitions ────────────────────────────────────────────────
// ─── Element type definitions ────────────────────────────────────────────────
const EL_TYPES = ['Image2D', 'Animation', 'ClippedText', 'HScrollText', 'CurrentTime'];
const EL_FIELDS = {
Image2D: [
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 0 },
@ -611,14 +640,50 @@
{ k: 'utcOffsetMinutes', l: 'UTC offset (min)', t: 'number', d: 0 },
],
};
const EL_FLAGS = {
Image2D: [], Animation: [], ClippedText: [],
HScrollText: [{ k: 'endless', l: 'Endless' }, { k: 'invertDirection', l: 'Invert direction' }],
CurrentTime: [{ k: 'clock12h', l: '12h mode' }, { k: 'showHours', l: 'Show hours' }, { k: 'showSeconds', l: 'Show seconds' }],
};
// ─── Helpers ──────────────────────────────────────────────────────────────────
// ─── Confirm dialog ───────────────────────────────────────────────────────────
function confirm(msg, buttons) {
// buttons: [{label,primary,action}]
return new Promise(resolve => {
const overlay = document.getElementById('confirm-overlay');
document.getElementById('confirm-msg').textContent = msg;
const btns = document.getElementById('confirm-btns');
btns.innerHTML = '';
buttons.forEach(b => {
const el = document.createElement('button');
el.className = 'cb' + (b.primary ? ' primary' : '');
el.textContent = b.label;
el.onclick = () => { overlay.classList.remove('show'); resolve(b.action); };
btns.appendChild(el);
});
overlay.classList.add('show');
});
}
// ─── Dirty tracking ───────────────────────────────────────────────────────────
function markDirty() {
isDirty = true;
document.getElementById('filename').classList.add('dirty');
}
function markClean() {
isDirty = false;
document.getElementById('filename').classList.remove('dirty');
}
async function guardDirty() {
if (!isDirty || !sections.length) return true;
const action = await confirm(
'You have unsaved changes. Loading a demo will replace the current editor state.',
[{ label: 'Cancel', action: false }, { label: 'Load anyway', primary: true, action: true }]
);
return action;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function newSec() {
secCounter++;
return {
@ -639,47 +704,35 @@
// ─── Mutations ────────────────────────────────────────────────────────────────
function addSection() {
const s = newSec();
sections.push(s);
activeSec = s.id;
activeEl = null;
render();
triggerPreview();
const s = newSec(); sections.push(s);
activeSec = s.id; activeEl = null;
markDirty(); render(); triggerPreview();
}
function removeSection(id) {
sections = sections.filter(s => s.id !== id);
if (activeSec === id) { activeSec = sections.length ? sections[sections.length - 1].id : null; activeEl = null; }
render();
triggerPreview();
markDirty(); render(); triggerPreview();
}
function toggleSection(id) {
const s = getSec(id); if (!s) return;
s.open = !s.open;
activeSec = id;
render();
updateMeta();
s.open = !s.open; activeSec = id; render(); updateMeta();
}
function setSectionFlag(flag, val) {
if (!activeSec) return;
const s = getSec(activeSec); if (!s) return;
s.flags[flag] = val;
triggerPreview();
s.flags[flag] = val; markDirty(); triggerPreview();
}
function addElement(secId) {
const s = getSec(secId); if (!s) return;
const el = newEl(EL_TYPES[0]);
s.elements.push(el);
activeSec = secId;
activeEl = { secId, elId: el.id };
render();
triggerPreview();
const el = newEl(EL_TYPES[0]); s.elements.push(el);
activeSec = secId; activeEl = { secId, elId: el.id };
markDirty(); render(); triggerPreview();
}
function removeElement(secId, elId) {
const s = getSec(secId); if (!s) return;
s.elements = s.elements.filter(e => e.id !== elId);
if (activeEl && activeEl.secId === secId && activeEl.elId === elId) activeEl = null;
render();
triggerPreview();
markDirty(); render(); triggerPreview();
}
function selectElement(secId, elId) {
activeSec = secId;
@ -690,18 +743,155 @@
const el = getEl(secId, elId); if (!el) return;
const fresh = newEl(type);
el.type = type; el.fields = fresh.fields; el.flags = fresh.flags;
render();
triggerPreview();
markDirty(); render(); triggerPreview();
}
function setElField(secId, elId, key, val) {
const el = getEl(secId, elId); if (!el) return;
el.fields[key] = val;
triggerPreview();
el.fields[key] = val; markDirty(); triggerPreview();
}
function setElFlag(secId, elId, key, val) {
const el = getEl(secId, elId); if (!el) return;
el.flags[key] = val;
triggerPreview();
el.flags[key] = val; markDirty(); triggerPreview();
}
// ─── Load demo into editor state ─────────────────────────────────────────────
const DEMO_DEFS = [
{
label: 'Checkerboard', build() {
const s = newSec(); s.name = 'Checkerboard';
const el = newEl('Image2D');
s.elements.push(el);
s.flags = { drawFront: false, clearBuffer: false };
return [s];
}
},
{
label: 'Blink', build() {
const s = newSec(); s.name = 'Blink'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('Animation'); el.fields.updateInterval = 12;
s.elements.push(el); return [s];
}
},
{
label: 'Text', build() {
const s = newSec(); s.name = 'Text'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('ClippedText'); el.fields.text = 'Hello, World!'; el.fields.yOffset = 32;
s.elements.push(el); return [s];
}
},
{
label: 'Scrolltext', build() {
const s = newSec(); s.name = 'Scrolltext'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('HScrollText');
el.fields.text = 'MONO DISPLAY — scrolling ticker — 🚀 ';
el.fields.yOffset = 32; el.fields.scrollSpeed = 50;
el.flags.endless = true; el.flags.invertDirection = false;
s.elements.push(el); return [s];
}
},
{
label: 'Time', build() {
const s = newSec(); s.name = 'Time'; s.flags = { drawFront: true, clearBuffer: true };
const defs = [
{ flags: {}, yOffset: 8 }, { flags: { clock12h: true }, xOffset: 40, yOffset: 16 },
{ flags: { clock12h: true, showHours: true }, yOffset: 24 },
{ flags: { clock12h: true, showHours: false }, xOffset: 40, yOffset: 32 },
{ flags: { clock12h: true, showSeconds: true }, xOffset: 120, yOffset: 32 },
];
defs.forEach(d => {
const el = newEl('CurrentTime');
Object.assign(el.fields, { utcOffsetMinutes: 120, xOffset: d.xOffset || 0, yOffset: d.yOffset });
el.flags = d.flags; s.elements.push(el);
});
return [s];
}
},
];
// ─── Preview via MonoDisplayFile (same as original demos) ────────────────────
const DEMO_PREVIEWS = {
Checkerboard() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
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({
width: W, height: H,
elements_always: [{ type: ElementType.Image2D, pixels, width: W, height: H }]
}).toBuffer();
},
Blink() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.Animation, width: W, height: H, updateInterval: 12,
frames: [{ pixels: new Uint8Array(W * H).fill(1) }, { pixels: new Uint8Array(W * H).fill(0) }]
}]
}
}).toBuffer();
},
Text() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{ type: ElementType.ClippedText, text: 'Hello, World!', xOffset: 0, yOffset: 32, width: W, height: 16 }]
}
}).toBuffer();
},
Scrolltext() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.HScrollText, text: 'MONO DISPLAY — scrolling ticker — 🚀 ',
xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50, flags: { endless: true, invertDirection: false }
}]
}
}).toBuffer();
},
Time() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true }, elements: [
{ flags: {}, type: ElementType.CurrentTime, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: true }, type: ElementType.CurrentTime, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: false }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showSeconds: true }, type: ElementType.CurrentTime, xOffset: 120, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
]
}
}).toBuffer();
},
};
function initDemos() {
const wrap = document.getElementById('demo-btns');
let activeBtn = null;
DEMO_DEFS.forEach(d => {
const btn = document.createElement('button');
btn.className = 'tb-btn'; btn.textContent = d.label;
btn.onclick = async () => {
const ok = await guardDirty();
if (!ok) return;
// load into editor
sections = d.build();
activeSec = sections[0]?.id || null; activeEl = null;
currentFilename = d.label.toLowerCase();
document.getElementById('filename').textContent = currentFilename;
markClean(); render();
// drive preview with full fidelity build
if (!window._mdDriver)
window._mdDriver = new window.MonoDisplay.MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
window._mdDriver.load(() => Promise.resolve(DEMO_PREVIEWS[d.label]()));
if (activeBtn) activeBtn.style.color = '';
btn.style.color = '#33ff66'; activeBtn = btn;
};
wrap.appendChild(btn);
});
}
// ─── Render ───────────────────────────────────────────────────────────────────
@ -716,15 +906,14 @@
}
function renderSection(s) {
const isActive = activeSec === s.id;
const isOpen = s.open;
const isActive = activeSec === s.id, isOpen = s.open;
return `
<div class="sec-card${isActive ? ' active' : ''}${isOpen ? ' open' : ''}" id="sc-${s.id}">
<div class="sec-hdr${isActive ? ' active' : ''}" onclick="toggleSection('${s.id}')">
<span class="sec-arrow"></span>
<span class="sec-label">${esc(s.name)}</span>
<span class="sec-badge">${s.elements.length} el</span>
<button class="sec-del" title="Remove section" onclick="event.stopPropagation();removeSection('${s.id}')">×</button>
<button class="x-btn" title="Remove section" onclick="event.stopPropagation();removeSection('${s.id}')">×</button>
</div>
${isOpen ? `
<div class="el-list">
@ -743,7 +932,7 @@
<div class="el-item-hdr" onclick="selectElement('${secId}','${el.id}')">
<span class="el-type">${el.type}</span>
<span class="el-name">${elSummary(el)}</span>
<button class="el-del" title="Remove" onclick="event.stopPropagation();removeElement('${secId}','${el.id}')">×</button>
<button class="x-btn" title="Remove element" onclick="event.stopPropagation();removeElement('${secId}','${el.id}')">×</button>
</div>
${isActive ? `
<div class="el-fields">
@ -792,67 +981,46 @@
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false;
}
// ─── Preview (stub — replace with real MonoDisplayFile/Driver calls) ──────────
// ─── Preview ──────────────────────────────────────────────────────────────────
let previewTimer = null;
function triggerPreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(() => {
try { buildPreview(); } catch (e) { console.warn('preview error', e); }
}, 150);
previewTimer = setTimeout(() => { try { buildPreview(); } catch (e) { console.warn('preview', e); } }, 150);
}
function buildPreview() {
if (!window.MonoDisplay) return; // lib not loaded yet
if (!window.MonoDisplay) return;
const { MonoDisplayDriver, MonoDisplayFile, ElementType } = window.MonoDisplay;
// Grab or init driver
if (!window._mdDriver) {
if (!window._mdDriver)
window._mdDriver = new MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
}
// Build the active section only (or first section as fallback)
const s = activeSec ? getSec(activeSec) : sections[0];
if (!s || !s.elements.length) {
// Clear display
window._mdDriver.load(() => Promise.resolve(
new MonoDisplayFile({ elements_always: [] }).toBuffer()
));
window._mdDriver.load(() => Promise.resolve(new MonoDisplayFile({ elements_always: [] }).toBuffer()));
return;
}
const elDefs = s.elements.map(el => {
const base = { type: ElementType[el.type], ...el.fields, flags: el.flags };
return base;
}).filter(e => e.type != null);
const file = new MonoDisplayFile({
elements_always: { flags: s.flags, elements: elDefs },
});
window._mdDriver.load(() => Promise.resolve(file.toBuffer()));
const elDefs = s.elements.map(el => ({ type: ElementType[el.type], ...el.fields, flags: el.flags })).filter(e => e.type != null);
window._mdDriver.load(() => Promise.resolve(
new MonoDisplayFile({ elements_always: { flags: s.flags, elements: elDefs } }).toBuffer()
));
}
// ─── Load / Export ────────────────────────────────────────────────────────────
function loadBin(input) {
const file = input.files[0];
if (!file) return;
const file = input.files[0]; if (!file) return;
currentFilename = file.name;
document.getElementById('filename').textContent = file.name;
// TODO: parse binary → sections state when you expose a fromBuffer() API
// For now just feed it straight to the driver for preview
const reader = new FileReader();
reader.onload = e => {
const buf = new Uint8Array(e.target.result);
if (window._mdDriver) {
window._mdDriver.load(() => Promise.resolve(buf));
}
if (!window._mdDriver && window.MonoDisplay)
window._mdDriver = new window.MonoDisplay.MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
if (window._mdDriver) window._mdDriver.load(() => Promise.resolve(buf));
};
reader.readAsArrayBuffer(file);
input.value = '';
input.value = ''; markClean();
}
function exportBin() {
if (!window.MonoDisplay) { alert('MonoDisplay library not loaded.'); return; }
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
// Export all sections — stub: only exports active/first
const s = activeSec ? getSec(activeSec) : sections[0];
if (!s) { alert('Nothing to export.'); return; }
const elDefs = s.elements.map(el => ({ type: ElementType[el.type], ...el.fields, flags: el.flags })).filter(e => e.type != null);
@ -861,100 +1029,11 @@
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = currentFilename.replace(/\.bin$/, '') + '.bin';
a.click();
}
// ─── Demos ────────────────────────────────────────────────────────────────────
const DEMOS = [
{
label: 'Checkerboard', make() {
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({
width: W, height: H,
elements_always: [{ type: ElementType.Image2D, pixels, width: W, height: H }],
}).toBuffer();
}
},
{
label: 'Blink', make() {
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.Animation, width: W, height: H, updateInterval: 12,
frames: [{ pixels: new Uint8Array(W * H).fill(1) }, { pixels: new Uint8Array(W * H).fill(0) }],
}],
}
}).toBuffer();
}
},
{
label: 'Text', make() {
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{ type: ElementType.ClippedText, text: 'Hello, World!', xOffset: 0, yOffset: 32, width: W, height: 16 }],
}
}).toBuffer();
}
},
{
label: 'Scrolltext', make() {
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.HScrollText, text: 'MONO DISPLAY — scrolling ticker — 🚀 ',
xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50,
flags: { endless: true, invertDirection: false },
}],
}
}).toBuffer();
}
},
{
label: 'Time', make() {
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [
{ flags: {}, type: ElementType.CurrentTime, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: true }, type: ElementType.CurrentTime, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: false }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showSeconds: true }, type: ElementType.CurrentTime, xOffset: 120, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
],
}
}).toBuffer();
}
},
];
function initDemos() {
if (!window.MonoDisplay) return;
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
const wrap = document.getElementById('demo-btns');
let activeBtn = null;
DEMOS.forEach(d => {
const btn = document.createElement('button');
btn.className = 'tb-btn';
btn.textContent = d.label;
btn.onclick = () => {
if (activeBtn) activeBtn.style.color = '';
btn.style.color = '#33ff66';
activeBtn = btn;
if (!window._mdDriver)
window._mdDriver = new window.MonoDisplay.MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
window._mdDriver.load(() => Promise.resolve(d.make()));
};
wrap.appendChild(btn);
});
a.click(); markClean();
}
// ─── Init ─────────────────────────────────────────────────────────────────────
render();
// Defer demo init until lib is available
if (window.MonoDisplay) { initDemos(); }
else { const iv = setInterval(() => { if (window.MonoDisplay) { clearInterval(iv); initDemos(); } }, 100); }
</script>

Loading…
Cancel
Save