Mandelbrot set
The Mandelbrot set with smooth escape-time colouring.
// ═══ typeshade example — Mandelbrot set (smooth escape-time) ═══//// The fractal every shader site ports first: iterate z ← z² + c where c is the// pixel, colour by the SMOOTH iteration count (the fractional remainder from// log₂ log₂ |z|² kills the discrete banding), and breathe the zoom into the// seahorse valley. Sibling to julia.ts (fixed plane, orbiting c) — here c is// the plane and the camera moves. f32 precision bounds the useful zoom depth,// which is exactly the limit the fp64 emulation (`fp64Lower`, and the// relative-to-centre trick the fp64-* examples use) exists to beat.// WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import { fn, module, u32, f32, vec2, vec3, vec4, sin, cos, dot, exp, log2, max, step, Loop, If, Break, Var, Let, f32T, vec4fT,} from '../src/index.js';import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';import type { ShaderExample } from './_shared.js';const U = fullscreenUniforms({ zoom: f32T, mouse: vec4fT });
// Iridescent cosine palette: 0.5 + 0.5·cos(2π(t + phase)).const palette = fn('palette', { t: f32T }, ({ t }) => { const ph = vec3(0.0, 0.33, 0.67); return vec3(0.5).add(cos(t.add(ph).mul(6.283)).mul(0.5));});
const fs = fn( 'fs', { vo: VsOut }, ({ vo }) => { const res = U.field.resolution; const p = screenCoords(vo.uv, res); // breathing zoom into the seahorse valley (−0.7453 + 0.1127i) const s = Let( exp(U.field.zoom.add(sin(U.field.time.mul(0.2)).mul(0.75).add(0.75)).neg()).mul(2.4), ); // pointer pans the view: the pointer maps into the same isotropic space as // p and offsets the centre, scaled by the current zoom. mu.w = 0 (never // touched) keeps the canonical seahorse-valley framing. const mu = U.field.mouse; const pan = Let( screenCoords(vec2(mu.x.div(res.x), mu.y.div(res.y)), res) .mul(s) .mul(mu.w), ); const c = vec2(p.x.mul(s).sub(0.7453).add(pan.x), p.y.mul(s).add(0.1127).add(pan.y)); const z = Var(vec2(0, 0)); const it = Var(f32(0)); Loop( u32(0), (i) => i.lt(u32(120)), () => { If(dot(z, z).gt(16), () => { Break(); }); // escaped z.assign(vec2(z.x.mul(z.x).sub(z.y.mul(z.y)).add(c.x), z.x.mul(z.y).mul(2).add(c.y))); it.assign(it.add(1)); }, ); // smooth iteration count — subtract the fractional escape overshoot const m = Let(dot(z, z)); const sn = it.sub(log2(max(log2(max(m, 1.0001)), 0.0001))).add(1); // interior (never escaped) stays black const inside = step(119.5, it); const col = palette({ t: sn.mul(0.035).add(U.field.time.mul(0.02)) }).mul(f32(1).sub(inside)); return vec4(col, 1); }, { stage: 'fragment', retAttr: '@location(0)' },);
const mandelbrotModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs],});
export const mandelbrot: ShaderExample = { id: 'mandelbrot', title: 'Mandelbrot set', blurb: 'The Mandelbrot set with smooth escape-time colouring — log₂ log₂ |z|² removes the iteration banding — breathing in and out of the seahorse valley. Move the pointer to pan the view; the zoom slider sets how deep the breath goes (f32 bounds the floor).', category: 'generic', file: 'mandelbrot.ts', module: mandelbrotModule, renderable: true, controls: { time: { kind: 'time' }, resolution: { kind: 'resolution' }, zoom: { kind: 'slider', label: 'Zoom', min: 0, max: 4, step: 0.1, value: 1.5 }, mouse: { kind: 'mouse' }, },};struct Uniforms { time: f32, resolution: vec2<f32>, zoom: f32, mouse: vec4<f32>,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn palette(t: f32) -> vec3<f32> { return (vec3<f32>(0.5) + (cos(((t + vec3<f32>(0.0, 0.33, 0.67)) * 6.283)) * 0.5));}
@vertexfn vs(@builtin(vertex_index) vi: u32) -> VsOut { let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0); let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
@fragmentfn fs(vo: VsOut) -> @location(0) vec4<f32> { let _v0 = (exp((-(U.zoom + ((sin((U.time * 0.2)) * 0.75) + 0.75)))) * 2.4); let _cse0 = vec2<f32>((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y)); let _cse4 = (U.resolution.x / U.resolution.y); let _v1 = ((vec2<f32>((((_cse0.x * 2.0) - 1.0) * _cse4), ((_cse0.y * 2.0) - 1.0)) * _v0) * U.mouse.w); var _v2: vec2<f32> = vec2<f32>(0.0, 0.0); var _v3: f32 = 0.0; let _cse3 = vec2<f32>((((vo.uv.x * 2.0) - 1.0) * _cse4), ((vo.uv.y * 2.0) - 1.0)); let _cse1 = _cse3.x; let _cse2 = _cse3.y; for (var _v4: u32 = 0u; (_v4 < 120u); _v4 = (_v4 + 1u)) { if ((dot(_v2, _v2) > 16.0)) { break; } let _lc0 = vec2<f32>((((_cse1 * _v0) - 0.7453) + _v1.x), (((_cse2 * _v0) + 0.1127) + _v1.y)); _v2 = vec2<f32>((((_v2.x * _v2.x) - (_v2.y * _v2.y)) + _lc0.x), (((_v2.x * _v2.y) * 2.0) + _lc0.y)); _v3 = (_v3 + 1.0); } let _v5 = dot(_v2, _v2); return vec4<f32>((palette(((((_v3 - log2(max(log2(max(_v5, 1.0001)), 0.0001))) + 1.0) * 0.035) + (U.time * 0.02))) * (1.0 - step(119.5, _v3))), 1.0);}#version 300 esprecision highp float;precision highp int;
out vec2 uv;
void main() { uint vi = uint(gl_VertexID); float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); gl_Position = vec4(_cse0, _cse1, 0.0, 1.0); uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));}#version 300 esprecision highp float;precision highp int;
layout(std140) uniform Uniforms { float time; vec2 resolution; float zoom; vec4 mouse;} U;vec3 palette(float t) { return (vec3(0.5) + (cos(((t + vec3(0.0, 0.33, 0.67)) * 6.283)) * 0.5));}in vec2 uv;layout(location = 0) out vec4 _ret;
void main() { float _v0 = (exp((-(U.zoom + ((sin((U.time * 0.2)) * 0.75) + 0.75)))) * 2.4); vec2 _cse0 = vec2((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y)); float _cse4 = (U.resolution.x / U.resolution.y); vec2 _v1 = ((vec2((((_cse0.x * 2.0) - 1.0) * _cse4), ((_cse0.y * 2.0) - 1.0)) * _v0) * U.mouse.w); vec2 _v2 = vec2(0.0, 0.0); float _v3 = 0.0; vec2 _cse3 = vec2((((uv.x * 2.0) - 1.0) * _cse4), ((uv.y * 2.0) - 1.0)); float _cse1 = _cse3.x; float _cse2 = _cse3.y; for (uint _v4 = 0u; (_v4 < 120u); _v4 = (_v4 + 1u)) { if ((dot(_v2, _v2) > 16.0)) { break; } vec2 _lc0 = vec2((((_cse1 * _v0) - 0.7453) + _v1.x), (((_cse2 * _v0) + 0.1127) + _v1.y)); _v2 = vec2((((_v2.x * _v2.x) - (_v2.y * _v2.y)) + _lc0.x), (((_v2.x * _v2.y) * 2.0) + _lc0.y)); _v3 = (_v3 + 1.0); } float _v5 = dot(_v2, _v2); _ret = vec4((palette(((((_v3 - log2(max(log2(max(_v5, 1.0001)), 0.0001))) + 1.0) * 0.035) + (U.time * 0.02))) * (1.0 - step(119.5, _v3))), 1.0);}Mandelbrot set, drawn at build time.
The WGSL and GLSL tabs are the compiler's own output at commit 26de7be8, read from the goldens its emit suite bakes (emit-goldens.test.ts).
This example is written against the fn() builder API, which the editor in the Playground does not take.