Compare commits

..

7 Commits
backup ... main

  1. 0
      Specification.rst
  2. 3
      backup/.htaccess
  3. 1
      backup/active.txt
  4. 5097
      backup/alphabet-extended.js
  5. 1756
      backup/app.js
  6. BIN
      backup/avj2305/Artikel1.bin
  7. BIN
      backup/avj2305/Artikel1.png
  8. BIN
      backup/avj2305/Artikel21.bin
  9. BIN
      backup/avj2305/Artikel21.png
  10. BIN
      backup/avj2305/Frosch161.bin
  11. BIN
      backup/avj2305/Frosch161.png
  12. BIN
      backup/avj2305/LaufSpruch1.bin
  13. BIN
      backup/avj2305/LaufSpruch1.png
  14. BIN
      backup/avj2305/LaufSpruch2.bin
  15. BIN
      backup/avj2305/LaufSpruch2.png
  16. BIN
      backup/avj2305/LaufSpruch3.bin
  17. BIN
      backup/avj2305/LaufSpruch3.png
  18. BIN
      backup/avj2305/MvAVJ.bin
  19. BIN
      backup/avj2305/MvAVJ.png
  20. BIN
      backup/avj2305/NotWidersetzen.bin
  21. BIN
      backup/avj2305/NotWidersetzen.png
  22. BIN
      backup/avj2305/Schildkroete.bin
  23. BIN
      backup/avj2305/Schildkroete.png
  24. BIN
      backup/avj2305/Widersetzen.bin
  25. BIN
      backup/avj2305/Widersetzen.png
  26. BIN
      backup/avj2305/Widersetzen1.bin
  27. BIN
      backup/avj2305/Widersetzen1.png
  28. BIN
      backup/avj2305/Zitat1.bin
  29. BIN
      backup/avj2305/Zitat1.png
  30. BIN
      backup/avj2305/Zitat2.bin
  31. BIN
      backup/avj2305/Zitat2.png
  32. BIN
      backup/avj2305/Zitat3.bin
  33. BIN
      backup/avj2305/Zitat3.png
  34. 165
      backup/binfmt.js
  35. 112
      backup/editor/index.html
  36. 5302
      backup/editor/public/mono-display.js
  37. 682
      backup/editor/style.css
  38. BIN
      backup/favicon.ico
  39. BIN
      backup/gpn/endscreen.bin
  40. 43
      backup/gpn/monoformat_bithelpers.php
  41. 213
      backup/gpn/monoformat_schema.php
  42. 1925
      backup/gpn/monoformat_structured.php
  43. BIN
      backup/gpn/noroom.bin
  44. 181
      backup/gpn/schedule.php
  45. BIN
      backup/gpn/template.bin
  46. 83
      backup/index.html
  47. 215
      backup/live.php
  48. BIN
      backup/out/cttue.disabled
  49. BIN
      backup/out/franzwerk.bin
  50. BIN
      backup/output.bin
  51. 203
      backup/schedule_bool.php
  52. 203
      backup/schedule_enum.php
  53. 53
      backup/show.php
  54. 84
      backup/styles.css
  55. 105
      backup/upload.php
  56. 2
      php/monoformat_structured.php
  57. 29
      php/schedule.php
  58. 23
      ts-editor/index.html
  59. 68
      ts-editor/mobile.css
  60. 102
      ts-editor/src/browser.ts
  61. 41
      ts-editor/style.css
  62. 5
      ts/src/driver.ts

@ -1,3 +0,0 @@
RewriteEngine on
RewriteCond %{REQUEST_URI} \.bin$ [NC]
RewriteRule ^(.*\.bin)$ /busanzeiger-webinterface/src/show.php [L,QSA]

@ -1 +0,0 @@
Schildkroete.bin

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

@ -1,165 +0,0 @@
// Binary Framebuffer Encoder
// Converts pixel data to binary format for bus display
const MAGIC = [66, 78, 23, 238];
const DrawEntryType = {
Image: 1,
Animation: 2,
HScroll: 3,
VScroll: 4,
Line: 5
};
function bitmapSizeBytes(width, height) {
return Math.floor((width * height + 7) / 8);
}
function setPixelInBitmap(x, y, memory, width, height, value) {
const index = y * width + x;
const byteIndex = Math.floor(index / 8);
const bitIndex = index % 8;
if (value) {
memory[byteIndex] |= 1 << bitIndex;
} else {
memory[byteIndex] &= ~(1 << bitIndex);
}
}
class BinaryFramebufferEncoder {
entries = [];
addImage(posX, posY, width, height, pixelData) {
const data = new Uint8Array(bitmapSizeBytes(width, height));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
setPixelInBitmap(x, y, data, width, height, pixelData[y][x]);
}
}
this.entries.push({
type: DrawEntryType.Image,
header: { posX, posY },
width,
height,
data
});
}
addAnimation(posX, posY, width, height, frameCount, updateInterval, frames) {
const frameSize = bitmapSizeBytes(width, height);
const data = new Uint8Array(frameSize * frameCount);
for (let f = 0; f < frameCount; f++) {
const frameData = data.subarray(f * frameSize, (f + 1) * frameSize);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
setPixelInBitmap(x, y, frameData, width, height, frames[f][y][x]);
}
}
}
this.entries.push({
type: DrawEntryType.Animation,
header: { posX, posY },
width,
height,
frameCount,
updateInterval,
data
});
}
encode() {
let totalSize = 5;
for (const entry of this.entries) {
totalSize += 3;
switch (entry.type) {
case DrawEntryType.Image:
totalSize += 2 + entry.data.length;
break;
case DrawEntryType.Animation:
totalSize += 4 + entry.data.length;
break;
}
}
const buffer = new Uint8Array(totalSize);
let pos = 0;
buffer.set(MAGIC, pos);
pos += 4;
buffer[pos++] = this.entries.length;
for (const entry of this.entries) {
buffer[pos++] = entry.type;
buffer[pos++] = entry.header.posX;
buffer[pos++] = entry.header.posY;
switch (entry.type) {
case DrawEntryType.Image:
buffer[pos++] = entry.width;
buffer[pos++] = entry.height;
buffer.set(entry.data, pos);
pos += entry.data.length;
break;
case DrawEntryType.Animation:
buffer[pos++] = entry.width;
buffer[pos++] = entry.height;
buffer[pos++] = entry.frameCount;
buffer[pos++] = entry.updateInterval;
buffer.set(entry.data, pos);
pos += entry.data.length;
break;
}
}
return buffer;
}
}
function parseJSONFrames(text, width = 120, height = 60, maxFrames = 50) {
const json = JSON.parse(text);
if (!Array.isArray(json)) {
throw new Error("JSON must be an array of frames");
}
const frames = [];
for (const frame of json) {
if (!Array.isArray(frame)) {
throw new Error("Each frame must be an array of coordinates");
}
const pixels = Array(height).fill(null).map(() => Array(width).fill(false));
for (const change of frame) {
const [x, y] = change;
if (y < pixels.length && x < pixels[y].length) {
pixels[y][x] = true;
}
}
if (frames.length >= maxFrames) continue;
frames.push(pixels);
}
return frames;
}
function jsonToBinary(jsonString, options = {}) {
const {
startX = 0,
startY = 0,
updateInterval = 3,
width = 120,
height = 60,
maxFrames = 50
} = options;
const encoder = new BinaryFramebufferEncoder();
const frames = parseJSONFrames(jsonString, width, height, maxFrames);
if (frames.length === 1) {
encoder.addImage(startX, startY, width, height, frames[0]);
} else {
encoder.addAnimation(startX, startY, width, height, frames.length, updateInterval, frames);
}
return encoder.encode();
}
// Helper function to drop every nth frame from a frame array
function dropEveryNthFrame(frames, n) {
if (n <= 1 || frames.length <= 1) return frames;
return frames.filter((frame, index) => (index + 1) % n !== 0);
}
// Helper function to keep 1 frame, drop 2 frames in repeating pattern
// Pattern: keep, drop, drop, keep, drop, drop, ...
function keepOneDropTwo(frames) {
if (frames.length <= 1) return frames;
return frames.filter((frame, index) => index % 3 === 0);
}

@ -1,112 +0,0 @@
<!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>
<script>!function(){var t=localStorage.getItem('theme');t&&document.documentElement.setAttribute('data-theme',t)}();</script>
<link href="style.css" rel="stylesheet"/>
</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="clearAll()">
<svg viewBox="0 0 24 24">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14H6L5 6" />
<path d="M10 11v6M14 11v6" />
</svg>
Clear all
</button>
<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 style="flex:1"></div>
<div class="tb-sep"></div>
<button class="tb-btn" id="theme-toggle" onclick="cycleTheme()">⊙ auto</button>
</div>
<div id="main">
<div id="left">
<span class="pv-label">preview · 120 × 60</span>
<div id="display-box">
<canvas id="canvas_root" width="120" height="60"></canvas>
</div>
<span class="pv-label">demos</span>
<div id="demo-btns"></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-drawBack" onchange="setSectionFlag('drawBack',this.checked)" />
drawBack
</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>
<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>
<!-- 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>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -1,682 +0,0 @@
:root {
--bg: #111;
--bg-bar: #1a1a1a;
--bg-raised: #161616;
--bg-hover: #1b1b1b;
--bg-hover2: #222;
--bg-sunken: #141414;
--bg-accent: #141e14;
--bg-active: #1e2e1e;
--bg-deep: #0f0f0f;
--bg-input: #0a0a0a;
--bg-canvas: #000;
--bg-xhover: #1e1010;
--bg-overlay: rgba(0,0,0,.6);
--bd: #2a2a2a;
--bd-inner: #1e1e1e;
--bd-deep: #1c1c1c;
--bd-card: #222;
--bd-btn: #2d2d2d;
--bd-type: #252525;
--bd-active: #2b3d2b;
--bd-hover: #444;
--bd-strong: #333;
--tx: #ccc;
--tx-hover: #bbb;
--tx-sub: #888;
--tx-label: #777;
--tx-meta: #666;
--tx-dim: #555;
--tx-faint: #444;
--tx-ghost: #333;
--tx-file: #3a3a3a;
--tx-empty: #2e2e2e;
--ac: #33ff66;
--dirty: #886622
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg: #f5f5f5;
--bg-bar: #e8e8e8;
--bg-raised: #efefef;
--bg-hover: #e5e5e5;
--bg-hover2: #e0e0e0;
--bg-sunken: #ebebeb;
--bg-accent: #e8f5e8;
--bg-active: #dff0df;
--bg-deep: #f0f0f0;
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
--bd-card: #ddd;
--bd-btn: #ccc;
--bd-type: #d8d8d8;
--bd-active: #7dc87d;
--bd-hover: #bbb;
--bd-strong: #ccc;
--tx: #222;
--tx-hover: #333;
--tx-sub: #555;
--tx-label: #666;
--tx-meta: #777;
--tx-dim: #888;
--tx-faint: #999;
--tx-ghost: #aaa;
--tx-file: #aaa;
--tx-empty: #bbb;
--ac: #1a9940;
--dirty: #b37a00
}
}
[data-theme="light"] {
--bg: #f5f5f5;
--bg-bar: #e8e8e8;
--bg-raised: #efefef;
--bg-hover: #e5e5e5;
--bg-hover2: #e0e0e0;
--bg-sunken: #ebebeb;
--bg-accent: #e8f5e8;
--bg-active: #dff0df;
--bg-deep: #f0f0f0;
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
--bd-card: #ddd;
--bd-btn: #ccc;
--bd-type: #d8d8d8;
--bd-active: #7dc87d;
--bd-hover: #bbb;
--bd-strong: #ccc;
--tx: #222;
--tx-hover: #333;
--tx-sub: #555;
--tx-label: #666;
--tx-meta: #777;
--tx-dim: #888;
--tx-faint: #999;
--tx-ghost: #aaa;
--tx-file: #aaa;
--tx-empty: #bbb;
--ac: #1a9940;
--dirty: #b37a00
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0
}
body {
background: var(--bg);
color: var(--tx);
font-family: monospace;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
font-size: 12px
}
#topbar {
height: 36px;
background: var(--bg-bar);
border-bottom: 1px solid var(--bd);
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
flex-shrink: 0
}
#topbar .app-name {
color: var(--tx-dim);
font-size: 11px;
letter-spacing: .12em;
text-transform: uppercase;
margin-right: 4px
}
.tb-btn {
background: var(--bg-raised);
color: var(--tx-sub);
border: 1px solid var(--bd-btn);
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: var(--bg-hover2);
color: var(--tx-hover);
border-color: var(--bd-hover)
}
.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: var(--tx-file);
font-size: 11px;
margin-left: 2px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.tb-sep {
width: 1px;
height: 18px;
background: var(--bd)
}
.dirty {
color: var(--dirty) !important
}
#main {
flex: 1;
display: flex;
min-height: 0
}
#left {
width: 48%;
border-right: 1px solid var(--bd-inner);
display: flex;
flex-direction: column;
padding: 14px;
gap: 10px;
overflow-y: auto;
flex-shrink: 0
}
#display-box {
width: 100%;
aspect-ratio: 2/1;
background: var(--bg-canvas);
border: 1px solid var(--bd);
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: var(--tx-ghost);
letter-spacing: .1em;
text-transform: uppercase
}
#demo-btns {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 6px
}
#demo-btns > div {
display: contents;
}
canvas.pixel-editor {
display: block;
cursor: crosshair;
image-rendering: pixelated;
max-width: 100%;
border: 1px solid var(--bd-inner);
}
.anim-editor {
display: flex;
flex-direction: column;
gap: 4px;
}
.anim-frame-tabs {
display: flex;
flex-wrap: wrap;
gap: 3px;
align-items: center;
}
.anim-frame-tab {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border: 1px solid var(--bd-inner);
border-radius: 3px;
cursor: pointer;
font-size: 11px;
user-select: none;
}
.anim-frame-tab.active {
border-color: var(--accent, #EC0);
color: var(--accent, #EC0);
}
.x-btn-sm {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0 2px;
font-size: 13px;
line-height: 1;
opacity: 0.6;
}
.x-btn-sm:hover { opacity: 1; }
#sec-meta {
background: var(--bg-sunken);
border: 1px solid var(--bd-inner);
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: var(--tx-faint);
min-width: 90px
}
.meta-val {
color: var(--tx-meta)
}
.green {
color: var(--ac)
}
select.meta-val {
background: var(--bg-input);
color: var(--ac);
border: 1px solid var(--bd-inner);
border-radius: 3px;
padding: 1px 4px;
}
.flag-check {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--tx-dim);
cursor: pointer
}
.flag-check input {
accent-color: var(--ac);
cursor: pointer
}
#right {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden
}
#right-hdr {
height: 36px;
background: var(--bg-sunken);
border-bottom: 1px solid var(--bd-inner);
display: flex;
align-items: center;
padding: 0 10px;
gap: 6px;
flex-shrink: 0
}
#right-hdr .rh-title {
flex: 1;
color: var(--tx-faint);
font-size: 11px;
letter-spacing: .08em;
text-transform: uppercase
}
#sections-wrap {
flex: 1;
overflow-y: auto;
padding: 8px
}
.empty-state {
color: var(--tx-empty);
font-size: 11px;
padding: 28px 16px;
text-align: center;
line-height: 2
}
/* section card */
.sec-card {
border: 1px solid var(--bd-card);
border-radius: 3px;
margin-bottom: 5px;
overflow: hidden
}
.sec-card.active {
border-color: var(--bd-active)
}
.sec-hdr {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 8px;
cursor: pointer;
user-select: none;
background: var(--bg-raised);
position: relative
}
.sec-hdr:hover {
background: var(--bg-hover)
}
.sec-card.active .sec-hdr,
.sec-hdr.active {
background: var(--bg-active)
}
.sec-arrow {
color: var(--tx-ghost);
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: var(--tx-label);
font-size: 11px
}
.sec-badge {
font-size: 10px;
color: var(--ac);
border: 1px solid var(--bd-active);
background: var(--bg-accent);
border-radius: 2px;
padding: 1px 6px;
flex-shrink: 0
}
/* red × - section & element */
.x-btn {
background: none;
border: none;
color: var(--bd);
cursor: pointer;
font-size: 15px;
line-height: 1;
padding: 2px 5px;
border-radius: 2px;
flex-shrink: 0;
transition: color .1s, background .1s
}
.x-btn:hover {
color: #ff4444;
background: var(--bg-xhover)
}
/* element */
.el-list {
background: var(--bg-deep);
border-top: 1px solid var(--bd-deep);
padding: 6px
}
.el-item {
border: 1px solid var(--bd-inner);
border-radius: 3px;
background: var(--bg-sunken);
margin-bottom: 4px;
overflow: hidden
}
.el-item.active {
border-color: var(--ac)
}
.el-item-hdr {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
cursor: pointer
}
.el-item-hdr:hover {
background: var(--bg-hover)
}
.el-type {
font-size: 10px;
color: var(--tx-meta);
border: 1px solid var(--bd-type);
border-radius: 2px;
padding: 1px 6px;
flex-shrink: 0
}
.el-item.active .el-type {
color: var(--ac);
border-color: var(--bd-active)
}
.el-name {
flex: 1;
color: var(--tx-dim);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px
}
.el-fields {
padding: 6px 8px 8px;
border-top: 1px solid var(--bg-bar);
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: var(--tx-faint)
}
.field input[type=text],
.field input[type=number],
.field input[type=datetime-local],
.field select,
.field textarea {
background: var(--bg-input);
color: var(--tx-sub);
border: 1px solid var(--bd-card);
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: var(--ac);
color: var(--tx)
}
.field textarea {
resize: vertical;
min-height: 44px;
line-height: 1.4
}
.field select option {
background: var(--bg-bar);
color: var(--tx-ghost)
}
.flags-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 2px 0
}
.add-el {
background: transparent;
color: var(--tx-ghost);
border: 1px dashed var(--bd-card);
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: var(--tx-sub);
border-color: var(--bd-hover)
}
/* confirm dialog */
#confirm-overlay {
display: none;
position: fixed;
inset: 0;
background: var(--bg-overlay);
z-index: 100;
align-items: center;
justify-content: center
}
#confirm-overlay.show {
display: flex
}
#confirm-box {
background: var(--bg-bar);
border: 1px solid var(--bd-strong);
border-radius: 4px;
padding: 20px 24px;
max-width: 340px;
width: 90%;
display: flex;
flex-direction: column;
gap: 14px
}
#confirm-msg {
color: var(--tx-ghost);
font-size: 12px;
line-height: 1.6
}
#confirm-btns {
display: flex;
gap: 8px;
justify-content: flex-end
}
.cb {
background: var(--bg-raised);
color: var(--tx-sub);
border: 1px solid var(--bd-btn);
border-radius: 3px;
padding: 4px 14px;
font-family: monospace;
font-size: 11px;
cursor: pointer
}
.cb:hover {
background: var(--bg-hover2);
color: var(--tx-hover)
}
.cb.primary {
border-color: var(--bd-active);
color: var(--ac)
}
.cb.primary:hover {
background: var(--bg-active)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

@ -1,43 +0,0 @@
<?php
namespace monoformat {
function isBitSet(int $value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return false;
}
$mask = 1 << $bit;
return ($value & $mask) == $mask;
}
function setBit(int &$value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
$value |= $mask;
}
function unsetBit(int &$value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
$value &= ~$mask;
}
function maybeSetBit(int &$value, int $bit, int $set) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
if ($set) {
$value |= $mask;
} else {
$value &= ~$mask;
}
}
} // namespace monoformat
?>

@ -1,213 +0,0 @@
<?php
namespace monoformat {
include_once("monoformat_bithelpers.php");
trait FlagStruct {
public static function fromJSON(array $json) {
$myClass = static::class;
$reflect = new \ReflectionClass($myClass);
$properties = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
$result = new $myClass();
foreach ($properties as $property) {
$name = $property->getName();
if (array_key_exists($name, $json)) {
$result->$name = (bool) $json[$name];
}
}
return $result;
}
public function toJSON(): array {
$result = [];
$myClass = static::class;
$reflect = new \ReflectionClass($myClass);
$properties = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
foreach ($properties as $property) {
$name = $property->getName();
$result[$name] = $this->$name;
}
return $result;
}
}
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case MultiTimeBasedDrawn = 3;
case ExpiryDate = 31;
case CustomFont = 32;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case Box = 6;
case ClippedText = 16;
case HScrollText = 17;
//case VScrollText = 18;
case CurrentTime = 32;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class ScrollFlags {
use FlagStruct;
public bool $endless = false;
public bool $invertDirection = false;
public bool $padBefore = false;
public bool $padAfter = false;
public static function fromBitField(int $bitfield): ScrollFlags {
$result = new ScrollFlags();
$result->endless = isBitSet($bitfield, 0);
$result->invertDirection = isBitSet($bitfield, 1);
$result->padBefore = isBitSet($bitfield, 2);
$result->padAfter = isBitSet($bitfield, 3);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->endless);
maybeSetBit($result, 1, $this->invertDirection);
maybeSetBit($result, 2, $this->padBefore);
maybeSetBit($result, 3, $this->padAfter);
return $result;
}
}
enum LineStyle : int {
case Solid = 0;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class LineFlags {
use FlagStruct;
public bool $dark = false;
public static function fromBitField(int $bitfield): LineFlags {
$result = new LineFlags();
$result->dark = isBitSet($bitfield, 0);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
return $result;
}
}
enum FillPatternStyle : int {
case Solid = 0;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class FillFlags {
use FlagStruct;
public bool $dark = false;
public static function fromBitField(int $bitfield): FillFlags {
$result = new FillFlags();
$result->dark = isBitSet($bitfield, 0);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
return $result;
}
}
class TextFlags {
use FlagStruct;
public bool $dark = false;
public bool $autoWordWrap = false;
public static function fromBitField(int $bitfield): TextFlags {
$result = new TextFlags();
$result->dark = isBitSet($bitfield, 0);
$result->autoWordWrap = isBitSet($bitfield, 1);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
maybeSetBit($result, 1, $this->autoWordWrap);
return $result;
}
}
class TimeDisplayFlags {
use FlagStruct;
public bool $use12h = false;
public bool $showHours = false;
public bool $showMinutes = false;
public bool $showSeconds = false;
public static function fromBitField(int $bitfield): TimeDisplayFlags {
$result = new TimeDisplayFlags();
$result->use12h = isBitSet($bitfield, 0);
$result->showHours = isBitSet($bitfield, 1);
$result->showMinutes = isBitSet($bitfield, 2);
$result->showSeconds = isBitSet($bitfield, 3);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->use12h);
maybeSetBit($result, 1, $this->showHours);
maybeSetBit($result, 2, $this->showMinutes);
maybeSetBit($result, 3, $this->showSeconds);
return $result;
}
}
} // namespace monoformat
?>

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -1,181 +0,0 @@
<?php
include_once("monoformat_schema.php");
include_once("monoformat_structured.php");
function replaceTalkInfo($str, $talks) {
return preg_replace_callback("/\\$\\{([^}]*)\\}/", function ($matches) use ($talks) {
if (str_contains($matches[1], ".")) {
[$id, $what] = explode(".", $matches[1], 2);
$id = (int) $id;
if ($id < 0 || $id >= count($talks)) {
return "";
}
$talk = $talks[$id];
if (array_key_exists($what, $talk)) {
return $talk[$what];
} else {
return "";
}
} else {
return "";
}
}, $str);
}
$roomMapping = [
"1020ba76eb04" => "a873f07f-4ab2-57ea-9183-99b187e8b0cf", // Medientheater
"001122334466" => "0273ef15-d23a-5413-9c74-3edc092b6ee8", // Kubus
"001122334477" => "80d66dbe-d978-57b5-86e2-b3d480136b9a", // Vortragssaal
];
if (!isset($_GET["mac"]) or !array_key_exists($_GET["mac"], $roomMapping)) {
/* We didn't find this device in the mapping, so let's just
* send a default file to the user.
*/
Header("Content-Type: application/octet-stream");
readfile(dirname(__FILE__) . "/noroom.bin");
exit(0);
}
$roomGUID = strtolower($roomMapping[$_GET["mac"]]);
$pretalx = json_decode(file_get_contents("https://cfp.gulas.ch/gpn24/schedule/export/schedule.json"), true);
$roomName = null;
foreach ($pretalx["schedule"]["conference"]["rooms"] as $room) {
$thisRoomGUID = strtolower($room["guid"]);
if ($thisRoomGUID === $roomGUID) {
$roomName = $room["name"];
break;
}
}
if ($roomName === null) {
die("Room not found!");
}
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($pretalx["schedule"]["conference"]["days"] as $day) {
if (!array_key_exists($roomName, $day["rooms"])) {
continue;
}
$roomTalks = $day["rooms"][$roomName];
foreach ($roomTalks as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$speakers = [];
foreach ($t["persons"] as $person) {
array_push($speakers, $person["name"]);
}
$talkBegin = new DateTimeImmutable($t["date"]);
$talkEnd = $talkBegin->add($duration);
if ($talkEnd < $now) {
continue;
}
$talk = [
"Time" => $talkBegin->format("H:i"),
"Duration" => $duration->format("H:i"),
"Persons" => implode(", ", $speakers),
"Title" => $t["title"],
"_start" => $talkBegin,
"_end" => $talkEnd,
];
array_push($talks, $talk);
}
}
$template = file_get_contents(dirname(__FILE__) . "/template.bin");
$template = monoformat\parseFile($template);
if (count($template->sections) != 1 or $template->sections[0]->sectionType() != monoformat\SectionType::AlwaysDrawn) {
die("Invalid section in template file");
}
$elements = [];
for ($i = 0; $i < $template->sections[0]->elementCount(); ++$i) {
array_push($elements, $template->sections[0]->elementAt($i));
}
$end_template = file_get_contents(dirname(__FILE__) . "/endscreen.bin");
$end_template = monoformat\parseFile($end_template);
if (count($end_template->sections) != 1 or $end_template->sections[0]->sectionType() != monoformat\SectionType::AlwaysDrawn) {
die("Invalid section in end screen template file");
}
$end_elements = [];
for ($i = 0; $i < $end_template->sections[0]->elementCount(); ++$i) {
array_push($end_elements, $end_template->sections[0]->elementAt($i));
}
$n = 42;
$last = false;
if ($n > count($talks)) {
$n = count($talks);
$last = true;
}
$lastEnd = 0;
$outputFile = new monoformat\File();
if (!$last) {
$section = new monoformat\ExpiryDateSection();
$section->setExpiryDate($talks[$n - 1]["_end"]->getTimestamp());
array_push($outputFile->sections, $section);
}
for ($i = 0; $i < $n; ++$i) {
$subTalks = array_slice($talks, $i);
$end = $subTalks[0]["_end"]->getTimestamp();
$section = new monoformat\TimeBasedDrawnSection();
$section->setDrawOnFront(true);
$section->setDrawOnBack(true);
$section->setClearBeforeDrawing(true);
$section->setStartTimestamp($lastEnd);
$section->setEndTimestamp($end);
foreach ($elements as $element) {
if ($element->elementType() == monoformat\ElementType::ClippedText) {
$x = $element->x();
$y = $element->y();
$width = $element->width();
$height = $element->height();
$textFlags = $element->textFlags();
$fontIndex = $element->fontIndex();
$text = replaceTalkInfo($element->text(), $subTalks);
$element = new monoformat\ClippedTextElement($x, $y, $width, $height, $textFlags, $fontIndex, $text);
} else if ($element->elementType() == monoformat\ElementType::HScrollText) {
$x = $element->x();
$y = $element->y();
$width = $element->width();
$height = $element->height();
$textFlags = $element->textFlags();
$flags = $element->flags();
$scrollSpeed = $element->scrollSpeed();
$fontIndex = $element->fontIndex();
$text = replaceTalkInfo($element->text(), $subTalks) . " ";
$element = new monoformat\HScrollTextElement($x, $y, $width, $height, $textFlags, $flags, $scrollSpeed, $fontIndex, $text);
}
$section->appendElement($element);
}
array_push($outputFile->sections, $section);
$lastEnd = $end;
}
if ($last) {
$section = new monoformat\TimeBasedDrawnSection();
$section->setDrawOnFront(true);
$section->setDrawOnBack(true);
$section->setClearBeforeDrawing(true);
$section->setStartTimestamp($lastEnd);
// Set this far enough in the future that it doesn't matter
$section->setEndTimestamp($lastEnd + 86400 * 300);
foreach ($end_elements as $element) {
$section->appendElement($element);
}
array_push($outputFile->sections, $section);
}
Header("Content-Type: application/octet-stream");
print(monoformat\serializeFile($outputFile));
?>

Binary file not shown.

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Busanzeiger Paint</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/omggif@1.0.10/omggif.min.js"></script>
<script src="alphabet-extended.js"></script>
<script src="binfmt.js"></script>
</head>
<body>
<h1>Busanzeiger Paint</h1>
<div id="modeToggle">
<label><input type="radio" name="mode" value="image" checked> Image Mode</label>
<label><input type="radio" name="mode" value="text"> Text Mode</label>
<label style="margin-left: 20px;"><input type="checkbox" id="animationCreation"> Animation Creation</label>
<label style="margin-left: 20px;">
Duration (seconds): <input type="number" id="animationDuration" min="0.1" max="60" step="0.1" value="3.0" style="width: 60px;">
<button id="revertDuration" title="Unlock and recalculate from frame count"></button>
</label>
</div>
<div id="dropzone">Drag and drop image(s) here or click to upload</div>
<div id="controls">
<input type="file" id="upload" accept="image/*" style="display:none;" />
<button id="invert">Invert</button>
<button id="dither">Dither: OFF</button>
<button id="export">Prepare Export</button>
<button id="downloadBinary">Download Binary</button>
<button id="draw">Draw</button>
<button id="erase">Erase</button>
<label>
Brush:
<select id="brushSize">
<option value="1" selected>1px</option>
<option value="2">2px</option>
<option value="4">4px</option>
<option value="6">6px</option>
</select>
</label>
<button id="reset">Reset</button>
</div>
<div id="frameControls" style="display: none; margin-top: 10px; display: flex; gap: 10px; align-items: center; justify-content: center;">
<button id="prevFrame">← Prev Frame</button>
<button id="playStop">▶ Play</button>
<span id="frameCounter"></span>
<button id="nextFrame">Next Frame →</button>
<button id="addEmptyFrame" style="display: none;">+ Empty Frame</button>
<button id="duplicateFrame" style="display: none;">+ Duplicate Frame</button>
<button id="deleteFrame" style="display: none;">🗑 Delete Frame</button>
</div>
<div id="textInputArea">
<label>Scrolling Text:<br>
<textarea id="scrollText" rows="4" cols="40"></textarea>
</label>
<label>Speed: <input type="number" id="scrollSpeed" min="0" max="63" value="4"></label>
<label><input type="checkbox" id="scrollRight"> Scroll Right</label>
<label><input type="checkbox" id="scrollWrap" checked> Wrap Around</label>
</div>
<div id="matrix"></div>
<form action="upload.php" method="post">
<textarea id="array_output" name="array_output" rows="4" cols="50"></textarea>
<textarea id="scrolls_output" name="scrolls" rows="4" cols="50" style="display:none;"></textarea>
<br>
Filename: <input type="text" id="fileName" name="fileName">
<br>
Passwort: <input type="password" id="password" name="password">
<button type="submit">Speichern</button>
</form>
<script src="app.js"></script>
</body>
</html>

@ -1,215 +0,0 @@
<?php
session_start();
define('PASSWORD', 'grund_cttue_gesetz');
define('FILES_DIR', __DIR__ . '/avj2305');
define('ACTIVE_FILE', __DIR__ . '/active.txt');
// =============================================================================
// HELPERS
// =============================================================================
function redirect(string $url): never {
header('Location: ' . $url);
exit;
}
function require_auth(): void {
if (empty($_SESSION['auth'])) redirect('?login');
}
function get_active(): string {
if (!file_exists(ACTIVE_FILE)) return '';
$v = trim(file_get_contents(ACTIVE_FILE));
return $v !== '' ? $v : '';
}
function set_active(string $filename): void {
file_put_contents(ACTIVE_FILE, $filename);
}
function get_bin_files(): array {
$files = glob(FILES_DIR . '/*.bin');
return $files ? array_map('basename', $files) : [];
}
function safe_filename(string $name): bool {
return $name === basename($name)
&& str_ends_with($name, '.bin')
&& !str_contains($name, "\0");
}
function has_png(string $bin_basename): bool {
$png = substr($bin_basename, 0, -4) . '.png';
return file_exists(FILES_DIR . '/' . $png);
}
function png_path(string $bin_basename): string {
return FILES_DIR . '/' . substr($bin_basename, 0, -4) . '.png';
}
// =============================================================================
// ACTIONS (POST handlers - redirect, never render)
// =============================================================================
function action_login(): never {
if (hash_equals(PASSWORD, $_POST['password'] ?? '')) {
$_SESSION['auth'] = true;
redirect('?edit');
}
redirect('?login&err=1');
}
function action_set_file(): never {
require_auth();
$f = $_POST['file'] ?? '';
if (safe_filename($f) && file_exists(FILES_DIR . '/' . $f)) {
set_active($f);
}
redirect('?edit');
}
// =============================================================================
// RENDERERS (GET handlers - output HTML, never redirect)
// =============================================================================
function render_login(): never {
$err = !empty($_GET['err']); ?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Login</title>
<style>
body{font:14px monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}
form{display:flex;flex-direction:column;gap:8px;width:220px}
input[type=password]{padding:6px;background:#222;border:1px solid #555;color:#eee}
button{padding:6px;background:#444;border:none;color:#eee;cursor:pointer}
button:hover{background:#555}
.err{color:#f66;font-size:12px}
</style>
</head>
<body>
<form method="post" action="?login">
<label>Password</label>
<input type="password" name="password" autofocus>
<button type="submit">Login</button>
<?php if ($err): ?><span class="err">Wrong password.</span><?php endif; ?>
</form>
</body></html>
<?php exit; }
function render_edit(): never {
require_auth();
$files = get_bin_files();
$active = get_active(); ?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Select File</title>
<style>
body{font:14px monospace;background:#111;color:#eee;padding:24px;margin:0}
h2{margin:0 0 16px}
ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px}
li form{margin:0}
.row{display:flex;align-items:center;gap:10px}
button{padding:6px 12px;background:#333;border:1px solid #555;color:#eee;cursor:pointer;flex:1;text-align:left}
button:hover{background:#444}
button.active{border-color:#6af;color:#6af}
.thumb{width:480px;height:270px;object-fit:cover;border:1px solid #444;background:#222;flex-shrink:0}
.nothumb{width:64px;height:64px;flex-shrink:0}
.none{color:#888}
</style>
</head>
<body>
<h2>Live file selector</h2>
<p>Active: <strong><?= $active !== '' ? htmlspecialchars($active) : '<span class="none">none</span>' ?></strong></p>
<?php if ($files): ?>
<ul>
<?php foreach ($files as $f): ?>
<li>
<form method="post" action="?edit">
<div class="row">
<?php if (has_png($f)): ?>
<img class="thumb" src="?img=<?= urlencode($f) ?>" alt="">
<?php else: ?>
<div class="nothumb"></div>
<?php endif; ?>
<input type="hidden" name="file" value="<?= htmlspecialchars($f) ?>">
<button type="submit"<?= $f === $active ? ' class="active"' : '' ?>><?= htmlspecialchars($f) ?></button>
</div>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="none">No .bin files found in avj2305/</p>
<?php endif; ?>
</body></html>
<?php exit; }
// =============================================================================
// FILE SERVER (default route)
// =============================================================================
function serve_png(): never {
require_auth();
$f = $_GET['img'] ?? '';
if (!safe_filename($f)) {
http_response_code(400); exit('Invalid filename.');
}
$path = png_path($f);
if (!file_exists($path)) {
http_response_code(404); exit('No preview.');
}
header('Content-Type: image/png');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
function serve_active_file(): never {
$active = get_active();
if ($active === '') {
http_response_code(404); exit('No active file set.');
}
if (!safe_filename($active)) {
http_response_code(500); exit('Invalid filename.');
}
$path = FILES_DIR . '/' . $active;
if (!file_exists($path)) {
http_response_code(404); exit('File not found.');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: inline; filename="' . addslashes($active) . '"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
// =============================================================================
// ROUTES
// =============================================================================
//
// GET /live.php -> serve active .bin file
// GET /live.php?login -> login page
// POST /live.php?login -> process login
// GET /live.php?edit -> file picker [auth required]
// POST /live.php?edit -> set active file [auth required]
// GET /live.php?img=x.bin -> serve x.png preview [auth required]
//
// =============================================================================
$method = $_SERVER['REQUEST_METHOD'];
$route = array_key_first($_GET) ?? '';
match (true) {
$method === 'POST' && $route === 'login' => action_login(),
$method === 'POST' && $route === 'edit' => action_set_file(),
$method === 'GET' && $route === 'login' => render_login(),
$method === 'GET' && $route === 'edit' => render_edit(),
$method === 'GET' && $route === 'img' => serve_png(),
default => serve_active_file(),
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,203 +0,0 @@
<?php
namespace monoformat {
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case CustomFont = 32;
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case ClippedText = 16;
case HScrollText = 17;
case CurrentTime = 32;
}
enum LineStyle : int {
case Solid = 0;
}
class Element {
public function serialize() {
return "";
}
}
class HScrollTextElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $flags = 0;
public $scrollSpeed = 0;
public $fontIndex = 0;
public $text = "";
public function serialize() {
$len = strlen($this->text);
$result = pack("vvvvvCCvv",
17,
$this->x,
$this->y,
$this->width,
$this->height,
$this->flags,
$this->scrollSpeed,
$this->fontIndex,
$len);
$result .= (string) $this->text;
if ($len % 4 != 0) {
$n = 4 - ($len % 4);
for ($i = 0; $i < $n; ++$i) {
$result .= chr(0);
}
}
return $result;
}
}
class CurrentTimeElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $fontIndex = 0;
public $utcOffset = 0;
public $flags = 0;
public function serialize() {
$result = pack("vvvvvvvv",
32,
$this->x,
$this->y,
$this->width,
$this->height,
$this->fontIndex,
$this->utcOffset,
$this->flags);
return $result;
}
}
class Section {
public function serialize() {
return "";
}
}
class AlwaysDrawnSection extends Section {
public $drawOnFront = false;
public $drawOnBack = false;
public $clearBeforeDrawing = false;
public $elements = [];
public function serialize() {
$flags = 0;
if ($this->drawOnFront) {
$flags |= 0x01;
}
if ($this->drawOnBack) {
$flags |= 0x02;
}
if ($this->clearBeforeDrawing) {
$flags |= 0x04;
}
$inner = pack("vv", $flags, count($this->elements));
foreach ($this->elements as $element) {
$inner .= $element->serialize();
}
$len = strlen($inner) + 4;
$len = substr(pack("V", $len), 0, 3);
return pack("C", 1) . $len . $inner;
}
}
class File {
public $sections = [];
public function serialize() {
$nSections = count($this->sections);
$result = "\xAF\x7E\x2B\x63";
$result .= pack("Vvv", 1, $nSections, 0);
foreach ($this->sections as $section) {
$result .= $section->serialize();
}
return $result;
}
}
} // namespace monoformat
namespace {
$roomName= "BOOL";
$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true);
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($data["schedule"]["conference"]["days"] as $day) {
foreach ($day["rooms"][$roomName] as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$talk = [
"date" => new DateTimeImmutable($t["date"]),
"duration" => $duration,
"title" => $t["title"],
];
$talkEnd = $talk["date"]->add($talk["duration"]);
if ($talkEnd < $now) {
continue;
}
if ($talk["date"] > $now && count($talks) > 2) {
break;
}
array_push($talks, $talk);
}
}
$elements = [];
function putText($x, $y, $width, $height, $text) {
global $elements;
$e = new monoformat\HScrollTextElement();
$e->x = $x;
$e->y = $y;
$e->width = $width;
$e->height = $height;
$e->flags = 0;
$e->scrollSpeed = 15;
$e->fontIndex = 0;
$e->text = $text;
array_push($elements, $e);
}
$i = 0;
foreach ($talks as $talk) {
putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i"));
putText(35, $i * 20, 85, 20, $talk["title"]);
++$i;
}
$section = new monoformat\AlwaysDrawnSection();
$section->drawOnFront = true;
$section->drawOnBack = true;
$section->clearBeforeDrawing = true;
$section->elements = $elements;
$file = new monoformat\File();
array_push($file->sections, $section);
Header("Content-Type: application/octet-stream");
print($file->serialize());
} // global
?>

@ -1,203 +0,0 @@
<?php
namespace monoformat {
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case CustomFont = 32;
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case ClippedText = 16;
case HScrollText = 17;
case CurrentTime = 32;
}
enum LineStyle : int {
case Solid = 0;
}
class Element {
public function serialize() {
return "";
}
}
class HScrollTextElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $flags = 0;
public $scrollSpeed = 0;
public $fontIndex = 0;
public $text = "";
public function serialize() {
$len = strlen($this->text);
$result = pack("vvvvvCCvv",
17,
$this->x,
$this->y,
$this->width,
$this->height,
$this->flags,
$this->scrollSpeed,
$this->fontIndex,
$len);
$result .= (string) $this->text;
if ($len % 4 != 0) {
$n = 4 - ($len % 4);
for ($i = 0; $i < $n; ++$i) {
$result .= chr(0);
}
}
return $result;
}
}
class CurrentTimeElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $fontIndex = 0;
public $utcOffset = 0;
public $flags = 0;
public function serialize() {
$result = pack("vvvvvvvv",
32,
$this->x,
$this->y,
$this->width,
$this->height,
$this->fontIndex,
$this->utcOffset,
$this->flags);
return $result;
}
}
class Section {
public function serialize() {
return "";
}
}
class AlwaysDrawnSection extends Section {
public $drawOnFront = false;
public $drawOnBack = false;
public $clearBeforeDrawing = false;
public $elements = [];
public function serialize() {
$flags = 0;
if ($this->drawOnFront) {
$flags |= 0x01;
}
if ($this->drawOnBack) {
$flags |= 0x02;
}
if ($this->clearBeforeDrawing) {
$flags |= 0x04;
}
$inner = pack("vv", $flags, count($this->elements));
foreach ($this->elements as $element) {
$inner .= $element->serialize();
}
$len = strlen($inner) + 4;
$len = substr(pack("V", $len), 0, 3);
return pack("C", 1) . $len . $inner;
}
}
class File {
public $sections = [];
public function serialize() {
$nSections = count($this->sections);
$result = "\xAF\x7E\x2B\x63";
$result .= pack("Vvv", 1, $nSections, 0);
foreach ($this->sections as $section) {
$result .= $section->serialize();
}
return $result;
}
}
} // namespace monoformat
namespace {
$roomName= "ENUM";
$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true);
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($data["schedule"]["conference"]["days"] as $day) {
foreach ($day["rooms"][$roomName] as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$talk = [
"date" => new DateTimeImmutable($t["date"]),
"duration" => $duration,
"title" => $t["title"],
];
$talkEnd = $talk["date"]->add($talk["duration"]);
if ($talkEnd < $now) {
continue;
}
if ($talk["date"] > $now && count($talks) > 2) {
break;
}
array_push($talks, $talk);
}
}
$elements = [];
function putText($x, $y, $width, $height, $text) {
global $elements;
$e = new monoformat\HScrollTextElement();
$e->x = $x;
$e->y = $y;
$e->width = $width;
$e->height = $height;
$e->flags = 0;
$e->scrollSpeed = 15;
$e->fontIndex = 0;
$e->text = $text;
array_push($elements, $e);
}
$i = 0;
foreach ($talks as $talk) {
putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i"));
putText(35, $i * 20, 85, 20, $talk["title"]);
++$i;
}
$section = new monoformat\AlwaysDrawnSection();
$section->drawOnFront = true;
$section->drawOnBack = true;
$section->clearBeforeDrawing = true;
$section->elements = $elements;
$file = new monoformat\File();
array_push($file->sections, $section);
Header("Content-Type: application/octet-stream");
print($file->serialize());
} // global
?>

@ -1,53 +0,0 @@
<?php
$path = 'out';
$files = scandir($path);
$binaryFiles = [];
foreach($files as $file) {
if(str_ends_with($file, 'bin')) {
$binaryFiles[] = $file;
}
}
$current = date('i') % sizeof($binaryFiles);
$downloadName = $binaryFiles[$current];
$filePath = __DIR__ . '/out/' . $downloadName;
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
echo 'File not found.';
echo $filePath;
exit;
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream'); // generic binary MIME
header('Content-Disposition: attachment; filename="' . basename($downloadName) . '"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, private');
header('Pragma: public');
header('Content-Length: ' . filesize($filePath));
while (ob_get_level()) {
ob_end_clean();
}
$chunkSize = 8192;
$handle = fopen($filePath, 'rb');
if ($handle === false) {
http_response_code(500);
echo 'Unable to open file.';
exit;
}
while (!feof($handle)) {
// Output a chunk and flush it immediately
echo fread($handle, $chunkSize);
flush(); // push to client
}
fclose($handle);
exit;
?>

@ -1,84 +0,0 @@
body {
font-family: sans-serif;
background: #111;
color: white;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
image-rendering: pixelated;
margin-top: 20px;
}
#matrix {
display: grid;
grid-template-columns: repeat(var(--col-count, 10), 1fr);
grid-gap: 1px;
margin-top: 20px;
}
.dot {
width: 10px;
height: 10px;
background: #222;
border-radius: 50%;
}
.dot.on {
background: orange;
}
#dropzone {
border: 2px dashed #555;
padding: 20px;
margin-top: 20px;
text-align: center;
width: 300px;
cursor: pointer;
}
#controls {
margin-top: 10px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
#textInputArea {
display: none;
margin-top: 10px;
}
input, textarea, select {
background: #222;
color: white;
border: 1px solid #555;
padding: 5px;
border-radius: 4px;
}
select option {
background: #222;
color: white;
}
button {
background: #333;
color: white;
border: 1px solid #555;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #444;
}
button.active {
background: #f60;
border-color: #f60;
}

@ -1,105 +0,0 @@
<?php
$width = 120;
$height = 60;
$PASSWORD = "Ky/6lG3kbA>/TM?C(V@S";
// --- Helpers ---
function setPixel(int $x, int $y, int $width, &$bitmap) {
if ($x < 0 || $y < 0) return;
$index = $y * $width + $x;
$byteIdx = intdiv($index, 8);
$bitPos = $index % 8;
$bitmap[$byteIdx] |= (1 << $bitPos);
}
function createImageBitmap(array $pixelsOn, int $width, int $height): array {
$totalBits = $width * $height;
$bitmapSize = intdiv($totalBits + 7, 8);
$bitmap = array_fill(0, $bitmapSize, 0);
foreach ($pixelsOn as [$x, $y]) {
setPixel($x, $y, $width, $bitmap);
}
return $bitmap;
}
function packBitmap(array $bitmap): string {
return implode('', array_map(fn($b) => pack('C', $b), $bitmap));
}
function createImageObject(int $width, int $height, array $bitmap, int $xOffset = 0, int $yOffset = 0): string {
$payload = pack('C', $width);
$payload .= pack('C', $height);
$payload .= packBitmap($bitmap);
$header = pack('C*', 0x01, $xOffset, $yOffset); // Type = 1
return $header . $payload;
}
function createScrollObject(int $xOffset, int $yOffset, int $width, int $height, int $contentWidth, array $bitmap, int $speed = 0, bool $directionRight = false, bool $wrap = true): string {
$CW = pack('v', $contentWidth);
$UF = (($wrap ? 1 : 0) << 7) | (($directionRight ? 1 : 0) << 6) | ($speed & 0x3F);
$payload = pack('C', $width);
$payload .= pack('C', $height);
$payload .= $CW;
$payload .= pack('C', $UF);
$payload .= packBitmap($bitmap);
$header = pack('C*', 0x03, $xOffset, $yOffset);
return $header . $payload;
}
function createBlob(array $objects, string $fileName) {
$blob = '';
$blob .= pack('C*', 0x42, 0x4E, 0x17, 0xEE);
$blob .= pack('C', count($objects));
foreach ($objects as $obj) {
$blob .= $obj;
}
file_put_contents($fileName, $blob);
echo "Blob written to {$fileName} (" . strlen($blob) . " bytes)\n";
}
// --- Validation ---
if (!isset($_POST['password'])) die("password missing.");
if (htmlspecialchars($_POST['password']) !== htmlspecialchars($PASSWORD)) die("wrong password.");
// --- Determine mode ---
$mode = $_POST['mode'] ?? 'image';
$objects = [];
$fileName = 'out/' . $_POST['fileName'] . '.bin' ?? 'out/' .'output.bin';
mkdir('out/');
// --- Handle Image Mode ---
if ($mode === 'image') {
$pixelsOn = json_decode($_POST['array_output'] ?? '[]', true);
if (!is_array($pixelsOn)) die("invalid or missing array_output for image mode.");
$bitmap = createImageBitmap($pixelsOn, $width, $height);
$objects[] = createImageObject($width, $height, $bitmap, 0, 0);
}
// --- Handle Text Mode (scrolling text) ---
elseif ($mode === 'text') {
$scrolls = json_decode($_POST['scrolls'] ?? '[]', true);
if (!is_array($scrolls) || empty($scrolls)) die("invalid or missing scrolls for text mode.");
foreach ($scrolls as $scroll) {
$pixelsOn = $scroll['pixelsOn'] ?? [];
$bitmap = createImageBitmap($pixelsOn, $scroll['contentWidth'], $scroll['height']);
$objects[] = createScrollObject(
$scroll['x'] ?? 0,
$scroll['y'] ?? 0,
$scroll['width'] ?? $width,
$scroll['height'] ?? 8,
$scroll['contentWidth'] ?? 60,
$bitmap,
$scroll['speed'] ?? 0,
$scroll['directionRight'] ?? false,
$scroll['wrap'] ?? true
);
}
}
else {
die("unknown mode: {$mode}");
}
createBlob($objects, $fileName);

@ -26,7 +26,7 @@ function serializePixels(array $pixels): string {
function unserializePixels(int $n, string $serialized): array {
$nBytes = intdiv($n + 7, 8);
if (strlen($serialized) != $nBytes) {
$actual = strlen($serializd);
$actual = strlen($serialized);
throw new \ValueError("Could not unserialize a pixel bitmap: the size $actual does not equal the expected $nBytes");
}
$result = array_fill(0, $n, false);

@ -1,5 +1,11 @@
<?php
$roomMapping = [
];
$passThroughMapping = [
];
include_once("monoformat_schema.php");
include_once("monoformat_structured.php");
@ -23,8 +29,27 @@ function replaceTalkInfo($str, $talks) {
}, $str);
}
$roomMapping = [
];
if (isset($_GET["mac"]) and array_key_exists($_GET["mac"], $passThroughMapping)) {
$fn = $passThroughMapping[$_GET["mac"]];
if (is_dir($fn)) {
$d = opendir($fn);
$files = [];
while (($e = readdir($d)) !== false) {
if (str_ends_with($e, ".bin")) {
array_push($files, $fn . "/" . $e);
}
}
sort($files);
if (!count($files)) {
die("No files found");
}
$n = (time() / 60) % count($files);
$fn = $files[$n];
}
Header("Content-Type: application/octet-stream");
readfile($fn);
exit(0);
}
if (!isset($_GET["mac"]) or !array_key_exists($_GET["mac"], $roomMapping)) {
/* We didn't find this device in the mapping, so let's just

@ -5,12 +5,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>MonoDisplay Editor</title>
<script>!function(){var t=localStorage.getItem('theme');t&&document.documentElement.setAttribute('data-theme',t)}();</script>
<link href="style.css" rel="stylesheet"/>
<script>!function () { var t = localStorage.getItem('theme'); t && document.documentElement.setAttribute('data-theme', t) }();</script>
<link href="style.css" rel="stylesheet" />
<link href="mobile.css" rel="stylesheet" />
</head>
<body>
<div id="topbar">
<span class="app-name">MonoDisplay</span>
<div class="tb-sep"></div>
@ -52,10 +52,13 @@
<div class="tb-sep"></div>
<button class="tb-btn" id="theme-toggle" onclick="cycleTheme()">⊙ auto</button>
</div>
<div id="main">
<div id="left">
<span class="pv-label">preview · 120 × 60</span>
<span class="pv-label">preview · <select onchange="changeDisplayLayout()">
<option>120 × 60</option>
<option>120 × 40</option>
</select></span>
</span>
<div id="display-box">
<canvas id="canvas_root" width="120" height="60"></canvas>
</div>
@ -87,25 +90,27 @@
</div>
</div>
</div>
<div id="right">
<div id="right-hdr">
<span class="rh-title">sections</span>
<button class="tb-btn" id="mob-add-section" 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>
</button>
</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>
<!-- 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>
</body>

@ -0,0 +1,68 @@
@media (max-width: 640px) {
#topbar {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
gap: 6px;
padding: 0 8px;
}
#topbar::-webkit-scrollbar {
display: none;
}
#topbar>* {
flex-shrink: 0;
}
#topbar .tb-btn {
font-size: 0;
gap: 0;
padding: 3px 8px;
}
#topbar .tb-btn svg {
width: 15px;
height: 15px;
}
#topbar #theme-toggle {
font-size: 11px;
}
#topbar button[onclick="addSection()"] {
display: none;
}
#mob-add-section {
display: flex;
}
#topbar [style*="flex:1"] {
display: none;
}
#main {
flex-direction: column;
overflow-y: auto;
}
#left,
#right {
width: 100%;
border-right: none;
flex-shrink: 0;
}
#left {
border-bottom: 1px solid var(--bd-inner);
padding: 10px;
gap: 8px;
}
#display-box {
aspect-ratio: 2/1;
width: 100%;
}
}

@ -26,7 +26,7 @@ import {
} from "libmonoformat";
const W = 120, H = 60;
let W = 120, H = 60;
// --- State -------------------------------------------------------------------
let file: MonoDisplayFile = new MonoDisplayFile([]);
@ -185,6 +185,15 @@ async function guardDirty(): Promise<boolean> {
);
}
function changeDisplayLayout() {
const sel = document.querySelector('.pv-label select') as HTMLSelectElement;
const [w, h] = sel.value.split('×').map(s => parseInt(s.trim()));
W = w as number;
H = h as number;
((window as any)._mdDriver as MonoDisplayDriver).setSize(W, H);
((window as any)._mdDriver as MonoDisplayDriver).load(() => Promise.resolve(file.toBuffer()));
}
// --- Helpers -----------------------------------------------------------------
function newSec(): MonoFormatElementsAlways {
return {
@ -425,7 +434,7 @@ function triggerPreview() {
function buildPreview() {
if (!(window as any)._mdDriver)
(window as any)._mdDriver = new MonoDisplayDriver("canvas_root", { onColor: "#EC0", offColor: "#000", fps: 25 });
(window as any)._mdDriver.load(() => Promise.resolve(file.toBuffer()));
changeDisplayLayout();
}
// --- Load / Export -----------------------------------------------------------
@ -518,82 +527,99 @@ const PixelCanvas: m.Component<{ img: MonoFormatPixelImage; onpaint: () => void
vnode.state.drawing = false;
vnode.state.drawValue = 1;
},
view({ attrs: { img, onpaint }, state }) {
function pixelFromEvent(e: MouseEvent): { x: number; y: number } | null {
const canvas = e.currentTarget as HTMLCanvasElement;
view({ attrs: { img, onpaint }, state: s }) {
function pixelFromCoords(canvas: HTMLCanvasElement, clientX: number, clientY: number) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = Math.floor((e.clientX - rect.left) * scaleX / PIXEL_SCALE);
const y = Math.floor((e.clientY - rect.top) * scaleY / PIXEL_SCALE);
const x = Math.floor((clientX - rect.left) * scaleX / PIXEL_SCALE);
const y = Math.floor((clientY - rect.top) * scaleY / PIXEL_SCALE);
if (x < 0 || x >= img.width || y < 0 || y >= img.height) return null;
return { x, y };
}
function paint(e: MouseEvent) {
const p = pixelFromEvent(e);
function paintAt(canvas: HTMLCanvasElement, clientX: number, clientY: number) {
const p = pixelFromCoords(canvas, clientX, clientY);
if (!p) return;
img.pixels[getPixelIndex(img, p.x, p.y)] = state.drawValue;
img.pixels[getPixelIndex(img, p.x, p.y)] = s.drawValue;
onpaint();
drawPixelCanvas(e.currentTarget as HTMLCanvasElement, img);
drawPixelCanvas(canvas, img);
}
function paint(e: MouseEvent) {
paintAt(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY);
}
function attachTouch(canvas: HTMLCanvasElement) {
canvas.addEventListener("touchstart", (e: TouchEvent) => {
e.preventDefault();
s.drawing = true;
const touch = e.changedTouches[0];
const p = pixelFromCoords(canvas, touch.clientX, touch.clientY);
if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1;
paintAt(canvas, touch.clientX, touch.clientY);
}, { passive: false });
canvas.addEventListener("touchmove", (e: TouchEvent) => {
e.preventDefault();
if (!s.drawing) return;
const touch = e.changedTouches[0];
paintAt(canvas, touch.clientX, touch.clientY);
}, { passive: false });
canvas.addEventListener("touchend", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false });
canvas.addEventListener("touchcancel", (e) => { e.preventDefault(); s.drawing = false; }, { passive: false });
}
return m("canvas.pixel-editor", {
width: img.width * PIXEL_SCALE,
height: img.height * PIXEL_SCALE,
oncreate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img),
oncreate: ({ dom }: m.VnodeDOM) => {
const canvas = dom as HTMLCanvasElement;
drawPixelCanvas(canvas, img);
attachTouch(canvas);
},
onupdate: ({ dom }: m.VnodeDOM) => drawPixelCanvas(dom as HTMLCanvasElement, img),
onmousedown: (e: MouseEvent) => {
state.drawing = true;
const p = pixelFromEvent(e);
if (p) state.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1;
s.drawing = true;
const p = pixelFromCoords(e.currentTarget as HTMLCanvasElement, e.clientX, e.clientY);
if (p) s.drawValue = img.pixels[getPixelIndex(img, p.x, p.y)] ? 0 : 1;
paint(e);
},
onmousemove: (e: MouseEvent) => { if (state.drawing) paint(e); },
onmouseup: () => { state.drawing = false; },
onmouseleave: () => { state.drawing = false; },
onmousemove: (e: MouseEvent) => { if (s.drawing) paint(e); },
onmouseup: () => { s.drawing = false; },
onmouseleave: () => { s.drawing = false; },
ondragover: (e: DragEvent) => {
e.preventDefault();
e.dataTransfer!.dropEffect = "copy";
},
ondrop: (e: DragEvent) => {
e.preventDefault();
const file = e.dataTransfer?.files[0];
if (!file || !file.type.startsWith("image/")) return;
const url = URL.createObjectURL(file);
const htmlImg = new Image();
htmlImg.onload = () => {
// Draw the dropped image into an offscreen canvas to read pixel data
const offscreen = document.createElement("canvas");
offscreen.width = htmlImg.width;
offscreen.height = htmlImg.height;
const ctx = offscreen.getContext("2d")!;
ctx.drawImage(htmlImg, 0, 0);
const imageData = ctx.getImageData(0, 0, htmlImg.width, htmlImg.height);
// Convert to your pixel format — adjust this to match your img structure
const scale = Math.min(1, 120 / htmlImg.width, 60 / htmlImg.height);
const w = Math.round(htmlImg.width * scale);
const h = Math.round(htmlImg.height * scale);
offscreen.width = w;
offscreen.height = h;
ctx.drawImage(htmlImg, 0, 0, w, h); // scales the image down while drawing
ctx.drawImage(htmlImg, 0, 0, w, h);
const imageData = ctx.getImageData(0, 0, w, h);
img.width = w;
img.height = h;
// then read imageData from the scaled offscreen canvas as before
img.pixels = new Uint8Array(w * h).map((_, i) => {
const r = imageData.data[i * 4] || 0;
const g = imageData.data[i * 4 + 1] || 0;
const b = imageData.data[i * 4 + 2] || 0;
const a = imageData.data[i * 4 + 3] || 0;
// transparent = 0, dark pixels = 1, light pixels = 0
const r = imageData.data[i * 4];
const g = imageData.data[i * 4 + 1];
const b = imageData.data[i * 4 + 2];
const a = imageData.data[i * 4 + 3];
return a > 128 && (r * 299 + g * 587 + b * 114) < 128_000 ? 0 : 1;
});
URL.revokeObjectURL(url);
m.redraw();
};
@ -916,7 +942,7 @@ async function clearAll() {
}
// Expose globals referenced by topbar inline handlers
Object.assign(window, { loadBin, exportBin, addSection, clearAll, cycleTheme });
Object.assign(window, { loadBin, exportBin, addSection, clearAll, cycleTheme, changeDisplayLayout });
// window.addEventListener("beforeunload", (e) => {
// if (isDirty) e.preventDefault();

@ -11,7 +11,7 @@
--bg-input: #0a0a0a;
--bg-canvas: #000;
--bg-xhover: #1e1010;
--bg-overlay: rgba(0,0,0,.6);
--bg-overlay: rgba(0, 0, 0, .6);
--bd: #2a2a2a;
--bd-inner: #1e1e1e;
--bd-deep: #1c1c1c;
@ -49,7 +49,7 @@
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bg-overlay: rgba(0, 0, 0, .4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
@ -87,7 +87,7 @@
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bg-overlay: rgba(0, 0, 0, .4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
@ -252,7 +252,8 @@ body {
gap: 5px;
margin-top: 6px
}
#demo-btns > div {
#demo-btns>div {
display: contents;
}
@ -263,17 +264,20 @@ canvas.pixel-editor {
max-width: 100%;
border: 1px solid var(--bd-inner);
}
.anim-editor {
display: flex;
flex-direction: column;
gap: 4px;
}
.anim-frame-tabs {
display: flex;
flex-wrap: wrap;
gap: 3px;
align-items: center;
}
.anim-frame-tab {
display: flex;
align-items: center;
@ -285,10 +289,12 @@ canvas.pixel-editor {
font-size: 11px;
user-select: none;
}
.anim-frame-tab.active {
border-color: var(--accent, #EC0);
color: var(--accent, #EC0);
}
.x-btn-sm {
background: none;
border: none;
@ -299,7 +305,10 @@ canvas.pixel-editor {
line-height: 1;
opacity: 0.6;
}
.x-btn-sm:hover { opacity: 1; }
.x-btn-sm:hover {
opacity: 1;
}
#sec-meta {
background: var(--bg-sunken);
@ -680,3 +689,25 @@ select.meta-val {
.cb.primary:hover {
background: var(--bg-active)
}
#mob-add-section {
display: none;
}
canvas.pixel-editor {
touch-action: none;
/* belt-and-suspenders alongside preventDefault */
-webkit-user-select: none;
user-select: none;
}
.pv-label select {
background: var(--bg-input);
color: var(--ac);
border: 1px solid var(--bd-inner);
border-radius: 3px;
padding: 1px 14px;
font-family: monospace;
font-size: 10px;
letter-spacing: .1em;
}

@ -88,6 +88,11 @@ export class MonoDisplayDriver {
}
}
setSize(w: number, h: number) {
this.opts.displayHeight = h;
this.opts.displayWidth = w;
}
stop(): void { this.renderer?.stop(); }
}

Loading…
Cancel
Save