Skip to content

Commit 1e561b5

Browse files
gyngclaude
andcommitted
Flip + Noise Generator + Smudge + Teletext: WebGL2 ports
Flip is a trivial coordinate-mirror single-pass. Noise Generator runs Perlin/Simplex/Worley FBM in-shader; the uint32 hash gives a slightly different exact pattern vs the JS reference's float64 multiply but the noise character is preserved. Smudge drops the JS reference's sequential outBuf-accumulator dependency in favour of a pull-model directional motion blur — a close visual approximation without the iteration-order sensitivity. Teletext is two-pass: pass A downsamples to a (2*columns × rows) cell map storing fg/bg colours per cell; pass B samples the cell map, evaluates sub-block luma, and draws gap- darkened pixels along each sub-block's trailing edge. Static max cell dim = 48×48 in shader; extreme configs fall back to JS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f4d41dd commit 1e561b5

8 files changed

Lines changed: 714 additions & 25 deletions

File tree

src/filters/flip.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { ENUM, PALETTE } from "constants/controlTypes";
22
import { nearest } from "palettes";
3-
import { cloneCanvas, fillBufferPixel, getBufferIndex, rgba, paletteGetColor } from "utils";
3+
import { cloneCanvas, fillBufferPixel, getBufferIndex, rgba, paletteGetColor, logFilterBackend } from "utils";
44
import { defineFilter } from "filters/types";
5+
import { applyPalettePassToCanvas, paletteIsIdentity } from "palettes/backend";
6+
import { flipGLAvailable, renderFlipGL } from "./flipGL";
57

68
const MODE = { HORIZONTAL: "HORIZONTAL", VERTICAL: "VERTICAL", BOTH: "BOTH" };
79

@@ -19,14 +21,31 @@ export const defaults = {
1921
palette: { ...optionTypes.palette.default, options: { levels: 256 } }
2022
};
2123

22-
const flipFilter = (input: any, options = defaults) => {
24+
type FlipOptions = typeof defaults & { _webglAcceleration?: boolean };
25+
26+
const flipFilter = (input: any, options: FlipOptions = defaults) => {
2327
const { mode, palette } = options;
28+
const W = input.width, H = input.height;
29+
30+
if (options._webglAcceleration !== false && flipGLAvailable()) {
31+
const flipX = mode === MODE.HORIZONTAL || mode === MODE.BOTH;
32+
const flipY = mode === MODE.VERTICAL || mode === MODE.BOTH;
33+
const rendered = renderFlipGL(input, W, H, flipX, flipY);
34+
if (rendered) {
35+
const identity = paletteIsIdentity(palette);
36+
const out = identity ? rendered : applyPalettePassToCanvas(rendered, W, H, palette);
37+
if (out) {
38+
logFilterBackend("Flip", "WebGL2", `mode=${mode}${identity ? "" : "+palettePass"}`);
39+
return out;
40+
}
41+
}
42+
}
43+
2444
const output = cloneCanvas(input, false);
2545
const inputCtx = input.getContext("2d");
2646
const outputCtx = output.getContext("2d");
2747
if (!inputCtx || !outputCtx) return input;
2848

29-
const W = input.width, H = input.height;
3049
const buf = inputCtx.getImageData(0, 0, W, H).data;
3150
const outBuf = new Uint8ClampedArray(buf.length);
3251

src/filters/flipGL.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
drawPass, ensureTexture, getGLCtx, getQuadVAO, glAvailable,
3+
linkProgram, readoutToCanvas, resizeGLCanvas, uploadSourceTexture,
4+
type Program,
5+
} from "gl";
6+
7+
// Coordinate mirror — horizontal, vertical, or both.
8+
const FS = `#version 300 es
9+
precision highp float;
10+
in vec2 v_uv;
11+
out vec4 fragColor;
12+
uniform sampler2D u_source;
13+
uniform vec2 u_res;
14+
uniform int u_flipX;
15+
uniform int u_flipY;
16+
17+
void main() {
18+
vec2 px = v_uv * u_res;
19+
float jsX = floor(px.x);
20+
float jsY = u_res.y - 1.0 - floor(px.y);
21+
22+
float sx = u_flipX == 1 ? u_res.x - 1.0 - jsX : jsX;
23+
float sy = u_flipY == 1 ? u_res.y - 1.0 - jsY : jsY;
24+
fragColor = texture(u_source, vec2((sx + 0.5) / u_res.x, 1.0 - (sy + 0.5) / u_res.y));
25+
}
26+
`;
27+
28+
type Cache = { prog: Program };
29+
let _cache: Cache | null = null;
30+
const initCache = (gl: WebGL2RenderingContext): Cache => {
31+
if (_cache) return _cache;
32+
_cache = { prog: linkProgram(gl, FS, ["u_source", "u_res", "u_flipX", "u_flipY"] as const) };
33+
return _cache;
34+
};
35+
36+
export const flipGLAvailable = (): boolean => glAvailable();
37+
38+
export const renderFlipGL = (
39+
source: HTMLCanvasElement | OffscreenCanvas,
40+
width: number, height: number,
41+
flipX: boolean, flipY: boolean,
42+
): HTMLCanvasElement | OffscreenCanvas | null => {
43+
const ctx = getGLCtx();
44+
if (!ctx) return null;
45+
const { gl, canvas } = ctx;
46+
const cache = initCache(gl);
47+
const vao = getQuadVAO(gl);
48+
resizeGLCanvas(canvas, width, height);
49+
const sourceTex = ensureTexture(gl, "flip:source", width, height);
50+
uploadSourceTexture(gl, sourceTex, source);
51+
drawPass(gl, null, width, height, cache.prog, () => {
52+
gl.activeTexture(gl.TEXTURE0);
53+
gl.bindTexture(gl.TEXTURE_2D, sourceTex.tex);
54+
gl.uniform1i(cache.prog.uniforms.u_source, 0);
55+
gl.uniform2f(cache.prog.uniforms.u_res, width, height);
56+
gl.uniform1i(cache.prog.uniforms.u_flipX, flipX ? 1 : 0);
57+
gl.uniform1i(cache.prog.uniforms.u_flipY, flipY ? 1 : 0);
58+
}, vao);
59+
return readoutToCanvas(canvas, width, height);
60+
};

src/filters/noiseGenerator.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import {
66
fillBufferPixel,
77
getBufferIndex,
88
rgba,
9-
paletteGetColor
9+
paletteGetColor,
10+
logFilterBackend,
1011
} from "utils";
12+
import { applyPalettePassToCanvas, paletteIsIdentity } from "palettes/backend";
13+
import { noiseGeneratorGLAvailable, renderNoiseGeneratorGL, type NoiseType } from "./noiseGeneratorGL";
1114

1215
const NOISE_TYPE = {
1316
PERLIN: "PERLIN",
@@ -145,17 +148,35 @@ const worleyNoise = (px: number, py: number, seed: number) => {
145148
return Math.sqrt(minDist);
146149
};
147150

148-
const noiseGenerator = (input: any, options = defaults) => {
151+
type NoiseGeneratorOptions = typeof defaults & { _frameIndex?: number; _webglAcceleration?: boolean };
152+
153+
const noiseGenerator = (input: any, options: NoiseGeneratorOptions = defaults) => {
149154
const { type, scale, octaves, seed: seedOpt, colorize, mix, palette } = options;
150-
const frameIndex = (options as { _frameIndex?: number })._frameIndex || 0;
155+
const frameIndex = options._frameIndex || 0;
156+
const W = input.width;
157+
const H = input.height;
158+
159+
if (options._webglAcceleration !== false && noiseGeneratorGLAvailable()) {
160+
const typeInt = type === NOISE_TYPE.SIMPLEX ? 1 : type === NOISE_TYPE.WORLEY ? 2 : 0;
161+
const rendered = renderNoiseGeneratorGL(
162+
input, W, H,
163+
typeInt as NoiseType, scale, octaves, seedOpt, frameIndex, colorize, mix,
164+
);
165+
if (rendered) {
166+
const identity = paletteIsIdentity(palette);
167+
const out = identity ? rendered : applyPalettePassToCanvas(rendered, W, H, palette);
168+
if (out) {
169+
logFilterBackend("Noise Generator", "WebGL2", `type=${type} octaves=${octaves}${identity ? "" : "+palettePass"}`);
170+
return out;
171+
}
172+
}
173+
}
151174

152175
const output = cloneCanvas(input, false);
153176
const inputCtx = input.getContext("2d");
154177
const outputCtx = output.getContext("2d");
155178
if (!inputCtx || !outputCtx) return input;
156179

157-
const W = input.width;
158-
const H = input.height;
159180
const buf = inputCtx.getImageData(0, 0, W, H).data;
160181
const outBuf = new Uint8ClampedArray(buf.length);
161182

src/filters/noiseGeneratorGL.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import {
2+
drawPass, ensureTexture, getGLCtx, getQuadVAO, glAvailable,
3+
linkProgram, readoutToCanvas, resizeGLCanvas, uploadSourceTexture,
4+
type Program,
5+
} from "gl";
6+
7+
// Perlin / Simplex / Worley FBM noise generator mixed over the source.
8+
// The hash uses exact uint32 wrap (vs JS float64 mid-multiply precision
9+
// loss), which shifts the exact pattern but keeps the noise character.
10+
const FS = `#version 300 es
11+
precision highp float;
12+
in vec2 v_uv;
13+
out vec4 fragColor;
14+
uniform sampler2D u_source;
15+
uniform vec2 u_res;
16+
uniform int u_type; // 0 gradient/perlin, 1 simplex, 2 worley
17+
uniform float u_scale;
18+
uniform int u_octaves;
19+
uniform int u_seed;
20+
uniform int u_frame;
21+
uniform int u_colorize;
22+
uniform float u_mix;
23+
24+
uint hashU(int x, int y, int seed) {
25+
uint h = uint(seed) + uint(x) * 374761393u + uint(y) * 668265263u;
26+
h = (h ^ (h >> 13u)) * 1274126177u;
27+
return h ^ (h >> 16u);
28+
}
29+
30+
float hashF(int x, int y, int seed) {
31+
return float(hashU(x, y, seed)) / 4294967296.0;
32+
}
33+
34+
float gradientNoise(float px, float py, int seed) {
35+
int x0 = int(floor(px));
36+
int y0 = int(floor(py));
37+
float fx = px - float(x0);
38+
float fy = py - float(y0);
39+
float u = fx * fx * (3.0 - 2.0 * fx);
40+
float v = fy * fy * (3.0 - 2.0 * fy);
41+
42+
float gxs[4] = float[4]( 1.0, -1.0, 1.0, -1.0);
43+
float gys[4] = float[4]( 1.0, 1.0, -1.0, -1.0);
44+
45+
int h00 = int(hashU(x0, y0, seed) & 3u);
46+
int h10 = int(hashU(x0 + 1, y0, seed) & 3u);
47+
int h01 = int(hashU(x0, y0 + 1, seed) & 3u);
48+
int h11 = int(hashU(x0 + 1, y0 + 1, seed) & 3u);
49+
float n00 = gxs[h00] * (px - float(x0)) + gys[h00] * (py - float(y0));
50+
float n10 = gxs[h10] * (px - float(x0 + 1)) + gys[h10] * (py - float(y0));
51+
float n01 = gxs[h01] * (px - float(x0)) + gys[h01] * (py - float(y0 + 1));
52+
float n11 = gxs[h11] * (px - float(x0 + 1)) + gys[h11] * (py - float(y0 + 1));
53+
float nx0 = n00 + u * (n10 - n00);
54+
float nx1 = n01 + u * (n11 - n01);
55+
return nx0 + v * (nx1 - nx0);
56+
}
57+
58+
float simplexNoise(float px, float py, int seed) {
59+
float F2 = 0.5 * (sqrt(3.0) - 1.0);
60+
float G2 = (3.0 - sqrt(3.0)) / 6.0;
61+
float s = (px + py) * F2;
62+
int i = int(floor(px + s));
63+
int j = int(floor(py + s));
64+
float t = float(i + j) * G2;
65+
float x0 = px - (float(i) - t);
66+
float y0 = py - (float(j) - t);
67+
int i1 = x0 > y0 ? 1 : 0;
68+
int j1 = x0 > y0 ? 0 : 1;
69+
float x1 = x0 - float(i1) + G2;
70+
float y1 = y0 - float(j1) + G2;
71+
float x2 = x0 - 1.0 + 2.0 * G2;
72+
float y2 = y0 - 1.0 + 2.0 * G2;
73+
74+
float gxs[4] = float[4]( 1.0, -1.0, 1.0, -1.0);
75+
float gys[4] = float[4]( 1.0, 1.0, -1.0, -1.0);
76+
77+
float result = 0.0;
78+
int ii[3]; int jj[3]; float xx[3]; float yy[3];
79+
ii[0] = i; jj[0] = j; xx[0] = x0; yy[0] = y0;
80+
ii[1] = i + i1; jj[1] = j + j1; xx[1] = x1; yy[1] = y1;
81+
ii[2] = i + 1; jj[2] = j + 1; xx[2] = x2; yy[2] = y2;
82+
for (int k = 0; k < 3; k++) {
83+
float tval = 0.5 - xx[k] * xx[k] - yy[k] * yy[k];
84+
if (tval < 0.0) continue;
85+
int h = int(hashU(ii[k], jj[k], seed) & 3u);
86+
result += tval * tval * tval * tval * (gxs[h] * xx[k] + gys[h] * yy[k]);
87+
}
88+
return 70.0 * result;
89+
}
90+
91+
float worleyNoise(float px, float py, int seed) {
92+
int ix = int(floor(px));
93+
int iy = int(floor(py));
94+
float minDist = 1e9;
95+
for (int dy = -1; dy <= 1; dy++) {
96+
for (int dx = -1; dx <= 1; dx++) {
97+
int cx = ix + dx;
98+
int cy = iy + dy;
99+
uint h = hashU(cx, cy, seed);
100+
float fpx = float(cx) + float(h & 0xffffu) / 65536.0;
101+
float fpy = float(cy) + float((h >> 16) & 0xffffu) / 65536.0;
102+
float ddx = px - fpx;
103+
float ddy = py - fpy;
104+
float d2 = ddx * ddx + ddy * ddy;
105+
if (d2 < minDist) minDist = d2;
106+
}
107+
}
108+
return sqrt(minDist);
109+
}
110+
111+
float sampleNoise(float px, float py, int seed) {
112+
if (u_type == 0) return gradientNoise(px, py, seed);
113+
if (u_type == 1) return simplexNoise(px, py, seed);
114+
return worleyNoise(px, py, seed);
115+
}
116+
117+
vec3 hslToRgb(float hue, float sat, float lit) {
118+
float c = (1.0 - abs(2.0 * lit - 1.0)) * sat;
119+
float hh = mod(mod(hue, 360.0) + 360.0, 360.0);
120+
float xc = c * (1.0 - abs(mod(hh / 60.0, 2.0) - 1.0));
121+
float m = lit - c * 0.5;
122+
vec3 rgb;
123+
if (hh < 60.0) rgb = vec3(c, xc, 0.0);
124+
else if (hh < 120.0) rgb = vec3(xc, c, 0.0);
125+
else if (hh < 180.0) rgb = vec3(0.0, c, xc);
126+
else if (hh < 240.0) rgb = vec3(0.0, xc, c);
127+
else if (hh < 300.0) rgb = vec3(xc, 0.0, c);
128+
else rgb = vec3(c, 0.0, xc);
129+
return floor((rgb + vec3(m)) * 255.0 + 0.5);
130+
}
131+
132+
void main() {
133+
vec2 px = v_uv * u_res;
134+
float jsX = floor(px.x);
135+
float jsY = u_res.y - 1.0 - floor(px.y);
136+
137+
float value = 0.0;
138+
float amplitude = 1.0;
139+
float frequency = 1.0;
140+
float maxAmp = 0.0;
141+
for (int o = 0; o < 8; o++) {
142+
if (o >= u_octaves) break;
143+
float nx = (jsX / u_scale) * frequency;
144+
float ny = (jsY / u_scale) * frequency;
145+
value += sampleNoise(nx, ny, u_seed + o * 1000 + u_frame * 7) * amplitude;
146+
maxAmp += amplitude;
147+
amplitude *= 0.5;
148+
frequency *= 2.0;
149+
}
150+
151+
float n = clamp((value / max(1e-6, maxAmp) + 1.0) * 0.5, 0.0, 1.0);
152+
153+
vec3 noiseRgb;
154+
if (u_colorize == 1) {
155+
noiseRgb = hslToRgb(n * 360.0, 0.8, 0.5);
156+
} else {
157+
float v = floor(n * 255.0 + 0.5);
158+
noiseRgb = vec3(v);
159+
}
160+
161+
vec4 src = texture(u_source, vec2((jsX + 0.5) / u_res.x, 1.0 - (jsY + 0.5) / u_res.y));
162+
vec3 srcRgb = src.rgb * 255.0;
163+
vec3 outRgb = floor(srcRgb * (1.0 - u_mix) + noiseRgb * u_mix + 0.5);
164+
fragColor = vec4(clamp(outRgb, 0.0, 255.0) / 255.0, src.a);
165+
}
166+
`;
167+
168+
type Cache = { prog: Program };
169+
let _cache: Cache | null = null;
170+
const initCache = (gl: WebGL2RenderingContext): Cache => {
171+
if (_cache) return _cache;
172+
_cache = { prog: linkProgram(gl, FS, [
173+
"u_source", "u_res", "u_type", "u_scale", "u_octaves",
174+
"u_seed", "u_frame", "u_colorize", "u_mix",
175+
] as const) };
176+
return _cache;
177+
};
178+
179+
export const noiseGeneratorGLAvailable = (): boolean => glAvailable();
180+
181+
export type NoiseType = 0 | 1 | 2;
182+
183+
export const renderNoiseGeneratorGL = (
184+
source: HTMLCanvasElement | OffscreenCanvas,
185+
width: number, height: number,
186+
type: NoiseType, scale: number, octaves: number,
187+
seed: number, frame: number, colorize: boolean, mix: number,
188+
): HTMLCanvasElement | OffscreenCanvas | null => {
189+
const ctx = getGLCtx();
190+
if (!ctx) return null;
191+
const { gl, canvas } = ctx;
192+
const cache = initCache(gl);
193+
const vao = getQuadVAO(gl);
194+
resizeGLCanvas(canvas, width, height);
195+
const sourceTex = ensureTexture(gl, "noiseGenerator:source", width, height);
196+
uploadSourceTexture(gl, sourceTex, source);
197+
drawPass(gl, null, width, height, cache.prog, () => {
198+
gl.activeTexture(gl.TEXTURE0);
199+
gl.bindTexture(gl.TEXTURE_2D, sourceTex.tex);
200+
gl.uniform1i(cache.prog.uniforms.u_source, 0);
201+
gl.uniform2f(cache.prog.uniforms.u_res, width, height);
202+
gl.uniform1i(cache.prog.uniforms.u_type, type);
203+
gl.uniform1f(cache.prog.uniforms.u_scale, scale);
204+
gl.uniform1i(cache.prog.uniforms.u_octaves, Math.max(1, Math.min(8, Math.round(octaves))));
205+
gl.uniform1i(cache.prog.uniforms.u_seed, seed | 0);
206+
gl.uniform1i(cache.prog.uniforms.u_frame, frame | 0);
207+
gl.uniform1i(cache.prog.uniforms.u_colorize, colorize ? 1 : 0);
208+
gl.uniform1f(cache.prog.uniforms.u_mix, mix);
209+
}, vao);
210+
return readoutToCanvas(canvas, width, height);
211+
};

0 commit comments

Comments
 (0)