|
|
|
|
@ -406,7 +406,7 @@ |
|
|
|
|
flex-shrink: 0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* red × — section & element */ |
|
|
|
|
/* red × - section & element */ |
|
|
|
|
.x-btn { |
|
|
|
|
background: none; |
|
|
|
|
border: none; |
|
|
|
|
@ -681,11 +681,11 @@ |
|
|
|
|
<div id="sec-meta"> |
|
|
|
|
<div class="meta-row"> |
|
|
|
|
<label>Selected section</label> |
|
|
|
|
<span class="meta-val green" id="meta-name">—</span> |
|
|
|
|
<span class="meta-val green" id="meta-name">-</span> |
|
|
|
|
</div> |
|
|
|
|
<div class="meta-row"> |
|
|
|
|
<label>Elements</label> |
|
|
|
|
<span class="meta-val" id="meta-count">—</span> |
|
|
|
|
<span class="meta-val" id="meta-count">-</span> |
|
|
|
|
</div> |
|
|
|
|
<div class="meta-row"> |
|
|
|
|
<label>Section flags</label> |
|
|
|
|
@ -728,7 +728,7 @@ |
|
|
|
|
let currentFilename = 'untitled'; |
|
|
|
|
let isDirty = false; |
|
|
|
|
|
|
|
|
|
// ─── Element type definitions ──────────────────────────────────────────────── |
|
|
|
|
// --- Element type definitions ------------------------------------------------ |
|
|
|
|
const EL_TYPES = ['Image2D', 'Animation', 'ClippedText', 'HScrollText', 'CurrentTime']; |
|
|
|
|
const EL_FIELDS = { |
|
|
|
|
Image2D: [ |
|
|
|
|
@ -746,7 +746,7 @@ |
|
|
|
|
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 }, |
|
|
|
|
], |
|
|
|
|
HScrollText: [ |
|
|
|
|
{ k: 'text', l: 'Text', t: 'text', d: 'Scrolling text — ', full: true }, |
|
|
|
|
{ k: 'text', l: 'Text', t: 'text', d: 'Scrolling text - ', full: true }, |
|
|
|
|
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 32 }, |
|
|
|
|
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 }, |
|
|
|
|
{ k: 'scrollSpeed', l: 'Scroll speed', t: 'number', d: 50 }, |
|
|
|
|
@ -763,7 +763,7 @@ |
|
|
|
|
CurrentTime: [{ k: 'clock12h', l: '12h mode' }, { k: 'showHours', l: 'Show hours' }, { k: 'showSeconds', l: 'Show seconds' }], |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// ─── Confirm dialog ─────────────────────────────────────────────────────────── |
|
|
|
|
// --- Confirm dialog ----------------------------------------------------------- |
|
|
|
|
function confirm(msg, buttons) { |
|
|
|
|
// buttons: [{label,primary,action}] |
|
|
|
|
return new Promise(resolve => { |
|
|
|
|
@ -782,7 +782,7 @@ |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Dirty tracking ─────────────────────────────────────────────────────────── |
|
|
|
|
// --- Dirty tracking ----------------------------------------------------------- |
|
|
|
|
function markDirty() { |
|
|
|
|
isDirty = true; |
|
|
|
|
document.getElementById('filename').classList.add('dirty'); |
|
|
|
|
@ -800,7 +800,7 @@ |
|
|
|
|
return action; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Helpers ───────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Helpers ----------------------------------------------------------------- |
|
|
|
|
function newSec() { |
|
|
|
|
secCounter++; |
|
|
|
|
return { |
|
|
|
|
@ -819,7 +819,7 @@ |
|
|
|
|
function getSec(id) { return sections.find(s => s.id === id) } |
|
|
|
|
function getEl(secId, elId) { const s = getSec(secId); return s && s.elements.find(e => e.id === elId) } |
|
|
|
|
|
|
|
|
|
// ─── Mutations ──────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Mutations ---------------------------------------------------------------- |
|
|
|
|
function addSection() { |
|
|
|
|
const s = newSec(); sections.push(s); |
|
|
|
|
activeSec = s.id; activeEl = null; |
|
|
|
|
@ -871,7 +871,7 @@ |
|
|
|
|
el.flags[key] = val; markDirty(); triggerPreview(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Load demo into editor state ───────────────────────────────────────────── |
|
|
|
|
// --- Load demo into editor state --------------------------------------------- |
|
|
|
|
const DEMO_DEFS = [ |
|
|
|
|
{ |
|
|
|
|
label: 'Checkerboard', build() { |
|
|
|
|
@ -900,7 +900,7 @@ |
|
|
|
|
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.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]; |
|
|
|
|
@ -925,7 +925,7 @@ |
|
|
|
|
}, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
// ─── Preview via MonoDisplayFile (same as original demos) ──────────────────── |
|
|
|
|
// --- Preview via MonoDisplayFile (same as original demos) -------------------- |
|
|
|
|
const DEMO_PREVIEWS = { |
|
|
|
|
Checkerboard() { |
|
|
|
|
const { MonoDisplayFile, ElementType } = window.MonoDisplay; |
|
|
|
|
@ -963,7 +963,7 @@ |
|
|
|
|
elements_always: { |
|
|
|
|
flags: { drawFront: true, clearBuffer: true }, |
|
|
|
|
elements: [{ |
|
|
|
|
type: ElementType.HScrollText, text: 'MONO DISPLAY — scrolling ticker — 🚀 ', |
|
|
|
|
type: ElementType.HScrollText, text: 'MONO DISPLAY - scrolling ticker - 🚀 ', |
|
|
|
|
xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50, flags: { endless: true, invertDirection: false } |
|
|
|
|
}] |
|
|
|
|
} |
|
|
|
|
@ -1011,7 +1011,7 @@ |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Render ─────────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Render ------------------------------------------------------------------- |
|
|
|
|
function render() { |
|
|
|
|
updateMeta(); |
|
|
|
|
const wrap = document.getElementById('sections-wrap'); |
|
|
|
|
@ -1085,20 +1085,20 @@ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function elSummary(el) { |
|
|
|
|
if (el.fields.text) return esc(el.fields.text.slice(0, 24) + (el.fields.text.length > 24 ? '…' : '')); |
|
|
|
|
if (el.fields.text) return esc(el.fields.text.slice(0, 24) + (el.fields.text.length > 24 ? '...' : '')); |
|
|
|
|
return `${el.fields.xOffset ?? 0}, ${el.fields.yOffset ?? 0}`; |
|
|
|
|
} |
|
|
|
|
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') } |
|
|
|
|
|
|
|
|
|
function updateMeta() { |
|
|
|
|
const s = activeSec ? getSec(activeSec) : null; |
|
|
|
|
document.getElementById('meta-name').textContent = s ? s.name : '—'; |
|
|
|
|
document.getElementById('meta-count').textContent = s ? s.elements.length + ' element(s)' : '—'; |
|
|
|
|
document.getElementById('meta-name').textContent = s ? s.name : '-'; |
|
|
|
|
document.getElementById('meta-count').textContent = s ? s.elements.length + ' element(s)' : '-'; |
|
|
|
|
document.getElementById('flag-drawFront').checked = s ? !!s.flags.drawFront : false; |
|
|
|
|
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Preview ────────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Preview ------------------------------------------------------------------ |
|
|
|
|
let previewTimer = null; |
|
|
|
|
function triggerPreview() { |
|
|
|
|
clearTimeout(previewTimer); |
|
|
|
|
@ -1120,17 +1120,69 @@ |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Load / Export ──────────────────────────────────────────────────────────── |
|
|
|
|
// --- Load / Export ------------------------------------------------------------ |
|
|
|
|
function parsedToSections(parsedSecs) { |
|
|
|
|
const TYPE_MAP = { 1: 'Image2D', 2: 'Animation', 16: 'ClippedText', 17: 'HScrollText', 32: 'CurrentTime' }; |
|
|
|
|
return parsedSecs |
|
|
|
|
.filter(s => s.sectionType === 1 || s.sectionType === 2) |
|
|
|
|
.map(s => { |
|
|
|
|
secCounter++; |
|
|
|
|
const elements = (s.elements || []).map(el => { |
|
|
|
|
const typeName = TYPE_MAP[el.type]; |
|
|
|
|
if (!typeName) return null; |
|
|
|
|
elCounter++; |
|
|
|
|
let fields = {}, flags = {}; |
|
|
|
|
switch (typeName) { |
|
|
|
|
case 'Image2D': |
|
|
|
|
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.image?.width ?? W, height: el.image?.height ?? H }; |
|
|
|
|
break; |
|
|
|
|
case 'Animation': |
|
|
|
|
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, updateInterval: el.updateInterval }; |
|
|
|
|
break; |
|
|
|
|
case 'ClippedText': |
|
|
|
|
fields = { text: el.text, xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height }; |
|
|
|
|
break; |
|
|
|
|
case 'HScrollText': |
|
|
|
|
fields = { text: el.text, xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, scrollSpeed: el.scrollSpeed }; |
|
|
|
|
flags = { endless: !!el.flags?.endless, invertDirection: !!el.flags?.invertDirection }; |
|
|
|
|
break; |
|
|
|
|
case 'CurrentTime': |
|
|
|
|
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, utcOffsetMinutes: el.utcOffsetMinutes }; |
|
|
|
|
flags = { clock12h: !!el.flags?.clock12h, showHours: !!el.flags?.showHours, showSeconds: !!el.flags?.showSeconds }; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
return { id: 'e' + elCounter, type: typeName, fields, flags }; |
|
|
|
|
}).filter(Boolean); |
|
|
|
|
return { |
|
|
|
|
id: 's' + secCounter, |
|
|
|
|
name: 'Section ' + secCounter, |
|
|
|
|
open: true, |
|
|
|
|
flags: { drawFront: !!s.flags?.drawFront, clearBuffer: !!s.flags?.clearBuffer }, |
|
|
|
|
elements |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function loadBin(input) { |
|
|
|
|
const file = input.files[0]; if (!file) return; |
|
|
|
|
currentFilename = file.name; |
|
|
|
|
document.getElementById('filename').textContent = file.name; |
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
reader.onload = e => { |
|
|
|
|
const buf = new Uint8Array(e.target.result); |
|
|
|
|
const arrayBuf = e.target.result; |
|
|
|
|
const buf = new Uint8Array(arrayBuf); |
|
|
|
|
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)); |
|
|
|
|
try { |
|
|
|
|
const parsed = new window.MonoDisplay.MonoDisplayParser().parse(arrayBuf); |
|
|
|
|
sections = parsedToSections(parsed.sections); |
|
|
|
|
activeSec = sections.length ? sections[0].id : null; |
|
|
|
|
activeEl = null; |
|
|
|
|
render(); |
|
|
|
|
} catch (err) { |
|
|
|
|
console.warn('Could not parse sections from bin:', err); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
reader.readAsArrayBuffer(file); |
|
|
|
|
input.value = ''; markClean(); |
|
|
|
|
@ -1149,7 +1201,7 @@ |
|
|
|
|
a.click(); markClean(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ─── Theme ──────────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Theme -------------------------------------------------------------------- |
|
|
|
|
var _THEMES = ['dark', 'light', 'auto']; |
|
|
|
|
function _getTheme() { return document.documentElement.getAttribute('data-theme') || 'auto'; } |
|
|
|
|
function _applyTheme(t) { |
|
|
|
|
@ -1172,11 +1224,11 @@ |
|
|
|
|
var t = _getTheme(), dr = _detectDR(); |
|
|
|
|
var lbl = { dark: '☾ dark', light: '☀ light', auto: '⊙ auto' }; |
|
|
|
|
btn.textContent = (dr ? 'DR · ' : '') + (lbl[t] || lbl.auto); |
|
|
|
|
btn.title = dr ? 'DarkReader active — controlling theme' : ('Theme: ' + t + ' (click to cycle)'); |
|
|
|
|
btn.title = dr ? 'DarkReader active - controlling theme' : ('Theme: ' + t + ' (click to cycle)'); |
|
|
|
|
} |
|
|
|
|
window.addEventListener('DOMContentLoaded', _updateThemeBtn); |
|
|
|
|
|
|
|
|
|
// ─── Init ───────────────────────────────────────────────────────────────────── |
|
|
|
|
// --- Init --------------------------------------------------------------------- |
|
|
|
|
render(); |
|
|
|
|
if (window.MonoDisplay) { initDemos(); } |
|
|
|
|
else { const iv = setInterval(() => { if (window.MonoDisplay) { clearInterval(iv); initDemos(); } }, 100); } |
|
|
|
|
|