Pult: Handy als Stream Deck mit Soundboard und Szenenwechsel
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
Paket bauen und veroeffentlichen / paket (push) Failing after 1m21s
- /pult als installierbare Web-App (Manifest, Service Worker, Symbole) - Soundboard: Sounds vom Handy hochladen, bearbeiten, im Stream spielen ueber die neue Browser-Source /sounds - Szenen, Clip und Stummschalter: der Stream-PC waehlt sich mit eigenem Schluessel beim Hub ein (fern:verbinden), Befehle warten auf OBS - Browser-Sources melden, welche Seite sie sind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70448967f5
commit
8be195d938
29 files changed
+2935
-28
No files matched your search
@@ -0,0 +1,133 @@
|
||||
// Zeichnet die App-Symbole fuer das Pult (PWA) und schreibt sie nach
|
||||
// apps/overlay-web/public/icons/. Ohne Grafikprogramm und ohne Abhaengigkeit:
|
||||
// ein paar abgerundete Rechtecke, als PNG von Hand kodiert.
|
||||
//
|
||||
// Aufruf: node scripts/make-icons.ts
|
||||
//
|
||||
// Die PNGs liegen im Repo. Neu zeichnen nur, wenn sich das Motiv aendert.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import zlib from 'node:zlib';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ZIEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..', 'apps', 'overlay-web', 'public', 'icons');
|
||||
|
||||
type Farbe = [number, number, number];
|
||||
const hex = (h: string): Farbe =>
|
||||
[parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
|
||||
|
||||
const TASTEN: Farbe[] = ['#a970ff', '#4fd1c5', '#ff4fa3', '#ffc53d'].map(hex);
|
||||
const OBEN = hex('#221a3a');
|
||||
const UNTEN = hex('#0d0b14');
|
||||
|
||||
/** Abstand zum Rand eines abgerundeten Rechtecks, negativ heisst innen. */
|
||||
function rundRect(x: number, y: number, x0: number, y0: number, x1: number, y1: number, r: number): number {
|
||||
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
||||
const hx = (x1 - x0) / 2 - r, hy = (y1 - y0) / 2 - r;
|
||||
const qx = Math.abs(x - cx) - hx, qy = Math.abs(y - cy) - hy;
|
||||
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r;
|
||||
}
|
||||
|
||||
type Motiv = {
|
||||
/** Hintergrund bis zum Rand (maskable, Apple) oder selbst abgerundet. */
|
||||
vollflaechig: boolean;
|
||||
/** Wo die vier Tasten sitzen, als Anteil der Kantenlaenge. */
|
||||
von: number; bis: number;
|
||||
};
|
||||
|
||||
function zeichne(groesse: number, m: Motiv): Buffer {
|
||||
const px = Buffer.alloc(groesse * groesse * 4);
|
||||
const ss = 4; // 4x4 Unterabtastung je Pixel
|
||||
const spalt = (m.bis - m.von) * 0.09;
|
||||
const taste = (m.bis - m.von - spalt) / 2;
|
||||
|
||||
for (let py = 0; py < groesse; py++) {
|
||||
for (let pxi = 0; pxi < groesse; pxi++) {
|
||||
let r = 0, g = 0, b = 0, a = 0;
|
||||
for (let sy = 0; sy < ss; sy++) {
|
||||
for (let sx = 0; sx < ss; sx++) {
|
||||
const x = (pxi + (sx + 0.5) / ss) / groesse;
|
||||
const y = (py + (sy + 0.5) / ss) / groesse;
|
||||
|
||||
const imGrund = m.vollflaechig || rundRect(x, y, 0, 0, 1, 1, 0.22) <= 0;
|
||||
if (!imGrund) continue;
|
||||
let c: Farbe = [0, 1, 2].map((i) => OBEN[i]! + (UNTEN[i]! - OBEN[i]!) * y) as Farbe;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const tx = m.von + (i % 2) * (taste + spalt);
|
||||
const ty = m.von + Math.floor(i / 2) * (taste + spalt);
|
||||
if (rundRect(x, y, tx, ty, tx + taste, ty + taste, taste * 0.26) <= 0) {
|
||||
// Oben etwas heller: sieht nach Taste aus statt nach Flaeche.
|
||||
const hell = 1.12 - 0.22 * ((y - ty) / taste);
|
||||
c = TASTEN[i]!.map((v) => Math.min(255, v * hell)) as Farbe;
|
||||
}
|
||||
}
|
||||
r += c[0]; g += c[1]; b += c[2]; a += 255;
|
||||
}
|
||||
}
|
||||
const n = ss * ss, o = (py * groesse + pxi) * 4;
|
||||
// Vormultipliziert gemittelt, damit die Kanten nicht dunkel ausfransen.
|
||||
const deck = a / n;
|
||||
px[o] = deck ? Math.round(r / (a / 255)) : 0;
|
||||
px[o + 1] = deck ? Math.round(g / (a / 255)) : 0;
|
||||
px[o + 2] = deck ? Math.round(b / (a / 255)) : 0;
|
||||
px[o + 3] = Math.round(deck);
|
||||
}
|
||||
}
|
||||
return png(groesse, groesse, px);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- PNG
|
||||
|
||||
const CRC = new Uint32Array(256).map((_, n) => {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
return c >>> 0;
|
||||
});
|
||||
function crc32(b: Buffer): number {
|
||||
let c = 0xffffffff;
|
||||
for (const v of b) c = CRC[(c ^ v) & 0xff]! ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
function block(typ: string, daten: Buffer): Buffer {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(daten.length);
|
||||
const td = Buffer.concat([Buffer.from(typ, 'latin1'), daten]);
|
||||
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
|
||||
return Buffer.concat([len, td, crc]);
|
||||
}
|
||||
function png(w: number, h: number, rgba: Buffer): Buffer {
|
||||
const kopf = Buffer.alloc(13);
|
||||
kopf.writeUInt32BE(w, 0); kopf.writeUInt32BE(h, 4);
|
||||
kopf[8] = 8; kopf[9] = 6; kopf[10] = 0; kopf[11] = 0; kopf[12] = 0; // 8 bit RGBA
|
||||
const zeilen = Buffer.alloc((w * 4 + 1) * h);
|
||||
for (let y = 0; y < h; y++) rgba.copy(zeilen, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4);
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
block('IHDR', kopf),
|
||||
block('IDAT', zlib.deflateSync(zeilen, { level: 9 })),
|
||||
block('IEND', Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- los
|
||||
|
||||
const NORMAL: Motiv = { vollflaechig: false, von: 0.2, bis: 0.8 };
|
||||
// Maskable: alles Wichtige im Kreis mit 40 % Radius, der Rest wird abgeschnitten.
|
||||
const MASKE: Motiv = { vollflaechig: true, von: 0.29, bis: 0.71 };
|
||||
// Apple rundet selbst und mag keine Durchsicht.
|
||||
const APPLE: Motiv = { vollflaechig: true, von: 0.22, bis: 0.78 };
|
||||
|
||||
fs.mkdirSync(ZIEL, { recursive: true });
|
||||
const dateien: Array<[string, number, Motiv]> = [
|
||||
['pult-192.png', 192, NORMAL],
|
||||
['pult-512.png', 512, NORMAL],
|
||||
['pult-maskable-512.png', 512, MASKE],
|
||||
['apple-touch-icon.png', 180, APPLE],
|
||||
];
|
||||
for (const [name, groesse, motiv] of dateien) {
|
||||
const daten = zeichne(groesse, motiv);
|
||||
fs.writeFileSync(path.join(ZIEL, name), daten);
|
||||
console.log(`${name} ${groesse}x${groesse} ${(daten.length / 1024).toFixed(1)} KB`);
|
||||
}
|
||||
Reference in new issue
Block a user