Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
70c29b12ea | 6 days ago |
@ -0,0 +1,3 @@ |
|||||||
|
RewriteEngine on |
||||||
|
RewriteCond %{REQUEST_URI} \.bin$ [NC] |
||||||
|
RewriteRule ^(.*\.bin)$ /busanzeiger-webinterface/src/show.php [L,QSA] |
||||||
@ -0,0 +1 @@ |
|||||||
|
Schildkroete.bin |
||||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 106 KiB |
@ -0,0 +1,165 @@ |
|||||||
|
// 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); |
||||||
|
} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
<!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> |
||||||
@ -0,0 +1,682 @@ |
|||||||
|
: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) |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,43 @@ |
|||||||
|
<?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 |
||||||
|
|
||||||
|
?> |
||||||
@ -0,0 +1,213 @@ |
|||||||
|
<?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 |
||||||
|
|
||||||
|
?> |
||||||
@ -0,0 +1,181 @@ |
|||||||
|
<?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)); |
||||||
|
|
||||||
|
?> |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
|
||||||
|
<!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> |
||||||
|
|
||||||
@ -0,0 +1,215 @@ |
|||||||
|
<?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(), |
||||||
|
}; |
||||||
@ -0,0 +1,203 @@ |
|||||||
|
<?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 |
||||||
|
|
||||||
|
?> |
||||||
@ -0,0 +1,203 @@ |
|||||||
|
<?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 |
||||||
|
|
||||||
|
?> |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
<?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; |
||||||
|
?> |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
<?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); |
||||||
@ -1,68 +0,0 @@ |
|||||||
@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%; |
|
||||||
} |
|
||||||
} |
|
||||||