Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:38:08 +02:00
commit c737a37f71
531 files changed
+119270

No files matched your search

+338
View File
@@ -0,0 +1,338 @@
'use strict';
/**
* gif-encoder.js — self-contained animated GIF89a encoder
* No dependencies. Exposes global GifEncoder via IIFE.
*
* Usage:
* const enc = new GifEncoder(640, 360, { fps: 10 });
* enc.addFrame(canvas.getContext('2d').getImageData(0,0,w,h).data);
* const bytes = enc.encode(); // Uint8Array — complete GIF89a file
* const url = URL.createObjectURL(new Blob([bytes], { type: 'image/gif' }));
*/
(function (root) {
// ── Utilities ──────────────────────────────────────────────────────────────────
function u16le(n) { return [n & 0xFF, (n >> 8) & 0xFF]; }
function ascii(s) {
const a = new Array(s.length);
for (let i = 0; i < s.length; i++) a[i] = s.charCodeAt(i);
return a;
}
// ── Median-cut colour quantisation ────────────────────────────────────────────
// Returns up to maxColors {r,g,b} objects representative of the palette.
function buildPalette(frames, maxColors) {
// Sample every 4th pixel from every 2nd frame for better colour coverage
const fStep = Math.max(1, frames.length >> 1);
const seen = new Set();
for (let fi = 0; fi < frames.length; fi += fStep) {
const d = frames[fi];
for (let i = 0; i < d.length; i += 16) { // every 4th pixel (×4 RGBA)
seen.add((d[i] << 16) | (d[i + 1] << 8) | d[i + 2]);
}
}
let colors = [...seen].map(c => ({ r: (c >> 16) & 0xFF, g: (c >> 8) & 0xFF, b: c & 0xFF }));
if (colors.length <= maxColors) return colors;
// Median cut
function splitBucket(bucket) {
let rMn=255,rMx=0,gMn=255,gMx=0,bMn=255,bMx=0;
for (const c of bucket) {
if (c.r < rMn) rMn=c.r; if (c.r > rMx) rMx=c.r;
if (c.g < gMn) gMn=c.g; if (c.g > gMx) gMx=c.g;
if (c.b < bMn) bMn=c.b; if (c.b > bMx) bMx=c.b;
}
const rR=rMx-rMn, gR=gMx-gMn, bR=bMx-bMn;
const axis = rR>=gR && rR>=bR ? 'r' : gR>=bR ? 'g' : 'b';
bucket.sort((a, b) => a[axis] - b[axis]);
const mid = bucket.length >> 1;
return [bucket.slice(0, mid), bucket.slice(mid)];
}
function representative(bucket) {
const n = bucket.length;
let r=0,g=0,b=0;
for (const c of bucket) { r+=c.r; g+=c.g; b+=c.b; }
return { r: (r/n+0.5)|0, g: (g/n+0.5)|0, b: (b/n+0.5)|0 };
}
let buckets = [colors];
while (buckets.length < maxColors) {
let li = 0;
for (let i = 1; i < buckets.length; i++)
if (buckets[i].length > buckets[li].length) li = i;
if (buckets[li].length <= 1) break;
const [a, b] = splitBucket(buckets[li]);
buckets.splice(li, 1, a, b);
}
return buckets.map(representative);
}
// ── 5-bit-per-channel colour lookup table (32³ = 32,768 entries) ─────────────
// 8× more accurate than the 4-bit (16³) LUT — max quantisation error per
// channel drops from ±8 to ±4 before the nearest-palette search.
function buildLUT(palette) {
const SZ = 32;
const lut = new Uint8Array(SZ * SZ * SZ);
for (let ri = 0; ri < SZ; ri++) {
for (let gi = 0; gi < SZ; gi++) {
for (let bi = 0; bi < SZ; bi++) {
const R = Math.round(ri * 255 / 31);
const G = Math.round(gi * 255 / 31);
const B = Math.round(bi * 255 / 31);
let best = 0, bestD = 1 << 30;
for (let pi = 0; pi < palette.length; pi++) {
const p = palette[pi];
const dr = R - p.r, dg = G - p.g, db = B - p.b;
const d = dr*dr + dg*dg + db*db;
if (d < bestD) { bestD = d; best = pi; }
}
lut[ri * SZ * SZ + gi * SZ + bi] = best;
}
}
}
return lut;
}
// ── Floyd-Steinberg dithering quantisation ────────────────────────────────────
// Eliminates colour banding by distributing per-pixel quantisation error to
// neighbouring pixels: right 7/16, below-left 3/16, below 5/16, below-right 1/16.
function quantizeFS(rgba, w, h, palette, lut) {
const SZ = 32;
const n = w * h;
// Float32 working buffers accumulate error without integer clamping
const R = new Float32Array(n);
const G = new Float32Array(n);
const B = new Float32Array(n);
for (let i = 0; i < n; i++) {
R[i] = rgba[i * 4];
G[i] = rgba[i * 4 + 1];
B[i] = rgba[i * 4 + 2];
}
const indices = new Uint8Array(n);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = y * w + x;
const r = Math.max(0, Math.min(255, R[i] + 0.5)) | 0;
const g = Math.max(0, Math.min(255, G[i] + 0.5)) | 0;
const b = Math.max(0, Math.min(255, B[i] + 0.5)) | 0;
// Nearest palette entry via 5-bit LUT (O(1) lookup)
const idx = lut[(r >> 3) * SZ * SZ + (g >> 3) * SZ + (b >> 3)];
indices[i] = idx;
// Quantisation error
const p = palette[idx];
const er = r - p.r, eg = g - p.g, eb = b - p.b;
// Distribute error to neighbours
if (x + 1 < w) {
R[i + 1] += er * 0.4375; // 7/16
G[i + 1] += eg * 0.4375;
B[i + 1] += eb * 0.4375;
}
if (y + 1 < h) {
if (x > 0) {
R[i + w - 1] += er * 0.1875; // 3/16
G[i + w - 1] += eg * 0.1875;
B[i + w - 1] += eb * 0.1875;
}
R[i + w] += er * 0.3125; // 5/16
G[i + w] += eg * 0.3125;
B[i + w] += eb * 0.3125;
if (x + 1 < w) {
R[i + w + 1] += er * 0.0625; // 1/16
G[i + w + 1] += eg * 0.0625;
B[i + w + 1] += eb * 0.0625;
}
}
}
}
return indices;
}
// ── GIF LZW encoder ───────────────────────────────────────────────────────────
// Returns an Array of bytes (including sub-block framing and block terminator).
function lzwEncode(pixels, minCodeSize) {
const clearCode = 1 << minCodeSize;
const eoiCode = clearCode + 1;
// Bit-writer
const out = [];
let buf = 0, bufBits = 0;
function write(code, bits) {
buf |= (code << bufBits);
bufBits += bits;
while (bufBits >= 8) { out.push(buf & 0xFF); buf >>>= 8; bufBits -= 8; }
}
let codeSize = minCodeSize + 1;
let nextCode = eoiCode + 1;
// Code table: maps (prefixCode << 8) | pixel → new code
const table = new Map();
write(clearCode, codeSize);
if (!pixels.length) {
write(eoiCode, codeSize);
if (bufBits > 0) out.push(buf & 0xFF);
return packSubBlocks(out);
}
let prefix = pixels[0];
for (let i = 1; i < pixels.length; i++) {
const pix = pixels[i];
const key = (prefix << 8) | pix;
const hit = table.get(key);
if (hit !== undefined) {
prefix = hit;
} else {
write(prefix, codeSize);
if (nextCode < 4096) {
table.set(key, nextCode);
if (nextCode === (1 << codeSize) && codeSize < 12) codeSize++;
nextCode++;
} else {
// Table full — emit clear code and reset
write(clearCode, codeSize);
table.clear();
nextCode = eoiCode + 1;
codeSize = minCodeSize + 1;
}
prefix = pix;
}
}
write(prefix, codeSize);
write(eoiCode, codeSize);
if (bufBits > 0) out.push(buf & 0xFF);
return packSubBlocks(out);
}
// Pack raw bytes into GIF sub-blocks (≤255 bytes each), terminated with 0x00.
function packSubBlocks(bytes) {
const result = [];
for (let i = 0; i < bytes.length; i += 255) {
const end = Math.min(i + 255, bytes.length);
result.push(end - i);
for (let j = i; j < end; j++) result.push(bytes[j]);
}
result.push(0); // block terminator
return result;
}
// ── GifEncoder class ──────────────────────────────────────────────────────────
class GifEncoder {
/**
* @param {number} width
* @param {number} height
* @param {{ fps?: number, maxColors?: number }} [opts]
*/
constructor(width, height, { fps = 10, maxColors = 256 } = {}) {
this.w = width;
this.h = height;
// GIF delay is in units of 1/100 s; minimum 2 (= 50 fps max)
this.delay = Math.max(2, Math.round(100 / fps));
this.maxColors = Math.max(2, Math.min(256, maxColors | 0 || 256));
this._frames = [];
}
/** @param {Uint8ClampedArray} data — RGBA pixel data (width × height × 4 bytes) */
addFrame(data) {
this._frames.push(data instanceof Uint8ClampedArray ? data : new Uint8ClampedArray(data.buffer ?? data));
}
get frameCount() { return this._frames.length; }
/**
* Encode all added frames into an animated GIF.
* @returns {Uint8Array}
*/
encode() {
const { w, h, delay, _frames: frames } = this;
if (!frames.length) return new Uint8Array(0);
// ── Build global colour palette (up to 256 colours) ────────────────────────
let palette = buildPalette(frames, this.maxColors);
// Must be exactly a power-of-2 size between 2 and 256
const palPow = Math.max(1, Math.ceil(Math.log2(Math.max(2, palette.length))));
const palSize = 1 << palPow;
while (palette.length < palSize) palette.push({ r: 0, g: 0, b: 0 });
const minCodeSize = Math.max(2, palPow);
const lut = buildLUT(palette);
// ── Assemble GIF binary ────────────────────────────────────────────────────
const out = [];
function push(...items) {
for (const item of items) {
if (Array.isArray(item)) { for (const b of item) out.push(b); }
else out.push(item);
}
}
// Header
push(ascii('GIF89a'));
// Logical Screen Descriptor
push(
u16le(w), u16le(h),
0x80 | (palPow - 1), // Global CT flag set; CT size = palPow−1
0, // Background colour index
0, // Pixel aspect ratio
);
// Global Colour Table (3 bytes per entry: R, G, B)
for (const c of palette) push(c.r, c.g, c.b);
// Application Extension — Netscape 2.0 (loop forever)
push(
0x21, 0xFF, 0x0B,
ascii('NETSCAPE2.0'),
0x03, 0x01, 0x00, 0x00, // loop count = 0 → infinite
0x00, // sub-block terminator
);
// ── Frames ─────────────────────────────────────────────────────────────────
for (const rgba of frames) {
// Floyd-Steinberg dithered quantisation for each frame
const indices = quantizeFS(rgba, w, h, palette, lut);
// Graphic Control Extension
push(
0x21, 0xF9, 0x04,
0x00, // packed: disposal = 0, no user input, no transparent
u16le(delay), // delay in 1/100 s units
0x00, // transparent colour index (unused)
0x00, // block terminator
);
// Image Descriptor
push(
0x2C,
u16le(0), u16le(0), // left, top
u16le(w), u16le(h),
0x00, // packed: no local CT, not interlaced
);
// Image Data
push(minCodeSize);
push(lzwEncode(indices, minCodeSize));
}
// Trailer
push(0x3B);
return new Uint8Array(out);
}
}
// Export
root.GifEncoder = GifEncoder;
}(typeof window !== 'undefined' ? window : global));