You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
963 lines
28 KiB
963 lines
28 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
<title>MonoDisplay Editor</title>
|
|
<style>
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0
|
|
}
|
|
|
|
body {
|
|
background: #111;
|
|
color: #ccc;
|
|
font-family: monospace;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
font-size: 12px
|
|
}
|
|
|
|
/* TOP BAR */
|
|
#topbar {
|
|
height: 36px;
|
|
background: #1a1a1a;
|
|
border-bottom: 1px solid #2a2a2a;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 0 12px;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
#topbar .app-name {
|
|
color: #555;
|
|
font-size: 11px;
|
|
letter-spacing: .12em;
|
|
text-transform: uppercase;
|
|
margin-right: 4px
|
|
}
|
|
|
|
.tb-btn {
|
|
background: #161616;
|
|
color: #888;
|
|
border: 1px solid #2d2d2d;
|
|
border-radius: 3px;
|
|
padding: 3px 10px;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
white-space: nowrap
|
|
}
|
|
|
|
.tb-btn:hover {
|
|
background: #222;
|
|
color: #bbb;
|
|
border-color: #444
|
|
}
|
|
|
|
.tb-btn svg {
|
|
width: 13px;
|
|
height: 13px;
|
|
stroke: currentColor;
|
|
fill: none;
|
|
stroke-width: 1.8;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
#file-input {
|
|
display: none
|
|
}
|
|
|
|
#filename {
|
|
color: #3a3a3a;
|
|
font-size: 11px;
|
|
margin-left: 2px;
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap
|
|
}
|
|
|
|
.tb-sep {
|
|
width: 1px;
|
|
height: 18px;
|
|
background: #2a2a2a
|
|
}
|
|
|
|
/* MAIN */
|
|
#main {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0
|
|
}
|
|
|
|
/* LEFT */
|
|
#left {
|
|
width: 48%;
|
|
border-right: 1px solid #1e1e1e;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 14px;
|
|
gap: 10px;
|
|
overflow-y: auto;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
#display-box {
|
|
width: 100%;
|
|
aspect-ratio: 2/1;
|
|
background: #000;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 3px;
|
|
overflow: hidden
|
|
}
|
|
|
|
#canvas_root {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
image-rendering: pixelated;
|
|
image-rendering: crisp-edges
|
|
}
|
|
|
|
.pv-label {
|
|
font-size: 10px;
|
|
color: #333;
|
|
letter-spacing: .1em;
|
|
text-transform: uppercase
|
|
}
|
|
|
|
/* SECTION META */
|
|
#sec-meta {
|
|
background: #141414;
|
|
border: 1px solid #1e1e1e;
|
|
border-radius: 3px;
|
|
padding: 8px 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11px
|
|
}
|
|
|
|
.meta-row label {
|
|
color: #444;
|
|
min-width: 90px
|
|
}
|
|
|
|
.meta-val {
|
|
color: #666
|
|
}
|
|
|
|
.green {
|
|
color: #33ff66
|
|
}
|
|
|
|
.flag-check {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: #555;
|
|
cursor: pointer
|
|
}
|
|
|
|
.flag-check input {
|
|
accent-color: #33ff66;
|
|
cursor: pointer
|
|
}
|
|
|
|
/* RIGHT */
|
|
#right {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden
|
|
}
|
|
|
|
#right-hdr {
|
|
height: 36px;
|
|
background: #141414;
|
|
border-bottom: 1px solid #1e1e1e;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 10px;
|
|
gap: 6px;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
#right-hdr .rh-title {
|
|
flex: 1;
|
|
color: #444;
|
|
font-size: 11px;
|
|
letter-spacing: .08em;
|
|
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;
|
|
padding: 8px
|
|
}
|
|
|
|
.empty-state {
|
|
color: #2e2e2e;
|
|
font-size: 11px;
|
|
padding: 28px 16px;
|
|
text-align: center;
|
|
line-height: 2
|
|
}
|
|
|
|
/* SECTION CARD */
|
|
.sec-card {
|
|
border: 1px solid #222;
|
|
border-radius: 3px;
|
|
margin-bottom: 5px;
|
|
overflow: hidden
|
|
}
|
|
|
|
.sec-card.active {
|
|
border-color: #2b3d2b
|
|
}
|
|
|
|
.sec-hdr {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 6px 8px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
background: #161616
|
|
}
|
|
|
|
.sec-hdr:hover {
|
|
background: #1b1b1b
|
|
}
|
|
|
|
.sec-card.active .sec-hdr {
|
|
background: #182018
|
|
}
|
|
|
|
.sec-arrow {
|
|
color: #333;
|
|
font-size: 13px;
|
|
line-height: 1;
|
|
transition: transform .12s;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
.sec-card.open .sec-arrow {
|
|
transform: rotate(90deg)
|
|
}
|
|
|
|
.sec-label {
|
|
flex: 1;
|
|
color: #777;
|
|
font-size: 11px
|
|
}
|
|
|
|
.sec-badge {
|
|
font-size: 10px;
|
|
color: #33ff66;
|
|
border: 1px solid #2b3d2b;
|
|
background: #141e14;
|
|
border-radius: 2px;
|
|
padding: 1px 6px;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
.sec-del {
|
|
background: none;
|
|
border: none;
|
|
color: #2a2a2a;
|
|
cursor: pointer;
|
|
font-size: 15px;
|
|
line-height: 1;
|
|
padding: 1px 4px;
|
|
border-radius: 2px;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
.sec-del:hover {
|
|
color: #cc3333;
|
|
background: #1e1010
|
|
}
|
|
|
|
/* ELEMENT LIST */
|
|
.el-list {
|
|
background: #0f0f0f;
|
|
border-top: 1px solid #1c1c1c;
|
|
padding: 6px
|
|
}
|
|
|
|
.el-item {
|
|
border: 1px solid #1e1e1e;
|
|
border-radius: 3px;
|
|
background: #141414;
|
|
margin-bottom: 4px;
|
|
overflow: hidden
|
|
}
|
|
|
|
.el-item.active {
|
|
border-color: #33ff66
|
|
}
|
|
|
|
.el-item-hdr {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 8px;
|
|
cursor: pointer
|
|
}
|
|
|
|
.el-item-hdr:hover {
|
|
background: #1a1a1a
|
|
}
|
|
|
|
.el-type {
|
|
font-size: 10px;
|
|
color: #666;
|
|
border: 1px solid #252525;
|
|
border-radius: 2px;
|
|
padding: 1px 6px;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
.el-item.active .el-type {
|
|
color: #33ff66;
|
|
border-color: #2b3d2b
|
|
}
|
|
|
|
.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
|
|
}
|
|
|
|
/* FIELDS */
|
|
.el-fields {
|
|
padding: 6px 8px 8px;
|
|
border-top: 1px solid #1a1a1a;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px
|
|
}
|
|
|
|
.fields-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 4px 10px
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px
|
|
}
|
|
|
|
.field.full {
|
|
grid-column: 1/-1
|
|
}
|
|
|
|
.field>label {
|
|
font-size: 10px;
|
|
color: #444
|
|
}
|
|
|
|
.field input[type=text],
|
|
.field input[type=number],
|
|
.field select,
|
|
.field textarea {
|
|
background: #0a0a0a;
|
|
color: #999;
|
|
border: 1px solid #222;
|
|
border-radius: 2px;
|
|
padding: 3px 6px;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
width: 100%;
|
|
outline: none
|
|
}
|
|
|
|
.field input:focus,
|
|
.field select:focus,
|
|
.field textarea:focus {
|
|
border-color: #33ff66;
|
|
color: #ccc
|
|
}
|
|
|
|
.field textarea {
|
|
resize: vertical;
|
|
min-height: 44px;
|
|
line-height: 1.4
|
|
}
|
|
|
|
.field select option {
|
|
background: #1a1a1a;
|
|
color: #aaa
|
|
}
|
|
|
|
.flags-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
padding: 2px 0
|
|
}
|
|
|
|
.add-el {
|
|
background: transparent;
|
|
color: #333;
|
|
border: 1px dashed #222;
|
|
border-radius: 3px;
|
|
padding: 5px 8px;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
text-align: center;
|
|
margin-top: 2px
|
|
}
|
|
|
|
.add-el:hover {
|
|
color: #888;
|
|
border-color: #444
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="topbar">
|
|
<span class="app-name">MonoDisplay</span>
|
|
<div class="tb-sep"></div>
|
|
<button class="tb-btn" onclick="document.getElementById('file-input').click()">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
<polyline points="17 8 12 3 7 8" />
|
|
<line x1="12" y1="3" x2="12" y2="15" />
|
|
</svg>
|
|
Load .bin
|
|
</button>
|
|
<input type="file" id="file-input" accept=".bin" onchange="loadBin(this)" />
|
|
<button class="tb-btn" onclick="exportBin()">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
<polyline points="7 10 12 15 17 10" />
|
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
</svg>
|
|
Export .bin
|
|
</button>
|
|
<div class="tb-sep"></div>
|
|
<button class="tb-btn" onclick="addSection()">
|
|
<svg viewBox="0 0 24 24">
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
</svg>
|
|
Add section
|
|
</button>
|
|
<span id="filename">untitled</span>
|
|
</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>
|
|
<div id="sec-meta">
|
|
<div class="meta-row">
|
|
<label>Selected section</label>
|
|
<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>
|
|
</div>
|
|
<div class="meta-row">
|
|
<label>Section flags</label>
|
|
<label class="flag-check">
|
|
<input type="checkbox" id="flag-drawFront" onchange="setSectionFlag('drawFront',this.checked)" />
|
|
drawFront
|
|
</label>
|
|
<label class="flag-check" style="margin-left:8px">
|
|
<input type="checkbox" id="flag-clearBuffer" onchange="setSectionFlag('clearBuffer',this.checked)" />
|
|
clearBuffer
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: section + element editor -->
|
|
<div id="right">
|
|
<div id="right-hdr">
|
|
<span class="rh-title">sections</span>
|
|
</div>
|
|
<div id="sections-wrap">
|
|
<div class="empty-state">No sections yet.<br />Use <b>Add section</b> to get started.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assumes mono-display.js is loaded in the real page -->
|
|
<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 secCounter = 0, elCounter = 0;
|
|
let currentFilename = 'untitled';
|
|
|
|
// ─── 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 },
|
|
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: H },
|
|
],
|
|
Animation: [
|
|
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 0 },
|
|
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: H },
|
|
{ k: 'updateInterval', l: 'Update interval', t: 'number', d: 12 },
|
|
],
|
|
ClippedText: [
|
|
{ k: 'text', l: 'Text', t: 'text', d: 'Hello, World!', 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 },
|
|
],
|
|
HScrollText: [
|
|
{ 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 },
|
|
],
|
|
CurrentTime: [
|
|
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 8 },
|
|
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 },
|
|
{ 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 ──────────────────────────────────────────────────────────────────
|
|
function newSec() {
|
|
secCounter++;
|
|
return {
|
|
id: 's' + secCounter, name: 'Section ' + secCounter, open: true,
|
|
flags: { drawFront: true, clearBuffer: true }, elements: []
|
|
};
|
|
}
|
|
function newEl(type) {
|
|
elCounter++;
|
|
const fields = {};
|
|
(EL_FIELDS[type] || []).forEach(f => fields[f.k] = f.d);
|
|
const flags = {};
|
|
(EL_FLAGS[type] || []).forEach(f => flags[f.k] = false);
|
|
return { id: 'e' + elCounter, type, fields, flags };
|
|
}
|
|
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 ────────────────────────────────────────────────────────────────
|
|
function addSection() {
|
|
const s = newSec();
|
|
sections.push(s);
|
|
activeSec = s.id;
|
|
activeEl = null;
|
|
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();
|
|
}
|
|
function toggleSection(id) {
|
|
const s = getSec(id); if (!s) return;
|
|
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();
|
|
}
|
|
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();
|
|
}
|
|
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();
|
|
}
|
|
function selectElement(secId, elId) {
|
|
activeSec = secId;
|
|
activeEl = activeEl && activeEl.secId === secId && activeEl.elId === elId ? null : { secId, elId };
|
|
render();
|
|
}
|
|
function changeElType(secId, elId, type) {
|
|
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();
|
|
}
|
|
function setElField(secId, elId, key, val) {
|
|
const el = getEl(secId, elId); if (!el) return;
|
|
el.fields[key] = val;
|
|
triggerPreview();
|
|
}
|
|
function setElFlag(secId, elId, key, val) {
|
|
const el = getEl(secId, elId); if (!el) return;
|
|
el.flags[key] = val;
|
|
triggerPreview();
|
|
}
|
|
|
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
function render() {
|
|
updateMeta();
|
|
const wrap = document.getElementById('sections-wrap');
|
|
if (!sections.length) {
|
|
wrap.innerHTML = '<div class="empty-state">No sections yet.<br/>Use <b>Add section</b> to get started.</div>';
|
|
return;
|
|
}
|
|
wrap.innerHTML = sections.map(s => renderSection(s)).join('');
|
|
}
|
|
|
|
function renderSection(s) {
|
|
const isActive = activeSec === s.id;
|
|
const 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>
|
|
</div>
|
|
${isOpen ? `
|
|
<div class="el-list">
|
|
${s.elements.map(el => renderElement(s.id, el)).join('')}
|
|
<button class="add-el" onclick="addElement('${s.id}')">+ add element</button>
|
|
</div>`: ''}
|
|
</div>`;
|
|
}
|
|
|
|
function renderElement(secId, el) {
|
|
const isActive = activeEl && activeEl.secId === secId && activeEl.elId === el.id;
|
|
const fields = EL_FIELDS[el.type] || [];
|
|
const flags = EL_FLAGS[el.type] || [];
|
|
return `
|
|
<div class="el-item${isActive ? ' active' : ''}" id="eli-${el.id}">
|
|
<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>
|
|
</div>
|
|
${isActive ? `
|
|
<div class="el-fields">
|
|
<div class="field full">
|
|
<label>Type</label>
|
|
<select onchange="changeElType('${secId}','${el.id}',this.value)">
|
|
${EL_TYPES.map(t => `<option value="${t}"${t === el.type ? ' selected' : ''}>${t}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="fields-grid">
|
|
${fields.map(f => `
|
|
<div class="field${f.full ? ' full' : ''}">
|
|
<label>${f.l}</label>
|
|
${f.t === 'text'
|
|
? `<textarea onchange="setElField('${secId}','${el.id}','${f.k}',this.value)">${esc(el.fields[f.k] ?? f.d)}</textarea>`
|
|
: `<input type="number" value="${el.fields[f.k] ?? f.d}" onchange="setElField('${secId}','${el.id}','${f.k}',+this.value)"/>`
|
|
}
|
|
</div>`).join('')}
|
|
</div>
|
|
${flags.length ? `
|
|
<div class="field full">
|
|
<label>Flags</label>
|
|
<div class="flags-row">
|
|
${flags.map(f => `
|
|
<label class="flag-check">
|
|
<input type="checkbox" ${el.flags[f.k] ? 'checked' : ''} onchange="setElFlag('${secId}','${el.id}','${f.k}',this.checked)"/>
|
|
${f.l}
|
|
</label>`).join('')}
|
|
</div>
|
|
</div>`: ''}
|
|
</div>`: ''}
|
|
</div>`;
|
|
}
|
|
|
|
function elSummary(el) {
|
|
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('flag-drawFront').checked = s ? !!s.flags.drawFront : false;
|
|
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false;
|
|
}
|
|
|
|
// ─── Preview (stub — replace with real MonoDisplayFile/Driver calls) ──────────
|
|
let previewTimer = null;
|
|
function triggerPreview() {
|
|
clearTimeout(previewTimer);
|
|
previewTimer = setTimeout(() => {
|
|
try { buildPreview(); } catch (e) { console.warn('preview error', e); }
|
|
}, 150);
|
|
}
|
|
function buildPreview() {
|
|
if (!window.MonoDisplay) return; // lib not loaded yet
|
|
const { MonoDisplayDriver, MonoDisplayFile, ElementType } = window.MonoDisplay;
|
|
|
|
// Grab or init driver
|
|
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()
|
|
));
|
|
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()));
|
|
}
|
|
|
|
// ─── Load / Export ────────────────────────────────────────────────────────────
|
|
function loadBin(input) {
|
|
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));
|
|
}
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
input.value = '';
|
|
}
|
|
|
|
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);
|
|
const buf = new MonoDisplayFile({ elements_always: { flags: s.flags, elements: elDefs } }).toBuffer();
|
|
const blob = new Blob([buf], { type: 'application/octet-stream' });
|
|
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);
|
|
});
|
|
}
|
|
|
|
// ─── 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>
|
|
</body>
|
|
|
|
</html> |