|
|
|
@ -25,7 +25,6 @@ |
|
|
|
font-size: 12px |
|
|
|
font-size: 12px |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* TOP BAR */ |
|
|
|
|
|
|
|
#topbar { |
|
|
|
#topbar { |
|
|
|
height: 36px; |
|
|
|
height: 36px; |
|
|
|
background: #1a1a1a; |
|
|
|
background: #1a1a1a; |
|
|
|
@ -97,14 +96,16 @@ |
|
|
|
background: #2a2a2a |
|
|
|
background: #2a2a2a |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* MAIN */ |
|
|
|
.dirty { |
|
|
|
|
|
|
|
color: #886622 !important |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#main { |
|
|
|
#main { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
display: flex; |
|
|
|
min-height: 0 |
|
|
|
min-height: 0 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* LEFT */ |
|
|
|
|
|
|
|
#left { |
|
|
|
#left { |
|
|
|
width: 48%; |
|
|
|
width: 48%; |
|
|
|
border-right: 1px solid #1e1e1e; |
|
|
|
border-right: 1px solid #1e1e1e; |
|
|
|
@ -140,7 +141,13 @@ |
|
|
|
text-transform: uppercase |
|
|
|
text-transform: uppercase |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* SECTION META */ |
|
|
|
#demo-btns { |
|
|
|
|
|
|
|
display: flex; |
|
|
|
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
gap: 5px; |
|
|
|
|
|
|
|
margin-top: 6px |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#sec-meta { |
|
|
|
#sec-meta { |
|
|
|
background: #141414; |
|
|
|
background: #141414; |
|
|
|
border: 1px solid #1e1e1e; |
|
|
|
border: 1px solid #1e1e1e; |
|
|
|
@ -185,7 +192,6 @@ |
|
|
|
cursor: pointer |
|
|
|
cursor: pointer |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* RIGHT */ |
|
|
|
|
|
|
|
#right { |
|
|
|
#right { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
display: flex; |
|
|
|
@ -213,38 +219,6 @@ |
|
|
|
text-transform: uppercase |
|
|
|
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 { |
|
|
|
#sections-wrap { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
overflow-y: auto; |
|
|
|
overflow-y: auto; |
|
|
|
@ -259,7 +233,7 @@ |
|
|
|
line-height: 2 |
|
|
|
line-height: 2 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* SECTION CARD */ |
|
|
|
/* section card */ |
|
|
|
.sec-card { |
|
|
|
.sec-card { |
|
|
|
border: 1px solid #222; |
|
|
|
border: 1px solid #222; |
|
|
|
border-radius: 3px; |
|
|
|
border-radius: 3px; |
|
|
|
@ -278,7 +252,8 @@ |
|
|
|
padding: 6px 8px; |
|
|
|
padding: 6px 8px; |
|
|
|
cursor: pointer; |
|
|
|
cursor: pointer; |
|
|
|
user-select: none; |
|
|
|
user-select: none; |
|
|
|
background: #161616 |
|
|
|
background: #161616; |
|
|
|
|
|
|
|
position: relative |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.sec-hdr:hover { |
|
|
|
.sec-hdr:hover { |
|
|
|
@ -317,24 +292,26 @@ |
|
|
|
flex-shrink: 0 |
|
|
|
flex-shrink: 0 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.sec-del { |
|
|
|
/* red × — section & element */ |
|
|
|
|
|
|
|
.x-btn { |
|
|
|
background: none; |
|
|
|
background: none; |
|
|
|
border: none; |
|
|
|
border: none; |
|
|
|
color: #2a2a2a; |
|
|
|
color: #2a2a2a; |
|
|
|
cursor: pointer; |
|
|
|
cursor: pointer; |
|
|
|
font-size: 15px; |
|
|
|
font-size: 15px; |
|
|
|
line-height: 1; |
|
|
|
line-height: 1; |
|
|
|
padding: 1px 4px; |
|
|
|
padding: 2px 5px; |
|
|
|
border-radius: 2px; |
|
|
|
border-radius: 2px; |
|
|
|
flex-shrink: 0 |
|
|
|
flex-shrink: 0; |
|
|
|
|
|
|
|
transition: color .1s, background .1s |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.sec-del:hover { |
|
|
|
.x-btn:hover { |
|
|
|
color: #cc3333; |
|
|
|
color: #ff4444; |
|
|
|
background: #1e1010 |
|
|
|
background: #1e1010 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* ELEMENT LIST */ |
|
|
|
/* element */ |
|
|
|
.el-list { |
|
|
|
.el-list { |
|
|
|
background: #0f0f0f; |
|
|
|
background: #0f0f0f; |
|
|
|
border-top: 1px solid #1c1c1c; |
|
|
|
border-top: 1px solid #1c1c1c; |
|
|
|
@ -382,25 +359,13 @@ |
|
|
|
.el-name { |
|
|
|
.el-name { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
color: #555; |
|
|
|
color: #555; |
|
|
|
font-size: 11px |
|
|
|
font-size: 11px; |
|
|
|
} |
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
text-overflow: ellipsis; |
|
|
|
.el-del { |
|
|
|
white-space: nowrap; |
|
|
|
background: none; |
|
|
|
max-width: 120px |
|
|
|
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 { |
|
|
|
.el-fields { |
|
|
|
padding: 6px 8px 8px; |
|
|
|
padding: 6px 8px 8px; |
|
|
|
border-top: 1px solid #1a1a1a; |
|
|
|
border-top: 1px solid #1a1a1a; |
|
|
|
@ -488,6 +453,70 @@ |
|
|
|
color: #888; |
|
|
|
color: #888; |
|
|
|
border-color: #444 |
|
|
|
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> |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
</head> |
|
|
|
|
|
|
|
|
|
|
|
@ -525,16 +554,13 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div id="main"> |
|
|
|
<div id="main"> |
|
|
|
<!-- LEFT: preview + meta --> |
|
|
|
|
|
|
|
<div id="left"> |
|
|
|
<div id="left"> |
|
|
|
<span class="pv-label">preview · 160 × 80</span> |
|
|
|
<span class="pv-label">preview · 160 × 80</span> |
|
|
|
<div id="display-box"> |
|
|
|
<div id="display-box"> |
|
|
|
<canvas id="canvas_root" width="160" height="80"></canvas> |
|
|
|
<canvas id="canvas_root" width="160" height="80"></canvas> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div id="demos"> |
|
|
|
|
|
|
|
<span class="pv-label">demos</span> |
|
|
|
<span class="pv-label">demos</span> |
|
|
|
<div id="demo-btns" style="display:flex;flex-wrap:wrap;gap:5px;margin-top:6px"></div> |
|
|
|
<div id="demo-btns"></div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div id="sec-meta"> |
|
|
|
<div id="sec-meta"> |
|
|
|
<div class="meta-row"> |
|
|
|
<div class="meta-row"> |
|
|
|
<label>Selected section</label> |
|
|
|
<label>Selected section</label> |
|
|
|
@ -558,7 +584,6 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- RIGHT: section + element editor --> |
|
|
|
|
|
|
|
<div id="right"> |
|
|
|
<div id="right"> |
|
|
|
<div id="right-hdr"> |
|
|
|
<div id="right-hdr"> |
|
|
|
<span class="rh-title">sections</span> |
|
|
|
<span class="rh-title">sections</span> |
|
|
|
@ -569,21 +594,25 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- Assumes mono-display.js is loaded in the real page --> |
|
|
|
<!-- confirm dialog --> |
|
|
|
<script src="./public/mono-display.js"></script> |
|
|
|
<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> |
|
|
|
<script> |
|
|
|
// ─── State ─────────────────────────────────────────────────────────────────── |
|
|
|
|
|
|
|
const W = 160, H = 80; |
|
|
|
const W = 160, H = 80; |
|
|
|
let sections = []; // [{id,name,open,flags:{drawFront,clearBuffer},elements:[…]}] |
|
|
|
let sections = []; |
|
|
|
let activeSec = null; // id |
|
|
|
let activeSec = null, activeEl = null; |
|
|
|
let activeEl = null; // {secId,elId} |
|
|
|
|
|
|
|
let secCounter = 0, elCounter = 0; |
|
|
|
let secCounter = 0, elCounter = 0; |
|
|
|
let currentFilename = 'untitled'; |
|
|
|
let currentFilename = 'untitled'; |
|
|
|
|
|
|
|
let isDirty = false; |
|
|
|
|
|
|
|
|
|
|
|
// ─── Element type definitions ───────────────────────────────────────────────── |
|
|
|
// ─── Element type definitions ──────────────────────────────────────────────── |
|
|
|
const EL_TYPES = ['Image2D', 'Animation', 'ClippedText', 'HScrollText', 'CurrentTime']; |
|
|
|
const EL_TYPES = ['Image2D', 'Animation', 'ClippedText', 'HScrollText', 'CurrentTime']; |
|
|
|
|
|
|
|
|
|
|
|
const EL_FIELDS = { |
|
|
|
const EL_FIELDS = { |
|
|
|
Image2D: [ |
|
|
|
Image2D: [ |
|
|
|
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 0 }, |
|
|
|
{ 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 }, |
|
|
|
{ k: 'utcOffsetMinutes', l: 'UTC offset (min)', t: 'number', d: 0 }, |
|
|
|
], |
|
|
|
], |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const EL_FLAGS = { |
|
|
|
const EL_FLAGS = { |
|
|
|
Image2D: [], Animation: [], ClippedText: [], |
|
|
|
Image2D: [], Animation: [], ClippedText: [], |
|
|
|
HScrollText: [{ k: 'endless', l: 'Endless' }, { k: 'invertDirection', l: 'Invert direction' }], |
|
|
|
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' }], |
|
|
|
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() { |
|
|
|
function newSec() { |
|
|
|
secCounter++; |
|
|
|
secCounter++; |
|
|
|
return { |
|
|
|
return { |
|
|
|
@ -639,47 +704,35 @@ |
|
|
|
|
|
|
|
|
|
|
|
// ─── Mutations ──────────────────────────────────────────────────────────────── |
|
|
|
// ─── Mutations ──────────────────────────────────────────────────────────────── |
|
|
|
function addSection() { |
|
|
|
function addSection() { |
|
|
|
const s = newSec(); |
|
|
|
const s = newSec(); sections.push(s); |
|
|
|
sections.push(s); |
|
|
|
activeSec = s.id; activeEl = null; |
|
|
|
activeSec = s.id; |
|
|
|
markDirty(); render(); triggerPreview(); |
|
|
|
activeEl = null; |
|
|
|
|
|
|
|
render(); |
|
|
|
|
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function removeSection(id) { |
|
|
|
function removeSection(id) { |
|
|
|
sections = sections.filter(s => s.id !== id); |
|
|
|
sections = sections.filter(s => s.id !== id); |
|
|
|
if (activeSec === id) { activeSec = sections.length ? sections[sections.length - 1].id : null; activeEl = null; } |
|
|
|
if (activeSec === id) { activeSec = sections.length ? sections[sections.length - 1].id : null; activeEl = null; } |
|
|
|
render(); |
|
|
|
markDirty(); render(); triggerPreview(); |
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function toggleSection(id) { |
|
|
|
function toggleSection(id) { |
|
|
|
const s = getSec(id); if (!s) return; |
|
|
|
const s = getSec(id); if (!s) return; |
|
|
|
s.open = !s.open; |
|
|
|
s.open = !s.open; activeSec = id; render(); updateMeta(); |
|
|
|
activeSec = id; |
|
|
|
|
|
|
|
render(); |
|
|
|
|
|
|
|
updateMeta(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function setSectionFlag(flag, val) { |
|
|
|
function setSectionFlag(flag, val) { |
|
|
|
if (!activeSec) return; |
|
|
|
if (!activeSec) return; |
|
|
|
const s = getSec(activeSec); if (!s) return; |
|
|
|
const s = getSec(activeSec); if (!s) return; |
|
|
|
s.flags[flag] = val; |
|
|
|
s.flags[flag] = val; markDirty(); triggerPreview(); |
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function addElement(secId) { |
|
|
|
function addElement(secId) { |
|
|
|
const s = getSec(secId); if (!s) return; |
|
|
|
const s = getSec(secId); if (!s) return; |
|
|
|
const el = newEl(EL_TYPES[0]); |
|
|
|
const el = newEl(EL_TYPES[0]); s.elements.push(el); |
|
|
|
s.elements.push(el); |
|
|
|
activeSec = secId; activeEl = { secId, elId: el.id }; |
|
|
|
activeSec = secId; |
|
|
|
markDirty(); render(); triggerPreview(); |
|
|
|
activeEl = { secId, elId: el.id }; |
|
|
|
|
|
|
|
render(); |
|
|
|
|
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function removeElement(secId, elId) { |
|
|
|
function removeElement(secId, elId) { |
|
|
|
const s = getSec(secId); if (!s) return; |
|
|
|
const s = getSec(secId); if (!s) return; |
|
|
|
s.elements = s.elements.filter(e => e.id !== elId); |
|
|
|
s.elements = s.elements.filter(e => e.id !== elId); |
|
|
|
if (activeEl && activeEl.secId === secId && activeEl.elId === elId) activeEl = null; |
|
|
|
if (activeEl && activeEl.secId === secId && activeEl.elId === elId) activeEl = null; |
|
|
|
render(); |
|
|
|
markDirty(); render(); triggerPreview(); |
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function selectElement(secId, elId) { |
|
|
|
function selectElement(secId, elId) { |
|
|
|
activeSec = secId; |
|
|
|
activeSec = secId; |
|
|
|
@ -690,18 +743,155 @@ |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
const fresh = newEl(type); |
|
|
|
const fresh = newEl(type); |
|
|
|
el.type = type; el.fields = fresh.fields; el.flags = fresh.flags; |
|
|
|
el.type = type; el.fields = fresh.fields; el.flags = fresh.flags; |
|
|
|
render(); |
|
|
|
markDirty(); render(); triggerPreview(); |
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function setElField(secId, elId, key, val) { |
|
|
|
function setElField(secId, elId, key, val) { |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
el.fields[key] = val; |
|
|
|
el.fields[key] = val; markDirty(); triggerPreview(); |
|
|
|
triggerPreview(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function setElFlag(secId, elId, key, val) { |
|
|
|
function setElFlag(secId, elId, key, val) { |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
const el = getEl(secId, elId); if (!el) return; |
|
|
|
el.flags[key] = val; |
|
|
|
el.flags[key] = val; markDirty(); triggerPreview(); |
|
|
|
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 ─────────────────────────────────────────────────────────────────── |
|
|
|
// ─── Render ─────────────────────────────────────────────────────────────────── |
|
|
|
@ -716,15 +906,14 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function renderSection(s) { |
|
|
|
function renderSection(s) { |
|
|
|
const isActive = activeSec === s.id; |
|
|
|
const isActive = activeSec === s.id, isOpen = s.open; |
|
|
|
const isOpen = s.open; |
|
|
|
|
|
|
|
return ` |
|
|
|
return ` |
|
|
|
<div class="sec-card${isActive ? ' active' : ''}${isOpen ? ' open' : ''}" id="sc-${s.id}"> |
|
|
|
<div class="sec-card${isActive ? ' active' : ''}${isOpen ? ' open' : ''}" id="sc-${s.id}"> |
|
|
|
<div class="sec-hdr${isActive ? ' active' : ''}" onclick="toggleSection('${s.id}')"> |
|
|
|
<div class="sec-hdr${isActive ? ' active' : ''}" onclick="toggleSection('${s.id}')"> |
|
|
|
<span class="sec-arrow">▶</span> |
|
|
|
<span class="sec-arrow">▶</span> |
|
|
|
<span class="sec-label">${esc(s.name)}</span> |
|
|
|
<span class="sec-label">${esc(s.name)}</span> |
|
|
|
<span class="sec-badge">${s.elements.length} el</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> |
|
|
|
</div> |
|
|
|
${isOpen ? ` |
|
|
|
${isOpen ? ` |
|
|
|
<div class="el-list"> |
|
|
|
<div class="el-list"> |
|
|
|
@ -743,7 +932,7 @@ |
|
|
|
<div class="el-item-hdr" onclick="selectElement('${secId}','${el.id}')"> |
|
|
|
<div class="el-item-hdr" onclick="selectElement('${secId}','${el.id}')"> |
|
|
|
<span class="el-type">${el.type}</span> |
|
|
|
<span class="el-type">${el.type}</span> |
|
|
|
<span class="el-name">${elSummary(el)}</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> |
|
|
|
</div> |
|
|
|
${isActive ? ` |
|
|
|
${isActive ? ` |
|
|
|
<div class="el-fields"> |
|
|
|
<div class="el-fields"> |
|
|
|
@ -792,67 +981,46 @@ |
|
|
|
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false; |
|
|
|
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ─── Preview (stub — replace with real MonoDisplayFile/Driver calls) ────────── |
|
|
|
// ─── Preview ────────────────────────────────────────────────────────────────── |
|
|
|
let previewTimer = null; |
|
|
|
let previewTimer = null; |
|
|
|
function triggerPreview() { |
|
|
|
function triggerPreview() { |
|
|
|
clearTimeout(previewTimer); |
|
|
|
clearTimeout(previewTimer); |
|
|
|
previewTimer = setTimeout(() => { |
|
|
|
previewTimer = setTimeout(() => { try { buildPreview(); } catch (e) { console.warn('preview', e); } }, 150); |
|
|
|
try { buildPreview(); } catch (e) { console.warn('preview error', e); } |
|
|
|
|
|
|
|
}, 150); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
function buildPreview() { |
|
|
|
function buildPreview() { |
|
|
|
if (!window.MonoDisplay) return; // lib not loaded yet |
|
|
|
if (!window.MonoDisplay) return; |
|
|
|
const { MonoDisplayDriver, MonoDisplayFile, ElementType } = window.MonoDisplay; |
|
|
|
const { MonoDisplayDriver, MonoDisplayFile, ElementType } = window.MonoDisplay; |
|
|
|
|
|
|
|
if (!window._mdDriver) |
|
|
|
// Grab or init driver |
|
|
|
|
|
|
|
if (!window._mdDriver) { |
|
|
|
|
|
|
|
window._mdDriver = new MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 }); |
|
|
|
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]; |
|
|
|
const s = activeSec ? getSec(activeSec) : sections[0]; |
|
|
|
if (!s || !s.elements.length) { |
|
|
|
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; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const elDefs = s.elements.map(el => ({ type: ElementType[el.type], ...el.fields, flags: el.flags })).filter(e => e.type != null); |
|
|
|
const elDefs = s.elements.map(el => { |
|
|
|
window._mdDriver.load(() => Promise.resolve( |
|
|
|
const base = { type: ElementType[el.type], ...el.fields, flags: el.flags }; |
|
|
|
new MonoDisplayFile({ elements_always: { flags: s.flags, elements: elDefs } }).toBuffer() |
|
|
|
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 ──────────────────────────────────────────────────────────── |
|
|
|
// ─── Load / Export ──────────────────────────────────────────────────────────── |
|
|
|
function loadBin(input) { |
|
|
|
function loadBin(input) { |
|
|
|
const file = input.files[0]; |
|
|
|
const file = input.files[0]; if (!file) return; |
|
|
|
if (!file) return; |
|
|
|
|
|
|
|
currentFilename = file.name; |
|
|
|
currentFilename = file.name; |
|
|
|
document.getElementById('filename').textContent = 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(); |
|
|
|
const reader = new FileReader(); |
|
|
|
reader.onload = e => { |
|
|
|
reader.onload = e => { |
|
|
|
const buf = new Uint8Array(e.target.result); |
|
|
|
const buf = new Uint8Array(e.target.result); |
|
|
|
if (window._mdDriver) { |
|
|
|
if (!window._mdDriver && window.MonoDisplay) |
|
|
|
window._mdDriver.load(() => Promise.resolve(buf)); |
|
|
|
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); |
|
|
|
reader.readAsArrayBuffer(file); |
|
|
|
input.value = ''; |
|
|
|
input.value = ''; markClean(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function exportBin() { |
|
|
|
function exportBin() { |
|
|
|
if (!window.MonoDisplay) { alert('MonoDisplay library not loaded.'); return; } |
|
|
|
if (!window.MonoDisplay) { alert('MonoDisplay library not loaded.'); return; } |
|
|
|
const { MonoDisplayFile, ElementType } = window.MonoDisplay; |
|
|
|
const { MonoDisplayFile, ElementType } = window.MonoDisplay; |
|
|
|
// Export all sections — stub: only exports active/first |
|
|
|
|
|
|
|
const s = activeSec ? getSec(activeSec) : sections[0]; |
|
|
|
const s = activeSec ? getSec(activeSec) : sections[0]; |
|
|
|
if (!s) { alert('Nothing to export.'); return; } |
|
|
|
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 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'); |
|
|
|
const a = document.createElement('a'); |
|
|
|
a.href = URL.createObjectURL(blob); |
|
|
|
a.href = URL.createObjectURL(blob); |
|
|
|
a.download = currentFilename.replace(/\.bin$/, '') + '.bin'; |
|
|
|
a.download = currentFilename.replace(/\.bin$/, '') + '.bin'; |
|
|
|
a.click(); |
|
|
|
a.click(); markClean(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ─── 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 ───────────────────────────────────────────────────────────────────── |
|
|
|
// ─── Init ───────────────────────────────────────────────────────────────────── |
|
|
|
render(); |
|
|
|
render(); |
|
|
|
// Defer demo init until lib is available |
|
|
|
|
|
|
|
if (window.MonoDisplay) { initDemos(); } |
|
|
|
if (window.MonoDisplay) { initDemos(); } |
|
|
|
else { const iv = setInterval(() => { if (window.MonoDisplay) { clearInterval(iv); initDemos(); } }, 100); } |
|
|
|
else { const iv = setInterval(() => { if (window.MonoDisplay) { clearInterval(iv); initDemos(); } }, 100); } |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|