shadertown 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,60 +1,88 @@
1
1
  export declare const shaderSources: {
2
+ readonly ascii: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// ascii\n// ---------------------------------------------------------------------------\n\n// ASCII — the layer redrawn as a density ramp of terminal glyphs.\n//\n// The glyphs are ten 3x5 bitmaps packed into ten integers, ordered by how\n// much ink they carry, so brightness picks the character the way the classic\n// ramp \" .:-+=*%#@\" does. Nothing is a texture: the font ships inside the\n// shader, which is what lets a vendored copy run with no assets at all.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let aspect = st_aspect();\n\n // Square cells, `cells` of them across the short axis.\n let cells = max(round(params.a), 4.0);\n let grid = vec2f(uv.x * aspect, uv.y) * cells;\n let id = floor(grid);\n let f = fract(grid);\n\n // One tap at the cell centre. Averaging would blur the pick without\n // changing it — the glyph is already the average, spatially.\n let centre = (id + 0.5) / cells / vec2f(aspect, 1.0);\n let srcColor = st_srcRgb(centre);\n let luma = pow(clamp(st_luma(srcColor), 0.0, 1.0), max(params.b, 0.1));\n\n // The font: bit = row * 3 + col, row 0 at the top, popcount rising with\n // the index so luma maps straight onto ink.\n var glyphs = array<u32, 10>(0u, 8192u, 1040u, 448u, 1488u, 3640u, 10922u, 21845u, 23405u, 32767u);\n let pick = clamp(i32(luma * 9.999), 0, 9);\n let mask = glyphs[pick];\n\n // The glyph body, inset from the cell so characters never touch.\n let g = (f - vec2f(0.5)) / 0.72 + vec2f(0.5);\n let colRow = vec2f(floor(g.x * 3.0), floor(g.y * 5.0));\n let inGlyph = step(0.0, g.x) * step(g.x, 0.9999) * step(0.0, g.y) * step(g.y, 0.9999);\n let bit = u32(colRow.y * 3.0 + colRow.x);\n let on = f32((mask >> bit) & 1u) * inGlyph;\n\n // Phosphor: the pure terminal tint, or the source's own colour pushed\n // through it — `colorize` slides between the two.\n let phosphor = params.colorC.rgb * (0.35 + luma * 0.85);\n let inkTone = mix(phosphor, srcColor * 1.2, clamp(params.c, 0.0, 1.0));\n\n var col = params.colorA.rgb * 0.35;\n col = mix(col, inkTone, on);\n\n // Scanlines and a slow refresh bar, for the monitor the font implies.\n let scan = 1.0 - params.d * 0.35 * (0.5 + 0.5 * sin(grid.y * TAU));\n let bar = 1.0 + params.d * 0.15 * smoothstep(0.08, 0.0, abs(fract(uv.y - t * 0.06) - 0.5) - 0.42);\n col = col * scan * bar;\n\n return st_post(col * params.intensity, uv);\n}\n";
2
3
  readonly aurora: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// aurora\n// ---------------------------------------------------------------------------\n\n// Aurora — layered curtains of domain-warped noise over a night sky.\n\nfn curtain(p : vec2f, offset : f32, t : f32) -> f32 {\n var q = p;\n q.x = q.x + st_fbm2(p * vec2f(1.4, 0.7) + vec2f(offset, t * 0.25), 4) * params.warp;\n let band = st_fbm2(vec2f(q.x * 2.2 + offset * 3.0, q.y * 0.6 - t * 0.4), 5);\n let ribbon = pow(clamp(band, 0.0, 1.0), 2.2);\n let falloff = smoothstep(0.62, -0.12, p.y);\n let top = smoothstep(-0.55, -0.05, p.y);\n return ribbon * falloff * top;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n // Night sky gradient, darkest at the horizon.\n var col = mix(vec3f(0.004, 0.008, 0.028), vec3f(0.02, 0.03, 0.07), smoothstep(1.0, -0.2, uv.y));\n\n // Stars, thinned out towards the aurora.\n let starCell = floor(uv * params.resolution / 3.0);\n let star = st_hash21(starCell);\n let twinkle = 0.6 + 0.4 * sin(t * 2.0 + star * TAU);\n col = col + vec3f(step(0.9985, star) * twinkle * params.d);\n\n // Three curtains at different depths.\n let layers = 3.0;\n var glow = 0.0;\n var tint = vec3f(0.0);\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let depth = 1.0 - fi / layers * 0.55;\n let amount = curtain(p * (1.0 + fi * 0.35), fi * 7.31, t) * depth;\n glow = glow + amount;\n tint = tint + st_palette(fi / (layers - 1.0)) * amount;\n }\n\n let energy = glow * params.intensity;\n col = col + tint * params.intensity;\n col = col + st_palette(0.5) * pow(energy, 3.0) * 0.35 * params.b;\n\n return st_post(st_tonemap(col * params.a), uv);\n}\n";
4
+ readonly beacon: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// beacon\n// ---------------------------------------------------------------------------\n\n// Beacon — a triangle of LEDs standing on a floor that catches what they throw.\n//\n// The glow is computed, never blurred. Each edge contributes an exponential\n// falloff off the line itself and a second one off the emitter nearest along\n// it, both analytic, so the halo is the same shape at any zoom and the LEDs\n// stay round at any pixel density. The floor is the same light field evaluated\n// at the mirrored point, which is a reflection for the price of a second\n// lookup.\n\nstruct Edge {\n strip : f32,\n cells : f32,\n}\n\nfn beaCross(a : vec2f, b : vec2f) -> f32 {\n return a.x * b.y - a.y * b.x;\n}\n\n// One edge, seen from `p`: the continuous strip, then its discrete emitters.\nfn beaEdge(p : vec2f, a : vec2f, b : vec2f, count : f32, bleed : f32, chase : f32, t : f32, seed : f32) -> Edge {\n let ba = b - a;\n let pa = p - a;\n let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n\n let strip = exp(-length(pa - ba * h) / max(bleed, 0.003));\n\n // Three cells, not one: the projection only ever names the nearest, and\n // switching between them leaves a seam wherever the halo is still bright.\n let index = floor(h * count);\n var cells = 0.0;\n for (var k = -1; k <= 1; k = k + 1) {\n let idx = clamp(index + f32(k), 0.0, count - 1.0);\n let centre = a + ba * ((idx + 0.5) / count);\n let pulse = mix(1.0, 0.5 + 0.5 * sin((idx / count) * TAU * 2.0 - t * 3.2 + seed), chase);\n cells = cells + exp(-length(p - centre) / max(bleed * 0.30, 0.001)) * pulse;\n }\n\n return Edge(strip, cells);\n}\n\n// Everything the three edges and the pointer put into one point.\nfn beaField(p : vec2f, v0 : vec2f, v1 : vec2f, v2 : vec2f, bleed : f32, t : f32) -> vec3f {\n let count = max(floor(params.a), 2.0);\n let chase = clamp(params.d, 0.0, 1.0);\n\n let e0 = beaEdge(p, v0, v1, count, bleed, chase, t, 0.0);\n let e1 = beaEdge(p, v1, v2, count, bleed, chase, t, 2.094);\n let e2 = beaEdge(p, v2, v0, count, bleed, chase, t, 4.188);\n\n let c0 = params.colorB.rgb;\n let c1 = params.colorC.rgb;\n let c2 = mix(params.colorB.rgb, params.colorC.rgb, 0.5);\n\n var light = c0 * (e0.strip * 0.35 + e0.cells);\n light = light + c1 * (e1.strip * 0.35 + e1.cells);\n light = light + c2 * (e2.strip * 0.35 + e2.cells);\n\n // A roving lamp under the cursor, so the frame answers the pointer even\n // though nothing in it moves.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), 0.5 - params.pointer.y);\n light = light + c1 * exp(-length(p - cursor) / max(bleed * 2.2, 0.01)) * 0.7;\n\n return light;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n // Flipped once, here: uv runs down the screen, and everything below reads\n // better written the way it is meant to be seen — apex up, floor beneath.\n let p = st_centered(uv) * vec2f(1.0, -1.0);\n\n let size = 0.30 * clamp(params.scale, 0.2, 3.0);\n // A rock rather than a spin: this is a front, and a front that turns all the\n // way round stops reading as one.\n let tilt = sin(t * 0.35) * 0.22;\n let lift = 0.06;\n let v0 = st_rot(tilt) * (vec2f(0.0, 1.0) * size) + vec2f(0.0, lift);\n let v1 = st_rot(tilt) * (vec2f(-0.866, -0.5) * size) + vec2f(0.0, lift);\n let v2 = st_rot(tilt) * (vec2f(0.866, -0.5) * size) + vec2f(0.0, lift);\n\n let bleed = max(params.b, 0.004);\n let floorY = -0.30;\n\n var col = params.colorA.rgb * 0.10;\n col = col + beaField(p, v0, v1, v2, bleed, t);\n\n // The plate the LEDs are mounted on. Signed distance to a convex polygon is\n // exact inside it, which is all a fill mask needs.\n let inside = min(\n min(beaCross(normalize(v1 - v0), p - v0), beaCross(normalize(v2 - v1), p - v1)),\n beaCross(normalize(v0 - v2), p - v2),\n );\n // One pixel wide at any density. A fixed width is a hairline on a phone and\n // a smear on a 5K panel, and this is the only hard edge in the frame.\n let plateEdge = max(fwidth(inside), 0.00001);\n let plate = smoothstep(-plateEdge, plateEdge, inside);\n col = mix(col, params.colorA.rgb * 0.30 + col * 0.25, plate);\n\n // Floor: the same field read at the mirrored point, widened with depth so it\n // smears the way a brushed surface does, and faded as it goes.\n let floorEdge = max(fwidth(p.y), 0.00001);\n let below = smoothstep(floorY + floorEdge, floorY - floorEdge, p.y);\n let depth = max(floorY - p.y, 0.0);\n let mirrored = vec2f(p.x, 2.0 * floorY - p.y);\n let rough = bleed * (1.0 + depth * 7.0);\n let bounce = beaField(mirrored, v0, v1, v2, rough, t) * exp(-depth * 4.5) * clamp(params.c, 0.0, 1.0);\n col = mix(col, params.colorA.rgb * 0.05 + bounce, below);\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
3
5
  readonly bloom: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// bloom\n// ---------------------------------------------------------------------------\n\n// Bloom — light bleeding out of the bright parts of the layer below.\n\n// Only what is already bright blooms. The soft knee keeps the threshold from\n// showing up as a hard outline around every highlight.\nfn bloom_bright(uv : vec2f) -> vec3f {\n let c = st_srcRgb(uv);\n return c * smoothstep(params.a, params.a + 0.25, st_luma(c));\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let base = st_srcRgb(uv);\n\n // A wide spiral over the thresholded image. Thresholding per tap, rather\n // than blurring first, is what keeps a dark neighbour from dimming a bloom.\n let taps = 16;\n let radius = params.b * 0.12;\n // Stretching the spiral sideways gives the horizontal streak an anamorphic\n // lens leaves on every point of light.\n let stretch = vec2f(1.0 + params.warp * 3.0, 1.0);\n\n var glow = vec3f(0.0);\n for (var i = 0; i < taps; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n glow = glow + bloom_bright(uv + vec2f(cos(angle), sin(angle)) * r * stretch);\n }\n glow = glow / f32(taps);\n\n // Tint pulls the halo towards the palette — the coating on a real lens is\n // rarely neutral, and a warm bloom over cool light is most of the look.\n glow = mix(glow, glow * st_palette(st_luma(glow)) * 2.0, clamp(params.d, 0.0, 1.0));\n\n let col = base + glow * params.c;\n return st_post(col * params.intensity, uv);\n}\n";
4
6
  readonly bokeh: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// bokeh\n// ---------------------------------------------------------------------------\n\n// Bokeh — out-of-focus lights drifting through depth. Additive, soft-edged.\n\nfn bokehLayer(p : vec2f, depth : f32, t : f32) -> vec3f {\n let density = mix(2.5, 7.0, depth);\n let drift = vec2f(t * 0.06 * (1.0 - depth * 0.7), -t * 0.09 * (1.0 - depth * 0.5));\n let grid = p * density + drift * density;\n let base = floor(grid);\n let frac = fract(grid) - 0.5;\n\n var acc = vec3f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = st_hash22(base + neighbour + depth * 91.7);\n if (rnd.x < 0.35) { continue; }\n\n let wobble = vec2f(sin(t * 0.5 + rnd.y * TAU), cos(t * 0.4 + rnd.x * TAU)) * 0.12;\n let centre = neighbour + rnd - 0.5 + wobble;\n let d = length(frac - centre);\n\n // A disc with a bright rim — how a real lens renders defocused points.\n let radius = (0.10 + rnd.y * 0.22) * params.a * mix(1.6, 0.5, depth);\n let disc = smoothstep(radius, radius * (1.0 - params.b), d);\n let rim = smoothstep(radius * (1.0 - params.b * 0.8), radius, d) * disc;\n\n let tint = st_palette(rnd.y);\n acc = acc + tint * (disc * 0.55 + rim * params.c) * mix(0.35, 1.0, 1.0 - depth);\n }\n }\n return acc;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n var col = mix(params.colorA.rgb * 0.10, vec3f(0.008, 0.008, 0.02), uv.y);\n for (var i = 0; i < 4; i = i + 1) {\n col = col + bokehLayer(p, f32(i) / 3.0, t) * params.d;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
5
7
  readonly caustics: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// caustics\n// ---------------------------------------------------------------------------\n\n// Caustics — light focused through a moving water surface onto a pool floor.\n\nfn causticField(p : vec2f, t : f32, sharpness : f32) -> f32 {\n var acc = 0.0;\n var q = p;\n // Stacked, counter-rotating cell fields approximate the focusing pattern.\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let rotated = st_rot(fi * 1.1 + t * 0.05 * (fi + 1.0)) * q;\n let wave = sin(rotated.x * 6.0 + t * (1.1 + fi * 0.3))\n + sin(rotated.y * 6.0 - t * (0.9 + fi * 0.2))\n + sin((rotated.x + rotated.y) * 4.0 + t * 0.7);\n acc = acc + abs(wave) / 3.0;\n q = q * 1.35;\n }\n let v = 1.0 - clamp(acc / 3.0, 0.0, 1.0);\n return pow(v, 1.0 + sharpness * 14.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 2.0;\n\n // Surface ripple displaces the floor lookup.\n let ripple = vec2f(st_fbm2(p * 1.5 + t * 0.1, 3), st_fbm2(p * 1.5 + 4.2 - t * 0.08, 3));\n p = p + (ripple - 0.5) * params.warp;\n\n let light = causticField(p, t, params.a);\n\n // Chromatic split — the three wavelengths focus at slightly different depths.\n let split = params.b * 0.03;\n let r = causticField(p + vec2f(split, 0.0), t, params.a);\n let b = causticField(p - vec2f(split, 0.0), t, params.a);\n\n let floorTone = mix(params.colorA.rgb, params.colorB.rgb, clamp(st_fbm2(p * 0.8, 3), 0.0, 1.0)) * 0.35;\n var col = floorTone;\n col = col + params.colorC.rgb * vec3f(r, light, b) * params.intensity * 2.2;\n\n // Depth falloff towards the pool edges.\n col = col * (0.55 + 0.45 * smoothstep(1.1, 0.1, length(st_centered(uv)) * params.c));\n\n return st_post(st_tonemap(col), uv);\n}\n";
6
8
  readonly dither: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// dither\n// ---------------------------------------------------------------------------\n\n// Dither — a smooth field quantised through an 8x8 Bayer matrix.\n// Deliberately low-fi: flat colour, a visible pattern, no antialiasing.\n\n// Ordered-dither threshold, built by interleaving the bits of y and x^y.\nfn bayer8(coord : vec2f) -> f32 {\n let x = i32(coord.x) & 7;\n let y = i32(coord.y) & 7;\n var index = 0;\n for (var i = 0u; i < 3u; i = i + 1u) {\n let shift = 2u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return f32(index) / 64.0;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Centred on 0.5 so every quantisation level gets used. Biasing the source\n // upwards instead just crushes the image to the top level.\n let field = st_fbm2(p * 2.0 + vec2f(t * 0.10, t * 0.06), 5);\n let sweep = clamp((field - 0.5) * 1.7 + (0.5 - uv.y) * params.c + 0.5, 0.0, 1.0);\n\n // Pixelate first, so the pattern reads the same at any resolution.\n let pixel = max(params.a, 1.0);\n let screenPos = floor(uv * params.resolution / pixel);\n let threshold = (bayer8(screenPos) - 0.5) / max(floor(params.b), 2.0);\n\n let levels = max(floor(params.b), 2.0);\n let stepped = min(floor(clamp(sweep + threshold * params.d * 2.0, 0.0, 1.0) * levels), levels - 1.0);\n let quantised = stepped / (levels - 1.0);\n\n let col = st_palette(quantised) * params.intensity;\n return st_post(col, uv);\n}\n";
7
9
  readonly duotone: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// duotone\n// ---------------------------------------------------------------------------\n\n// Duotone — the layer's brightness read through your palette.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let src = st_srcRgb(uv);\n var l = st_luma(src);\n\n // Black and white point first, contrast about the midpoint after. In the\n // other order a hard contrast would clip before the range is set.\n l = clamp((l - params.a) / max(params.b - params.a, 0.01), 0.0, 1.0);\n l = clamp((l - 0.5) * max(params.c, 0.01) + 0.5, 0.0, 1.0);\n\n let mapped = st_palette(l);\n // Keeping some of the original chroma stops a busy photograph from turning\n // into a flat two-colour poster when that is not what you wanted.\n let col = mix(mapped, mix(mapped, src, 0.85), clamp(params.d, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
8
10
  readonly edges: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// edges\n// ---------------------------------------------------------------------------\n\n// Edges — the layer reduced to its outlines.\n\nfn edges_luma(srcUv : vec2f) -> f32 {\n return st_luma(st_srcAt(srcUv).rgb);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let s = st_srcUv(uv);\n // Widening the step is what turns a hairline into a drawn line. Sub-texel\n // steps would just sample the same texel three times.\n let e = st_srcTexel() * max(params.a, 0.5);\n\n let tl = edges_luma(s + vec2f(-e.x, e.y));\n let tc = edges_luma(s + vec2f( 0.0, e.y));\n let tr = edges_luma(s + vec2f( e.x, e.y));\n let ml = edges_luma(s + vec2f(-e.x, 0.0));\n let mr = edges_luma(s + vec2f( e.x, 0.0));\n let bl = edges_luma(s + vec2f(-e.x, -e.y));\n let bc = edges_luma(s + vec2f( 0.0, -e.y));\n let br = edges_luma(s + vec2f( e.x, -e.y));\n\n // Sobel: the same kernel turned ninety degrees for the second axis.\n let gx = (tr + 2.0 * mr + br) - (tl + 2.0 * ml + bl);\n let gy = (tl + 2.0 * tc + tr) - (bl + 2.0 * bc + br);\n let gradient = length(vec2f(gx, gy));\n\n let line = smoothstep(params.b, params.b + 0.2, gradient);\n\n // The ground under the lines: the palette's dark end, with as much of the\n // original showing through as `fill` asks for.\n let ground = mix(st_palette(0.0), st_srcAt(s).rgb, clamp(params.c, 0.0, 1.0));\n var col = mix(ground, st_palette(1.0), line);\n\n // A little bleed off the line reads as ink, not as aliasing.\n col = col + st_palette(1.0) * pow(line, 3.0) * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
11
+ readonly eyes: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// eyes\n// ---------------------------------------------------------------------------\n\n// Eyes — a wall of eyes that look where the pointer goes.\n//\n// Each cell is one eye: an almond of sclera, an iris that travels toward the\n// cursor, a lid that blinks on its own clock. The gaze is the interactive\n// part, but the idle frame carries itself — with no pointer the eyes wander,\n// each on its own phase, so a card is never a grid of dead centres.\n\n// Where this eye is looking, as an offset inside its socket.\nfn eyeGaze(id : vec2f, cursorDir : vec2f, cursorDist : f32, t : f32, follow : f32) -> vec2f {\n let seed = st_hash22(id);\n // Idle wander: a slow lissajous per eye, desynchronised by the hash.\n let wt = t * (0.5 + seed.x * 0.4) + seed.y * TAU;\n let wander = vec2f(cos(wt), sin(wt * 1.31)) * 0.12;\n\n // Tracking: full deflection quickly, so even a far cursor is clearly seen.\n let track = cursorDir * min(cursorDist * 0.8, 1.0) * 0.16;\n return mix(wander, track, follow);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let density = params.scale * 4.0;\n let p = st_centered(uv) * density;\n\n let id = floor(p);\n let f = fract(p) - 0.5;\n\n // The cursor in the same lattice space as the eyes. uv runs down the\n // screen and so does rendering, so no flip is needed for the gaze to\n // point the right way.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5) * density;\n let toCursor = cursor - (id + vec2f(0.5));\n let cursorDist = length(toCursor);\n let cursorDir = toCursor / max(cursorDist, 0.0001);\n\n let seed = st_hash21(id);\n\n // Blink: a brief pulse on a per-eye clock. The squeeze narrows the almond\n // rather than sliding a lid, which is one smoothstep instead of a shape.\n let blinkPhase = fract(t * 0.13 * params.b + seed * 7.0);\n let closed = smoothstep(0.055, 0.0, abs(blinkPhase - 0.5));\n let open = (1.0 - closed) * (0.72 + 0.28 * seed);\n\n // Almond: the sclera's half-height as a function of x, pinched to zero at\n // the corners. Everything else is clipped against this.\n let halfWidth = 0.40;\n let xNorm = clamp(f.x / halfWidth, -1.0, 1.0);\n let lidHeight = open * 0.26 * sqrt(max(1.0 - xNorm * xNorm, 0.0));\n let aa = max(fwidth(f.y), 0.002);\n\n let inX = smoothstep(halfWidth + aa, halfWidth - aa, abs(f.x));\n let sclera = smoothstep(lidHeight + aa, lidHeight - aa, abs(f.y)) * inX;\n\n // Iris and pupil ride the gaze; both are clamped by the almond, so a look\n // to the side slides the iris under the lids the way a real eye does.\n let gaze = eyeGaze(id, cursorDir, cursorDist, t, params.a);\n let irisR = params.c * 0.16;\n let dIris = length(f - gaze);\n let iris = smoothstep(irisR + aa, irisR - aa, dIris) * sclera;\n let pupilR = irisR * clamp(params.d, 0.15, 0.9);\n let pupil = smoothstep(pupilR + aa, pupilR - aa, dIris) * sclera;\n\n // One fixed catchlight sells the sphere; it does not follow the gaze.\n let gleam = smoothstep(0.05, 0.0, length(f - gaze - vec2f(-0.05, -0.06))) * pupil;\n\n // Iris shading: darker at the rim, striated toward the pupil.\n let ring = clamp((dIris / max(irisR, 0.001)), 0.0, 1.0);\n let stria = 0.85 + 0.15 * sin(atan2(f.y - gaze.y, f.x - gaze.x) * 18.0 + seed * TAU);\n let irisTone = params.colorB.rgb * mix(1.15, 0.45, ring * ring) * stria;\n\n var col = params.colorA.rgb * (0.5 + 0.12 * st_hash21(id + 31.7));\n col = mix(col, params.colorC.rgb * (0.9 - 0.35 * abs(f.y) * 2.0), sclera);\n col = mix(col, irisTone, iris);\n col = mix(col, vec3f(0.02), pupil);\n col = mix(col, vec3f(1.0), gleam);\n\n // A soft shadow above each eye seats it in the wall.\n col = col * (1.0 - 0.25 * smoothstep(0.0, -0.3, f.y) * (1.0 - sclera));\n\n return st_post(col * params.intensity, uv);\n}\n";
9
12
  readonly "film-grain": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// film-grain\n// ---------------------------------------------------------------------------\n\n// Film grain — a texture pass. Dark wash, heavy stock grain, halation, scanlines.\n\nfn wash(p : vec2f, t : f32) -> f32 {\n return st_fbm2(p * 1.2 + vec2f(t * 0.05, t * 0.03), 4);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Chromatic aberration: read the same wash at three lateral offsets.\n let ca = params.c * 0.02;\n let wr = wash(p + vec2f(ca, 0.0), t);\n let wg = wash(p, t);\n let wb = wash(p - vec2f(ca, 0.0), t);\n\n var col = vec3f(\n st_palette(clamp(wr * 1.3 - 0.1, 0.0, 1.0)).r,\n st_palette(clamp(wg * 1.3 - 0.1, 0.0, 1.0)).g,\n st_palette(clamp(wb * 1.3 - 0.1, 0.0, 1.0)).b\n ) * params.intensity;\n\n // Stock grain, sampled per cell so it stays the same size at any DPR.\n let cell = floor(uv * params.resolution / max(params.d, 0.5));\n let n1 = st_hash21(cell + floor(t * 24.0) * 17.13);\n let n2 = st_hash21(cell.yx + floor(t * 24.0) * 3.71);\n col = col + (n1 * 0.65 + n2 * 0.35 - 0.5) * params.a;\n\n // Scanlines.\n let lines = sin(uv.y * params.resolution.y * 1.5) * 0.5 + 0.5;\n col = col * (1.0 - lines * params.b);\n\n // Halation blooming out of the brightest areas.\n col = col + st_palette(1.0) * pow(clamp(st_luma(col), 0.0, 1.0), 4.0) * 0.4;\n\n return st_post(col, uv);\n}\n";
10
13
  readonly flow: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// flow\n// ---------------------------------------------------------------------------\n\n// Flow — the layer pushed around by a slow, turbulent field.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale * 2.5;\n\n // Two fbm fields sampled a long way apart stand in for a divergence-free\n // field. It is not one, but the eye reads the swirl and not the maths.\n let drift = vec2f(t * 0.05, t * 0.03);\n // Turbulence trades soft hills for sharp crests. Ridged noise gives the flow\n // filaments and stringy detail; smooth noise gives it broad, lazy currents.\n let turbulence = clamp(params.a, 0.0, 1.0);\n let fx = mix(st_fbm2(p + drift, 4), st_ridge2(p + drift, 3), turbulence);\n let fy = mix(\n st_fbm2(p + drift + vec2f(17.3, 9.1), 4),\n st_ridge2(p + drift + vec2f(17.3, 9.1), 3),\n turbulence,\n );\n var field = vec2f(fx, fy) - 0.5;\n\n // Curl the field so it circulates rather than pulling everything one way.\n field = mix(field, vec2f(-field.y, field.x), clamp(params.b, 0.0, 1.0));\n\n let shove = field * params.warp * 0.12;\n\n // Smearing along the flow, rather than in a circle, is what makes it look\n // stirred instead of merely blurred.\n let smear = params.c * 0.06;\n let step = normalize(field + vec2f(0.0001)) * smear;\n var col = vec3f(0.0);\n for (var i = 0; i < 5; i = i + 1) {\n let k = (f32(i) / 4.0 - 0.5);\n // Inset, so the field has material to push around at the border rather\n // than running off the picture and smearing the last row of texels.\n col = col + st_srcAt(st_srcUvInset(uv + shove + step * k, 0.1)).rgb;\n }\n col = col / 5.0;\n\n // Tint by how hard the field is pushing, so the fast water shows colour.\n let speed = clamp(length(field) * 2.0, 0.0, 1.0);\n col = mix(col, col * st_palette(speed) * 1.8, clamp(params.d, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
11
14
  readonly glass: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// glass\n// ---------------------------------------------------------------------------\n\n// Glass — a refracting, frosted sheet over the layer behind it.\n\n// Two slow counter-drifting fields. Mixing smooth hills with sharp ridges is\n// what reads as a thick pane; either one alone reads as water.\nfn glass_height(p : vec2f, t : f32) -> f32 {\n let hills = st_fbm2(p + vec2f(t * 0.06, -t * 0.04), 3);\n let ridges = st_ridge2(p * 0.7 - vec2f(t * 0.03, t * 0.05), 2);\n return mix(hills, ridges, 0.55);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale * 3.0;\n\n // Slope of the height field stands in for the surface normal. Forward\n // differences, not central: three evaluations instead of five, and the half\n // texel of bias is invisible next to the refraction it feeds.\n let e = 0.006;\n let h = glass_height(p, t);\n let hx = glass_height(p + vec2f(e, 0.0), t);\n let hy = glass_height(p + vec2f(0.0, e), t);\n let slope = vec2f(hx - h, hy - h) / e;\n\n let bend = st_srcWarp(-slope * params.warp * 0.012);\n let base = st_srcUv(uv) + bend;\n\n // Frost is a small spiral of taps around the bent coordinate. The height\n // rotates the spiral, so neighbouring pixels blur along different axes and\n // the result grains up instead of banding.\n let taps = 6;\n let frost = params.a * 0.03;\n var blurred = st_srcAt(base).rgb;\n for (var i = 0; i < taps; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963 + h * 5.0;\n let radius = frost * sqrt((fi + 0.5) / f32(taps));\n blurred = blurred + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n blurred = blurred / f32(taps + 1);\n\n // Dispersion: the red and blue ends bend by different amounts, so steep\n // slopes fringe the way real glass does.\n let spread = st_srcWarp(-slope * params.b * 0.004);\n var col = vec3f(\n st_srcAt(base + spread).r,\n blurred.g,\n st_srcAt(base - spread).b,\n );\n col = mix(blurred, col, clamp(params.b, 0.0, 1.0));\n\n // Steep ground catches the light. Squaring keeps the rims thin.\n let steep = clamp(length(slope) * 0.35, 0.0, 1.0);\n let rim = steep * steep * params.c;\n col = col + mix(vec3f(1.0), st_palette(h), 0.5) * rim * 0.6;\n\n // A pane is rarely colourless. Tint pulls the whole sheet toward the palette.\n col = mix(col, col * st_palette(h * 0.7 + 0.15) * 1.6, clamp(params.d, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
15
+ readonly gravity: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// gravity\n// ---------------------------------------------------------------------------\n\n// Gravity — a starfield lensed by a mass that rides the pointer.\n//\n// The lens is the physics, simplified once: a point mass deflects light by\n// the square of its Einstein radius over the impact distance, so the sample\n// position is the pixel pushed away from the mass by exactly that much. That\n// one line produces everything the picture is known for — stars smeared into\n// arcs, the ring at the Einstein radius, and the doubled images inside it —\n// with no machinery beyond remapping where the background is read.\n\n// A layer of stars, in whatever space the lens hands us.\nfn starLayer(q : vec2f, cells : f32, t : f32, density : f32) -> f32 {\n let g = q * cells;\n let id = floor(g);\n let rnd = st_hash22(id);\n if (rnd.x > density) { return 0.0; }\n let centre = id + 0.3 + rnd * 0.4;\n let d = length(g - centre);\n let twinkle = 0.7 + 0.3 * sin(t * (1.0 + rnd.y * 3.0) + rnd.x * TAU);\n return smoothstep(0.08, 0.0, d) * twinkle * (0.4 + rnd.y);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.2);\n\n let mass = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5)\n / max(params.scale, 0.2);\n\n // The bend. Clamping the impact distance keeps the singularity from\n // spraying one pixel's noise across the middle of the frame.\n let thetaE = max(params.a, 0.02) * 0.18;\n let r = p - mass;\n let d = max(length(r), 0.012);\n let bend = r / d * (thetaE * thetaE / d);\n let q = p - bend + vec2f(t * 0.008, 0.0);\n\n // The event horizon: inside it, nothing arrives. Smooth, and scaled well\n // under the Einstein radius so the ring stays lit.\n let horizon = thetaE * 0.35;\n let shadow = smoothstep(horizon, horizon * 1.3, d);\n\n // Three star layers at different scales, all read through the same lens,\n // plus a faint nebula so the arcs have something soft to smear.\n var sky = vec3f(0.0);\n sky = sky + params.colorC.rgb * starLayer(q, 7.0, t, params.d * 0.4);\n sky = sky + params.colorC.rgb * starLayer(q + 31.7, 13.0, t * 1.3, params.d * 0.5) * 0.7;\n sky = sky + params.colorB.rgb * starLayer(q + 77.3, 23.0, t * 0.7, params.d * 0.6) * 0.45;\n sky = sky + st_palette(st_fbm2(q * 1.4, 3)) * 0.08;\n\n // The spacetime grid, read through the same remap so it visibly funnels\n // into the mass. Soft bands rather than fwidth lines: the derivative\n // explodes at the horizon and would ring alias there.\n let grid = pow(0.5 + 0.5 * sin(q.x * 28.0), 20.0) + pow(0.5 + 0.5 * sin(q.y * 28.0), 20.0);\n sky = sky + params.colorB.rgb * grid * params.c * 0.4;\n\n var col = params.colorA.rgb * 0.05 + sky * shadow;\n\n // The photon ring: light that grazed the horizon, brightest exactly at the\n // Einstein radius.\n let ring = exp(-abs(d - thetaE) * 60.0 / max(thetaE, 0.05));\n col = col + st_palette(0.75) * ring * params.b;\n\n // A thin hot rim on the shadow's edge itself.\n col = col + params.colorC.rgb * exp(-abs(d - horizon) * 160.0) * params.b * 0.8;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
12
16
  readonly halftone: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// halftone\n// ---------------------------------------------------------------------------\n\n// Halftone — a duotone image reproduced as a rotated dot screen.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // The continuous-tone image being screened. Centred on 0.5 so the screen\n // actually has a range of dot sizes to work with instead of saturating.\n let field = st_fbm2(p * 1.8 + vec2f(t * 0.12, t * 0.07), 4);\n let sweep = clamp((field - 0.5) * 1.6 + (p.x + p.y) * 0.35 + 0.5, 0.0, 1.0);\n\n // Dot screen, rotated the way a real halftone plate would be.\n let angle = params.c * PI;\n let grid = st_rot(angle) * uv * params.resolution / max(params.a, 2.0);\n let cellPos = fract(grid) - 0.5;\n\n let ink = pow(sweep, 1.0 / max(params.d, 0.05));\n // 0.62 lets neighbouring dots touch at full ink while leaving the corners\n // open — past ~0.71 the screen fills solid and stops reading as halftone.\n let radius = sqrt(ink) * 0.62;\n let coverage = 1.0 - smoothstep(radius - params.b, radius + params.b, length(cellPos));\n\n let paper = params.colorA.rgb;\n let inkColor = mix(params.colorB.rgb, params.colorC.rgb, sweep);\n\n let col = mix(paper, inkColor, coverage * params.intensity);\n return st_post(col, uv);\n}\n";
13
17
  readonly julia: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// julia\n// ---------------------------------------------------------------------------\n\n// Julia — escape-time fractal with orbit-trap colouring. A moving c parameter\n// walks the set through connected and dust-like shapes.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var z = st_centered(uv) * (3.4 / max(params.scale, 0.05));\n z = st_rot(t * 0.03 * params.d) * z;\n\n // The seed orbits a cardioid edge, which is where the interesting shapes live.\n let angle = t * 0.12;\n let c = vec2f(\n 0.7885 * cos(angle) * params.a,\n 0.7885 * sin(angle) * params.a\n ) + vec2f(params.b - 0.5, 0.0) * 0.6;\n\n let maxIter = 96;\n var i = 0;\n var trap = 1e9;\n var escaped = false;\n\n for (; i < maxIter; i = i + 1) {\n z = vec2f(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;\n trap = min(trap, abs(length(z) - 1.0));\n if (dot(z, z) > 64.0) { escaped = true; break; }\n }\n\n if (!escaped) {\n // Interior: shade by how close the orbit came to the trap circle.\n let inner = st_palette(clamp(trap * 4.0, 0.0, 1.0)) * 0.5;\n return st_post(inner * params.intensity, uv);\n }\n\n // Smooth iteration count removes the banding of a raw integer count.\n let smoothed = f32(i) - log2(max(log2(length(z)), 1.0)) + 4.0;\n let shade = fract(smoothed * 0.09 * params.c + t * 0.04);\n\n // Points that escape immediately are the empty outer plane; letting them\n // take a full palette colour washes the whole frame out.\n let escapeDepth = clamp(smoothed / 18.0, 0.0, 1.0);\n\n var col = st_paletteCyclic(shade) * pow(escapeDepth, 1.15);\n col = col * (0.25 + 0.9 * clamp(1.0 - trap * 1.1, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
14
18
  readonly kaleidoscope: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// kaleidoscope\n// ---------------------------------------------------------------------------\n\n// Kaleidoscope — polar mirror folding over a drifting noise field.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n\n let segments = max(floor(params.a), 2.0);\n var angle = atan2(p.y, p.x);\n let radius = length(p);\n\n // Fold the angle into one wedge, then mirror it.\n let wedge = TAU / segments;\n angle = angle + t * 0.15 * params.d;\n angle = angle - wedge * floor(angle / wedge);\n angle = abs(angle - wedge * 0.5);\n\n var q = vec2f(cos(angle), sin(angle)) * radius;\n q = q + vec2f(st_fbm2(q * 3.0 + t * 0.2, 4) - 0.5) * params.warp;\n\n let rings = sin(radius * 22.0 * params.b - t * 2.0) * 0.5 + 0.5;\n let field = st_fbm2(q * 4.0 + vec2f(0.0, t * 0.3), 5);\n\n var col = st_palette(fract(field * 1.6 + rings * params.c));\n\n // Bright core, dark rim — reads like looking down a tube.\n col = col * (0.4 + 0.9 * smoothstep(0.75, 0.0, radius));\n col = col + st_palette(1.0) * pow(clamp(1.0 - radius * 2.2, 0.0, 1.0), 3.0) * 0.6;\n\n return st_post(col * params.intensity, uv);\n}\n";
15
19
  readonly lens: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// lens\n// ---------------------------------------------------------------------------\n\n// Lens — barrel distortion, colour fringing and soft corners.\n\n// Brown-Conrady, truncated after the second radial term. That is enough to\n// bend a straight line convincingly and cheap enough to run three times.\nfn lens_distort(p : vec2f, k : f32) -> vec2f {\n let r2 = dot(p, p);\n return p * (1.0 + k * r2 + k * k * r2 * r2 * 0.35);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let aspect = st_aspect();\n let k = params.warp * 0.6;\n\n // Barrel distortion throws the corners outside the frame, which would leave\n // a black border around every preview. Measure how far the corner moves and\n // pull the whole field back in by that much, so bending the picture does not\n // also shrink it. Pincushion pulls inwards on its own and needs no help,\n // hence the clamp at 1.\n let corner = vec2f(aspect, 1.0) * 0.5;\n let stretch = length(lens_distort(corner, k)) / max(length(corner), 0.0001);\n let fill = 1.0 / max(stretch, 1.0);\n\n // Centred and aspect-corrected, so the distortion is radial about the frame\n // rather than about the corner of a stretched uv square.\n let p = (uv - 0.5) * vec2f(aspect, 1.0) * fill / max(params.c, 0.05);\n\n // Each channel gets its own constant. That is what glass does, and it is why\n // the fringing is invisible in the middle and worst in the corners.\n let spread = params.b * 0.06;\n let toUv = vec2f(1.0 / aspect, 1.0);\n let centre = lens_distort(p, k) * toUv + 0.5;\n let red = lens_distort(p, k * (1.0 - spread)) * toUv + 0.5;\n let blue = lens_distort(p, k * (1.0 + spread)) * toUv + 0.5;\n\n // Corners sit off the focal plane, so they lose sharpness first.\n let falloff = clamp(length(p) * 1.5, 0.0, 1.0);\n let soft = params.a * falloff * falloff * 0.02;\n\n var col = vec3f(\n st_srcSoften(red, soft).r,\n st_srcSoften(centre, soft).g,\n st_srcSoften(blue, soft).b,\n );\n\n // Past the frame the sampler repeats its last row. Fade to black instead.\n col = col * st_srcInside(st_srcUv(centre));\n\n // Barrel distortion pulls light towards the middle, so the corners dim.\n col = col * (1.0 - params.d * falloff * falloff);\n\n return st_post(col * params.intensity, uv);\n}\n";
16
20
  readonly "liquid-metal": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// liquid-metal\n// ---------------------------------------------------------------------------\n\n// Liquid metal — a warped height field lit like polished chrome.\n// The look lives in the banded environment reflection, not in the noise: keep\n// the field low-frequency, and let the bands do the work.\n\nfn height(p : vec2f, t : f32) -> f32 {\n var q = p;\n q = q + vec2f(st_fbm2(q * 0.9 + vec2f(t * 0.18, 0.0), 3),\n st_fbm2(q * 0.9 + vec2f(0.0, t * 0.15), 3)) * params.warp * 1.6;\n return st_fbm2(q * 1.1 + vec2f(0.0, t * 0.08), 3);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 2.2;\n let t = st_time();\n\n // Fixed sampling offset in field units: a resolution-derived epsilon makes\n // the surface look completely different in a card and in a hero.\n let eps = 0.012;\n let h = height(p, t);\n let hx = height(p + vec2f(eps, 0.0), t);\n let hy = height(p + vec2f(0.0, eps), t);\n\n let slope = params.a * 0.55;\n let n = normalize(vec3f((h - hx) / eps * slope, (h - hy) / eps * slope, 1.0));\n\n let lightDir = normalize(vec3f(cos(t * 0.35) * 0.7, sin(t * 0.35) * 0.7, 0.9));\n let viewDir = vec3f(0.0, 0.0, 1.0);\n let halfway = normalize(lightDir + viewDir);\n\n let diffuse = clamp(dot(n, lightDir), 0.0, 1.0);\n let spec = pow(clamp(dot(n, halfway), 0.0, 1.0), 16.0 + params.b * 180.0);\n let fresnel = pow(1.0 - clamp(dot(n, viewDir), 0.0, 1.0), 4.0);\n\n // Wide, smooth reflection bands. Reflecting the view around the normal and\n // reading a ramp off it is what separates chrome from rough plastic.\n let reflected = reflect(-viewDir, n);\n let band = reflected.y * 1.6 + reflected.x * 0.5 + t * 0.05;\n let env = st_palette(clamp(band * 0.5 + 0.5, 0.0, 1.0));\n\n // A second, tighter band set gives the hard highlight streaks of polish.\n let streak = pow(abs(sin(band * PI * 1.5)), 8.0 + params.b * 40.0);\n\n var col = env * (0.35 + 0.75 * diffuse) * params.c;\n col = col + st_palette(1.0) * streak * 0.6 * params.c;\n col = col + vec3f(1.0) * spec * params.intensity * 1.4;\n col = col + st_palette(0.9) * fresnel * params.d;\n\n return st_post(st_tonemap(col), uv);\n}\n";
21
+ readonly lumen: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// lumen\n// ---------------------------------------------------------------------------\n\n// Lumen — a lattice of dots, each one a lamp and a wall at the same time.\n//\n// The lattice is what makes this affordable. Every dot is the same size, so the\n// nearest one to any point is simply the nearest lattice node, and the distance\n// to the whole field collapses to `length(p - round(p)) - radius`. That is an\n// exact distance, not an estimate, so a ray crosses a dozen cells in a handful\n// of steps and the shadows it casts run the width of the frame.\n\nfn lumEmission(id : vec2f, t : f32, lit : f32) -> vec3f {\n let seed = st_hash21(id);\n // A wave crossing the lattice decides which dots are on. Each dot carries its\n // own phase, so the front arrives as a scatter rather than as a straight line.\n let wave = 0.5 + 0.5 * sin(dot(id, vec2f(0.31, 0.19)) - t * 1.6 + seed * TAU);\n let on = smoothstep(1.0 - lit, 1.0 - lit + 0.28, wave);\n return st_palette(fract(seed * 0.7 + t * 0.04)) * on * on * 2.6;\n}\n\n// Jimenez's interleaved gradient noise. Turning every pixel's fan by the same\n// angle bands the penumbrae into spokes, and the obvious fixes trade that for\n// something worse: white noise clumps, and a low-discrepancy sequence lays the\n// offset down in diagonal stripes. This one was built for exactly this job.\nfn lumIgn(pixel : vec2f) -> f32 {\n return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));\n}\n\n// One ray through the field. It stops at the first dot it touches and brings\n// back that dot's light — which is nothing at all if the dot is dark.\nfn lumTrace(origin : vec2f, dir : vec2f, start : f32, t : f32, radius : f32, lit : f32, reach : f32) -> vec3f {\n var travel = start;\n for (var i = 0; i < 30; i = i + 1) {\n let p = origin + dir * travel;\n let id = round(p);\n let d = length(p - id) - radius;\n if (d < 0.006) {\n let fall = travel / max(reach, 0.05);\n return lumEmission(id, t, lit) / (1.0 + fall * fall);\n }\n travel = travel + max(d, 0.02);\n if (travel > 16.0) { break; }\n }\n return vec3f(0.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n // Lattice units: one unit is one cell, which is what lets `round` find the\n // nearest dot.\n let p = st_centered(uv) * params.scale * 11.0;\n\n let radius = clamp(params.b, 0.02, 0.45);\n let lit = clamp(params.c, 0.0, 1.0);\n let rays = i32(clamp(params.a, 3.0, 40.0));\n\n let id = round(p);\n let d = length(p - id) - radius;\n\n let cellId = floor(uv * params.resolution);\n let dither = lumIgn(cellId);\n let turn = dither * TAU;\n\n // Leave from just outside the dot the pixel is standing on, so a dot is lit\n // by its neighbours instead of by itself, and stagger the start so the fans\n // do not all clear the same dot at the same distance.\n let start = max(-d, 0.0) + 0.015 + dither * 0.02;\n\n var sum = vec3f(0.0);\n for (var i = 0; i < rays; i = i + 1) {\n // A third of a wedge of slop on top of the ordered offset. Full white\n // noise per ray throws the stratification away and the frame goes\n // sandy; this much only breaks the pattern's grip on the lattice.\n let slop = (st_hash21(cellId + f32(i) * 17.31) - 0.5) * 0.35;\n let angle = turn + TAU * (f32(i) + 0.5 + slop) / f32(rays);\n sum = sum + lumTrace(p, vec2f(cos(angle), sin(angle)), start, t, radius, lit, params.d);\n }\n let gathered = sum / f32(rays) * params.intensity;\n\n // The air carries the radiance straight; a dot shows its own light plus a\n // little of what reaches it, over a grey body so the dark ones stay visible.\n let body = params.colorA.rgb * 0.55 + lumEmission(id, t, lit) + gathered * 0.4;\n // One pixel wide whatever the lattice is scaled to. The dots are the only\n // hard edge here, so a fixed width would show as the density changed.\n let dotEdge = max(fwidth(d), 0.00001);\n let mask = 1.0 - smoothstep(-dotEdge, dotEdge, d);\n\n var col = mix(gathered, body, mask);\n\n return st_post(st_tonemap(col), uv);\n}\n";
22
+ readonly magnet: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// magnet\n// ---------------------------------------------------------------------------\n\n// Magnet — the field of three charges, one of them riding the pointer.\n//\n// Nothing here is traced. The stream function of a point charge is its angle\n// and the potential is the log of its distance, so field lines are level sets\n// of one sum and equipotentials level sets of the other — the entire plot is\n// two sin() calls over closed forms. The continuity of the picture depends on\n// the charges being integers: sin(n * sum of atan2) survives the branch cuts\n// only when n times each charge is whole, which is why the pointer charge\n// slider steps by one.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.2);\n\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5)\n / max(params.scale, 0.2);\n\n // Two fixed poles and the pointer. The pointer's charge is a control so it\n // can attract, repel, or sit out entirely at zero.\n let sep = 0.28;\n let poleA = vec2f(-sep, 0.0);\n let poleB = vec2f(sep, 0.0);\n let qC = round(clamp(params.c, -2.0, 2.0));\n\n let rA = p - poleA;\n let rB = p - poleB;\n let rC = p - cursor;\n let dA = max(length(rA), 0.015);\n let dB = max(length(rB), 0.015);\n let dC = max(length(rC), 0.015);\n\n // Stream function and potential, superposed.\n let psi = atan2(rA.y, rA.x) - atan2(rB.y, rB.x) + qC * atan2(rC.y, rC.x);\n let phi = log(dA) - log(dB) + qC * log(dC);\n\n // Field strength, for brightness: strong near a pole, dead in the middle\n // of nowhere. The E-field of a 2D charge falls off as 1/r.\n let eField = rA / (dA * dA) - rB / (dB * dB) + qC * rC / (dC * dC);\n let strength = length(eField);\n let energy = strength / (2.5 + strength);\n\n // Field lines flow along themselves: the phase moves with time, so the\n // lines read as current rather than as a printed diagram.\n let lineCount = max(round(params.a), 2.0);\n let lines = pow(0.5 + 0.5 * sin(psi * lineCount - t * 1.5 * params.b), 8.0);\n\n // Equipotentials stand still and stay faint — they are the grid the flow\n // is read against.\n let rings = pow(0.5 + 0.5 * sin(phi * 7.0), 12.0) * 0.35;\n\n var col = params.colorA.rgb * 0.10;\n col = col + st_palette(energy) * lines * energy * 2.4;\n col = col + params.colorB.rgb * rings * energy;\n\n // The poles themselves: hot cores, colour-coded by sign.\n col = col + params.colorC.rgb * exp(-dA * 24.0) * 1.4;\n col = col + params.colorB.rgb * exp(-dB * 24.0) * 1.4;\n let cursorTone = mix(params.colorB.rgb, params.colorC.rgb, step(0.0, qC));\n col = col + cursorTone * exp(-dC * 24.0) * 1.4 * abs(qC) * 0.5;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
23
+ readonly melt: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// melt\n// ---------------------------------------------------------------------------\n\n// Melt — the layer runs downhill, and the pointer is the heat.\n//\n// The displacement reads upward: a melting pixel shows what used to be above\n// it, so the whole effect is sampling the source at a smaller y. How much\n// smaller is a column-coherent noise — coherent down each column, varied\n// across them — which is what makes drips instead of fog. The streak pass\n// resamples the climb and keeps the brightest tap, the way a run of paint\n// carries its highlight down with it.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let aspect = st_aspect();\n\n // The heat field: a global simmer plus the pointer's blowtorch. Melting\n // grows toward the bottom of the frame because runs accumulate.\n let toCursor = (uv - params.pointer) * vec2f(aspect, 1.0);\n let heat = params.a * 0.5 + exp(-dot(toCursor, toCursor) / max(params.d * params.d, 0.001)) * params.a;\n let gravityGain = smoothstep(0.0, 0.7, uv.y);\n\n // Drips: one noise per column family, drifting slowly so the runs crawl.\n let dripNoise = st_fbm2(vec2f(uv.x * params.b * 8.0, t * 0.3), 3);\n let drip = pow(dripNoise, 2.0) * heat * (0.3 + gravityGain);\n\n // A slight sideways wobble keeps the runs from being perfectly plumb.\n let wobble = (st_noise2(vec2f(uv.x * 20.0, uv.y * 4.0 - t * 0.5)) - 0.5) * params.warp * 0.02 * heat;\n\n let base = uv + vec2f(wobble, -drip * 0.25);\n var col = st_srcAt(st_srcUvInset(base, 0.06)).rgb;\n\n // Streaks: walk back up the drip path and keep the brightest thing seen.\n // Weighted toward the origin so the smear fades as it falls.\n var streak = col;\n for (var i = 1; i <= 6; i = i + 1) {\n let fi = f32(i) / 6.0;\n let tap = st_srcAt(st_srcUvInset(base + vec2f(0.0, -drip * 0.25 * fi), 0.06)).rgb;\n streak = max(streak, tap * (1.0 - fi * 0.5));\n }\n col = mix(col, streak, clamp(params.c, 0.0, 1.0) * smoothstep(0.05, 0.3, drip));\n\n // The melt line glows faintly where the drip gradient is steep — molten\n // edges, strongest under the torch.\n let glow = smoothstep(0.15, 0.6, drip) * heat * 0.3;\n col = col + params.colorC.rgb * glow;\n\n return st_post(col * params.intensity, uv);\n}\n";
17
24
  readonly "mesh-gradient": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// mesh-gradient\n// ---------------------------------------------------------------------------\n\n// Mesh gradient — soft colour centres orbiting behind frosted glass.\n\n// A gaussian well, raised to a power so nearby colours dominate rather than\n// averaging. Without the exponent every centre contributes everywhere and the\n// middle of the frame turns grey.\nfn blob(p : vec2f, centre : vec2f, radius : f32) -> f32 {\n let d = length(p - centre);\n let g = exp(-d * d / max(radius * radius, 0.0001));\n return g * g * g;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n\n // Gentle domain warp keeps the blend from looking like plain radial gradients.\n let warpNoise = vec2f(\n st_fbm2(p * 1.6 + vec2f(0.0, t * 0.12), 3),\n st_fbm2(p * 1.6 + vec2f(5.2, t * 0.12 + 1.3), 3)\n );\n p = p + (warpNoise - 0.5) * params.warp;\n\n let r = params.a;\n let spread = params.b;\n\n let c0 = vec2f(cos(t * 0.31) * spread, sin(t * 0.24) * spread * 0.6);\n let c1 = vec2f(cos(t * 0.19 + 2.1) * spread * 0.9, sin(t * 0.37 + 1.1) * spread * 0.7);\n let c2 = vec2f(cos(t * 0.27 + 4.3) * spread * 0.8, sin(t * 0.21 + 3.7) * spread * 0.75);\n let c3 = vec2f(cos(t * 0.15 + 5.6) * spread, sin(t * 0.33 + 0.4) * spread * 0.5);\n\n // Sharpening the weights before normalising is what keeps the colours\n // distinct. Plain gaussian weights average towards grey in the middle.\n let sharpen = 2.6;\n let w0 = pow(blob(p, c0, r), sharpen);\n let w1 = pow(blob(p, c1, r * 0.9), sharpen);\n let w2 = pow(blob(p, c2, r * 1.15), sharpen);\n let w3 = pow(blob(p, c3, r * 0.8), sharpen);\n let total = w0 + w1 + w2 + w3 + 1e-6;\n\n let base = mix(params.colorA.rgb, params.colorB.rgb, 0.5) * 0.25;\n var col = (params.colorA.rgb * w0\n + params.colorB.rgb * w1\n + params.colorC.rgb * w2\n + mix(params.colorA.rgb, params.colorC.rgb, 0.5) * w3) / total;\n\n // Coverage decides how much of the dark backdrop shows through.\n let coverage = clamp(pow(total, 1.0 / sharpen) * params.c, 0.0, 1.0);\n col = mix(base, col, coverage) * params.intensity;\n\n return st_post(col, uv);\n}\n";
18
25
  readonly mosaic: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// mosaic\n// ---------------------------------------------------------------------------\n\n// Mosaic — the layer rebuilt out of tiles.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n // Cells are counted across the short axis and scaled by aspect, so a tile\n // stays square whatever shape the canvas is.\n let cells = vec2f(max(params.a, 2.0) * st_aspect(), max(params.a, 2.0));\n let cell = floor(uv * cells);\n let middle = (cell + 0.5) / cells;\n\n // One tap at the tile's centre, not an average of it. Cheaper, and the\n // hard colour is the point — averaging would only reinvent a blur.\n var tile = st_srcRgb(middle);\n\n // Posterise after sampling: quantising the tile colour is what makes it look\n // assembled out of a limited set rather than merely coarse.\n let steps = max(params.d, 2.0);\n tile = floor(tile * steps + 0.5) / steps;\n\n let local = fract(uv * cells) - 0.5;\n\n // Square tile or round dot, and everything between. The dot grows with the\n // tile's brightness, so dark areas thin out the way a printed screen does.\n let square = max(abs(local.x), abs(local.y));\n let round = length(local) / max(st_luma(tile), 0.05) * 0.55;\n let shape = mix(square, round, clamp(params.b, 0.0, 1.0));\n\n let edge = 0.5 - clamp(params.c, 0.0, 0.45);\n // Anti-alias in tile units. A fixed smoothstep width is far narrower than a\n // device pixel once the tiles are small, which is what turns clean grout into\n // a moire of hard, uneven lines.\n let aa = max(cells.y / max(params.resolution.y, 1.0), 0.002);\n let mask = 1.0 - smoothstep(edge - aa, edge + aa, shape);\n\n let ground = st_palette(0.0);\n let col = mix(ground, tile, mask);\n\n return st_post(col * params.intensity, uv);\n}\n";
19
26
  readonly "neon-grid": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// neon-grid\n// ---------------------------------------------------------------------------\n\n// Neon grid — perspective floor, banded sun, horizon haze. Synthwave by the book.\n\nfn gridLines(coord : vec2f, weight : f32) -> f32 {\n let width = max(fwidth(coord), vec2f(0.0001));\n let g = abs(fract(coord - 0.5) - 0.5) / width;\n return 1.0 - smoothstep(0.0, weight, min(g.x, g.y));\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv);\n let horizon = 0.02;\n\n // Floor is evaluated unconditionally: fwidth needs uniform control flow.\n let depth = 1.0 / max(p.y - horizon, 0.004);\n let floorCoord = vec2f(p.x * depth * 0.35, depth * 0.35 - t * params.b);\n let line = gridLines(floorCoord * params.a, 1.8);\n let below = step(horizon, p.y);\n let fade = smoothstep(0.0, 0.09, p.y - horizon) * smoothstep(1.4, 0.12, depth * 0.05);\n\n // Sky above the horizon.\n let skyT = clamp((horizon - p.y) * 2.4, 0.0, 1.0);\n var col = mix(params.colorA.rgb * 0.32, vec3f(0.015, 0.008, 0.05), skyT);\n\n // Sun: a disc sliced by horizontal bands, resting on the horizon.\n let sunCentre = vec2f(0.0, horizon - 0.17);\n let sunDist = length((p - sunCentre) * vec2f(1.0, 1.25));\n let sunMask = smoothstep(0.20, 0.19, sunDist);\n let bands = step(0.38, fract((p.y - sunCentre.y) * 26.0 + t * 0.4))\n * step(sunCentre.y + 0.015, p.y);\n let sunColor = mix(params.colorC.rgb, params.colorB.rgb,\n clamp((p.y - sunCentre.y + 0.2) * 2.5, 0.0, 1.0));\n col = mix(col, sunColor, sunMask * (1.0 - bands));\n col = col + sunColor * smoothstep(0.55, 0.0, sunDist) * 0.3 * params.d;\n\n // Floor: darken the plate, then lay the neon lines over it.\n col = mix(col, col * 0.3, below);\n col = col + params.colorB.rgb * line * fade * below * params.intensity * 1.8;\n\n // Horizon haze.\n col = col + params.colorB.rgb * exp(-abs(p.y - horizon) * 80.0) * params.c;\n\n return st_post(st_tonemap(col), uv);\n}\n";
27
+ readonly "neon-sign": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// neon-sign\n// ---------------------------------------------------------------------------\n\n// Neon sign — the layer's edges bent into glass tube and lit.\n//\n// The tube is the level set where the layer's luma crosses a threshold, which\n// is why this works on anything but sings on text: a glyph's boundary is one\n// clean crossing, and the band around it becomes the glass. The halo is not a\n// blur of the tube — it is the blurred luma itself, which is cheaper and puts\n// the light where the ink is rather than haloing both sides of every edge\n// equally.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n\n let sharp = st_luma(st_srcRgb(uv));\n let soft = st_luma(st_srcBlur(uv, params.warp * 0.03));\n let wide = (st_luma(st_srcBlur(uv, params.warp * 0.055))\n + st_luma(st_srcBlur(uv, params.warp * 0.11))) * 0.5;\n\n // The tube: a band where luma crosses the threshold. fwidth keeps it one\n // apparent width whatever resolution the source arrived at.\n let threshold = clamp(params.a, 0.05, 0.95);\n let aa = max(fwidth(soft), 0.008);\n let tube = 1.0 - smoothstep(0.0, aa * 3.0 + 0.02, abs(soft - threshold));\n\n // Flicker: mains hum under an occasional sputter. The sputter is a hash on\n // a coarse clock so the dropouts arrive as events, not as noise.\n let hum = 0.96 + 0.04 * sin(t * 60.0);\n let sputter = 1.0 - params.c * step(0.93, st_hash11(floor(t * 18.0))) * st_hash11(floor(t * 47.0));\n let power = hum * sputter;\n\n // The wall behind the sign, faintly lit by the sign's own spill.\n var col = params.colorA.rgb * (0.10 + wide * 0.5 * power);\n\n // The fill: the inside of the letterforms, glowing dimly like a lit tube's\n // backing rather than reading as a flat sticker.\n let inside = smoothstep(threshold, threshold + 0.2, sharp);\n col = col + params.colorB.rgb * inside * params.b * 0.45 * power;\n\n // Halo then core: the colour sits in the halo and the core burns to white,\n // which is how a real tube photographs.\n col = col + params.colorB.rgb * pow(wide, 1.5) * 1.6 * power;\n col = col + mix(params.colorB.rgb, params.colorC.rgb, 0.8) * tube * 2.2 * power;\n\n return st_post(st_tonemap(col * params.intensity * (0.7 + params.d * 0.6)), uv);\n}\n";
28
+ readonly orb: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// orb\n// ---------------------------------------------------------------------------\n\n// Orb — a glass sphere with a liquid blob suspended in it.\n//\n// The glass is analytic and the blob is marched, which is the only way this\n// fits in one pass. A sphere is two roots of a quadratic, so entry, exit and\n// both normals are exact; the ray only has to be stepped between them. What is\n// inside is a handful of lobes melted together with a smooth minimum, which is\n// what keeps it reading as one liquid body rather than as balls in a bag.\n//\n// Nothing here has a hard edge. The silhouette is antialiased off a field that\n// is smooth across the whole frame, and the blob off its own distance — a\n// marched surface has the width of a pixel available to it for free, and using\n// it costs one `mix`.\n\nfn orbSmin(a : f32, b : f32, k : f32) -> f32 {\n let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n return mix(b, a, h) - k * h * (1.0 - h);\n}\n\n/**\n * The blob. `spread` pulls the lobes out of the middle and ripples the skin,\n * so 0 is one round drop and 1 is a body that churns and pinches.\n */\nfn orbCore(p : vec3f, t : f32, spread : f32, lobes : i32) -> f32 {\n let reach = mix(0.02, 0.30, spread);\n var d = length(p) - mix(0.42, 0.20, spread);\n\n for (var i = 0; i < lobes; i = i + 1) {\n let fi = f32(i);\n let phase = t * (0.5 + fi * 0.11) + fi * 1.2566;\n let centre = vec3f(\n sin(phase * 1.10 + fi * 1.7),\n cos(phase * 0.90 + fi * 2.3),\n sin(phase * 0.70 + fi * 3.1),\n ) * reach;\n let radius = mix(0.16, 0.13 + 0.05 * sin(phase * 1.7), spread);\n d = orbSmin(d, length(p - centre) - radius, 0.19);\n }\n\n // A slow swell over the skin — low enough in frequency to read as liquid\n // rather than as contour lines. It breaks the distance bound a little, which\n // is why the march steps short of what it is told.\n return d + sin(p.x * 5.0 + t) * sin(p.y * 5.0 - t * 0.8) * sin(p.z * 5.0 + t * 0.6)\n * 0.045 * spread;\n}\n\n// Four taps rather than six. The extra pair buys nothing you can see through\n// glass, and this is the only place the blob is sampled off the ray.\nfn orbNormal(p : vec3f, t : f32, spread : f32, lobes : i32) -> vec3f {\n let e = vec2f(1.0, -1.0) * 0.0025;\n return normalize(\n e.xyy * orbCore(p + e.xyy, t, spread, lobes) +\n e.yyx * orbCore(p + e.yyx, t, spread, lobes) +\n e.yxy * orbCore(p + e.yxy, t, spread, lobes) +\n e.xxx * orbCore(p + e.xxx, t, spread, lobes),\n );\n}\n\n// A seamless sweeping up from a dark floor to a lit wall, with two softboxes\n// in it. No cubemap to load, and the highlights land where a photographer\n// would have put them.\nfn orbStudio(dir : vec3f, lights : f32) -> vec3f {\n let up = clamp(dir.y * 0.5 + 0.5, 0.0, 1.0);\n var col = mix(params.colorA.rgb * 0.6, params.colorA.rgb * 2.8, up * up);\n\n let key = pow(max(dot(dir, normalize(vec3f(-0.45, 0.72, -0.52))), 0.0), 24.0);\n col = col + params.colorC.rgb * key * 3.2 * lights;\n\n let fill = pow(max(dot(dir, normalize(vec3f(0.78, -0.20, -0.58))), 0.0), 9.0);\n col = col + params.colorB.rgb * fill * 1.0 * lights;\n\n return col;\n}\n\n// Yaw about the vertical, then pitch about the horizontal. Written out rather\n// than built as a 3x3, because two 2x2s are all the prelude carries and all\n// this needs.\nfn orbTurn(v : vec3f, yaw : mat2x2f, pitch : mat2x2f) -> vec3f {\n let flat = yaw * v.xz;\n let tipped = pitch * vec2f(v.y, flat.y);\n return vec3f(flat.x, tipped.x, tipped.y);\n}\n\n// Refraction that falls back to a bounce. `refract` answers zero under total\n// internal reflection, and a zero direction reads as a black hole in the glass.\nfn orbBend(incident : vec3f, normal : vec3f, eta : f32) -> vec3f {\n let bent = refract(incident, normal, eta);\n if (dot(bent, bent) < 0.0001) { return reflect(incident, normal); }\n return normalize(bent);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let lights = params.d;\n\n // uv runs down the screen and the studio is lit from above, so the y flip\n // happens once, here, and the rest of this works in an ordinary y-up world.\n let plate = st_centered(uv) * vec2f(1.0, -1.0);\n let zoom = 2.4 / max(params.scale, 0.2);\n\n var ro = vec3f(0.0, 0.0, -3.2);\n var rd = normalize(vec3f(plate * zoom, 2.6));\n\n // The pointer turns the whole studio, so the highlights travel across the\n // glass rather than the glass merely sliding under them.\n let look = (params.pointer - vec2f(0.5)) * vec2f(1.1, 0.7);\n ro = orbTurn(ro, st_rot(look.x), st_rot(-look.y));\n rd = orbTurn(rd, st_rot(look.x), st_rot(-look.y));\n\n // How far the ray passes from the centre of the ball, less its radius: 0\n // exactly on the silhouette, and smooth across the whole frame. That is what\n // lets one `fwidth` measure the edge — and it has to be taken here, before\n // any branch, because a derivative asks the neighbouring pixels and they\n // have to have walked the same path to answer.\n let rim = length(ro - rd * dot(ro, rd)) - 1.0;\n let aa = max(fwidth(rim), 0.0001);\n let cover = 1.0 - smoothstep(-aa, aa, rim);\n\n let backdrop = orbStudio(rd, lights);\n if (cover <= 0.0) {\n return st_post(st_tonemap(backdrop * params.intensity), uv);\n }\n\n // Clamped rather than branched: on the last covered pixel the ray is tangent,\n // and a tangent hit is the right answer there.\n let b = dot(ro, rd);\n let c = dot(ro, ro) - 1.0;\n let h = max(b * b - c, 0.0);\n\n let entry = ro + rd * (-b - sqrt(h));\n let normal = entry;\n let fresnel = 0.04 + 0.96 * pow(1.0 - clamp(dot(-rd, normal), 0.0, 1.0), 5.0);\n let mirrored = orbStudio(reflect(rd, normal), lights);\n\n let eta = 1.0 / (1.0 + clamp(params.warp, 0.0, 2.0) * 0.45);\n let inside = orbBend(rd, normal, eta);\n\n let spread = clamp(params.a, 0.0, 1.0);\n let lobes = i32(clamp(params.c, 1.0, 8.0));\n\n // One pixel, in world units, where the blob sits. Anything nearer the\n // surface than this is a partly covered pixel rather than a miss, which is\n // the whole of the blob's antialiasing.\n let px = 3.2 * zoom / (2.6 * max(params.resolution.y, 1.0));\n let edge = px * 1.6;\n\n // March between the two walls of the glass. Stepping by the smaller of the\n // blob distance and the distance left to the far wall stops the ray leaving\n // through the shell it is meant to be trapped in; the 0.8 pays for the\n // ripple, which the distance does not account for.\n var travel = 0.01;\n var nearPos = entry;\n var nearD = 1e9;\n var nearTravel = 0.0;\n for (var i = 0; i < 72; i = i + 1) {\n let pos = entry + inside * travel;\n let shell = length(pos) - 1.0;\n if (shell > -0.0005) { break; }\n\n let d = orbCore(pos, t, spread, lobes);\n if (d < nearD) {\n nearD = d;\n nearPos = pos;\n nearTravel = travel;\n }\n if (d < edge * 0.25) { break; }\n\n travel = travel + max(min(d * 0.8, -shell), 0.002);\n }\n\n // The ray left. On a unit sphere the far wall is exactly this far along, so\n // the exit needs no search. Split it three ways on the way out: blue bends\n // hardest, the way round a real prism does it.\n let exitT = max(-2.0 * dot(entry, inside), 0.0);\n let exitN = -(entry + inside * exitT);\n let split = clamp(params.b, 0.0, 1.0) * 0.06;\n let escaped = vec3f(\n orbStudio(orbBend(inside, exitN, (1.0 + split) / eta), lights).r,\n orbStudio(orbBend(inside, exitN, 1.0 / eta), lights).g,\n orbStudio(orbBend(inside, exitN, (1.0 - split) / eta), lights).b,\n );\n\n // Land the hit on the surface before anything reads it. The march only ever\n // stops within an epsilon of the blob, and that epsilon is a terrace: read\n // coverage or depth straight off it and the body wears contour lines. Three\n // steps of the distance itself close the gap to nothing.\n var hitPos = nearPos;\n for (var i = 0; i < 3; i = i + 1) {\n hitPos = hitPos + inside * orbCore(hitPos, t, spread, lobes) * 0.9;\n }\n let hitTravel = nearTravel + dot(hitPos - nearPos, inside);\n\n // Coverage from the settled distance, not from the marched one. A ray that\n // reached the surface is fully inside the body; one that only came near it\n // covers part of a pixel, which is the whole of the blob's antialiasing.\n let struck = 1.0 - smoothstep(0.0, edge, max(orbCore(hitPos, t, spread, lobes), 0.0));\n\n // The blob, lit soft. Wrapped diffuse rather than a hard lambert: it is\n // meant to read as something liquid suspended in the glass, not as a\n // machined part.\n let n = orbNormal(hitPos, t, spread, lobes);\n let key = normalize(vec3f(-0.45, 0.72, -0.52));\n let wrap = clamp(dot(n, key) * 0.5 + 0.5, 0.0, 1.0);\n let sheen = pow(clamp(dot(reflect(inside, n), key), 0.0, 1.0), 22.0);\n let tint = st_palette(clamp(0.22 + n.y * 0.20 + hitTravel * 0.22, 0.0, 1.0));\n // A rim on the blob as well as on the glass. A body lit only from the front\n // reads as painted; the edge catching the room is what makes it look wet.\n let edgeOn = pow(1.0 - clamp(dot(-inside, n), 0.0, 1.0), 3.0);\n let body = tint * (0.10 + wrap * wrap * 1.15)\n + orbStudio(n, lights) * 0.5\n + orbStudio(reflect(inside, n), lights) * edgeOn * 0.8\n + params.colorC.rgb * sheen * lights;\n\n let transmitted = mix(escaped, body, struck);\n let path = mix(exitT, hitTravel, struck);\n\n // Beer's law over the distance actually travelled in the glass, so a long\n // chord through the middle takes more colour than a graze at the rim.\n let absorb = exp(-(vec3f(1.0) - params.colorB.rgb) * path * 0.9);\n var glass = mix(transmitted * absorb, mirrored, fresnel);\n glass = glass + params.colorC.rgb * pow(fresnel, 2.0) * 0.35 * lights;\n\n return st_post(st_tonemap(mix(backdrop, glass, cover) * params.intensity), uv);\n}\n";
29
+ readonly parallax: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// parallax\n// ---------------------------------------------------------------------------\n\n// Parallax — ridgelines in depth, and the pointer moves the camera.\n//\n// Five silhouettes, each a one-dimensional ridged fbm thresholded against the\n// pixel's height. Depth is faked twice over: haze mixes each layer toward the\n// sky, and the pointer displaces each layer by its own parallax factor — the\n// near ridge slides furthest, which is the one cue that makes a flat stack\n// read as a landscape you are standing in.\n\nfn ridgeLine(x : f32, seed : f32, octaves : i32) -> f32 {\n return st_ridge2(vec2f(x, seed * 19.7), octaves);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv);\n // y up, because everything below is written about mountains and horizons.\n let yUp = -p.y;\n\n let look = (params.pointer - vec2f(0.5)) * params.c;\n\n // Sky: a vertical ramp with a low sun in it.\n let skyT = clamp(yUp * 1.6 + 0.35, 0.0, 1.0);\n var col = mix(params.colorB.rgb * 0.55, params.colorA.rgb, skyT);\n let sunPos = vec2f(0.28 - look.x * 0.15, 0.16 + look.y * 0.1);\n let sunDist = length(vec2f(p.x, yUp) - sunPos);\n col = col + params.colorC.rgb * exp(-sunDist * 9.0) * params.d;\n col = col + params.colorC.rgb * smoothstep(0.05, 0.03, sunDist) * params.d * 1.5;\n\n // Layers, far to near. Each one is drawn over everything behind it, so the\n // loop is the compositor.\n let layers = 5;\n for (var i = 0; i < layers; i = i + 1) {\n let depth = f32(i) / f32(layers - 1); // 0 far, 1 near\n let seed = f32(i) + 1.0;\n\n // The camera: time scrolls the world, the pointer pans it, and both move\n // the near layers more. The vertical shift is smaller — a head tilts\n // less than it turns.\n let drift = t * 0.05 * mix(0.15, 1.0, depth);\n let panX = look.x * mix(0.05, 0.55, depth);\n let panY = look.y * mix(0.02, 0.22, depth);\n\n let x = (p.x + panX + drift) * mix(1.2, 2.6, depth) * params.scale;\n let base = mix(0.12, -0.30, depth);\n let amp = mix(0.10, 0.26, depth) * params.b;\n let ridge = base + ridgeLine(x, seed, 4) * amp * params.warp + panY;\n\n let aa = max(fwidth(yUp), 0.002);\n let mask = smoothstep(ridge + aa, ridge - aa, yUp);\n\n // Haze: far layers are mostly sky. The near ridge is nearly the ink\n // colour, which is what gives the stack its contrast.\n let tone = mix(params.colorB.rgb * 0.8, params.colorA.rgb * 0.06, depth * depth);\n let haze = mix(col, tone, mix(0.45, 1.0, depth));\n col = mix(col, haze, mask);\n\n // Rim light on each ridge where it faces the sun.\n let rim = smoothstep(aa * 3.0, 0.0, abs(yUp - ridge)) * exp(-sunDist * 2.0);\n col = col + params.colorC.rgb * rim * params.d * 0.4 * mask;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
20
30
  readonly plasma: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// plasma\n// ---------------------------------------------------------------------------\n\n// Plasma — stacked sine fields, the oldest trick in the demoscene.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 6.0;\n let t = st_time();\n\n var v = 0.0;\n v = v + sin(p.x * 1.1 + t);\n v = v + sin(p.y * 1.3 + t * 1.17);\n v = v + sin((p.x + p.y) * 0.8 + t * 0.83);\n v = v + sin(length(p * params.a) * 1.4 - t * 1.6);\n\n // Rotating secondary field adds the interference bloom.\n let q = st_rot(t * 0.15 * params.b) * p;\n v = v + sin(q.x * 0.9 - q.y * 1.4 + t * 0.6) * params.c;\n\n let band = v * 0.125 + 0.5 + t * 0.05;\n var col = st_paletteCyclic(band * params.d);\n\n // Contour lines pick out the field's iso-levels.\n let contour = abs(fract(v * 0.9) - 0.5) * 2.0;\n col = col * (1.0 - smoothstep(0.85, 1.0, contour) * 0.55);\n\n return st_post(col * params.intensity, uv);\n}\n";
31
+ readonly radiance: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// radiance\n// ---------------------------------------------------------------------------\n\n// Radiance — light traced from where it lands back to whatever emitted it.\n//\n// Global illumination taken the short way round. There is no light pass and no\n// probe grid to merge: the room is four signed distances, so every pixel can\n// fan rays into it and march each one to its source inside the same fragment\n// that needs the answer. What that buys is a true shadow — hard where a lamp is\n// small, soft where it is near, and cast by geometry that moves every frame.\n\nstruct Surface {\n dist : f32,\n emit : vec3f,\n}\n\nfn radDisc(p : vec2f, centre : vec2f, radius : f32) -> f32 {\n return length(p - centre) - radius;\n}\n\nfn radBar(p : vec2f, centre : vec2f, half : vec2f, angle : f32) -> f32 {\n let q = abs(st_rot(angle) * (p - centre)) - half;\n return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0);\n}\n\nfn radNearer(a : Surface, b : Surface) -> Surface {\n if (b.dist < a.dist) { return b; }\n return a;\n}\n\n// Three drifting lamps, the pointer, and three turning bars that emit nothing.\n// A ray that lands on a bar comes back with black, which is the whole of the\n// shadowing — no shadow term, just an emitter that is not there.\nfn radScene(p : vec2f, t : f32, walls : f32) -> Surface {\n var hit = Surface(1e9, vec3f(0.0));\n\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let angle = t * (0.23 + fi * 0.11) + fi * 2.094;\n let centre = vec2f(cos(angle) * (0.34 - fi * 0.07), sin(angle * 1.4) * (0.24 - fi * 0.05));\n let lamp = Surface(\n radDisc(p, centre, 0.016 + fi * 0.007),\n st_palette(fi * 0.5) * (3.4 - fi * 0.7),\n );\n hit = radNearer(hit, lamp);\n }\n\n // The pointer is the brightest lamp in the room, and it sits in the middle\n // until something moves it — the still frame has to stand on its own.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5);\n hit = radNearer(hit, Surface(radDisc(p, cursor, 0.028), st_palette(1.0) * 4.2));\n\n if (walls > 0.001) {\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let orbit = fi * 2.094 - t * 0.17;\n let centre = vec2f(cos(orbit), sin(orbit)) * 0.21;\n let bar = radBar(p, centre, vec2f(0.17, 0.012) * walls, orbit * 1.7 + fi);\n hit = radNearer(hit, Surface(bar, vec3f(0.0)));\n }\n }\n\n return hit;\n}\n\n// Jimenez's interleaved gradient noise. Turning every pixel's fan by the same\n// angle bands the penumbrae into spokes, and the obvious fixes trade that for\n// something worse: white noise clumps, and a low-discrepancy sequence lays the\n// offset down in diagonal stripes. This one was built for exactly this job.\nfn radIgn(pixel : vec2f) -> f32 {\n return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));\n}\n\n// One ray, marched until it meets something or runs out of room. `start` skips\n// the surface the pixel is already standing on, so a lamp does not shadow\n// itself into a black disc.\nfn radTrace(origin : vec2f, dir : vec2f, start : f32, t : f32, reach : f32, walls : f32) -> vec3f {\n var travel = start;\n for (var i = 0; i < 26; i = i + 1) {\n let hit = radScene(origin + dir * travel, t, walls);\n if (hit.dist < 0.0025) {\n // Inverse square, offset by one so a pixel touching a lamp does not\n // divide by nothing and blow the frame out.\n let fall = travel / max(reach, 0.02);\n return hit.emit / (1.0 + fall * fall);\n }\n travel = travel + max(hit.dist, 0.004);\n if (travel > 3.0) { break; }\n }\n return vec3f(0.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.05);\n\n let rays = i32(clamp(params.a, 3.0, 48.0));\n let walls = params.c;\n\n let cellId = floor(uv * params.resolution);\n let dither = radIgn(cellId);\n let turn = dither * TAU;\n\n let standing = radScene(p, t, walls);\n // Staggering where the rays begin as well as where they point breaks up the\n // ring the shared start distance would otherwise leave around every lamp.\n let start = max(-standing.dist, 0.0) + 0.004 + dither * 0.006;\n\n var sum = vec3f(0.0);\n for (var i = 0; i < rays; i = i + 1) {\n // A third of a wedge of slop on top of the ordered offset. Full white\n // noise per ray throws the stratification away and the frame goes\n // sandy; this much only breaks the pattern's grip on the lattice.\n let slop = (st_hash21(cellId + f32(i) * 17.31) - 0.5) * 0.35;\n let angle = turn + TAU * (f32(i) + 0.5 + slop) / f32(rays);\n sum = sum + radTrace(p, vec2f(cos(angle), sin(angle)), start, t, params.b, walls);\n }\n\n var col = sum / f32(rays) * params.intensity;\n\n // A lamp lights its own pixels. Without this the rays all leave from its\n // surface facing outwards, and the brightest thing in the room is a hole.\n col = col + standing.emit * (1.0 - smoothstep(-0.002, 0.002, standing.dist));\n\n // Skylight: the floor of the image, so the bars read as objects in a room\n // rather than as holes cut out of it.\n col = col + params.colorA.rgb * params.d;\n\n return st_post(st_tonemap(col), uv);\n}\n";
21
32
  readonly reveal: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// reveal\n// ---------------------------------------------------------------------------\n\n// Reveal — the layer wiped in over a colour ground.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n // At speed 0, progress is exactly the control: a scrubber to drive from your\n // own code, bound to a scroll position or a load event.\n //\n // Running, it wipes in, holds the layer for most of the cycle, then clears.\n // A linear loop would spend half its time on an empty ground, and the offset\n // starts it revealed so the first frame is the picture and not the backdrop.\n let phase = fract(params.a + 0.4 + st_time() * 0.25);\n let cycled = smoothstep(0.0, 0.3, phase) * (1.0 - smoothstep(0.9, 1.0, phase));\n let progress = select(cycled, params.a, params.speed < 0.001);\n\n // The field the threshold cuts through. Noise dissolves, a rotated ramp\n // wipes, and `shape` slides between the two.\n let noise = st_fbm2(st_centered(uv) * params.scale * 4.0, 3);\n let axis = st_rot(params.c * TAU) * (uv - 0.5);\n let ramp = clamp(axis.x + 0.5, 0.0, 1.0);\n let field = mix(noise, ramp, clamp(params.b, 0.0, 1.0));\n\n // Widen the threshold band to soften the boundary. It has to cover the\n // whole 0..1 range at both ends, or the wipe never finishes.\n let soft = max(params.d, 0.001);\n let cut = progress * (1.0 + 2.0 * soft) - soft;\n let mask = 1.0 - smoothstep(cut - soft, cut + soft, field);\n\n // The ground follows the clean ramp, never the noise. Colouring it by the\n // dissolve field would leave static behind the wipe instead of a backdrop.\n let ground = st_palette(ramp);\n var col = mix(ground, st_srcRgb(uv), mask);\n\n // A bright rim rides the boundary, the way a burn does.\n let rim = 1.0 - abs(mask - 0.5) * 2.0;\n col = col + st_palette(1.0) * pow(rim, 3.0) * params.warp;\n\n return st_post(col * params.intensity, uv);\n}\n";
22
33
  readonly ripple: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// ripple\n// ---------------------------------------------------------------------------\n\n// Ripple — interference between travelling circular waves.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n let sourceCount = 5;\n var wave = 0.0;\n var gradient = vec2f(0.0);\n\n for (var i = 0; i < sourceCount; i = i + 1) {\n let fi = f32(i);\n let phase = fi * 2.399963;\n let orbit = 0.28 * params.b;\n let src = vec2f(cos(t * 0.23 + phase) * orbit, sin(t * 0.31 + phase * 1.7) * orbit);\n let delta = p - src;\n let d = length(delta) + 0.0001;\n let k = 40.0 * params.a;\n let decay = 1.0 / (1.0 + d * 6.0);\n let s = sin(d * k - t * 4.0 + phase) * decay;\n wave = wave + s;\n gradient = gradient + (delta / d) * cos(d * k - t * 4.0 + phase) * decay * k;\n }\n\n wave = wave / f32(sourceCount);\n gradient = gradient / f32(sourceCount);\n\n // Treat the wave slope as a refracting surface and read the palette through it.\n let refracted = clamp(0.5 + wave * 1.4 + gradient.x * 0.02 * params.c, 0.0, 1.0);\n var col = st_palette(refracted);\n\n // Caustic highlights where the wavefronts focus.\n let caustic = pow(clamp(1.0 - abs(wave) * 3.2, 0.0, 1.0), 6.0);\n col = col + vec3f(1.0) * caustic * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
34
+ readonly shatter: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// shatter\n// ---------------------------------------------------------------------------\n\n// Shatter — the layer breaks into shards that fly from the pointer.\n//\n// The shards are a voronoi partition, and the trick is the direction of the\n// mapping: the shards do not move, their contents do. Each cell samples the\n// source from a position swung back toward the impact point, so as progress\n// runs the picture inside every shard slides and rotates away from the\n// cursor while the crack pattern holds still. At speed 0 the progress slider\n// is a scrubber, same contract as reveal — bind it to a scroll position and\n// the break happens under the reader's thumb.\n\nstruct Shard {\n seed : vec2f,\n centre : vec2f,\n edge : f32,\n}\n\n// Nearest voronoi seed and the distance margin to the second-nearest, which\n// is what a crack line is drawn from.\nfn shardAt(g : vec2f, t : f32) -> Shard {\n let id = floor(g);\n var best = 1e9;\n var second = 1e9;\n var bestSeed = vec2f(0.0);\n var bestCentre = vec2f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let cell = id + vec2f(f32(x), f32(y));\n let rnd = st_hash22(cell);\n let centre = cell + rnd;\n let d = length(g - centre);\n if (d < best) {\n second = best;\n best = d;\n bestSeed = rnd;\n bestCentre = centre;\n } else if (d < second) {\n second = d;\n }\n }\n }\n return Shard(bestSeed, bestCentre, second - best);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let aspect = st_aspect();\n\n // Progress: the slider, plus a smooth break-and-reassemble loop whenever\n // the clock is running. With speed at 0, st_time is 0 and only the slider\n // remains.\n let cycle = 0.5 - 0.5 * cos(t * 0.5);\n let progress = clamp(params.a + cycle, 0.0, 1.0);\n // Ease the launch so shards leave with a snap rather than a drift.\n let flight = progress * progress * (3.0 - 2.0 * progress);\n\n let cells = max(params.b, 2.0);\n let p = vec2f(uv.x * aspect, uv.y);\n let shard = shardAt(p * cells, t);\n\n let impact = vec2f(params.pointer.x * aspect, params.pointer.y);\n let centreUv = shard.centre / cells;\n let away = centreUv - impact;\n let dist = length(away);\n let dir = away / max(dist, 0.0001);\n\n // Close shards fly hard, far ones barely shiver — the impact has a radius.\n let force = exp(-dist * 1.4) * (0.4 + shard.seed.x * 0.6);\n let travel = dir * flight * force * params.warp * 0.55;\n\n // Spin each shard around its own centre as it flies.\n let spin = (shard.seed.y - 0.5) * flight * params.c * 2.0;\n let swung = st_rot(-spin) * (p - centreUv) + centreUv;\n\n // Contents sampled from where the shard came from: back toward the impact.\n let readAt = (swung - travel) / vec2f(aspect, 1.0);\n var col = st_srcAt(st_srcUvInset(readAt, 0.08)).rgb;\n\n // Shards dim slightly as they fly, and the ground shows through the gaps.\n let gap = smoothstep(0.10, 0.02, shard.edge) * flight;\n col = col * (1.0 - flight * force * 0.5);\n col = mix(col, params.colorA.rgb, gap);\n\n // Cracks: dark seam with a bright glint riding it, growing with progress.\n let seam = smoothstep(0.09, 0.015, shard.edge);\n col = col * (1.0 - seam * 0.6 * progress);\n col = col + params.colorC.rgb * seam * flight * force * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
23
35
  readonly shockwave: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// shockwave\n// ---------------------------------------------------------------------------\n\n// Shockwave — expanding rings that shove the layer aside as they pass.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let aspect = st_aspect();\n // Pointer, so the wave starts wherever the cursor is when tracking is on.\n let centre = params.pointer;\n let offset = (uv - centre) * vec2f(aspect, 1.0);\n let dist = length(offset);\n let dir = offset / max(dist, 0.0001);\n\n let t = st_time() * 0.3;\n let rings = 3;\n let width = max(params.b * 0.12, 0.005);\n\n // Each ring is a gaussian band around its own radius, evenly staggered in\n // phase. A ring loses force as it grows, the way a real front spends itself.\n var push = 0.0;\n var crest = 0.0;\n for (var i = 0; i < rings; i = i + 1) {\n let phase = fract(t + f32(i) / f32(rings));\n let radius = phase * 1.3 * max(params.a, 0.1);\n let band = exp(-pow((dist - radius) / width, 2.0));\n let force = band * pow(1.0 - phase, max(params.c, 0.1));\n // Signed by which side of the crest we are on, so the front compresses\n // ahead of itself and rarefies behind — that is what makes it read as a\n // wave and not a glowing ring.\n push = push + force * sign(dist - radius);\n crest = crest + band * (1.0 - phase);\n }\n\n // Read a slightly smaller crop, so the front has material to shove around\n // even at the frame's edge. Without it the border goes black wherever a ring\n // is passing through it.\n let base = st_srcUvInset(uv, 0.09);\n let shove = st_srcWarp(dir * push * params.warp * 0.06);\n let spread = shove * params.d * 0.3;\n\n var col = vec3f(\n st_srcAt(base + shove + spread).r,\n st_srcAt(base + shove).g,\n st_srcAt(base + shove - spread).b,\n );\n col = col + st_palette(0.85) * crest * crest * 0.35;\n\n return st_post(col * params.intensity, uv);\n}\n";
24
36
  readonly silk: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// silk\n// ---------------------------------------------------------------------------\n\n// Silk — anisotropic sheen across warped fibre lines. Soft, cloth-like.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale;\n\n // Two-stage domain warp gives the fabric its folds.\n let w1 = vec2f(st_fbm2(p * 1.1 + vec2f(0.0, t * 0.08), 4),\n st_fbm2(p * 1.1 + vec2f(3.7, t * 0.06), 4));\n p = p + (w1 - 0.5) * params.warp * 2.0;\n let w2 = st_fbm2(p * 2.6 + vec2f(t * 0.05, 0.0), 5);\n\n // Fibre direction: dense parallel lines bent by the warp.\n let fibres = sin((p.x * 18.0 + p.y * 6.0) * params.a + w2 * 12.0 * params.b);\n let sheen = pow(clamp(fibres * 0.5 + 0.5, 0.0, 1.0), 3.0);\n\n // The base tone comes from the fold height, the highlight from the fibres.\n var col = st_palette(clamp(w2 * 1.4 - 0.1, 0.0, 1.0));\n col = col + st_palette(1.0) * sheen * params.c;\n\n // Slight darkening in the creases sells the depth.\n let crease = smoothstep(0.55, 0.15, w2);\n col = col * (1.0 - crease * params.d * 0.7);\n\n return st_post(col * params.intensity, uv);\n}\n";
25
37
  readonly starfield: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// starfield\n// ---------------------------------------------------------------------------\n\n// Starfield — a warp corridor. Layered depth slices flying at the camera.\n\n// Two independent hashes per cell; sharing one seed across layers is what\n// produces the diagonal moiré you get from adding a scalar to both components.\nfn cellRandom(cell : vec2f, layerSeed : f32) -> vec3f {\n let a = st_hash22(cell + vec2f(layerSeed * 37.19, layerSeed * 11.73));\n let b = st_hash21(cell.yx + layerSeed * 91.31);\n return vec3f(a, b);\n}\n\nfn layer(p : vec2f, layerIndex : f32, t : f32) -> vec3f {\n let z = fract(layerIndex * 0.1667 - t * 0.12);\n let scaleByDepth = mix(16.0, 2.0, z);\n let fade = smoothstep(0.0, 0.22, z) * smoothstep(1.0, 0.7, z);\n if (fade <= 0.0) { return vec3f(0.0); }\n\n // Rotating each slice breaks the alignment between neighbouring layers.\n let grid = st_rot(layerIndex * 1.37) * p * scaleByDepth;\n let base = floor(grid);\n let frac = fract(grid) - 0.5;\n\n var acc = vec3f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = cellRandom(base + neighbour, layerIndex);\n if (rnd.z < 0.72) { continue; }\n\n let point = neighbour + rnd.xy - 0.5;\n var offset = frac - point;\n // Motion blur: stretch each star along its radial direction.\n let radial = normalize(p + vec2f(1e-5));\n let along = dot(offset, radial);\n let across = offset - radial * along;\n offset = radial * along / (1.0 + params.b * 8.0 * (1.0 - z)) + across;\n\n // A tight inverse-square core keeps stars as points. Widen the epsilon\n // and six layers of them blur into fog.\n let d = length(offset);\n let brightness = 0.00022 * params.a * (0.35 + rnd.z);\n acc = acc + st_palette(rnd.y) * (brightness / (d * d + 0.000018)) * fade;\n }\n }\n return acc;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n p = st_rot(t * 0.05 * params.d) * p;\n\n var col = vec3f(0.004, 0.005, 0.012);\n for (var i = 0; i < 6; i = i + 1) {\n col = col + layer(p, f32(i), t);\n }\n\n // Core glow at the vanishing point.\n let core = params.c * 0.05 / (length(p) + 0.06);\n col = col + st_palette(1.0) * core;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
26
38
  readonly swirl: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// swirl\n// ---------------------------------------------------------------------------\n\n// Swirl — a vortex that winds the layer around a point.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let aspect = st_aspect();\n let centre = params.pointer;\n let offset = (uv - centre) * vec2f(aspect, 1.0);\n let dist = length(offset);\n\n // Falloff decides the whole character: 1 is a gentle whirlpool, 4 is a tight\n // knot with still water around it.\n let radius = max(params.a, 0.02);\n let falloff = pow(clamp(1.0 - dist / radius, 0.0, 1.0), max(params.b, 0.1));\n let angle = falloff * params.warp * 4.0 + st_time() * params.c;\n\n // Rotation about the centre pushes the corners outside a rectangle, so the\n // read is inset enough to keep them on the picture.\n let toUv = vec2f(1.0 / aspect, 1.0);\n let twisted = st_rot(angle) * offset;\n let source = st_srcUvInset(twisted * toUv + centre, 0.06);\n\n // Winding the channels by slightly different angles smears colour along the\n // direction of travel, which is what sells the spin.\n let apart = params.d * 0.15 * falloff;\n let ahead = st_srcUvInset((st_rot(angle + apart) * offset) * toUv + centre, 0.06);\n let behind = st_srcUvInset((st_rot(angle - apart) * offset) * toUv + centre, 0.06);\n\n let col = vec3f(\n st_srcAt(ahead).r,\n st_srcAt(source).g,\n st_srcAt(behind).b,\n );\n\n return st_post(col * params.intensity, uv);\n}\n";
39
+ readonly tesla: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// tesla\n// ---------------------------------------------------------------------------\n\n// Tesla — a plasma globe whose filament reaches for your finger.\n//\n// The bolts are analytic: each one is a base angle plus a noise wiggle that\n// grows with radius, and a pixel's distance to it is its angular error times\n// its radius. No marching, no segments — the whole filament costs two fbm\n// calls. The pointer is the interactive part: one bolt always aims at it,\n// clamped to the glass, which is exactly what the real toy does when a hand\n// touches the dome.\n\nfn wrapAngle(x : f32) -> f32 {\n return x - TAU * floor(x / TAU + 0.5);\n}\n\n// The glow one filament deposits at polar point (radius, theta).\nfn boltGlow(radius : f32, theta : f32, baseAngle : f32, t : f32, seed : f32, width : f32) -> vec2f {\n // The wiggle grows from nothing at the electrode, so every bolt is rooted\n // at the centre no matter how hard it thrashes at the rim.\n let sway = (st_fbm2(vec2f(radius * 3.5 - t * 2.2, seed * 43.7), 3) - 0.5)\n * params.warp * 2.4 * radius;\n let d = abs(wrapAngle(theta - baseAngle - sway)) * radius;\n\n // Core and halo, both against a width that thins as the bolt travels.\n let w = width * (0.5 + radius);\n let core = exp(-d / max(w * 0.012, 0.0005));\n let halo = exp(-d / max(w * 0.05, 0.002));\n return vec2f(core, halo);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.2);\n\n let R = 0.42;\n let radius = length(p);\n let theta = atan2(p.y, p.x);\n\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5)\n / max(params.scale, 0.2);\n\n let bolts = i32(clamp(params.a, 2.0, 8.0));\n var core = 0.0;\n var halo = 0.0;\n var tint = vec3f(0.0);\n\n for (var i = 0; i < bolts; i = i + 1) {\n let fi = f32(i);\n let seed = st_hash11(fi + 1.0);\n // Each bolt drifts around the globe at its own slow rate, and re-rolls\n // its heading on a coarse clock — the jump is what reads as electricity.\n let jump = floor(t * (1.5 + seed * 2.0) * params.c + seed * 9.0);\n let baseAngle = (fi + st_hash11(jump + fi * 17.0)) / f32(bolts) * TAU;\n let flicker = 0.55 + 0.45 * st_hash11(jump * 3.1 + fi);\n\n let g = boltGlow(radius, theta, baseAngle, t + seed * 8.0, seed, params.b);\n core = core + g.x * flicker;\n halo = halo + g.y * flicker;\n tint = tint + st_palette(seed) * g.y * flicker;\n }\n\n // The seeking bolt. It aims at the cursor, clamped to the inside of the\n // glass, and fades out when the cursor sits on the electrode where an\n // angle stops meaning anything.\n let cursorR = length(cursor);\n let aimR = min(cursorR, R - 0.015);\n let aim = atan2(cursor.y, cursor.x);\n let alive = smoothstep(0.06, 0.16, cursorR);\n let g = boltGlow(radius, theta, aim, t, 0.77, params.b * 0.8);\n let reach = smoothstep(aimR + 0.06, aimR - 0.02, radius);\n core = core + g.x * 1.8 * alive * reach;\n halo = halo + g.y * 1.4 * alive * reach;\n tint = tint + st_palette(0.9) * g.y * alive * reach;\n\n // Contact bloom where the filament meets the glass or the finger.\n let touch = cursor / max(cursorR, 0.0001) * aimR;\n halo = halo + exp(-length(p - touch) * 22.0) * 1.6 * alive;\n\n // Assemble inside the globe: electrode, haze, filaments.\n let inside = smoothstep(R + 0.004, R - 0.004, radius);\n let electrode = exp(-radius * 55.0);\n var col = params.colorA.rgb * (0.05 + params.d * 0.12 * (1.0 - radius / R));\n col = col + tint * 0.3 + params.colorB.rgb * halo * 0.45;\n col = col + params.colorC.rgb * (core * 1.6 + electrode * 0.9);\n col = col * inside;\n\n // The glass: a thin rim, brightest where filaments are near it.\n let rim = exp(-abs(radius - R) * 120.0);\n col = col + params.colorB.rgb * rim * (0.15 + halo * 0.6);\n\n // A faint stand shadow below, so the globe sits on something.\n col = col + params.colorA.rgb * 0.06 * smoothstep(0.1, 0.5, p.y) * (1.0 - inside);\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
40
+ readonly torch: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// torch\n// ---------------------------------------------------------------------------\n\n// Torch — a light in your hand, moving over a rock wall in the dark.\n//\n// The wall is a heightfield and the lamp is the pointer, so the shading is\n// honest: the fbm that draws the rock is differentiated for a normal, and the\n// relief answers the light's position — crevices light up and go dark as the\n// torch passes over them, which is what makes the surface read as carved\n// rather than printed.\n\nfn rockHeight(p : vec2f) -> f32 {\n let crack = 1.0 - abs(st_noise2(p * 2.2) * 2.0 - 1.0);\n return st_fbm2(p * 5.0, 4) * 0.6 + st_ridge2(p * 11.0, 3) * 0.2 - crack * crack * 0.35;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5) * params.scale;\n\n // The wall. Two extra height taps buy the normal; relief scales the slope\n // rather than the height, so the texture stays put while the depth changes.\n let eps = 0.008;\n let h = rockHeight(p);\n let hx = rockHeight(p + vec2f(eps, 0.0));\n let hy = rockHeight(p + vec2f(0.0, eps));\n let slope = vec2f(hx - h, hy - h) / eps * params.warp;\n let normal = normalize(vec3f(-slope, 1.0));\n\n // The torch: just above the wall, guttering. Two noise taps at different\n // rates — a slow breathe and a fast sputter — read as flame, where a sine\n // would read as a siren.\n let gutter = 0.85\n + 0.10 * (st_noise2(vec2f(t * 6.0, 3.7)) - 0.5) * 2.0 * params.b\n + 0.05 * (st_noise2(vec2f(t * 23.0, 9.1)) - 0.5) * 2.0 * params.b;\n let lampHeight = 0.35;\n let toLamp = vec3f(cursor - p, lampHeight);\n let lampDist = length(toLamp);\n let lightDir = toLamp / lampDist;\n\n // The beam: an inverse-square lamp inside a soft-edged pool of light.\n let radius = max(params.a, 0.1);\n let pool = smoothstep(radius, radius * 0.25, length(cursor - p));\n let diffuse = max(dot(normal, lightDir), 0.0) / (1.0 + lampDist * lampDist * 3.0);\n let spec = pow(max(dot(normal, normalize(lightDir + vec3f(0.0, 0.0, 1.0))), 0.0), 24.0);\n\n // The rock's own colour varies with a second, coarser field, so the wall\n // has geology as well as relief.\n let vein = st_fbm2(p * 1.3 + 17.0, 3);\n let albedo = mix(params.colorA.rgb, params.colorA.rgb * 2.6 + vec3f(0.04), smoothstep(0.3, 0.7, vein));\n\n var col = albedo * (params.d * 0.15);\n col = col + albedo * params.colorB.rgb * diffuse * pool * gutter * 4.2;\n col = col + params.colorC.rgb * spec * pool * gutter * 0.35;\n\n // Dust motes drifting through the beam. Only lit air shows them, which is\n // the whole reason a torch beam is visible at all.\n var motes = 0.0;\n for (var i = 0; i < 2; i = i + 1) {\n let fi = f32(i);\n let mp = p * (5.0 + fi * 3.0) + vec2f(t * (0.08 + fi * 0.05), t * 0.15);\n let cell = floor(mp);\n let rnd = st_hash22(cell);\n let d = length(fract(mp) - 0.5 - (rnd - 0.5) * 0.6);\n motes = motes + smoothstep(0.045, 0.0, d) * step(0.82, rnd.x);\n }\n col = col + params.colorC.rgb * motes * pool * gutter * params.c * 0.9;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
27
41
  readonly truchet: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// truchet\n// ---------------------------------------------------------------------------\n\n// Truchet — quarter-arc tiles that flip in place. Hard, graphic, no noise wash.\n\nfn arcTile(f : vec2f, flipped : bool, weight : f32) -> f32 {\n var q = f;\n if (flipped) { q.x = 1.0 - q.x; }\n let d1 = abs(length(q) - 0.5);\n let d2 = abs(length(q - vec2f(1.0, 1.0)) - 0.5);\n let d = min(d1, d2);\n return 1.0 - smoothstep(weight, weight + 0.02, d);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 6.0;\n p = st_rot(t * 0.04 * params.d) * p;\n p = p + vec2f(t * 0.25 * params.c, 0.0);\n\n let base = floor(p);\n let f = fract(p);\n\n // Each tile flips on its own clock.\n let rnd = st_hash21(base);\n let phase = sin(t * 0.9 + rnd * TAU) * 0.5 + 0.5;\n let flipped = (rnd + phase * params.b) > 1.0;\n\n let line = arcTile(f, flipped, params.a * 0.12);\n\n let tint = st_palette(fract(rnd + t * 0.05));\n let bg = params.colorA.rgb * 0.14;\n var col = mix(bg, tint * params.intensity, line);\n\n // A soft bloom off the strokes keeps it from looking flat.\n col = col + tint * line * 0.25;\n\n return st_post(col, uv);\n}\n";
28
42
  readonly tunnel: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// tunnel\n// ---------------------------------------------------------------------------\n\n// Tunnel — a raymarched corridor with emissive rings and distance fog.\n\n// The corridor centre wanders as you fly down it. Keep the amplitude well\n// under the radius, or the ray leaves the tube before it has travelled.\nfn tunnelPath(z : f32) -> vec2f {\n return vec2f(sin(z * 0.28) * 0.42, cos(z * 0.21) * 0.34);\n}\n\n// Distance from a point to the wall, measured from inside the tube.\nfn sceneSdf(pos : vec3f, radius : f32, ribs : f32) -> f32 {\n let q = pos.xy - tunnelPath(pos.z);\n let r = length(q);\n let theta = atan2(q.y, q.x);\n let corrugation = sin(pos.z * 2.4) * 0.05 * ribs + sin(theta * 6.0 + pos.z * 0.5) * 0.05 * ribs;\n return radius + corrugation - r;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * 2.0;\n let t = st_time() * 2.0;\n\n let ro = vec3f(tunnelPath(t), t);\n // Aim slightly along the corridor so the walls sweep past rather than\n // sitting still around a fixed vanishing point.\n let ahead = tunnelPath(t + 2.0) - tunnelPath(t);\n let rd = normalize(vec3f(p / max(params.scale, 0.2) + ahead * 0.45, 1.6));\n\n var travelled = 0.05;\n var hit = false;\n for (var i = 0; i < 96; i = i + 1) {\n let d = sceneSdf(ro + rd * travelled, params.a, params.b);\n if (d < 0.004 * travelled) {\n hit = true;\n break;\n }\n travelled = travelled + max(d * 0.7, 0.01);\n if (travelled > 30.0) { break; }\n }\n\n if (!hit) {\n return st_post(vec3f(0.0), uv);\n }\n\n let pos = ro + rd * travelled;\n let q = pos.xy - tunnelPath(pos.z);\n let theta = atan2(q.y, q.x);\n\n // Wall texture in tunnel space: seams around, plates along. Both are thin\n // lines on a dark wall — wide bands read as a kaleidoscope, not a corridor.\n let seams = 1.0 - smoothstep(0.0, 0.16, abs(fract(theta / TAU * 8.0) - 0.5) * 2.0);\n let plates = 1.0 - smoothstep(0.0, 0.2, abs(fract(pos.z * 0.9) - 0.5) * 2.0);\n let grime = st_fbm2(vec2f(theta * 2.4, pos.z * 0.7), 3);\n let surface = clamp(0.12 + grime * 0.35 + max(seams, plates) * 0.55, 0.0, 1.0);\n\n // Cheap normal from the SDF gradient, lit from a lamp at the camera.\n let e = vec2f(0.01, 0.0);\n let n = normalize(vec3f(\n sceneSdf(pos + e.xyy, params.a, params.b) - sceneSdf(pos - e.xyy, params.a, params.b),\n sceneSdf(pos + e.yxy, params.a, params.b) - sceneSdf(pos - e.yxy, params.a, params.b),\n sceneSdf(pos + e.yyx, params.a, params.b) - sceneSdf(pos - e.yyx, params.a, params.b),\n ));\n let lamp = clamp(dot(n, -rd), 0.0, 1.0);\n\n // Falling off with distance as well as with the lamp angle is what makes\n // the near wall read as near.\n let nearness = 1.0 / (1.0 + travelled * 0.35);\n var col = st_palette(clamp(surface, 0.0, 1.0))\n * (0.10 + 1.15 * lamp * nearness) * params.intensity;\n\n // Emissive rings at fixed intervals down the corridor.\n let ring = pow(1.0 - abs(fract(pos.z * 0.5) * 2.0 - 1.0), 20.0);\n col = col + st_palette(1.0) * ring * params.c * 5.0;\n\n // Fog swallows the far end.\n col = col * exp(-travelled * 0.10 * (0.4 + params.d * 2.4));\n\n return st_post(st_tonemap(col), uv);\n}\n";
29
43
  readonly voronoi: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// voronoi\n// ---------------------------------------------------------------------------\n\n// Voronoi — animated cells with lit edges. Crystalline, hard-edged.\n\nstruct Cell {\n f1 : f32,\n f2 : f32,\n id : vec2f,\n}\n\nfn voronoi(p : vec2f, t : f32, jitter : f32) -> Cell {\n let base = floor(p);\n let frac = fract(p);\n var f1 = 8.0;\n var f2 = 8.0;\n var id = vec2f(0.0);\n\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = st_hash22(base + neighbour);\n let point = neighbour + 0.5 + (sin(t + rnd * TAU) * 0.5) * jitter;\n let d = length(point - frac);\n if (d < f1) {\n f2 = f1;\n f1 = d;\n id = base + neighbour;\n } else if (d < f2) {\n f2 = d;\n }\n }\n }\n return Cell(f1, f2, id);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 8.0;\n let t = st_time();\n\n let cell = voronoi(p, t, params.a);\n let edge = cell.f2 - cell.f1;\n\n let tone = st_hash21(cell.id);\n var col = st_palette(fract(tone + t * 0.03 * params.d)) * (0.25 + 0.75 * tone);\n\n // Depth: darken the cell interior, then draw the ridge between cells.\n col = col * mix(1.0, smoothstep(0.0, 0.55, cell.f1), params.c);\n\n let ridge = 1.0 - smoothstep(0.0, params.b, edge);\n col = col + st_palette(1.0) * ridge * params.intensity;\n\n return st_post(col, uv);\n}\n";
30
44
  };
31
45
  /** Whether a shader invents its picture or reads one from a bound texture. */
32
46
  export declare const shaderKinds: {
47
+ readonly ascii: "source";
33
48
  readonly aurora: "generative";
49
+ readonly beacon: "generative";
34
50
  readonly bloom: "source";
35
51
  readonly bokeh: "generative";
36
52
  readonly caustics: "generative";
37
53
  readonly dither: "generative";
38
54
  readonly duotone: "source";
39
55
  readonly edges: "source";
56
+ readonly eyes: "generative";
40
57
  readonly "film-grain": "generative";
41
58
  readonly flow: "source";
42
59
  readonly glass: "source";
60
+ readonly gravity: "generative";
43
61
  readonly halftone: "generative";
44
62
  readonly julia: "generative";
45
63
  readonly kaleidoscope: "generative";
46
64
  readonly lens: "source";
47
65
  readonly "liquid-metal": "generative";
66
+ readonly lumen: "generative";
67
+ readonly magnet: "generative";
68
+ readonly melt: "source";
48
69
  readonly "mesh-gradient": "generative";
49
70
  readonly mosaic: "source";
50
71
  readonly "neon-grid": "generative";
72
+ readonly "neon-sign": "source";
73
+ readonly orb: "generative";
74
+ readonly parallax: "generative";
51
75
  readonly plasma: "generative";
76
+ readonly radiance: "generative";
52
77
  readonly reveal: "source";
53
78
  readonly ripple: "generative";
79
+ readonly shatter: "source";
54
80
  readonly shockwave: "source";
55
81
  readonly silk: "generative";
56
82
  readonly starfield: "generative";
57
83
  readonly swirl: "source";
84
+ readonly tesla: "generative";
85
+ readonly torch: "generative";
58
86
  readonly truchet: "generative";
59
87
  readonly tunnel: "generative";
60
88
  readonly voronoi: "generative";