1import { OPS2 as OPS_CHEM, OP_META2 as META_CHEM } from "./glitch-ops-wave2.js"; 2import { OPS2 as OPS_OPT, OP_META2 as META_OPT } from "./glitch-ops-wave2-optical.js"; 3import { OPS2 as OPS_TIME, OP_META2 as META_TIME } from "./glitch-ops-wave2-temporal.js"; 4/* glitch-ops.js â a stackable rack of manipulation + transformation effects. 5 6 Each op takes a canvas and mutates it in place, so they compose in any order 7 (the effects rack applies them in sequence). Anything random is driven by a 8 seeded PRNG, so a given seed reproduces the same glitch exactly â chaos you 9 can keep, in the site's reproducible-by-seed idiom. Zero dependencies. */ 10 11function mulberry32(a) { 12 return function () { 13 a |= 0; a = (a + 0x6D2B79F5) | 0; 14 let t = Math.imul(a ^ (a >>> 15), 1 | a); 15 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; 16 return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 17 }; 18} 19export function rngFrom(seed) { 20 if (typeof seed === "function") return seed; 21 let h = 2166136261; const s = String(seed == null ? 1 : seed); 22 for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619); 23 return mulberry32(h >>> 0); 24} 25 26const scratch = (w, h) => { const c = document.createElement("canvas"); c.width = w; c.height = h; return c; }; 27const luma = (d, i) => (d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114) / 255; 28 29// Pixel sort (Asendorf): sort contiguous runs whose luma sits in [low, high]. 30export function pixelSort(canvas, p = {}) { 31 const low = p.low ?? 0.25, high = p.high ?? 0.8, vertical = p.axis === "col"; 32 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 33 const img = ctx.getImageData(0, 0, w, h), d = img.data; 34 const idx = vertical ? (k, line) => (k * w + line) * 4 : (k, line) => (line * w + k) * 4; 35 const lines = vertical ? w : h, len = vertical ? h : w; 36 for (let line = 0; line < lines; line++) { 37 let start = -1; 38 for (let k = 0; k <= len; k++) { 39 const l = k < len ? luma(d, idx(k, line)) : -1; 40 const inBand = k < len && l >= low && l <= high; 41 if (inBand && start < 0) start = k; 42 else if (!inBand && start >= 0) { 43 const run = []; 44 for (let j = start; j < k; j++) { const i = idx(j, line); run.push([d[i], d[i + 1], d[i + 2], d[i + 3], luma(d, i)]); } 45 run.sort((a, b) => a[4] - b[4]); 46 for (let j = start; j < k; j++) { const i = idx(j, line), c = run[j - start]; d[i] = c[0]; d[i + 1] = c[1]; d[i + 2] = c[2]; d[i + 3] = c[3]; } 47 start = -1; 48 } 49 } 50 } 51 ctx.putImageData(img, 0, 0); 52} 53 54// RGB shift / chromatic aberration. 55export function rgbShift(canvas, p = {}) { 56 const dx = Math.round(p.dx ?? 6), dy = Math.round(p.dy ?? 0); 57 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 58 const s = ctx.getImageData(0, 0, w, h).data, out = ctx.createImageData(w, h), o = out.data; 59 const cl = (v, m) => (v < 0 ? 0 : v > m ? m : v); 60 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 61 const i = (y * w + x) * 4; 62 const ri = (cl(y + dy, h - 1) * w + cl(x + dx, w - 1)) * 4; 63 const bi = (cl(y - dy, h - 1) * w + cl(x - dx, w - 1)) * 4; 64 o[i] = s[ri]; o[i + 1] = s[i + 1]; o[i + 2] = s[bi + 2]; o[i + 3] = 255; 65 } 66 ctx.putImageData(out, 0, 0); 67} 68 69// Databend: shift the raw RGB byte stream (a non-multiple of 3 tears channels 70// diagonally), with an optional XOR mask â the classic corrupt-the-bytes look. 71export function databend(canvas, p = {}) { 72 const shift = Math.max(1, Math.round(p.shift ?? 13)), xor = (p.xor | 0) & 255; 73 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height, n = w * h; 74 const img = ctx.getImageData(0, 0, w, h), d = img.data; 75 const rgb = new Uint8Array(n * 3); 76 for (let i = 0, j = 0; i < n; i++) { const k = i * 4; rgb[j++] = d[k]; rgb[j++] = d[k + 1]; rgb[j++] = d[k + 2]; } 77 const len = rgb.length, out = new Uint8Array(len); 78 for (let i = 0; i < len; i++) { let v = rgb[(i + shift) % len]; if (xor) v ^= xor; out[i] = v; } 79 for (let i = 0, j = 0; i < n; i++) { const k = i * 4;
79 d[k] = out[j++]; d[k + 1] = out[j++]; d[k + 2] = out[j++]; } 80 ctx.putImageData(img, 0, 0); 81} 82 83// Slice glitch: displace random horizontal bands. 84export function slice(canvas, p = {}) { 85 const count = Math.max(1, Math.round(p.count ?? 9)), maxShift = p.maxShift ?? 0.14, rng = rngFrom(p.seed); 86 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 87 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 88 ctx.clearRect(0, 0, w, h); 89 let y = 0; 90 for (let nb = 0; nb < count && y < h; nb++) { 91 const bh = Math.max(2, Math.round((h / count) * (0.4 + rng() * 1.2))); 92 const sx = Math.round((rng() * 2 - 1) * maxShift * w); 93 ctx.drawImage(tmp, 0, y, w, bh, sx, y, w, bh); 94 if (sx > 0) ctx.drawImage(tmp, w - sx, y, sx, bh, 0, y, sx, bh); 95 else if (sx < 0) ctx.drawImage(tmp, 0, y, -sx, bh, w + sx, y, -sx, bh); 96 y += bh; 97 } 98 if (y < h) ctx.drawImage(tmp, 0, y, w, h - y, 0, y, w, h - y); 99} 100 101// Wave / ripple displacement. 102export function wave(canvas, p = {}) { 103 const amp = p.amp ?? 0.03, freq = p.freq ?? 6, phase = p.phase ?? 0, vertical = p.axis === "col"; 104 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 105 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 106 ctx.clearRect(0, 0, w, h); 107 if (!vertical) for (let y = 0; y < h; y++) { const dx = Math.sin((y / h) * freq * 6.2832 + phase) * amp * w; ctx.drawImage(tmp, 0, y, w, 1, dx, y, w, 1); } 108 else for (let x = 0; x < w; x++) { const dy = Math.sin((x / w) * freq * 6.2832 + phase) * amp * h; ctx.drawImage(tmp, x, 0, 1, h, x, dy, 1, h); } 109} 110 111// Mirror / kaleidoscope symmetry. 112export function mirror(canvas, p = {}) { 113 const mode = p.mode || "quad", ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 114 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 115 ctx.clearRect(0, 0, w, h); 116 const q = (sx, sy) => { ctx.save(); ctx.translate(sx < 0 ? w : 0, sy < 0 ? h : 0); ctx.scale(sx, sy); ctx.drawImage(tmp, 0, 0, w / 2, h / 2, 0, 0, w / 2, h / 2); ctx.restore(); }; 117 if (mode === "x") { ctx.drawImage(tmp, 0, 0, w / 2, h, 0, 0, w / 2, h); ctx.save(); ctx.translate(w, 0); ctx.scale(-1, 1); ctx.drawImage(tmp, 0, 0, w / 2, h, 0, 0, w / 2, h); ctx.restore(); } 118 else if (mode === "y") { ctx.drawImage(tmp, 0, 0, w, h / 2, 0, 0, w, h / 2); ctx.save(); ctx.translate(0, h); ctx.scale(1, -1); ctx.drawImage(tmp, 0, 0, w, h / 2, 0, 0, w, h / 2); ctx.restore(); } 119 else { q(1, 1); q(-1, 1); q(1, -1); q(-1, -1); } 120} 121 122// Echo / ghost feedback. 123export function echo(canvas, p = {}) { 124 const dx = p.dx ?? 14, dy = p.dy ?? 8, alpha = p.alpha ?? 0.5; 125 const ctx = canvas.getContext("2d"); 126 const tmp = scratch(canvas.width, canvas.height); tmp.getContext("2d").drawImage(canvas, 0, 0); 127 ctx.save(); ctx.globalAlpha = Math.max(0, Math.min(1, alpha)); ctx.globalCompositeOperation = "lighter"; 128 ctx.drawImage(tmp, dx, dy); ctx.restore(); 129} 130 131export function invert(canvas) { 132 const ctx = canvas.getContext("2d"), img = ctx.getImageData(0, 0, canvas.width, canvas.height), d = img.data; 133 for (let i = 0; i < d.length; i += 4) { d[i] = 255 - d[i]; d[i + 1] = 255 - d[i + 1]; d[i + 2] = 255 - d[i + 2]; } 134 ctx.putImageData(img, 0, 0); 135} 136 137export function posterize(canvas, p = {}) { 138 const levels = Math.max(2, Math.round(p.levels ?? 4)), step = 255 / (levels - 1); 139 const ctx = canvas.getContext("2d"), img = ctx.getImageData(0, 0, canvas.width, canvas.height), d = img.data; 140 for (let i = 0; i < d.length; i += 4) { d[i] = Math.round(d[i] / step) * step; d[i + 1] = Math.round(d[i + 1] / step) * step; d[i + 2] = Math.round(d[i + 2] / step) * step; } 141 ctx.putImageData(img, 0, 0); 142} 143 144// 4x4 ordered Bayer matrix (normalised thresholds), shared by dither. 145const BAYER4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]; 146 147// Ordered (Bayer) dither to N levels per channel â the classic 8-bit / PS1 148// gradient break-up, applied AFTER the engine so it stacks with anything. 149export function dither(canvas, p = {}) { 150 const L = Math.max(2, Math.round(p.levels ?? 4)); 151 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 152 const img = ctx.getImageData(0, 0, w, h), d = img.data; 153 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 154 const m = (BAYER4[(x & 3) + (y & 3) * 4] + 0.5) / 16, i = (y * w + x) * 4; 155 for (let c = 0; c < 3; c++) {
156 const scaled = (d[i + c] / 255) * (L - 1), lower = Math.floor(scaled); 157 d[i + c] = ((scaled - lower > m ? lower + 1 : lower) / (L - 1)) * 255; 158 } 159 } 160 ctx.putImageData(img, 0, 0); 161} 162 163// Halftone dot-screen: bright cells become bigger dots on ink â a print / 164// op-art register (Zain's textile + plotter-density DNA). 165export function halftone(canvas, p = {}) { 166 const cell = Math.max(3, Math.round(p.cell ?? 6)), mono = p.color === "mono"; 167 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 168 const tmp = scratch(w, h), tctx = tmp.getContext("2d"); tctx.drawImage(canvas, 0, 0); 169 const s = tctx.getImageData(0, 0, w, h).data; 170 ctx.fillStyle = "#07070c"; ctx.fillRect(0, 0, w, h); 171 for (let cy = 0; cy < h; cy += cell) for (let cx = 0; cx < w; cx += cell) { 172 const mx = Math.min(w - 1, cx + (cell >> 1)), my = Math.min(h - 1, cy + (cell >> 1)), i = (my * w + mx) * 4; 173 const l = luma(s, i), r = l * (cell * 0.62); 174 if (r < 0.35) continue; 175 ctx.fillStyle = mono ? "#e9e6df" : `rgb(${s[i]},${s[i + 1]},${s[i + 2]})`; 176 ctx.beginPath(); ctx.arc(cx + cell / 2, cy + cell / 2, r, 0, 6.2832); ctx.fill(); 177 } 178} 179 180// ASCII / text-mode: each cell becomes a glyph chosen by luminance. Colour 181// modes cover terminal phosphor (green/amber), ink, or the source tint. 182const ASCII_RAMP = " .:-=+*oz#%@"; 183export function ascii(canvas, p = {}) { 184 const cell = Math.max(5, Math.round(p.cell ?? 10)), mode = p.color || "source"; 185 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 186 const tmp = scratch(w, h), tctx = tmp.getContext("2d"); tctx.drawImage(canvas, 0, 0); 187 const s = tctx.getImageData(0, 0, w, h).data; 188 ctx.fillStyle = "#05050a"; ctx.fillRect(0, 0, w, h); 189 ctx.font = "700 " + cell + "px " + "ui-monospace, monospace"; 190 ctx.textBaseline = "top"; 191 const tint = mode === "green" ? [120, 255, 150] : mode === "amber" ? [255, 176, 64] : mode === "ink" ? [233, 230, 223] : null; 192 for (let cy = 0; cy < h; cy += cell) for (let cx = 0; cx < w; cx += cell) { 193 let lr = 0, lg = 0, lb = 0, ll = 0, n = 0; 194 for (let y = cy; y < cy + cell && y < h; y += 2) for (let x = cx; x < cx + cell && x < w; x += 2) { 195 const i = (y * w + x) * 4; lr += s[i]; lg += s[i + 1]; lb += s[i + 2]; ll += luma(s, i); n++; 196 } 197 if (!n) continue; 198 const l = ll / n, ch = ASCII_RAMP[Math.min(ASCII_RAMP.length - 1, Math.floor(l * ASCII_RAMP.length))]; 199 if (ch === " ") continue; 200 if (tint) { const g = 0.5 + 0.5 * l; ctx.fillStyle = `rgb(${tint[0] * g | 0},${tint[1] * g | 0},${tint[2] * g | 0})`; } 201 else ctx.fillStyle = `rgb(${(lr / n) | 0},${(lg / n) | 0},${(lb / n) | 0})`; 202 ctx.fillText(ch, cx, cy); 203 } 204} 205
206// CRT scanlines as a stackable pass (the engine has its own; this lets you 207// stack a heavier line over any effect chain). 208export function scanlines(canvas, p = {}) { 209 const strength = Math.max(0, Math.min(0.9, p.strength ?? 0.4)), gap = Math.max(2, Math.round(p.gap ?? 2)); 210 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 211 const img = ctx.getImageData(0, 0, w, h), d = img.data; 212 for (let y = 0; y < h; y++) { 213 const f = (y % gap === 0) ? (1 - strength) : (1 + strength * 0.12); 214 for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; d[i] *= f; d[i + 1] *= f; d[i + 2] *= f; } 215 } 216 ctx.putImageData(img, 0, 0); 217} 218 219// NTSC / composite chroma bleed: smear R and B horizontally, keep luma sharp â 220// the color-fringe of a VHS pause or a PS2 over composite. 221export function bleed(canvas, p = {}) { 222 const rad = Math.max(1, Math.round(p.radius ?? 6)); 223 const ctx = canvas.getContext("2d"), w = canvas.width, h = canvas.height; 224 const img = ctx.getImageData(0, 0, w, h), d = img.data, win = rad * 2 + 1; 225 for (let y = 0; y < h; y++) { 226 let sr = 0, sb = 0; const base = y * w; 227 for (let x = 0; x < Math.min(rad, w); x++) { const i = (base + x) * 4; sr += d[i]; sb += d[i + 2]; } 228 for (let x = 0; x < w; x++) { 229 const add = x + rad, sub = x - rad - 1; 230 if (add < w) { const ia = (base + add) * 4; sr += d[ia]; sb += d[ia + 2]; } 231 if (sub >= 0) { const is = (base + sub) * 4; sr -= d[is]; sb -= d[is + 2]; } 232 const lo = Math.max(0, x - rad), hi = Math.min(w - 1, x + rad), cnt = hi - lo + 1, i = (base + x) * 4; 233 d[i] = sr / cnt; d[i + 2] = sb / cnt; 234 } 235 } 236 ctx.putImageData(img, 0, 0); 237} 238 239// --------------------------------------------------------------------------- 240// "Feel" modifiers â a soft/hard/angular/wavy/goopy/bubbly/sparkly/starry rack, 241// each an original canvas-2D op built from a real, sourced technique (Rec.709 242// bloom, unsharp mask, Voronoi crystallize, orthogonal multi-octave sine, 243// iq domain warp, spherical refraction, golden-angle cross-glints, hashed star 244// field) and scaled by a single `amount` in [0,1] so a whisper and a scream are 245// the same control. Sources in project-docs; the transform into this idiom is ours. 246// --------------------------------------------------------------------------- 247const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x); 248function blurCanvas(src, px) { 249 const c = scratch(src.width, src.height), x = c.getContext("2d"); 250 x.filter = "blur(" + px + "px)"; x.drawImage(src, 0, 0); x.filter = "none"; return c; 251} 252// cheap value-noise fBm for domain warping 253function _h2(x, y) { const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return n - Math.floor(n); } 254function _vnoise(x, y) { 255 const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi; 256 const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf); 257 const a = _h2(xi, yi), b = _h2(xi + 1, yi), c = _h2(xi, yi + 1), d = _h2(xi + 1, yi + 1); 258 return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v; 259} 260function _fbm(x, y, oct) { let s = 0, amp = 0.5, f = 1; for (let i = 0; i < oct; i++) { s += amp * _vnoise(x * f, y * f); f *= 2; amp *= 0.5; } return s; } 261 262// soft â Orton dreamy glow: screen-blend a gaussian-blurred copy over the base. 263export function soften(canvas, p = {}) { 264 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 265 const b = blurCanvas(canvas, 2 + a * 20), ctx = canvas.getContext("2d"); 266 ctx.save(); ctx.globalAlpha = Math.min(1, 0.35 + a * 0.5); ctx.globalCompositeOperation = "screen"; 267 ctx.drawImage(b, 0, 0); ctx.restore(); 268} 269 270// hard â unsharp mask: out = src + amount*(src - blur), per channel.
271export function sharpen(canvas, p = {}) { 272 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 273 const strength = a * 2.4, w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 274 const b = blurCanvas(canvas, 1.0 + a * 2.0).getContext("2d").getImageData(0, 0, w, h).data; 275 const img = ctx.getImageData(0, 0, w, h), d = img.data; 276 for (let i = 0; i < d.length; i += 4) for (let c = 0; c < 3; c++) { 277 const v = d[i + c] + strength * (d[i + c] - b[i + c]); d[i + c] = v < 0 ? 0 : v > 255 ? 255 : v; 278 } 279 ctx.putImageData(img, 0, 0); 280} 281 282// angular â Voronoi crystallize: nearest jittered site (argmin sq-dist), flat-fill by region average. 283export function crystallize(canvas, p = {}) { 284 const a = clamp01(p.amount ?? 0.5), rng = rngFrom(p.seed); 285 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 286 const cell = Math.max(6, Math.round(8 + a * 46)); 287 const gx = Math.ceil(w / cell) + 1, gy = Math.ceil(h / cell) + 1, gn = gx * gy; 288 const sx = new Float32Array(gn), sy = new Float32Array(gn); 289 for (let j = 0; j < gy; j++) for (let i = 0; i < gx; i++) { const k = j * gx + i; sx[k] = (i + rng()) * cell; sy[k] = (j + rng()) * cell; } 290 const img = ctx.getImageData(0, 0, w, h), d = img.data; 291 const sr = new Float32Array(gn), sg = new Float32Array(gn), sb = new Float32Array(gn), cn = new Float32Array(gn), idx = new Int32Array(w * h); 292 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 293 const ci = Math.min(gx - 1, (x / cell) | 0), cj = Math.min(gy - 1, (y / cell) | 0); 294 let best = -1, bd = 1e18; 295 for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) { 296 const ii = ci + di, jj = cj + dj; if (ii < 0 || jj < 0 || ii >= gx || jj >= gy) continue; 297 const k = jj * gx + ii, ddx = x - sx[k], ddy = y - sy[k], dd = ddx * ddx + ddy * ddy; 298 if (dd < bd) { bd = dd; best = k; } 299 } 300 const pi = (y * w + x) * 4; idx[y * w + x] = best; sr[best] += d[pi]; sg[best] += d[pi + 1]; sb[best] += d[pi + 2]; cn[best]++; 301 } 302 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { const k = idx[y * w + x], c = cn[k] || 1, pi = (y * w + x) * 4; d[pi] = sr[k] / c; d[pi + 1] = sg[k] / c; d[pi + 2] = sb[k] / c; } 303 ctx.putImageData(img, 0, 0); 304} 305 306// wavy â orthogonal multi-octave sine displacement (dx from y, dy from x). 307export function wavy(canvas, p = {}) { 308 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 309 const t = p.phase ?? 0, w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 310 const A0 = a * h * 0.06, f0 = p.freq ?? 3, TAU = 6.2831853; 311 const dxY = new Float32Array(h), dyX = new Float32Array(w); 312 for (let y = 0; y < h; y++) { let s = 0, A = A0, f = f0; for (let i = 0; i < 3; i++) { s += A * Math.sin(TAU * f * (y / h) + t * (0.6 + 0.2 * i)); A *= 0.5; f *= 2; } dxY[y] = s; } 313 for (let x = 0; x < w; x++) { let s = 0, A = A0, f = f0; for (let i = 0; i < 3; i++) { s += A * Math.sin(TAU * f * (x / w) + t * (0.7 + 0.2 * i)); A *= 0.5; f *= 2; } dyX[x] = s; } 314 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 315 const src = tmp.getContext("2d").getImageData(0, 0, w, h).data, out = ctx.createImageData(w, h), o = out.data; 316 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 317 let sxp = (x + dxY[y]) | 0, syp = (y + dyX[x]) | 0; 318 sxp = sxp < 0 ? 0 : sxp >= w ? w - 1 : sxp; syp = syp < 0 ? 0 : syp >= h ? h - 1 : syp; 319 const si = (syp * w + sxp) * 4, di = (y * w + x) * 4; 320 o[di] = src[si]; o[di + 1] = src[si + 1]; o[di + 2] = src[si + 2]; o[di + 3] = 255; 321 } 322 ctx.putImageData(out, 0, 0); 323} 324 325// goopy â iq domain warp: sample src at uv + fbm-driven offset (coarse field, bilinear up). 326export function goop(canvas, p = {}) { 327 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 328 const t = p.phase ?? 0, w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 329 const amp = a * Math.min(w, h) * 0.16, scale = p.scale ?? 0.006, step = 4; 330 const gw = Math.ceil(w / step) + 2, gh = Math.ceil(h / step) + 2; 331 const DX = new Float32Array(gw * gh), DY = new Float32Array(gw * gh); 332 for (let j = 0; j < gh; j++) for (let i = 0;
332 i < gw; i++) { 333 const x = i * step * scale, y = j * step * scale, k = j * gw + i; 334 DX[k] = amp * (_fbm(x + 1.7, y + 9.2 + t * 0.08, 4) - 0.5); 335 DY[k] = amp * (_fbm(x + 8.3, y + 2.8 - t * 0.08, 4) - 0.5); 336 } 337 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 338 const src = tmp.getContext("2d").getImageData(0, 0, w, h).data, out = ctx.createImageData(w, h), o = out.data; 339 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 340 const gi = x / step, gj = y / step, i0 = gi | 0, j0 = gj | 0, fx = gi - i0, fy = gj - j0, k = j0 * gw + i0; 341 const dx = DX[k] + (DX[k + 1] - DX[k]) * fx + (DX[k + gw] - DX[k]) * fy; 342 const dy = DY[k] + (DY[k + 1] - DY[k]) * fx + (DY[k + gw] - DY[k]) * fy; 343 let sxp = (x + dx) | 0, syp = (y + dy) | 0; 344 sxp = sxp < 0 ? 0 : sxp >= w ? w - 1 : sxp; syp = syp < 0 ? 0 : syp >= h ? h - 1 : syp; 345 const si = (syp * w + sxp) * 4, di = (y * w + x) * 4; 346 o[di] = src[si]; o[di + 1] = src[si + 1]; o[di + 2] = src[si + 2]; o[di + 3] = 255; 347 } 348 ctx.putImageData(out, 0, 0); 349} 350 351// bubbly â drifting metaball bubbles, each a spherical refraction lens with a Fresnel rim. 352export function bubbly(canvas, p = {}) { 353 const a = clamp01(p.amount ?? 0.5), rng = rngFrom(p.seed), t = p.phase ?? 0; 354 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"), m = Math.min(w, h), N = 4 + Math.round(a * 9), ior = 1.33; 355 const bx = [], by = [], br = []; 356 for (let i = 0; i < N; i++) { 357 const ph = rng() * 6.283, sp = 0.2 + rng() * 0.5, ax = 0.2 + rng() * 0.6, ay = 0.2 + rng() * 0.6; 358 bx.push((0.5 + 0.4 * Math.sin(t * sp + ph)) * w * 0.0 + ax * w); 359 by.push(ay * h + Math.sin(t * sp * 0.8 + ph) * 0.05 * h); 360 bx[i] = ax * w + Math.cos(t * sp + ph) * 0.06 * w; 361 br.push((0.07 + 0.13 * rng()) * m * (0.7 + a)); 362 } 363 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 364 const src = tmp.getContext("2d").getImageData(0, 0, w, h).data, img = ctx.getImageData(0, 0, w, h), d = img.data; 365 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 366 let best = -1, bnd = 2; 367 for (let i = 0; i < N; i++) { const nd = Math.hypot(x - bx[i], y - by[i]) / br[i]; if (nd < 1 && nd < bnd) { bnd = nd; best = i; } } 368 const di = (y * w + x) * 4; 369 if (best < 0) continue; 370 const dx = x - bx[best], dy = y - by[best], nd = bnd, z = Math.sqrt(1 - nd * nd); 371 const mag = 1 + (1 / ior - 1) * z; 372 let sxp = (bx[best] + dx * mag) | 0, syp = (by[best] + dy * mag) | 0; 373 sxp = sxp < 0 ? 0 : sxp >= w ? w - 1 : sxp; syp = syp < 0 ? 0 : syp >= h ? h - 1 : syp; 374 const si = (syp * w + sxp) * 4, rim = Math.pow(nd, 3.5) * 200 * (0.4 + a); 375 d[di] = Math.min(255, src[si] + rim * 0.7); d[di + 1] = Math.min(255, src[si + 1] + rim * 0.9); d[di + 2] = Math.min(255, src[si + 2] + rim); 376 } 377 ctx.putImageData(img, 0, 0); 378} 379 380// sparkly â cross-glints seeded on the brightest pixel of each grid cell (golde
380n rays, additive). 381export function sparkle(canvas, p = {}) { 382 const a = clamp01(p.amount ?? 0.5), t = p.phase ?? 0, w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 383 const grid = Math.max(10, Math.round(30 - a * 16)), T = 0.6, pts = a < 0.5 ? 4 : 6; 384 const data = ctx.getImageData(0, 0, w, h).data; 385 ctx.save(); ctx.globalCompositeOperation = "lighter"; ctx.lineCap = "round"; 386 for (let cy = 0; cy < h; cy += grid) for (let cx = 0; cx < w; cx += grid) { 387 let bl = 0, bx = -1, by = -1; 388 for (let y = cy; y < cy + grid && y < h; y += 2) for (let x = cx; x < cx + grid && x < w; x += 2) { 389 const i = (y * w + x) * 4, l = (data[i] * 0.2126 + data[i + 1] * 0.7152 + data[i + 2] * 0.0722) / 255; 390 if (l > bl) { bl = l; bx = x; by = y; } 391 } 392 if (bl < T || bx < 0) continue; 393 const tw = 0.55 + 0.45 * Math.sin(t * 3 + (bx + by) * 0.05), len = grid * (0.7 + a) * (0.5 + bl - T) * (0.6 + 0.4 * tw); 394 if (len < 2) continue; 395 for (let k = 0; k < pts; k++) { 396 const ang = t * 0.3 + 6.2831853 * k / pts, ex = bx + Math.cos(ang) * len, ey = by + Math.sin(ang) * len; 397 const g = ctx.createLinearGradient(bx, by, ex, ey); 398 g.addColorStop(0, "rgba(255,250,235," + (0.5 * tw).toFixed(3) + ")"); g.addColorStop(1, "rgba(255,250,235,0)"); 399 ctx.strokeStyle = g; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(ex, ey); ctx.stroke(); 400 } 401 } 402 ctx.restore(); 403} 404 405// starry â additive procedural star field (hashed grid + radial falloff + twinkle) over the canvas. 406export function starry(canvas, p = {}) { 407 const a = clamp01(p.amount ?? 0.5), t = p.phase ?? 0, w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 408 const density = 8 + Math.round(a * 30), th = 1 - (0.28 + a * 0.5), cw = w / density, ch = h / density; 409 const img = ctx.getImageData(0, 0, w, h), d = img.data, rad = Math.max(cw, ch) * 0.42; 410 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 411 const ci = (x / cw) | 0, cj = (y / ch) | 0, b = _h2(ci, cj + 19.1); 412 if (b < th) continue; 413 const spx = (ci + _h2(ci, cj)) * cw, spy = (cj + _h2(ci + 7.3, cj)) * ch; 414 const dist = Math.hypot(x - spx, y - spy), g = Math.max(0, 1 - dist / rad); 415 if (g <= 0) continue; 416 const tw = 0.55 + 0.45 * Math.sin(t * 2 + b * 25), add = Math.pow(g, 4) * (b - th) / (1 - th) * tw * 255; 417 const i = (y * w + x) * 4, warm = 0.85 + 0.15 * _h2(ci + 3.1, cj + 5.7); 418 d[i] = Math.min(255, d[i] + add); d[i + 1] = Math.min(255, d[i + 1] + add * warm); d[i + 2] = Math.min(255, d[i + 2] + add * (1.1 - 0.2 * warm)); 419 } 420 ctx.putImageData(img, 0, 0); 421} 422 423// melt â columns drip downward by luminance, the wet-paint slide. 424export function melt(canvas, p = {}) { 425 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 426 const rng = rngFrom(p.seed), t = p.phase ?? 0; 427 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 428 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 429 const src = tmp.getContext("2d").getImageData(0, 0, w, h).data; 430 const img = ctx.getImageData(0, 0, w, h), d = img.data; 431 const maxDrip = a * h * 0.42; 432 for (let x = 0; x < w; x++) { 433 // each column drips by its own brightness plus a seeded wobble 434 let acc = 0, n = 0; 435 for (let y = 0; y < h; y += 6) { acc += luma(src, (y * w + x) * 4); n++; } 436 const bright = acc / (n || 1); 437 const drip = Math.round(maxDrip * (0.25 + bright * 0.75) * (0.55 + 0.45 * Math.sin(x * 0.07 + rng() * 6.28 + t * 0.6))); 438 if (drip <= 0) continue; 439 for (let y = h - 1; y >= 0; y--) { 440 const sy = y - drip < 0 ? 0 : y - drip, si = (sy * w + x) * 4, di = (y * w + x) * 4; 441 d[di] = src[si]; d[di + 1] = src[si + 1]; d[di + 2] = src[si + 2]; 442 } 443 } 444 ctx.putImageData(img, 0, 0); 445} 446 447// kaleido â true polar fold: N mirrored wedges around the centre. 448export function kaleido(canvas, p = {}) { 449 const a = clamp01(p.amount ?? 0.5); 450 const seg = Math.max(2, Math.round(2 + a * 10)), t = p.phase ?? 0; 451 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 452 const tmp = scratch(w, h); tmp.getContext("2d").drawImage(canvas, 0, 0); 453 const src = tmp.getContext("2d").getImageData(0, 0, w, h).data; 454 const out = ctx.createImageData(w, h), o = out.data; 455 const cx = w / 2, cy = h / 2, wedge = Math.PI * 2 / seg, spin = t * 0.15; 456 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 457 const dx = x - cx, dy = y - cy, r = Math.hypot(dx, dy); 458 let ang = Math.atan2(dy, dx) + spin; 459 ang = ((ang % wedge) + wedge) % wedge; // fold into one wedge 460 if (ang > wedge / 2) ang = wedge - ang; // mirror it
461 let sx = Math.round(cx + Math.cos(ang) * r), sy = Math.round(cy + Math.sin(ang) * r); 462 sx = sx < 0 ? 0 : sx >= w ? w - 1 : sx; sy = sy < 0 ? 0 : sy >= h ? h - 1 : sy; 463 const si = (sy * w + sx) * 4, di = (y * w + x) * 4; 464 o[di] = src[si]; o[di + 1] = src[si + 1]; o[di + 2] = src[si + 2]; o[di + 3] = 255; 465 } 466 ctx.putImageData(out, 0, 0); 467} 468 469// prism â radial chromatic dispersion: channels split along the radius. 470export function prism(canvas, p = {}) { 471 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 472 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 473 const img = ctx.getImageData(0, 0, w, h), s = img.data, out = ctx.createImageData(w, h), o = out.data; 474 const cx = w / 2, cy = h / 2, k = a * 0.09; 475 const px = (x, y, c) => { const xi = x < 0 ? 0 : x >= w ? w - 1 : x | 0, yi = y < 0 ? 0 : y >= h ? h - 1 : y | 0; return s[(yi * w + xi) * 4 + c]; }; 476 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 477 const dx = x - cx, dy = y - cy, di = (y * w + x) * 4; 478 o[di] = px(cx + dx * (1 + k), cy + dy * (1 + k), 0); 479 o[di + 1] = s[di + 1]; 480 o[di + 2] = px(cx + dx * (1 - k), cy + dy * (1 - k), 2); 481 o[di + 3] = 255; 482 } 483 ctx.putImageData(out, 0, 0); 484} 485 486// neon â Sobel edges, colourised and added back as glowing wire. 487export function neon(canvas, p = {}) { 488 const a = clamp01(p.amount ?? 0.5); if (a <= 0.002) return; 489 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 490 const img = ctx.getImageData(0, 0, w, h), d = img.data; 491 const L = new Float32Array(w * h); 492 for (let i = 0, j = 0; i < d.length; i += 4, j++) L[j] = luma(d, i); 493 const edge = new Float32Array(w * h); 494 let peak = 0.0001; 495 for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) { 496 const i = y * w + x; 497 const gx = -L[i - w - 1] - 2 * L[i - 1] - L[i + w - 1] + L[i - w + 1] + 2 * L[i + 1] + L[i + w + 1]; 498 const gy = -L[i - w - 1] - 2 * L[i - w] - L[i - w + 1] + L[i + w - 1] + 2 * L[i + w] + L[i + w + 1]; 499 const g = Math.sqrt(gx * gx + gy * gy); edge[i] = g; if (g > peak) peak = g; 500 } 501 const gain = a * 2.1; 502 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 503 const i = y * w + x, e = Math.pow(edge[i] / peak, 0.75) * gain, k = i * 4; 504 if (e <= 0.002) continue; 505 d[k] = Math.min(255, d[k] * (1 - a * 0.35) + e * 255 * 1.0); 506 d[k + 1] = Math.min(255, d[k + 1] * (1 - a * 0.35) + e * 255 * 0.35); 507 d[k + 2] = Math.min(255, d[k + 2] * (1 - a * 0.35) + e * 255 * 0.9); 508 } 509 ctx.putImageData(img, 0, 0); 510} 511 512// mosaic â set the picture in tesserae. Built the way a mosaicist works rather 513// than as a pixel grid: tiles are irregular and jittered, separated by real 514// grout, each tile carries one averaged colour with its own tone variation, and 515// each is bedded at its own slight angle so a raking light glints across the 516// field tile by tile (the Byzantine gold-ground trick). Modes: stone (matte 517// smalti), gold (gold-leaf ground with travelling glint), glass (translucent, 518// brighter grout), trencadis (Gaudi: bigger irregular shards, wide grout). 519export function mosaic(canvas, p = {}) { 520 const a = clamp01(p.amount ?? 0.5); 521 const mode = p.mode || "stone"; 522 const rng = rngFrom(p.seed), t = p.phase ?? 0; 523 const w = canvas.width, h = canvas.height, ctx = canvas.getContext("2d"); 524 const shard = mode === "trencadis"; 525 // tile pitch: bigger amount = bigger, bolder tesserae 526 const cell = Math.max(5, Math.round((shard ? 14 : 7) + a * (shard ? 54 : 34))); 527 const jit = shard ? 0.46 : 0.24; // how far a seed strays from its grid slot
528 const grout = (p.grout ?? (shard ? 0.34 : 0.20)) * (0.6 + a * 0.7); 529 530 const gx = Math.ceil(w / cell) + 2, gy = Math.ceil(h / cell) + 2, gn = gx * gy; 531 const sx = new Float32Array(gn), sy = new Float32Array(gn), ang = new Float32Array(gn), ton = new Float32Array(gn); 532 for (let j = 0; j < gy; j++) for (let i = 0; i < gx; i++) { 533 const k = j * gx + i; 534 sx[k] = (i - 0.5 + 0.5 + (rng() - 0.5) * 2 * jit) * cell; 535 sy[k] = (j - 0.5 + 0.5 + (rng() - 0.5) * 2 * jit) * cell; 536 ang[k] = rng() * 6.2831853; // the angle this tile is bedded at 537 ton[k] = 0.82 + rng() * 0.36; // tile-to-tile tone variation 538 } 539 540 const img = ctx.getImageData(0, 0, w, h), d = img.data; 541 const sr = new Float32Array(gn), sg = new Float32Array(gn), sb = new Float32Array(gn), cn = new Float32Array(gn); 542 const idx = new Int32Array(w * h), edge = new Float32Array(w * h); 543 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 544 const ci = Math.min(gx - 1, ((x / cell) | 0) + 1), cj = Math.min(gy - 1, ((y / cell) | 0) + 1); 545 let b1 = -1, d1 = 1e18, d2 = 1e18; 546 for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) { 547 const ii = ci + di, jj = cj + dj; if (ii < 0 || jj < 0 || ii >= gx || jj >= gy) continue; 548 const k = jj * gx + ii, ddx = x - sx[k], ddy = y - sy[k], dd = ddx * ddx + ddy * ddy; 549 if (dd < d1) { d2 = d1; d1 = dd; b1 = k; } else if (dd < d2) { d2 = dd; } 550 } 551 if (b1 < 0) continue; 552 const q = y * w + x, pi = q * 4; 553 idx[q] = b1; 554 // distance to the tile boundary: the classic (d2 - d1) Voronoi edge measure 555 edge[q] = (Math.sqrt(d2) - Math.sqrt(d1)) * 0.5; 556 sr[b1] += d[pi]; sg[b1] += d[pi + 1]; sb[b1] += d[pi + 2]; cn[b1]++; 557 } 558 559 const lx = p.lightX == null ? 0.5 : p.lightX, ly = p.lightY == null ? 0.35 : p.lightY; 560 const lightAng = Math.atan2(ly - 0.5, lx - 0.5) + t * 0.25; 561 const sharp = 2.0 + (p.glint ?? 0.5) * 10; 562 const groutPx = Math.max(0.6, grout * cell * 0.5); 563 const gr = shard ? 22 : 12, gg = shard ? 20 : 11, gb = shard ? 24 : 13; // grout colour 564 565 for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { 566 const q = y * w + x, k = idx[q], c = cn[k] || 1, pi = q * 4; 567 let r = sr[k] / c, g = sg[k] / c, b = sb[k] / c; 568 r *= ton[k]; g *= ton[k]; b *= ton[k]; 569 if (mode === "gold") { 570 // gold leaf: push the tile toward gold, then let its bedding angle catch the light 571 const lum = (r * 0.299 + g * 0.587 + b * 0.114) / 255; 572 r = 255 * (0.20 + lum * 0.86); g = 255 * (0.13 + lum * 0.66); b = 255 * (0.03 + lum * 0.22); 573 const glint = Math.pow(Math.max(0, 0.5 + 0.5 * Math.cos(ang[k] - lightAng)), sharp); 574 r = Math.min(255, r + glint * 205); g = Math.min(255, g + glint * 180); b = Math.min(255, b + glint * 110); 575 } else if (mode === "glass") { 576 const glint = Math.pow(Math.max(0, 0.5 + 0.5 * Math.cos(ang[k] - lightAng)), sharp * 1.4); 577 r = Math.min(255, r * 1.06 + glint * 120); g = Math.min(255, g * 1.06 + glint * 130); b = Math.min(255, b * 1.06 + glint * 150); 578 } 579 // grout: fade the tile out toward its boundary onto the bedding mortar 580 const e = edge[q] < groutPx ? edge[q] / groutPx : 1; 581 const m = e * e * (3 - 2 * e); // smoothstep 582 d[pi] = gr + (r - gr) * m; d[pi + 1] = gg + (g - gg) * m; d[pi + 2] = gb + (b - gb) * m; 583 } 584 ctx.putImageData(img, 0, 0); 585} 586 587// Datamosh, after entro_play's WebGL datamoshing and FFglitch practice: the 588// previous frame smeared along a procedural block motion field, refreshed in 589// periodic bands like sparse I-frames. The caller maintains step.prev as a 590// snapshot of the LAST composed frame, so the smear feeds on its own output. 591function datamosh(canvas, step) { 592 const prev = step.prev; 593 if (!prev || !prev.width) return; 594 const g = canvas.getContext("2d"); 595 const W = canvas.width, H = canvas.height; 596 const amt = step.amount === undefined ? 0.6 : step.amount; 597 const rng = rngFrom((step.seed || "mosh") + "|field"); 598 const bs = Math.max(12, Math.round(Math.min(W, H) / 22)); 599 const t = step.phase || 0; 600 // refresh band: a strip of honest current frame sweeps through, everything 601 // else re-projects the stale frame with per-block drift 602 const bandY = ((t * 0.13) % 1) * H; 603 const bandH = H * (0.10 + 0.25 * (1 - amt)); 604 for (let y = 0; y < H; y += bs) { 605 const inBand = y + bs > bandY && y < bandY + bandH; 606 if (inBand) continue; 607 for (let x = 0; x < W; x += bs) { 608 const n1 = rng(), n2 = rng(); 609 const swirl = Math.sin((x / W) * 5.1 + t * 0.7 + n1 * 6.28) + Math.
609cos((y / H) * 4.3 - t * 0.5 + n2 * 6.28); 610 const mag = amt * (4 + 22 * Math.abs(swirl) * 0.5); 611 const vx = Math.round(Math.cos(n1 * 6.28 + swirl) * mag); 612 const vy = Math.round(Math.sin(n2 * 6.28 - swirl) * mag); 613 const sx = Math.max(0, Math.min(prev.width - bs, x + vx)); 614 const sy = Math.max(0, Math.min(prev.height - bs, y + vy)); 615 g.drawImage(prev, sx, sy, bs, bs, x, y, bs, bs); 616 } 617 } 618} 619 620// Slit scan, after the Kinect slit-scan lineage: each horizontal strip of the 621// output samples a different moment from a short ring of past frames, so 622// motion shears into time. The caller maintains the ring at half resolution. 623function slitscan(canvas, step) { 624 const ring = step.ring, len = step.len || 0, head = step.head || 0; 625 if (!ring || !len) return; 626 const g = canvas.getContext("2d"); 627 const W = canvas.width, H = canvas.height; 628 const amt = step.amount === undefined ? 0.6 : step.amount; 629 const strips = 48; 630 const sh = Math.ceil(H / strips); 631 const col = step.axis === "col"; 632 const n = col ? Math.ceil(W / sh) : strips; 633 for (let i = 0; i < n; i++) { 634 const frac = i / n; 635 const wobble = 0.5 + 0.5 * Math.sin(frac * 6.28 * 2 + (step.phase || 0) * 0.6); 636 const age = Math.min(len - 1, Math.floor(frac * wobble * amt * (len - 1))); 637 const src = ring[(head - age + len * 4) % len]; 638 if (!src || !src.width) continue; 639 if (col) { 640 const x = i * sh, sxs = Math.floor((x / W) * src.width), sws = Math.max(1, Math.floor((sh / W) * src.width)); 641 g.drawImage(src, sxs, 0, sws, src.height, x, 0, sh, H); 642 } else { 643 const y = i * sh, sys = Math.floor((y / H) * src.height), shs = Math.max(1, Math.floor((sh / H) * src.height)); 644 g.drawImage(src, 0, sys, src.width, shs, 0, y, W, sh); 645 } 646 } 647} 648 649export const OPS = { 650 ...OPS_CHEM, ...OPS_OPT, ...OPS_TIME, 651 soften, sharpen, crystallize, wavy, goop, bubbly, sparkle, starry, melt, kaleido, prism, neon, mosaic, 652 dither, halftone, posterize, pixelSort, databend, rgbShift, slice, wave, echo, mirror, ascii, bleed, scanlines, invert, 653 datamosh, slitscan, 654}; 655 656// Metadata for the effects rack UI. Array order IS the apply pipeline 657// (restructure -> warp -> tone -> glitch -> screen -> light overlays); the UI 658// groups the chips into shelves by `cat`, independent of this order. 659export const OP_META = [ 660 { op: "mosaic", label: "Mosaic", cat: "feel", desc: "square tiles averaged from the picture underneath" }, 661 { op: "crystallize", label: "Angular", cat: "feel", desc: "shatters the frame into angular facets" }, 662 { op: "kaleido", label: "Kaleido", cat: "feel", desc: "mirrors a wedge around the centre into radial symmetry" }, 663 { op: "wavy", label: "Wavy", cat: "feel", desc: "rolls the rows on a sine, like heat off tarmac" }, 664 { op: "goop", label: "Goopy", cat: "feel", desc: "drags the pixels into slow viscous runs" }, 665 { op: "melt", label: "Melty", cat: "feel", desc: "lets the picture sag downward from its bright edges" }, 666 { op: "bubbly", label: "Bubbly", cat: "feel", desc: "lifts rounded blisters out of the surface" }, 667 { op: "soften", label: "Soft", cat: "feel", desc: "a diffusion bloom, the picture seen through breath" }, 668 { op: "sharpen", label: "Hard", cat: "feel", desc: "hardens edges by subtracting its own blur" }, 669 { op: "neon", label: "Neon", cat: "feel", desc: "keeps the edges and lights them, drops the fill" }, 670 { op: "prism", label: "Prism", cat: "feel", desc: "splits the frame through a glass wedge" }, 671 { op: "dither", label: "Dither", cat: "tone", desc: "ordered Bayer dither, tone carried by a dot pattern" }, 672 { op: "halftone", label: "Halftone", cat: "tone", desc: "a print screen: tone becomes dot size on a rotated grid" }, 673 { op: "posterize", label: "Posterize", cat: "tone", desc: "collapses the tones to a few flat steps" }, 674 { op: "invert", label: "Invert", cat: "tone", desc: "swaps light for dark; at part strength it greys, so use it near full" }, 675 { op: "pixelSort", label: "Pixel sort", cat: "glitch", desc: "sorts runs of pixels by brightness into smears" }, 676 { op: "databend", label: "Databend", cat: "glitch", desc: "corrupts the bytes as if the file were damaged" }, 677 { op: "rgbShift", label: "RGB shift", cat: "glitch", desc: "pulls the three colour channels apart" }, 678 { op: "slice", label: "Slice", cat: "glitch", desc: "cuts the frame into bands and slides them" }, 679 { op: "wave", label: "Wave", cat: "glitch", desc: "a standing wave displaces each row" }, 680 { op: "datamosh", label: "Datamosh", cat: "glitch", desc: "keeps the motion but loses the frame it belonged to" }, 681 { op: "slitscan", label: "Slit scan", cat: "glitch", desc: "each column samples a different moment" }, 682 { op: "echo", label: "Echo", cat: "glitch", desc: "ghosts a displaced copy at falling opacity" }, 683 { op: "mirror", label: "Mirror", cat: "glitch", desc: "folds one half of the frame onto the other" }, 684 { op: "bleed", label: "Bleed", cat: "tone", desc: "lets the ink run past its own edges" }, 685 { op: "scanlines", label: "Scanlines", cat: "tone", desc: "the dark line between the lit rows of a tube" }, 686 { op: "sparkle", label: "Sparkly", cat: "feel", desc: "scatters specular points across the highlights" }, 687 { op: "starry", label: "Starry", cat: "feel", desc: "seeds a field of small stars over the dark areas" }, 688 ...META_CHEM, ...META_OPT, ...META_TIME, 689]; 690 691// Apply an ordered list of { op, ...params } steps to a canvas, in place. 692/* applyOpsWet(canvas, list, mix) 693 694 A real strength control for the whole rack. Most ops here take their own 695 named parameters (posterize takes `levels`, pixelSort takes `low`/`high`, 696 databend takes `shift`/`xor`) and never read `amount` at all, because the 697 Retro Engine gives each one its own dial. A surface that offers a single 698 strength slider and passes `amount` therefore moves nothing for roughly a 699 third of the rack. 700
701 So strength is applied where it always works: the ops run at full force on a 702 copy, and that copy is composited back over the original at `mix`. Wet over 703 dry. Every op gains a meaningful strength, including the binary ones, and the 704 result is predictable across the whole rack. */ 705export function applyOpsWet(canvas, list, mix = 1) { 706 const m = Math.max(0, Math.min(1, Number(mix))); 707 if (!list || !list.length || m <= 0) return; 708 if (m >= 0.999) { applyOps(canvas, list); return; } 709 const w = canvas.width, h = canvas.height; 710 if (!w || !h) return; 711 const wet = document.createElement("canvas"); 712 wet.width = w; wet.height = h; 713 const wctx = wet.getContext("2d"); 714 if (!wctx) return; 715 wctx.drawImage(canvas, 0, 0); 716 applyOps(wet, list); 717 const ctx = canvas.getContext("2d"); 718 if (!ctx) return; 719 ctx.save(); 720 ctx.setTransform(1, 0, 0, 1, 0, 0); // callers may be mid-transform 721 ctx.globalAlpha = m; 722 ctx.globalCompositeOperation = "source-over"; 723 ctx.drawImage(wet, 0, 0); 724 ctx.restore(); 725} 726 727export function applyOps(canvas, list) { 728 for (const step of list || []) { const fn = OPS[step.op]; if (fn) { try { fn(canvas, step); } catch (_) {} } } 729}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.