Compare commits
7 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
debce36336 | 4 days ago |
|
|
395e5efa54 | 4 days ago |
|
|
faeb89fb42 | 4 days ago |
|
|
2417aee259 | 4 days ago |
|
|
8efc44d1c2 | 4 days ago |
|
|
2b60c30f7f | 4 days ago |
|
|
e7e60d4de6 | 6 days ago |
@ -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 |
|
||||||
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
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> |
|
||||||
@ -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) |
|
||||||
} |
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -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 |
|
||||||
|
|
||||||
?> |
|
||||||
@ -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)); |
|
||||||
|
|
||||||
?> |
|
||||||
@ -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(), |
|
||||||
}; |
|
||||||
@ -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); |
|
||||||
@ -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%; |
||||||
|
} |
||||||
|
} |
||||||