Files
Playtube-StreamDock/tools/raster.mjs
T

147 lines
5.4 KiB
JavaScript

// Winziger Software-Rasterizer + PNG-Encoder (keine Abhaengigkeiten), fuer die Tasten-Icons.
import { deflateSync } from 'node:zlib';
const SUPERSAMPLE = 4;
let crcTable = null;
function crc32(buffer) {
if (!crcTable) {
crcTable = new Uint32Array(256);
for (let n = 0; n < 256; n += 1) {
let c = n;
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
crcTable[n] = c >>> 0;
}
}
let crc = 0xffffffff;
for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
return (crc ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const head = Buffer.alloc(8);
head.writeUInt32BE(data.length, 0);
head.write(type, 4, 'ascii');
const tail = Buffer.alloc(4);
tail.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
return Buffer.concat([head, data, tail]);
}
export function encodePng(width, height, rgba) {
const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8; // Bit-Tiefe
header[9] = 6; // RGBA
const stride = width * 4 + 1;
const rows = Buffer.alloc(stride * height);
for (let y = 0; y < height; y += 1) {
Buffer.from(rgba.buffer, rgba.byteOffset + y * width * 4, width * 4).copy(rows, y * stride + 1);
}
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', header),
chunk('IDAT', deflateSync(rows, { level: 9 })),
chunk('IEND', Buffer.alloc(0)),
]);
}
export function parseColor(hex) {
const value = hex.replace('#', '');
return [0, 2, 4].map((i) => Number.parseInt(value.slice(i, i + 2), 16));
}
/** Leinwand mit Kantenglaettung; Formen werden per Punkt-in-Form-Test mit Supersampling gefuellt. */
export class Canvas {
constructor(size) {
this.size = size;
this.data = new Uint8ClampedArray(size * size * 4);
}
/** contains(x, y) -> boolean in Pixel-Koordinaten; bounds = [x0, y0, x1, y1] begrenzt die Suche. */
fill(contains, color, bounds = [0, 0, this.size, this.size]) {
// color: "#rrggbb", [r, g, b] oder Funktion (x, y) => [r, g, b] (Farbverlaeufe)
const colorAt = typeof color === 'function' ? color
: ((fixed) => () => fixed)(typeof color === 'string' ? parseColor(color) : color);
const clamp = (v) => Math.max(0, Math.min(this.size, v));
const x0 = clamp(Math.floor(bounds[0]));
const y0 = clamp(Math.floor(bounds[1]));
const x1 = clamp(Math.ceil(bounds[2]));
const y1 = clamp(Math.ceil(bounds[3]));
const total = SUPERSAMPLE * SUPERSAMPLE;
for (let y = y0; y < y1; y += 1) {
for (let x = x0; x < x1; x += 1) {
let hits = 0;
for (let sy = 0; sy < SUPERSAMPLE; sy += 1) {
for (let sx = 0; sx < SUPERSAMPLE; sx += 1) {
if (contains(x + (sx + 0.5) / SUPERSAMPLE, y + (sy + 0.5) / SUPERSAMPLE)) hits += 1;
}
}
if (hits) this.blend((y * this.size + x) * 4, colorAt(x + 0.5, y + 0.5), hits / total);
}
}
}
blend(index, [r, g, b], alpha) {
const dstA = this.data[index + 3] / 255;
const outA = alpha + dstA * (1 - alpha);
[r, g, b].forEach((value, offset) => {
this.data[index + offset] = (value * alpha + this.data[index + offset] * dstA * (1 - alpha)) / outA;
});
this.data[index + 3] = outA * 255;
}
roundRect(x, y, w, h, radius, color) {
this.fill((px, py) => {
const cx = Math.max(x + radius, Math.min(x + w - radius, px));
const cy = Math.max(y + radius, Math.min(y + h - radius, py));
return px >= x && px <= x + w && py >= y && py <= y + h && (px - cx) ** 2 + (py - cy) ** 2 <= radius ** 2;
}, color, [x, y, x + w, y + h]);
}
circle(cx, cy, radius, color) {
this.fill((px, py) => (px - cx) ** 2 + (py - cy) ** 2 <= radius ** 2, color,
[cx - radius, cy - radius, cx + radius, cy + radius]);
}
/** Kreisring bzw. Bogen (Winkel in Radiant, 0 = rechts, im Uhrzeigersinn). */
ring(cx, cy, outer, inner, color, from = 0, to = Math.PI * 2) {
this.fill((px, py) => {
const d = (px - cx) ** 2 + (py - cy) ** 2;
if (d > outer ** 2 || d < inner ** 2) return false;
let angle = Math.atan2(py - cy, px - cx);
if (angle < 0) angle += Math.PI * 2;
return angle >= from && angle <= to;
}, color, [cx - outer, cy - outer, cx + outer, cy + outer]);
}
polygon(points, color) {
const xs = points.map((p) => p[0]);
const ys = points.map((p) => p[1]);
this.fill((px, py) => {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i, i += 1) {
const [xi, yi] = points[i];
const [xj, yj] = points[j];
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) inside = !inside;
}
return inside;
}, color, [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)]);
}
/** Strecke mit Breite `width` und runden Enden. */
line(x1, y1, x2, y2, width, color) {
const half = width / 2;
const dx = x2 - x1;
const dy = y2 - y1;
this.fill((px, py) => {
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy || 1)));
return (px - (x1 + t * dx)) ** 2 + (py - (y1 + t * dy)) ** 2 <= half * half;
}, color, [Math.min(x1, x2) - half, Math.min(y1, y2) - half, Math.max(x1, x2) + half, Math.max(y1, y2) + half]);
}
png() {
return encodePng(this.size, this.size, this.data);
}
}