|
| 1 | +import { |
| 2 | + drawPass, ensureTexture, getGLCtx, getQuadVAO, glAvailable, |
| 3 | + linkProgram, readoutToCanvas, resizeGLCanvas, uploadSourceTexture, |
| 4 | + type Program, |
| 5 | +} from "gl"; |
| 6 | + |
| 7 | +// Mandelbrot / Julia iterator with smooth escape colouring. Colour |
| 8 | +// source can be the image (sampled by escape-time wrap) or an HSL |
| 9 | +// rainbow. Matches the JS reference's per-pixel maths. |
| 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 = Mandelbrot, 1 = Julia |
| 17 | +uniform int u_colorSource; // 0 = image, 1 = palette hue |
| 18 | +uniform float u_zoom; |
| 19 | +uniform vec2 u_centre; |
| 20 | +uniform int u_iterations; |
| 21 | +uniform vec2 u_julia; |
| 22 | +
|
| 23 | +vec3 hslToRgb(float hue, float sat, float lit) { |
| 24 | + float c = (1.0 - abs(2.0 * lit - 1.0)) * sat; |
| 25 | + float hh = mod(mod(hue, 360.0) + 360.0, 360.0); |
| 26 | + float xc = c * (1.0 - abs(mod(hh / 60.0, 2.0) - 1.0)); |
| 27 | + float m = lit - c * 0.5; |
| 28 | + vec3 rgb; |
| 29 | + if (hh < 60.0) rgb = vec3(c, xc, 0.0); |
| 30 | + else if (hh < 120.0) rgb = vec3(xc, c, 0.0); |
| 31 | + else if (hh < 180.0) rgb = vec3(0.0, c, xc); |
| 32 | + else if (hh < 240.0) rgb = vec3(0.0, xc, c); |
| 33 | + else if (hh < 300.0) rgb = vec3(xc, 0.0, c); |
| 34 | + else rgb = vec3(c, 0.0, xc); |
| 35 | + return floor((rgb + vec3(m)) * 255.0 + 0.5); |
| 36 | +} |
| 37 | +
|
| 38 | +void main() { |
| 39 | + vec2 px = v_uv * u_res; |
| 40 | + float jsX = floor(px.x); |
| 41 | + float jsY = u_res.y - 1.0 - floor(px.y); |
| 42 | +
|
| 43 | + float aspect = u_res.x / u_res.y; |
| 44 | + float rangeX = 3.0 / u_zoom; |
| 45 | + float rangeY = rangeX / aspect; |
| 46 | +
|
| 47 | + float x0 = u_centre.x + (jsX / u_res.x - 0.5) * rangeX; |
| 48 | + float y0 = u_centre.y + (jsY / u_res.y - 0.5) * rangeY; |
| 49 | +
|
| 50 | + float zr, zi, cr, ci; |
| 51 | + if (u_type == 1) { |
| 52 | + zr = x0; zi = y0; |
| 53 | + cr = u_julia.x; ci = u_julia.y; |
| 54 | + } else { |
| 55 | + zr = 0.0; zi = 0.0; |
| 56 | + cr = x0; ci = y0; |
| 57 | + } |
| 58 | +
|
| 59 | + int iter = 0; |
| 60 | + for (int i = 0; i < 500; i++) { |
| 61 | + if (i >= u_iterations) break; |
| 62 | + if (zr * zr + zi * zi >= 4.0) break; |
| 63 | + float tmp = zr * zr - zi * zi + cr; |
| 64 | + zi = 2.0 * zr * zi + ci; |
| 65 | + zr = tmp; |
| 66 | + iter = i + 1; |
| 67 | + } |
| 68 | +
|
| 69 | + vec3 outRgb; |
| 70 | + if (iter == u_iterations) { |
| 71 | + outRgb = vec3(0.0); |
| 72 | + } else { |
| 73 | + float mag = sqrt(zr * zr + zi * zi); |
| 74 | + float t = (float(iter) + 1.0 - log2(log2(mag))) / float(u_iterations); |
| 75 | + if (u_colorSource == 0) { |
| 76 | + float srcX = mod(floor(t * u_res.x), u_res.x); |
| 77 | + float srcY = mod(floor(t * u_res.y), u_res.y); |
| 78 | + outRgb = texture(u_source, vec2((srcX + 0.5) / u_res.x, 1.0 - (srcY + 0.5) / u_res.y)).rgb * 255.0; |
| 79 | + } else { |
| 80 | + outRgb = hslToRgb(t * 360.0 * 3.0, 0.9, 0.5); |
| 81 | + } |
| 82 | + } |
| 83 | +
|
| 84 | + fragColor = vec4(clamp(outRgb, 0.0, 255.0) / 255.0, 1.0); |
| 85 | +} |
| 86 | +`; |
| 87 | + |
| 88 | +type Cache = { prog: Program }; |
| 89 | +let _cache: Cache | null = null; |
| 90 | +const initCache = (gl: WebGL2RenderingContext): Cache => { |
| 91 | + if (_cache) return _cache; |
| 92 | + _cache = { prog: linkProgram(gl, FS, [ |
| 93 | + "u_source", "u_res", "u_type", "u_colorSource", |
| 94 | + "u_zoom", "u_centre", "u_iterations", "u_julia", |
| 95 | + ] as const) }; |
| 96 | + return _cache; |
| 97 | +}; |
| 98 | + |
| 99 | +export const fractalGLAvailable = (): boolean => glAvailable(); |
| 100 | + |
| 101 | +export const renderFractalGL = ( |
| 102 | + source: HTMLCanvasElement | OffscreenCanvas, |
| 103 | + width: number, height: number, |
| 104 | + typeIsJulia: boolean, colorFromImage: boolean, |
| 105 | + zoom: number, centreX: number, centreY: number, |
| 106 | + iterations: number, juliaR: number, juliaI: number, |
| 107 | +): HTMLCanvasElement | OffscreenCanvas | null => { |
| 108 | + const ctx = getGLCtx(); |
| 109 | + if (!ctx) return null; |
| 110 | + const { gl, canvas } = ctx; |
| 111 | + const cache = initCache(gl); |
| 112 | + const vao = getQuadVAO(gl); |
| 113 | + resizeGLCanvas(canvas, width, height); |
| 114 | + const sourceTex = ensureTexture(gl, "fractal:source", width, height); |
| 115 | + uploadSourceTexture(gl, sourceTex, source); |
| 116 | + drawPass(gl, null, width, height, cache.prog, () => { |
| 117 | + gl.activeTexture(gl.TEXTURE0); |
| 118 | + gl.bindTexture(gl.TEXTURE_2D, sourceTex.tex); |
| 119 | + gl.uniform1i(cache.prog.uniforms.u_source, 0); |
| 120 | + gl.uniform2f(cache.prog.uniforms.u_res, width, height); |
| 121 | + gl.uniform1i(cache.prog.uniforms.u_type, typeIsJulia ? 1 : 0); |
| 122 | + gl.uniform1i(cache.prog.uniforms.u_colorSource, colorFromImage ? 0 : 1); |
| 123 | + gl.uniform1f(cache.prog.uniforms.u_zoom, zoom); |
| 124 | + gl.uniform2f(cache.prog.uniforms.u_centre, centreX, centreY); |
| 125 | + gl.uniform1i(cache.prog.uniforms.u_iterations, Math.max(1, Math.min(500, Math.round(iterations)))); |
| 126 | + gl.uniform2f(cache.prog.uniforms.u_julia, juliaR, juliaI); |
| 127 | + }, vao); |
| 128 | + return readoutToCanvas(canvas, width, height); |
| 129 | +}; |
0 commit comments