shadertown 0.20.0 → 0.20.2

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,8 +1,9 @@
1
1
  // GENERATED by scripts/build-shaders.mjs — do not edit by hand.
2
2
  // Each entry is the shared prelude plus one shader, ready to hand to vgpu's
3
3
  // `effect()`. Shipping strings means consumers need no WGSL loader.
4
- // Annotated rather than inferred: with this many sources the literal type
5
- // outgrows what tsc will serialize into a declaration file.
4
+ //
5
+ // The kinds, the graph flavours and the name type live in `catalogue.ts`:
6
+ // naming a shader must not cost the reader every shader's source.
6
7
  export const shaderSources = {
7
8
  "alloy": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return st_tonemap(c); }\n\n// @tone-map\n// A studio variant supplies st_ribbon(uv, id) and st_sphere(id).\n// RGB stays linear HDR until the shared present pass. Alpha stores camera\n// distance for refraction and focus; it is never used as surface opacity.\n\nstruct StudioUniforms { mesh : vec4f, lens : vec4f }\n@group(0) @binding(1) var<uniform> su : StudioUniforms;\n@group(0) @binding(2) var t_studio : texture_2d<f32>;\n@group(0) @binding(3) var t_glass : texture_2d<f32>;\n@group(0) @binding(4) var t_shadow : texture_2d<f32>;\n\nstruct StudioPatch { low : vec4f, high : vec4f, points : array<vec4f,85> }\n@group(0) @binding(5) var<storage,read_write> patchWrite : array<StudioPatch>;\n@group(0) @binding(6) var<storage,read> patches : array<StudioPatch>;\n\n// Each invocation builds one bounded patch of the same sculpted surface\n// used by the rasterizer. Glass rays can then see offscreen ribbon geometry.\n@compute @workgroup_size(1) fn cs_studio(@builtin(global_invocation_id) index : vec3u) {\n let chunk = index.x;\n if (chunk>=72u) { return; }\n var low = vec3f(1000.0); var high = vec3f(-1000.0);\n for (var u=0u; u<5u; u++) {\n for (var v=0u; v<17u; v++) {\n let uv = vec2f(f32((chunk%24u)*4u+u)/96.0,f32(v)/16.0);\n let p = st_ribbon(uv,chunk/24u);\n patchWrite[chunk].points[u*17u+v] = vec4f(p,0.0);\n low = min(low,p); high = max(high,p);\n }\n }\n patchWrite[chunk].low = vec4f(low-0.002,0.0);\n patchWrite[chunk].high = vec4f(high+0.002,0.0);\n}\n\nfn studioEye() -> vec3f { return vec3f((params.pointer - vec2f(0.5)) * vec2f(0.22, -0.14), 7.5); }\nfn studioFocal() -> f32 { return 3.40909 * params.scale; }\nfn studioClip(p : vec3f) -> vec4f {\n let q = p - studioEye();\n return vec4f(q.x * studioFocal() / st_aspect(), q.y * studioFocal(), -q.z * 1.002506 - 0.100251, -q.z);\n}\nfn studioRay(uv : vec2f) -> vec3f {\n return normalize(vec3f((uv * 2.0 - 1.0) * vec2f(st_aspect(), -1.0) / studioFocal(), -1.0));\n}\nfn studioUv(p : vec3f) -> vec2f {\n let clip = studioClip(p);\n return clip.xy / max(clip.w, 0.01) * vec2f(0.5, -0.5) + 0.5;\n}\n\nfn studioLight() -> vec3f { return normalize(vec3f(-3.0, 4.0, 5.0)); }\nfn studioShadowClip(p : vec3f) -> vec4f {\n let light = studioLight();\n let right = normalize(cross(vec3f(0.0, 1.0, 0.0), light));\n let up = cross(light, right);\n return vec4f(dot(p,right)/6.0, dot(p,up)/6.0, 0.5-dot(p,light)/20.0, 1.0);\n}\n\nstruct StudioVertex {\n @builtin(position) clip : vec4f,\n @location(0) world : vec3f,\n @location(1) normal : vec3f,\n @location(2) tangent : vec3f,\n @location(3) uv : vec2f,\n}\n\nfn studioMeshUv(index : u32) -> vec2f {\n let lanes = u32(su.mesh.y);\n let cell = index / 6u;\n let corners = array<vec2f,6>(vec2f(0,0),vec2f(1,0),vec2f(0,1),vec2f(0,1),vec2f(1,0),vec2f(1,1));\n return (vec2f(f32(cell/lanes), f32(cell%lanes)) + corners[index%6u]) / su.mesh.xy;\n}\n\n@vertex fn vs_studio(@builtin(vertex_index) vi : u32, @builtin(instance_index) id : u32) -> StudioVertex {\n let uv = studioMeshUv(vi);\n let p = st_ribbon(uv, id);\n let du = st_ribbon(uv+vec2f(0.00015,0),id)-st_ribbon(uv-vec2f(0.00015,0),id);\n let dv = st_ribbon(uv+vec2f(0,0.00015),id)-st_ribbon(uv-vec2f(0,0.00015),id);\n var out : StudioVertex;\n out.clip = studioClip(p); out.world = p; out.normal = normalize(cross(du,dv));\n out.tangent = normalize(du); out.uv = uv;\n return out;\n}\n\n@vertex fn vs_shadow(@builtin(vertex_index) vi : u32, @builtin(instance_index) id : u32) -> @builtin(position) vec4f {\n return studioShadowClip(st_ribbon(studioMeshUv(vi), id));\n}\n@fragment fn fs_shadow(@builtin(position) p : vec4f) -> @location(0) f32 { return p.z; }\n\n// Rectangular area lights in angular space. Surface reflections all sample\n// this same environment, including rays that leave the visible scene.\nfn studioPanel(d : vec3f, axis : vec3f, size : vec2f, feather : f32) -> f32 {\n let forward = normalize(axis);\n let right = normalize(cross(vec3f(0,1,0), forward));\n let up = cross(forward, right);\n let facing = dot(d,forward);\n let p = vec2f(dot(d,right),dot(d,up))/max(facing,0.001);\n let edge = abs(p)-size;\n return (1.0-smoothstep(-feather,feather,max(edge.x,edge.y))) * smoothstep(0.0,0.1,facing);\n}\n\nfn studioEnvironment(d : vec3f) -> vec3f {\n let metal = st_toLinear(params.colorA.rgb);\n let silver = st_toLinear(params.colorB.rgb);\n let gold = st_toLinear(params.colorC.rgb);\n let sky = smoothstep(-0.7,0.85,d.y);\n var col = mix(metal*0.85,silver*0.32,sky);\n col += silver*st_noise3(d*5.0)*0.05;\n col += silver * studioPanel(d,vec3f(-2.5,4.0,3.0),vec2f(0.65,0.28),0.4) * 2.4;\n col += silver * vec3f(0.55,0.79,1.0) * studioPanel(d,vec3f(-4.0,0.5,-1.5),vec2f(0.24,1.4),0.2) * 2.8;\n col += gold * studioPanel(d,vec3f(3.5,2.0,0.2),vec2f(0.12,0.85),0.2) * 6.5 * params.d;\n col += gold * studioPanel(d,vec3f(0.5,-3.0,-2.0),vec2f(1.1,0.06),0.12) * 4.0 * params.d;\n // Broad dark flags make the bright panels read as reflected light.\n col *= 1.0 - 0.55 * studioPanel(d,vec3f(0.0,0.1,4.0),vec2f(0.4,0.7),0.18);\n return col;\n}\n\nfn studioVisibility(p : vec3f, n : vec3f) -> f32 {\n let projected = studioShadowClip(p + n * 0.012);\n let uv = projected.xy * vec2f(0.5,-0.5) + 0.5;\n let size = vec2i(textureDimensions(t_shadow));\n var visibility = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let coord = clamp(vec2i(uv*vec2f(size))+vec2i(x,y)*2,vec2i(0),size-1);\n visibility += select(0.0,1.0,projected.z-0.0015 <= textureLoad(t_shadow,coord,0).x);\n }\n }\n return visibility / 9.0;\n}\n\nfn studioBrush(uv : vec2f) -> f32 {\n let x = cos(uv.y*TAU);\n return x*220.0 + sin(x*17.0)*1.3 + sin(uv.x*23.0+x*6.0)*0.7;\n}\n\nfn studioMaterial(p : vec3f, normal : vec3f, tangent : vec3f, uv : vec2f, view : vec3f, footprint : f32) -> vec3f {\n var n = normal * select(-1.0,1.0,dot(normal,view)>=0.0);\n let across = normalize(cross(n,tangent));\n let phase = studioBrush(uv);\n let filtered = (1.0-smoothstep(1.5,3.0,footprint))*smoothstep(0.03,0.3,abs(dot(n,view)));\n let scratch = sin(phase) * 0.6 + sin(phase*0.431+2.0) * 0.4;\n n = normalize(n+across*scratch*0.085*params.b*filtered);\n let r = reflect(-view,n);\n let spread = 0.035 + params.b*0.065;\n let rt = normalize(tangent-r*dot(tangent,r));\n var reflection = studioEnvironment(r)*0.4;\n reflection += studioEnvironment(normalize(r+rt*spread))*0.24;\n reflection += studioEnvironment(normalize(r-rt*spread))*0.24;\n reflection += studioEnvironment(normalize(r+rt*spread*2.0))*0.06;\n reflection += studioEnvironment(normalize(r-rt*spread*2.0))*0.06;\n let f0 = mix(st_toLinear(params.colorA.rgb),vec3f(0.85),0.55);\n let fresnel = f0 + (1.0-f0)*pow(1.0-max(dot(n,view),0.0),5.0);\n let shadow = studioVisibility(p,n);\n var col = reflection*fresnel*(0.85+0.15*shadow);\n col *= 0.94 + 0.06*scratch*filtered*params.b;\n let groove = pow(0.5+0.5*sin(phase),12.0);\n col *= 1.0 - groove*0.18*params.b*filtered;\n col += st_toLinear(params.colorA.rgb)*0.08;\n let depth = length(p-studioEye());\n let haze = smoothstep(8.0,13.0,depth)*0.23;\n col = mix(col,st_toLinear(params.colorB.rgb)*0.45,haze);\n return col*params.intensity;\n}\n\n@fragment fn fs_metal(in : StudioVertex) -> @location(0) vec4f {\n // Bound the phase gradient before sampling its oscillations. Differencing\n // the oscillating phase can report a false zero and reveal subpixel grooves.\n // The bounds follow studioBrush: 220 + 17*1.3 + 6*0.7 < 247; 23*0.7 = 16.1.\n let footprint = fwidth(cos(in.uv.y*TAU))*247.0 + fwidth(in.uv.x)*16.1;\n let col = studioMaterial(in.world,normalize(in.normal),normalize(in.tangent),in.uv,normalize(studioEye()-in.world),footprint);\n return vec4f(col,length(in.world-studioEye()));\n}\n\n@fragment fn fs_background(@location(0) uv : vec2f) -> @location(0) vec4f {\n let q = vec2f(uv.x*st_aspect(),uv.y);\n let mist = st_fbm2(q*3.0+vec2f(st_time()*0.008,0),4);\n let sky = smoothstep(0.22,1.4,uv.x*0.9+(1.0-uv.y)*0.55);\n var col = mix(st_toLinear(params.colorA.rgb)*0.4,st_toLinear(params.colorB.rgb)*0.95,sky);\n col += st_toLinear(params.colorB.rgb)*(mist-0.3)*0.22;\n col += st_toLinear(params.colorC.rgb)*exp(-length((uv-vec2f(0.88,0.05))*vec2f(st_aspect(),1.0))*3.0)*0.14*params.d;\n return vec4f(max(col,vec3f(0.0))*params.intensity,100.0);\n}\n\nfn studioSphereHit(ro : vec3f, rd : vec3f, sphere : vec4f) -> f32 {\n let offset = ro-sphere.xyz;\n let b = dot(offset,rd);\n let h = b*b-dot(offset,offset)+sphere.w*sphere.w;\n if (h < 0.0 || sphere.w < 0.001) { return 100.0; }\n let near = -b-sqrt(h);\n return select(100.0,near,near>0.0);\n}\n\nfn studioTriangle(ro : vec3f, rd : vec3f, a : vec3f, b : vec3f, c : vec3f) -> vec3f {\n let e1=b-a; let e2=c-a;\n let crossRay=cross(rd,e2);\n let det=dot(e1,crossRay);\n if (abs(det)<1e-8) { return vec3f(100.0,0,0); }\n let s=ro-a;\n let u=dot(s,crossRay)/det;\n let q=cross(s,e1);\n let v=dot(rd,q)/det;\n let t=dot(e2,q)/det;\n if (u<0.0 || v<0.0 || u+v>1.0 || t<0.02) { return vec3f(100.0,0,0); }\n return vec3f(t,u,v);\n}\n\nfn studioTrace(ro : vec3f, rd : vec3f) -> vec4f {\n var nearest=100.0;\n var uv=vec2f(0.0); var id=0u;\n let inv=select(vec3f(1.0),vec3f(-1.0),rd<vec3f(0.0))/max(abs(rd),vec3f(1e-7));\n for (var chunk=0u; chunk<72u; chunk++) {\n let a=(patches[chunk].low.xyz-ro)*inv;\n let b=(patches[chunk].high.xyz-ro)*inv;\n let low=min(a,b); let high=max(a,b);\n let entry=max(max(low.x,low.y),low.z);\n let exit=min(min(high.x,high.y),high.z);\n if (exit<max(entry,0.02) || entry>nearest) { continue; }\n for (var u=0u; u<4u; u++) {\n for (var v=0u; v<16u; v++) {\n let index=u*17u+v;\n let p0=patches[chunk].points[index].xyz;\n let p1=patches[chunk].points[index+17u].xyz;\n let p2=patches[chunk].points[index+1u].xyz;\n let p3=patches[chunk].points[index+18u].xyz;\n let first=studioTriangle(ro,rd,p0,p1,p2);\n let second=studioTriangle(ro,rd,p3,p2,p1);\n var hit=first;\n if (second.x<first.x) { hit=vec3f(second.x,1.0-second.y,1.0-second.z); }\n if (hit.x<nearest) {\n nearest=hit.x;\n uv=(vec2f(f32((chunk%24u)*4u+u),f32(v))+hit.yz)/vec2f(96.0,16.0);\n id=chunk/24u;\n }\n }\n }\n }\n if (nearest>=100.0) { return vec4f(0.0); }\n let p=ro+rd*nearest;\n let du=st_ribbon(uv+vec2f(0.00015,0),id)-st_ribbon(uv-vec2f(0.00015,0),id);\n let dv=st_ribbon(uv+vec2f(0,0.00015),id)-st_ribbon(uv-vec2f(0,0.00015),id);\n // Secondary rays cover a larger footprint; omit subpixel brushing there.\n let col=studioMaterial(p,normalize(cross(du,dv)),normalize(du),uv,-rd,PI);\n return vec4f(col,1.0);\n}\n\n@fragment fn fs_glass(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSampleLevel(t_studio,linearSampler,uv,0.0);\n let eye = studioEye();\n let ray = studioRay(uv);\n var nearest = scene.a;\n var id = 8u;\n for (var i=0u; i<8u; i++) {\n let hit = studioSphereHit(eye,ray,st_sphere(i));\n if (hit<nearest) { nearest=hit; id=i; }\n }\n if (id==8u) { return scene; }\n let sphere = st_sphere(id);\n let pos = eye+ray*nearest;\n let normal = normalize(pos-sphere.xyz);\n let reflected = reflect(ray,normal);\n let inside = refract(ray,normal,1.0/1.46);\n let thickness = -2.0*dot(inside,normal)*sphere.w;\n let exit = pos+inside*thickness;\n let exitNormal = normalize(exit-sphere.xyz);\n let transmitted = refract(inside,-exitNormal,1.46);\n let through = studioTrace(exit+transmitted*0.025,transmitted);\n let mirror = studioTrace(pos+normal*0.025,reflected);\n let environment = studioEnvironment(reflected)*params.intensity;\n let reflection = mix(environment,mirror.rgb,mirror.a);\n let backdropUv = clamp(studioUv(exit+transmitted*4.0),vec2f(0.001),vec2f(0.999));\n let backdrop = textureSampleLevel(t_studio,linearSampler,backdropUv,0.0).rgb;\n let transmission = mix(backdrop,through.rgb,through.a);\n let absorption = exp(-(vec3f(1.0)-params.colorA.rgb)*thickness*2.6);\n let fresnel = 0.035+0.965*pow(1.0-max(dot(-ray,normal),0.0),5.0);\n var col = mix(transmission*absorption,reflection,fresnel);\n col += environment*(0.055+0.1*pow(1.0-max(dot(-ray,normal),0.0),2.0));\n return vec4f(col,nearest);\n}\n\nfn studioCoc(depth : f32) -> f32 {\n let near = max(su.lens.x-depth,0.0)*3.3;\n let far = min(max(depth-7.8,0.0)*0.6,2.5);\n return clamp((near+far)*su.lens.y,0.0,12.0);\n}\n\n@fragment fn fs_focus(@location(0) uv : vec2f) -> @location(0) vec4f {\n let center = textureSampleLevel(t_glass,linearSampler,uv,0.0);\n let coc = studioCoc(center.a);\n if (coc<0.3) { return center; }\n let size = vec2f(textureDimensions(t_glass));\n var sum = center.rgb;\n var weights = 1.0;\n let count = select(24,12,st_low());\n for (var i=0; i<count; i++) {\n let angle = f32(i)*2.399963;\n let radius = sqrt((f32(i)+0.5)/f32(count))*coc;\n let offset = vec2f(cos(angle),sin(angle))*radius/size;\n let sample = textureSampleLevel(t_glass,linearSampler,uv+offset,0.0);\n // Reject distant background samples when blurring a foreground silhouette.\n let weight = select(1.0,0.08,sample.a>center.a+0.8 && center.a<5.0);\n sum += sample.rgb*weight; weights += weight;\n }\n return vec4f(sum/weights,center.a);\n}\n\n// ---------------------------------------------------------------------------\n// alloy\n// ---------------------------------------------------------------------------\n\n// Alloy's sculpted paths. The studio prelude supplies the materials and camera.\n\nfn alloyBezier(a : vec3f, b : vec3f, c : vec3f, d : vec3f, u : f32) -> vec3f {\n let v = 1.0 - u;\n return v*v*v*a + 3.0*v*v*u*b + 3.0*v*u*u*c + u*u*u*d;\n}\n\nfn alloyPath(u : f32, id : u32) -> vec3f {\n let segment = min(u32(u * 3.0), 2u);\n let t = clamp(u * 3.0 - f32(segment), 0.0, 1.0);\n var a : vec3f; var b : vec3f; var c : vec3f; var d : vec3f;\n if (id == 0u) {\n if (segment == 0u) {\n a=vec3f(-3.0,3.8,-0.8); b=vec3f(-3.8,0.3,-0.3); c=vec3f(-1.8,-1.7,0.9); d=vec3f(0.0,-1.25,0.8);\n } else if (segment == 1u) {\n a=vec3f(0.0,-1.25,0.8); b=vec3f(1.8,-0.8,0.7); c=vec3f(4.3,-0.2,0.2); d=vec3f(2.8,1.35,-0.4);\n } else {\n a=vec3f(2.8,1.35,-0.4); b=vec3f(1.3,2.9,-1.0); c=vec3f(1.5,2.6,-1.1); d=vec3f(2.1,4.2,-1.0);\n }\n } else if (id == 1u) {\n if (segment == 0u) {\n a=vec3f(-3.8,3.5,0.0); b=vec3f(-4.2,0.2,0.4); c=vec3f(-2.6,-0.1,0.5); d=vec3f(-1.7,0.6,0.2);\n } else if (segment == 1u) {\n a=vec3f(-1.7,0.6,0.2); b=vec3f(-0.8,1.3,-0.1); c=vec3f(-0.1,1.8,-1.1); d=vec3f(0.7,0.6,-1.6);\n } else {\n a=vec3f(0.7,0.6,-1.6); b=vec3f(1.5,-0.6,-2.1); c=vec3f(0.5,-1.5,-1.5); d=vec3f(3.7,-3.7,-1.0);\n }\n } else {\n if (segment == 0u) {\n a=vec3f(-4.3,3.3,-2.0); b=vec3f(-2.6,2.0,-2.0); c=vec3f(-2.7,0.3,-2.0); d=vec3f(-1.4,-0.5,-1.6);\n } else if (segment == 1u) {\n a=vec3f(-1.4,-0.5,-1.6); b=vec3f(-0.1,-1.3,-1.2); c=vec3f(2.2,-2.1,-1.6); d=vec3f(3.0,-1.1,-2.0);\n } else {\n a=vec3f(3.0,-1.1,-2.0); b=vec3f(3.8,-0.1,-2.4); c=vec3f(4.2,1.0,-2.0); d=vec3f(5.8,2.8,-1.0);\n }\n }\n var p = alloyBezier(a,b,c,d,t);\n // Keep the main loop behind the upper ribbon at their left-hand crossing.\n if (id == 0u) {\n let crossing = (u - 0.15) / 0.18;\n p.z -= 1.8 * exp(-crossing * crossing);\n }\n let clock = st_time() * 0.17;\n p.z += sin(u * TAU + clock + f32(id)) * 0.08;\n p.y += sin(u * PI + clock) * 0.035;\n return p;\n}\n\nfn st_ribbon(uv : vec2f, id : u32) -> vec3f {\n let u = clamp(uv.x, 0.0, 1.0);\n let center = alloyPath(u, id);\n let tangent = normalize(alloyPath(min(u + 0.0002, 1.0), id) - alloyPath(max(u - 0.0002, 0.0), id));\n let side = normalize(cross(vec3f(0.0, 0.0, 1.0), tangent));\n let normal = normalize(cross(tangent, side));\n var twist = 0.25 + sin(u * 7.0 + 0.4) * 0.75;\n var width = 0.72 + 0.15 * sin(u * PI);\n if (id == 1u) { twist = -0.5 + u * 5.4; width = 0.45 + 0.12 * sin(u * PI); }\n if (id == 2u) { twist = u * 4.0 - 1.1; width = 0.62; }\n twist += params.warp * sin(u * TAU + st_time() * 0.12) * 0.38;\n let across = side * cos(twist) + normal * sin(twist);\n let up = normal * cos(twist) - side * sin(twist);\n let v = uv.y * TAU;\n let x = cos(v);\n return center + across * x * width * params.a + up * (sin(v) * 0.014 + (1.0-x*x) * 0.22);\n}\n\nfn st_sphere(i : u32) -> vec4f {\n var spheres = array<vec4f,8>(\n vec4f(0.82,0.43,1.7,0.65), vec4f(-2.72,-1.86,2.65,1.37),\n vec4f(-4.0,0.32,0.9,0.35), vec4f(-0.48,0.26,0.7,0.17),\n vec4f(2.2,0.55,0.2,0.14), vec4f(3.45,-1.05,0.8,0.21),\n vec4f(-1.67,1.45,-0.4,0.12), vec4f(-3.15,0.05,-0.6,0.11),\n );\n var s = spheres[i];\n let t = st_time() * 0.14;\n return vec4f(s.xyz + vec3f(sin(t+f32(i)*2.1)*0.035, sin(t*0.8+f32(i))*0.045, 0.0), s.w*params.c);\n}\n",
8
9
  "archipelago": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// archipelago\n// ---------------------------------------------------------------------------\n\n// Archipelago — a nautical chart on a slow drift: warped fbm islands,\n// contour lines, hillshade.\n//\n// The islands are one level of domain warp over fbm with no falloff at all —\n// unbounded terrain thresholded at sea level, so the map scrolls forever\n// without repeating. Relief comes free from screen derivatives; the chart's\n// ink keeps its width from fwidth.\n//\n// The wells are the chart: colorA water, colorB land, colorC the shore sand.\n// The ink derives itself — dark ink on a light chart, light ink on a dark\n// one — so the night-watch preset needs no fourth well.\n\nfn terrain(p : vec2f) -> f32 {\n let q = vec2f(\n st_fbm2(p + vec2f(5.2, 1.3), 3),\n st_fbm2(p + vec2f(1.7, 9.2), 3),\n );\n return st_fbm2(p + (q - vec2f(0.48)) * params.warp, 5);\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 * 2.6 + vec2f(t * 0.03, t * 0.012);\n\n let ink = mix(params.colorA.rgb * 0.2, mix(params.colorA.rgb, vec3f(1.0), 0.6), step(st_luma(params.colorA.rgb), 0.32));\n\n let h = terrain(p);\n let sea = mix(0.68, 0.42, clamp(params.b, 0.0, 1.0));\n let land = h - sea;\n\n // Hillshade from screen derivatives: quad-granular, which on a chart reads\n // as engraving rather than error, and it costs no extra terrain reads.\n let grad = vec2f(dpdx(h), dpdy(h)) / max(fwidth(h), 1e-4);\n let shade = clamp(0.62 + dot(grad, normalize(vec2f(-0.6, -0.6))) * 0.38, 0.0, 1.0);\n\n var col : vec3f;\n if (land > 0.0) {\n // Sand at the waterline, then field, forest, rock.\n col = mix(params.colorC.rgb, params.colorB.rgb, smoothstep(0.0, 0.05, land));\n col = mix(col, params.colorB.rgb * 0.6, smoothstep(0.08, 0.2, land));\n col = mix(col, vec3f(0.62, 0.59, 0.63), smoothstep(0.2, 0.3, land));\n col = col * mix(1.0, shade, params.d);\n } else {\n // Three bathymetric tints, plus the old-map glow that hugs the coast.\n let depth = -land;\n col = mix(params.colorA.rgb * 1.25, params.colorA.rgb, smoothstep(0.0, 0.12, depth));\n col = mix(col, params.colorA.rgb * 0.62, smoothstep(0.12, 0.3, depth));\n col = col + params.colorA.rgb * 0.5 * exp(-depth * 14.0);\n }\n\n // Contour ink. fwidth keeps every line a pixel or two wide whatever the\n // slope; every fifth line is drawn heavier, as charts do.\n let levels = h * params.a;\n let f = min(fract(levels), 1.0 - fract(levels));\n let wd = max(fwidth(levels), 1e-4);\n let line = 1.0 - smoothstep(wd * 0.6, wd * 1.8, f);\n let heavy = 0.6 + 0.4 * step(fract(floor(levels) / 5.0 + 0.05), 0.15);\n let inkAmt = select(0.14, 0.3, land > 0.0) * params.c;\n col = mix(col, ink, line * heavy * inkAmt);\n\n // The coastline itself gets a firm stroke.\n let cw = max(fwidth(h), 1e-4);\n let stroke = 1.0 - smoothstep(cw * 0.8, cw * 2.4, abs(land));\n col = mix(col, ink, stroke * 0.65 * params.c);\n\n // Paper grain, so flat water is never a dead fill.\n col = col + (st_noise2(p * 60.0) - 0.5) * 0.035;\n\n return st_post(col * params.intensity, uv);\n}\n",
@@ -12,10 +13,10 @@ export const shaderSources = {
12
13
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
13
14
  "billow": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// shadertown glow prelude — prepended (after the shared and graph preludes)\n// to every shader in src/shaders/graph/glow/.\n//\n// The simplest flavour: one full-screen scene pass into the HDR target, then\n// the shared bloom chain and the present pass. It exists for the shaders that\n// are light on dark — threads, beams, scanned bands — where a real bloom is\n// the difference between a drawing of light and light. The variant file below\n// this prelude supplies exactly one function:\n//\n// fn st_scene(uv : vec2f) -> vec4f\n//\n// `uv` is the canvas uv; the return value is linear-light colour in rgb, and\n// in w how much of the pixel is light the chain may bleed. The present pass\n// converts to sRGB on the way out, so wells go through `st_toLinear` first.\n\n@fragment fn fs_scene(@location(0) uv : vec2f) -> @location(0) vec4f {\n return st_scene(uv);\n}\n\n// ---------------------------------------------------------------------------\n// billow\n// ---------------------------------------------------------------------------\n\n// Billow — a sea of soft waves marched from a low camera, fading into fog.\n//\n// The surface is two crossed sines with a slow swell and a turbulence riding\n// on them, raymarched from a camera tilted down toward the horizon. Distance\n// is fog: the wave colour gives way to the horizon colour as the march runs\n// long, and the crests catch a third colour by height. The camera yaws and\n// pitches a little with the pointer, which is what gives the sea its depth.\n// colorA the horizon (and the fog), colorB the wave body, colorC the crests\n// a amplitude, b swell, c height, d fog depth; warp is the parallax\n\nconst BILLOW_FAR : f32 = 20000.0;\nconst BILLOW_STEPS : i32 = 80;\n\nfn billowSurface(r : vec3f, freq : vec2f, tc : vec4f, amplitude : f32, swell : f32, height : f32) -> f32 {\n var mx = r.x + tc.x;\n mx = mx + swell * sin((r.y + mx) / 20.0 + tc.y);\n var my = r.y - tc.z;\n my = my + swell * 0.57 * cos(r.x / 23.0 + tc.w);\n return r.z - (sin(mx * freq.x) * amplitude + sin(my * freq.y) * amplitude + height);\n}\n\nfn billowMarch(pos : vec3f, dir : vec3f, freq : vec2f, tc : vec4f, amplitude : f32, swell : f32, height : f32) -> f32 {\n var dist = 0.0;\n let steps = st_steps(BILLOW_STEPS);\n for (var i = 0; i < steps; i = i + 1) {\n let d = billowSurface(pos + dist * dir, freq, tc, amplitude, swell, height);\n if (abs(d) < 0.1) { break; }\n dist = dist + 0.9 * d;\n if (!(abs(dist) < BILLOW_FAR)) { return BILLOW_FAR; }\n }\n return dist;\n}\n\nfn billowRotX(a : f32) -> mat3x3f {\n let c = cos(a);\n let s = sin(a);\n return mat3x3f(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c);\n}\n\nfn billowRotY(a : f32) -> mat3x3f {\n let c = cos(a);\n let s = sin(a);\n return mat3x3f(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c);\n}\n\nfn st_scene(uv : vec2f) -> vec4f {\n let t = st_time();\n let waveScale = clamp(params.scale, 0.3, 2.5) * 0.6;\n let freq = vec2f(waveScale / 7.0, (waveScale * 0.9) / 3.0);\n let tc = vec4f(t / 0.130, t / 0.810, t / 0.200, t / 0.710);\n let amplitude = clamp(params.a, 0.2, 6.0);\n let swell = clamp(params.b, 0.0, 60.0);\n let height = clamp(params.c, 0.0, 12.0);\n let fogDepth = clamp(params.d, 3.0, 40.0);\n\n // A fisheye-ish lens: the view direction is tipped by the angle from the\n // centre, then spun to the pixel's bearing, then the whole camera tilts\n // down toward the sea.\n let vfov = (PI / 2.3);\n let cam = vec3f(0.0, 0.0, 30.0);\n // y down, as the canvas uv already is: the near sea is at the foot of the\n // frame and the fog at the top.\n let p = vec2f((uv.x - 0.5) * st_aspect(), uv.y - 0.5);\n let ulen = length(p);\n var dir = vec3f(0.0, 0.0, -1.0);\n dir = billowRotX(vfov * ulen) * dir;\n let nuv = select(vec2f(1.0, 0.0), p / max(ulen, 1e-5), ulen > 1e-5);\n dir = mat3x3f(nuv.x, -nuv.y, 0.0, nuv.y, nuv.x, 0.0, 0.0, 0.0, 1.0) * dir;\n dir = billowRotY(1.11) * dir;\n\n // The pointer turns the camera a little: the sea gains depth as the hand\n // moves across it. The glow graph eases the pointer, so this never snaps.\n let parallax = clamp(params.warp, 0.0, 2.0) * 0.4;\n let yaw = (params.pointer.x - 0.5) * parallax;\n let pitch = (0.5 - params.pointer.y) * parallax;\n dir = billowRotY(yaw) * dir;\n dir = billowRotX(pitch) * dir;\n\n let dist = billowMarch(cam, dir, freq, tc, amplitude, swell, height);\n let pos = cam + dist * dir;\n\n let horizon = st_toLinear(params.colorA.rgb);\n let wave = st_toLinear(params.colorB.rgb);\n let crest = st_toLinear(params.colorC.rgb);\n\n let near = clamp(fogDepth / max(dist, 0.001), 0.0, 1.0);\n let lift = clamp(pos.z * 0.08 + 0.5, 0.0, 1.0);\n let body = mix(wave, crest, lift);\n let col = mix(horizon, body, near) * params.intensity;\n\n // Only the crests are light the bloom may bleed; the fog and body stay put.\n let lit = smoothstep(0.55, 1.0, lift) * near * 0.7;\n return vec4f(col, lit);\n}\n",
14
15
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let taps = st_steps(12);\n var sum = 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;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / f32(taps + 1);\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 = st_steps(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",
15
- "blossom": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// blossom\n// ---------------------------------------------------------------------------\n\n// Blossom — one flower, drawn large enough to be the header.\n//\n// Two rings of rose-curve petals, the inner rotated half a sector and\n// darkened, around a phyllotaxis seed head: florets at r = c*sqrt(n),\n// theta = n * 137.508 degrees, found by inverting the radius and testing a\n// handful of candidate indices. Each seed gets a domed normal, so the head\n// shades like geometry without a single ray marched.\n\nconst GOLDEN_ANGLE : f32 = 2.39996323;\n\nfn petalRing(p : vec2f, count : f32, radius : f32, spin : f32, aa : f32) -> f32 {\n let r = length(p);\n let ang = atan2(p.y, p.x) + spin;\n // The rose curve, sharpened so petals separate instead of scalloping.\n let lobe = pow(abs(cos(count * ang * 0.5)), 1.4);\n let edge = radius * (0.35 + 0.65 * lobe);\n return smoothstep(edge + aa, edge - aa, r);\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.2;\n\n // The whole flower breathes and leans, more at the rim than the heart.\n let breathe = 1.0 + 0.03 * sin(t * 0.7) * params.b;\n p = st_rot(0.05 * sin(t * 0.45) + t * 0.02) * p * (1.0 / breathe);\n\n let aa = max(fwidth(p.x), 0.003);\n let r = length(p);\n\n // Backdrop: a quiet radial pool of colorA, so the flower owns the light.\n var col = params.colorA.rgb * mix(1.1, 0.45, smoothstep(0.0, 1.4, r));\n\n // Pollen: a dozen soft motes drifting up behind the flower, each a small\n // gaussian rather than a lit pixel.\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let rnd = st_hash22(vec2f(fi * 3.7, fi * 1.9));\n let cycle = fract(t * (0.02 + rnd.y * 0.03) + rnd.x);\n let motePos = vec2f(\n (rnd.x - 0.5) * 2.6 + sin(t * 0.3 + fi) * 0.08,\n (0.5 - cycle) * 2.2,\n );\n let fade = smoothstep(0.0, 0.2, cycle) * smoothstep(1.0, 0.8, cycle);\n col = col + params.colorC.rgb * exp(-length(p - motePos) * mix(24.0, 60.0, rnd.y)) * fade * 0.14 * params.c;\n }\n\n let petals = floor(params.a);\n let ringR = 0.78;\n\n // Outer ring: petals lit from the heart outward, edged a shade darker so\n // each one separates from its neighbours.\n let outer = petalRing(p, petals, ringR, 0.0, aa);\n if (outer > 0.0) {\n let ang = atan2(p.y, p.x);\n let lobe = pow(abs(cos(petals * ang * 0.5)), 1.4);\n let edge = ringR * (0.35 + 0.65 * lobe);\n var tone = mix(params.colorB.rgb, params.colorC.rgb, smoothstep(0.1, edge, r));\n tone = tone * (0.92 + 0.08 * lobe);\n // The last stretch before the rim falls into shadow — a drawn outline\n // without drawing one.\n tone = tone * mix(1.0, 0.72, smoothstep(edge - 0.09, edge, r));\n col = mix(col, tone, outer);\n }\n\n // Inner ring: half a sector round, smaller, in the outer ring's shadow.\n let inner = petalRing(p, petals, ringR * 0.58, PI / max(petals, 1.0), aa);\n if (inner > 0.0) {\n let tone = mix(params.colorB.rgb * 0.55, params.colorC.rgb * 0.7, smoothstep(0.05, ringR * 0.5, r));\n col = mix(col, tone, inner);\n }\n\n // The seed head. Invert r = c*sqrt(n) for a first guess at the floret,\n // then test neighbours — the true nearest can be a Fibonacci stride away,\n // but at this size a miss reads as texture.\n let headR = 0.26 * params.d;\n if (r < headR) {\n let c = headR / sqrt(320.0);\n let guess = (r / c) * (r / c);\n var best = 1e9;\n var bestSeed = vec2f(0.0);\n var bestN = 0.0;\n for (var k = -3; k <= 3; k = k + 1) {\n let n = max(round(guess) + f32(k), 0.0);\n let q = c * sqrt(n) * vec2f(cos(n * GOLDEN_ANGLE + t * 0.05), sin(n * GOLDEN_ANGLE + t * 0.05));\n let d = length(p - q);\n if (d < best) {\n best = d;\n bestSeed = q;\n bestN = n;\n }\n }\n let seedR = c * 0.75;\n let dome = clamp(1.0 - (best / seedR) * (best / seedR), 0.0, 1.0);\n // A domed normal lit from the upper left, no geometry required.\n let nrm = normalize(vec3f((p - bestSeed) / seedR, sqrt(max(dome, 0.02))));\n let lit = clamp(dot(nrm, normalize(vec3f(-0.5, 0.6, 0.65))), 0.0, 1.0);\n var head = params.colorC.rgb * (0.25 + 0.75 * lit) * (0.7 + 0.3 * st_hash11(bestN));\n head = mix(params.colorB.rgb * 0.2, head, smoothstep(0.0, 0.25, dome));\n col = mix(col, head, smoothstep(headR, headR - aa * 2.0, r));\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
16
+ "blossom": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// blossom\n// ---------------------------------------------------------------------------\n\n// Petals are separate curved surfaces, painted from the back ring forward.\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let sway = params.b * sin(t * 0.42) * 0.035;\n let p = st_rot(sway) * (st_centered(uv) * params.scale * 2.2 - vec2f(sway, sway * 0.4));\n let aa = max(2.2 * params.scale / params.resolution.y, 0.0005);\n let r = length(p);\n var col = params.colorA.rgb * (0.32 + 0.55 * exp(-r * r * 1.7));\n let light = normalize(vec3f(-0.45, -0.6, 0.8));\n let count = floor(params.a);\n\n for (var ring = 0; ring < 3; ring += 1) {\n let fr = f32(ring);\n for (var i = 0; i < 14; i += 1) {\n if (f32(i) >= count) { break; }\n let id = f32(i) + fr * 17.0;\n let rnd = st_hash22(vec2f(id + 1.0, id * 3.1));\n let angle = f32(i) * TAU / count + fr * PI / count + (rnd.x - 0.5) * 0.12;\n let q = st_rot(-angle) * p;\n let len = (0.78 - fr * 0.13) * (0.92 + rnd.y * 0.14)\n * (1.0 + params.b * 0.018 * sin(t * 0.65 + id));\n let u = q.x / len;\n let bend = sin(clamp(u, 0.0, 1.0) * PI) * (rnd.x - 0.5) * 0.11;\n let width = (0.18 - fr * 0.027) * pow(max(sin(clamp(u, 0.0, 1.0) * PI), 0.0), 0.65);\n let side = q.y - bend;\n let dist = abs(side) - width;\n let mask = (1.0 - smoothstep(-aa, aa, dist)) * smoothstep(0.025, 0.065, u)\n * (1.0 - smoothstep(0.98, 1.0, u));\n let shadow = (1.0 - smoothstep(-0.008, 0.035, abs(side - 0.018) - width))\n * smoothstep(0.05, 0.10, u) * (1.0 - smoothstep(0.96, 1.025, u));\n col *= 1.0 - shadow * 0.28;\n let v = side / max(width, 0.008);\n let slope = vec2f(-0.22 + u * 0.6, v * 0.65);\n let normalXY = st_rot(angle) * slope;\n let normal = normalize(vec3f(normalXY, 1.0));\n let diffuse = max(dot(normal, light), 0.0);\n let vein = sin(v * 31.0 + sin(u * 12.0 + rnd.x * 4.0) * 0.65);\n let fibres = st_noise2(vec2f(u * 70.0, v * 140.0) + id);\n var petal = mix(params.colorB.rgb, params.colorC.rgb, smoothstep(0.05, 0.95, u) * 0.85);\n petal *= (0.40 + 0.68 * diffuse) * (0.96 + vein * 0.026 + fibres * 0.035);\n // A narrow fold and translucent lip describe the curl without an outline.\n petal *= 1.0 - 0.13 * exp(-v * v * 100.0) * sin(clamp(u, 0.0, 1.0) * PI);\n petal += params.colorC.rgb * pow(abs(clamp(v, -1.0, 1.0)), 10.0) * 0.12;\n petal *= mix(0.54, 1.0, smoothstep(0.05, 0.42, u));\n col = mix(col, petal, mask);\n }\n }\n\n let headR = 0.12 * params.d;\n let headMask = 1.0 - smoothstep(headR - aa, headR + aa, r);\n if (headMask > 0.0) {\n let grid = p * 125.0;\n let cell = floor(grid);\n var nearest = 10.0;\n var delta = vec2f(0.0);\n var variation = 0.0;\n for (var y = -1; y <= 1; y += 1) {\n for (var x = -1; x <= 1; x += 1) {\n let id = cell + vec2f(f32(x), f32(y));\n let rnd = st_hash22(id);\n let d = grid - (id + 0.25 + rnd * 0.5);\n if (dot(d, d) < nearest) { nearest = dot(d, d); delta = d; variation = rnd.x; }\n }\n }\n let bead = sqrt(max(1.0 - nearest * 5.0, 0.0));\n let normal = normalize(vec3f(delta * 2.0, max(bead, 0.1)));\n let dome = sqrt(max(1.0 - r * r / (headR * headR), 0.0));\n let head = mix(params.colorB.rgb * 0.16, params.colorC.rgb * vec3f(0.85, 0.62, 0.28), bead)\n * (0.35 + 0.65 * max(dot(normal, light), 0.0)) * (0.45 + 0.55 * dome) * (0.8 + variation * 0.3);\n col = mix(col, head, headMask);\n }\n for (var i = 0; i < 9; i += 1) {\n let rnd = st_hash22(vec2f(f32(i) + 2.0, 17.3));\n let cycle = fract(t * 0.024 + rnd.y);\n let q = vec2f((rnd.x - 0.5) * 2.7 + sin(t * 0.25 + f32(i)) * 0.04, 1.2 - cycle * 2.4);\n let fade = sin(cycle * PI);\n col += params.colorC.rgb * exp(-dot(p - q, p - q) * 18000.0) * fade * params.c * 0.25;\n }\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
16
17
  "blueprint": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let taps = st_steps(12);\n var sum = 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;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / f32(taps + 1);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// @reads-source\n// shadertown drafting prelude — prepended (after the shared, source and graph\n// preludes) to every shader in src/shaders/graph/drafting/.\n//\n// A drafting graph reads a layer the way a draughtsman reads a part: it\n// measures it first, then draws around it. The passes, in the order the\n// runtime (src/graph.ts) encodes them:\n//\n// fs_bounds the layer's ink, boxed — a 256² sampling of the source\n// reduced to a 1×1 texture holding (min, max) in source uv\n// fs_reduce one 4×4 reduction step of that box, run three times\n// fs_seed the box, padded, rasterised into a 512² grid: edge texels\n// become seeds for the jump flood\n// fs_jfa one jump-flood step; ten of them turn seeds into the\n// nearest edge for every texel\n// fs_distance nearest edge to a signed distance in source pixels, and\n// a filterable texture the plate pass can sample\n// fs_plate the drawing: supplied by the variant file\n// fs_brightAlpha the shared bloom gate — the plate's alpha, not its\n// luminance, so a bright mat does not glow and the lines do\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_plate(uv : vec2f) -> vec4f\n//\n// `uv` is the canvas uv; the return value is linear-light colour plus, in w,\n// how much of this pixel is ink the bloom chain may pick up. The helpers here\n// hand it the measured box, the fitted drawing rectangle, and the distance\n// field, so a variant is the look and none of the plumbing.\n\nstruct DraftUniforms {\n // jump-flood step in texels, distance-field grid size, two spares\n step : vec4f,\n}\n\n@group(0) @binding(4) var<uniform> du : DraftUniforms;\n// (min.xy, max.xy) of the ink, in source uv. 1×1 after the reductions.\n@group(0) @binding(5) var t_bounds : texture_2d<f32>;\n// The previous reduction level, or a jump-flood ping-pong partner.\n@group(0) @binding(6) var t_reduce : texture_2d<f32>;\n@group(0) @binding(7) var t_jfa : texture_2d<f32>;\n// The finished signed distance field: x is distance in source pixels, y the\n// soft ink at that texel.\n@group(0) @binding(8) var t_sdf : texture_2d<f32>;\n// The label face: sixteen glyphs in a strip, drawn by the runtime with the\n// browser's monospace font. Alpha is coverage.\n@group(0) @binding(9) var t_glyphs : texture_2d<f32>;\n\n/** The sampling grid the bounds pass reads the source through, per side. */\nconst ST_BOUNDS_GRID : f32 = 256.0;\n\n// --- ink ---------------------------------------------------------------------\n\n/**\n * How much ink sits at a source uv, 0 to 1.\n *\n * Bright is ink: the convention the text and SVG sources render to. The\n * threshold is the shader's `a` slot so a photograph can be cut where it\n * reads best, and the band around it is wide enough to keep the source's own\n * anti-aliasing rather than reintroduce stair steps. Outside the source there\n * is no ink at all, so a layer with ink on its border still gets an outline\n * there instead of the sampler's smear.\n */\nfn st_ink(srcUv : vec2f) -> f32 {\n let luma = st_luma(st_srcAt(srcUv).rgb);\n let threshold = clamp(params.a, 0.05, 0.95);\n return smoothstep(threshold - 0.12, threshold + 0.12, luma) * st_srcInside(srcUv);\n}\n\n// --- the box ----------------------------------------------------------------\n\n/** The sentinel an empty box carries: min above max, so `max(min)` heals it. */\nconst ST_EMPTY_BOX : vec4f = vec4f(2.0, 2.0, -1.0, -1.0);\n\n@fragment fn fs_bounds(@builtin(position) position : vec4f) -> @location(0) vec4f {\n // Each texel of the 64² output owns a 4×4 block of the 256² sampling grid.\n let block = floor(position.xy) * 4.0;\n var box = ST_EMPTY_BOX;\n for (var y = 0; y < 4; y = y + 1) {\n for (var x = 0; x < 4; x = x + 1) {\n let uv = (block + vec2f(f32(x), f32(y)) + 0.5) / ST_BOUNDS_GRID;\n if (st_ink(uv) > 0.5) {\n box = vec4f(min(box.xy, uv), max(box.zw, uv));\n }\n }\n }\n return box;\n}\n\n@fragment fn fs_reduce(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let block = vec2i(position.xy) * 4;\n var box = ST_EMPTY_BOX;\n for (var y = 0; y < 4; y = y + 1) {\n for (var x = 0; x < 4; x = x + 1) {\n let v = textureLoad(t_reduce, block + vec2i(x, y), 0);\n box = vec4f(min(box.xy, v.xy), max(box.zw, v.zw));\n }\n }\n return box;\n}\n\n/**\n * The measured box, in source uv, grown by half a sampling cell so the ink\n * the grid found is inside it rather than on its edge. A layer with no ink at\n * all boxes the whole layer: there is still a plate to draw.\n */\nfn st_plateBox() -> vec4f {\n let b = textureLoad(t_bounds, vec2i(0), 0);\n if (b.z < b.x || b.w < b.y) {\n return vec4f(0.0, 0.0, 1.0, 1.0);\n }\n let halfCell = 0.5 / ST_BOUNDS_GRID;\n return vec4f(b.xy - halfCell, b.zw + halfCell);\n}\n\n/** The box in source pixels: what the dimension lines measure. */\nfn st_plateBoxPx(box : vec4f) -> vec2f {\n return max((box.zw - box.xy) * max(source.size, vec2f(1.0)), vec2f(1.0));\n}\n\n/**\n * The region the distance field covers: the box plus a margin on every side,\n * so the outline and anything drawn just off the ink has a distance to read.\n * A tenth of the longer side, which is more than the widest stroke a control\n * can ask for.\n */\nfn st_plateField(box : vec4f) -> vec4f {\n let sizePx = st_plateBoxPx(box);\n let padPx = max(sizePx.x, sizePx.y) * 0.1 + 2.0;\n let pad = padPx / max(source.size, vec2f(1.0));\n return vec4f(box.xy - pad, box.zw + pad);\n}\n\n/** Source pixels per distance-field texel, per axis. */\nfn st_fieldTexelPx(field : vec4f, grid : f32) -> vec2f {\n return (field.zw - field.xy) * max(source.size, vec2f(1.0)) / grid;\n}\n\n// --- the distance field ---------------------------------------------------------\n\n/**\n * Seeds: the texels where the ink changes between neighbours. Coordinates are\n * texel indices, and a texel that is not a seed carries −1 so the flood can\n * tell. z keeps the soft ink for the sign later, w says whether this texel\n * itself was an edge.\n */\n@fragment fn fs_seed(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let grid = du.step.y;\n let field = st_plateField(st_plateBox());\n let texel = vec2f(1.0) / grid;\n let coord = floor(position.xy);\n let uv = (coord + 0.5) * texel;\n let at = field.xy + uv * (field.zw - field.xy);\n let span = (field.zw - field.xy) * texel;\n\n let here = st_ink(at);\n let side = step(0.5, here);\n var edge = 0.0;\n edge = max(edge, abs(step(0.5, st_ink(at + vec2f(span.x, 0.0))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at - vec2f(span.x, 0.0))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at + vec2f(0.0, span.y))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at - vec2f(0.0, span.y))) - side));\n\n let seed = select(vec2f(-1.0), coord, edge > 0.5);\n return vec4f(seed, here, edge);\n}\n\n/**\n * One jump-flood step: look `step` texels away in the eight directions, keep\n * whichever seed is nearest. Distances are measured in source pixels, not\n * texels, because the box is rarely square and the field grid always is.\n */\n@fragment fn fs_jfa(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let n = i32(textureDimensions(t_jfa).x);\n let grid = f32(n);\n let texelPx = st_fieldTexelPx(st_plateField(st_plateBox()), grid);\n let coord = vec2i(position.xy);\n let here = vec2f(coord) + 0.5;\n let jump = i32(du.step.x);\n\n let own = textureLoad(t_jfa, coord, 0);\n var best = own.xy;\n var bestD = select(1e20, length((own.xy + 0.5 - here) * texelPx), own.x >= 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 q = coord + vec2i(x, y) * jump;\n if (q.x < 0 || q.y < 0 || q.x >= n || q.y >= n) { continue; }\n let s = textureLoad(t_jfa, q, 0);\n if (s.x < 0.0) { continue; }\n let d = length((s.xy + 0.5 - here) * texelPx);\n if (d < bestD) {\n bestD = d;\n best = s.xy;\n }\n }\n }\n return vec4f(best, own.z, own.w);\n}\n\n/**\n * Nearest edge to a signed distance, in source pixels: negative inside the\n * ink. An edge texel estimates its own sub-texel offset from the soft ink,\n * which is what lets a bilinear read of this texture land the outline on the\n * source's own anti-aliased boundary rather than on the texel grid.\n */\n@fragment fn fs_distance(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let grid = f32(textureDimensions(t_jfa).x);\n let texelPx = st_fieldTexelPx(st_plateField(st_plateBox()), grid);\n let coord = vec2i(position.xy);\n let v = textureLoad(t_jfa, coord, 0);\n let here = vec2f(coord) + 0.5;\n\n let far = length(texelPx) * grid;\n let d = select(far, length((v.xy + 0.5 - here) * texelPx), v.x >= 0.0);\n let withSign = select(d, -d, v.z > 0.5);\n let atEdge = (0.5 - v.z) * 2.0 * min(texelPx.x, texelPx.y);\n return vec4f(select(withSign, atEdge, v.w > 0.5), v.z, 0.0, 1.0);\n}\n\n// --- the drawing ----------------------------------------------------------------\n\n/**\n * Where the measured box lands on the canvas.\n *\n * Contained, never covered: the whole part has to be on the plate for the\n * dimension lines to mean anything. `zoom` is the fraction of the canvas the\n * part may fill on its limiting axis; the rectangle is in the centred,\n * aspect-corrected space `st_centered` gives, y down, one unit tall.\n */\nstruct PlateFit {\n // top-left and bottom-right, centred canvas units\n min : vec2f,\n max : vec2f,\n // canvas device pixels per source pixel\n scale : f32,\n}\n\nfn st_plateFit(box : vec4f, zoom : f32, centre : vec2f) -> PlateFit {\n let sizePx = st_plateBoxPx(box);\n let avail = vec2f(st_aspect(), 1.0) * zoom;\n let unitsPerPx = min(avail.x / sizePx.x, avail.y / sizePx.y);\n let size = sizePx * unitsPerPx;\n var fit : PlateFit;\n fit.min = centre - size * 0.5;\n fit.max = centre + size * 0.5;\n fit.scale = unitsPerPx * params.resolution.y;\n return fit;\n}\n\n/** A canvas position, in `st_centered` units, to the source uv under it. */\nfn st_plateToSource(p : vec2f, box : vec4f, fit : PlateFit) -> vec2f {\n return box.xy + (p - fit.min) / (fit.max - fit.min) * (box.zw - box.xy);\n}\n\n/**\n * Signed distance to the ink under a canvas position, in canvas device\n * pixels. Off the field it is simply far away.\n */\nfn st_plateDistance(p : vec2f, box : vec4f, fit : PlateFit) -> f32 {\n let field = st_plateField(box);\n let srcUv = st_plateToSource(p, box, fit);\n let fuv = (srcUv - field.xy) / (field.zw - field.xy);\n let inside = step(0.0, fuv.x) * step(fuv.x, 1.0) * step(0.0, fuv.y) * step(fuv.y, 1.0);\n let d = textureSample(t_sdf, linearSampler, clamp(fuv, vec2f(0.0), vec2f(1.0))).x * fit.scale;\n return select(1e5, d, inside > 0.5);\n}\n\n// --- lettering --------------------------------------------------------------------\n// The dimension labels are set from a glyph atlas the runtime draws once with\n// the browser's monospace face (src/graph.ts, `GLYPHS`): sixteen cells in a\n// strip, in this order — the digits, a colon, W, H, p, x and a space. A label\n// is laid out in cells the height of one glyph and `ST_GLYPH_ADVANCE` of that\n// wide, and each cell samples its glyph out of the strip.\n\nconst ST_GLYPH_COUNT : f32 = 16.0;\n/** Cell width over cell height in the atlas — 40 over 64. */\nconst ST_GLYPH_ADVANCE : f32 = 0.625;\n\nconst ST_GLYPH_COLON : u32 = 10u;\nconst ST_GLYPH_W : u32 = 11u;\nconst ST_GLYPH_H : u32 = 12u;\nconst ST_GLYPH_P : u32 = 13u;\nconst ST_GLYPH_X : u32 = 14u;\nconst ST_GLYPH_SPACE : u32 = 15u;\n\n/**\n * Coverage of one glyph at a position inside its cell, 0 to 1 across and\n * down. Sampled at level zero by choice: the call sits inside a branch, and\n * the mip-free form has no uniformity to keep.\n */\nfn st_glyph(glyph : u32, cell : vec2f) -> f32 {\n let texel = 1.0 / vec2f(textureDimensions(t_glyphs));\n let inCell = clamp(cell, texel * vec2f(ST_GLYPH_COUNT, 1.0), vec2f(1.0) - texel * vec2f(ST_GLYPH_COUNT, 1.0));\n let uv = vec2f((f32(glyph) + inCell.x) / ST_GLYPH_COUNT, inCell.y);\n return textureSampleLevel(t_glyphs, linearSampler, uv, 0.0).a;\n}\n\n/** How many digits a count prints with, at least one. */\nfn st_digitCount(value : u32) -> u32 {\n var n = 1u;\n var v = value;\n while (v >= 10u) {\n v = v / 10u;\n n = n + 1u;\n }\n return n;\n}\n\n/** The glyph at position `slot` of \"W: 479 px\" — `axis` picks the letter. */\nfn st_labelGlyph(slot : u32, axis : u32, value : u32) -> u32 {\n let digits = st_digitCount(value);\n if (slot == 0u) { return axis; }\n if (slot == 1u) { return ST_GLYPH_COLON; }\n if (slot == 2u) { return ST_GLYPH_SPACE; }\n if (slot < 3u + digits) {\n // Digits print most significant first: peel from the right.\n var v = value;\n for (var i = 3u + digits - 1u; i > slot; i = i - 1u) { v = v / 10u; }\n return v % 10u;\n }\n if (slot == 3u + digits) { return ST_GLYPH_SPACE; }\n if (slot == 4u + digits) { return ST_GLYPH_P; }\n if (slot == 5u + digits) { return ST_GLYPH_X; }\n return ST_GLYPH_SPACE;\n}\n\n/** Width of the label, in glyph heights. */\nfn st_labelWidth(value : u32) -> f32 {\n return f32(6u + st_digitCount(value)) * ST_GLYPH_ADVANCE;\n}\n\n/**\n * Coverage of the label at a position measured in glyph heights, origin at\n * the label's top-left. `st_labelWidth` says how far it runs; it is one\n * glyph tall.\n */\nfn st_label(tp : vec2f, axis : u32, value : u32) -> f32 {\n if (tp.x < 0.0 || tp.y < 0.0 || tp.y >= 1.0 || tp.x >= st_labelWidth(value)) { return 0.0; }\n let slot = floor(tp.x / ST_GLYPH_ADVANCE);\n let cell = vec2f((tp.x - slot * ST_GLYPH_ADVANCE) / ST_GLYPH_ADVANCE, tp.y);\n return st_glyph(st_labelGlyph(u32(slot), axis, value), cell);\n}\n\n// --- the passes the runtime drives ---------------------------------------------\n\n@fragment fn fs_plate(@location(0) uv : vec2f) -> @location(0) vec4f {\n return st_plate(uv);\n}\n\n// ---------------------------------------------------------------------------\n// blueprint\n// ---------------------------------------------------------------------------\n\n// Blueprint — the layer measured and drawn up as a technical plate.\n//\n// The ink of the layer (a logo, a wordmark, a line of text) is boxed, its\n// outline is inked at a set weight, and the inside is hatched at forty-five\n// degrees. Below and beside it run the dimension lines a draughtsman would\n// add — ticks, extension lines and a label with the size in pixels, set in a\n// bitmap face that lives in the shader. Behind it all is the mat: grained\n// paper under a fine grid, with construction lines meeting at the pointer.\n//\n// The plumbing — the box, the fit, the distance field — is the drafting\n// prelude's. This file is the look: three wells, four slots.\n// colorA the mat, colorB the ink, colorC the hatching and the grid\n// a threshold, b hatch spacing, c line weight, d grid cells\n\n/** A one-device-pixel line at distance `d` px, one pixel of anti-aliasing. */\nfn bp_hairline(d : f32, weight : f32) -> f32 {\n return 1.0 - smoothstep(weight * 0.5 - 0.6, weight * 0.5 + 0.6, d);\n}\n\n/** Distance from `p` to the segment a–b, all in device pixels. */\nfn bp_segment(p : vec2f, a : vec2f, b : vec2f) -> f32 {\n let ab = b - a;\n let t = clamp(dot(p - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\n return length(p - (a + ab * t));\n}\n\nfn st_plate(uv : vec2f) -> vec4f {\n let res = params.resolution;\n let t = st_time();\n let p = st_centered(uv);\n // Everything below is measured in device pixels: one canvas unit is the\n // canvas's height, so lines keep their weight whatever the resolution.\n let px = p * res.y;\n\n let paper = st_toLinear(params.colorA.rgb);\n let ink = st_toLinear(params.colorB.rgb);\n let pencil = st_toLinear(params.colorC.rgb);\n\n let weight = clamp(params.c, 0.5, 8.0);\n let hair = max(1.0, weight * 0.4);\n\n // --- the part, fitted to the plate -------------------------------------------\n // Sits a little high, so the width label below it has its own room.\n let box = st_plateBox();\n let zoom = clamp(params.scale, 0.3, 1.6) * 0.72;\n let fit = st_plateFit(box, zoom, vec2f(0.0, -0.03));\n let fitMin = fit.min * res.y;\n let fitMax = fit.max * res.y;\n let sd = st_plateDistance(p, box, fit);\n\n // --- the mat -------------------------------------------------------------------\n // Paper, not film: the speckle stays put while everything else moves.\n let speckle = st_hash21(floor(uv * res));\n let fibre = st_noise2(uv * res * 0.11);\n var col = paper * (0.90 + speckle * 0.16 + (fibre - 0.5) * 0.10);\n\n // The grid: `d` is how many cells run down the height, drawn one pixel\n // wide, with every fourth line heavier the way graph paper is ruled. Zero\n // cells is no grid, and no construction lines either.\n let cells = clamp(params.d, 0.0, 32.0);\n let grid = step(0.5, cells);\n let cell = res.y / max(cells, 1.0);\n let gridUv = px + res * 0.5;\n let minorD = abs(fract(gridUv / cell + 0.5) - 0.5) * cell;\n let majorD = abs(fract(gridUv / (cell * 4.0) + 0.5) - 0.5) * cell * 4.0;\n let minorLine = bp_hairline(min(minorD.x, minorD.y), 1.0);\n let majorLine = bp_hairline(min(majorD.x, majorD.y), 1.0);\n col = mix(col, pencil, max(minorLine * 0.18, majorLine * 0.45) * grid);\n\n // Construction lines from the corners of the sheet meet at the pointer, and\n // a crosshair marks the meeting. With no hand on the canvas the pointer is\n // the centre, which is where a draughtsman would put it anyway.\n let cursor = (params.pointer - 0.5) * res;\n let sheet = res * 0.5;\n var diag = 1e5;\n diag = min(diag, bp_segment(px, cursor, vec2f(-sheet.x, -sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f( sheet.x, -sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f(-sheet.x, sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f( sheet.x, sheet.y)));\n col = mix(col, pencil, bp_hairline(diag, 1.0) * 0.22 * grid);\n\n let arm = res.y * 0.014;\n let toCursor = abs(px - cursor);\n let hairCross = max(\n bp_hairline(toCursor.y, 1.0) * step(toCursor.x, arm) * step(arm * 0.25, toCursor.x),\n bp_hairline(toCursor.x, 1.0) * step(toCursor.y, arm) * step(arm * 0.25, toCursor.y),\n );\n col = mix(col, ink, hairCross * 0.8);\n\n // --- the part -------------------------------------------------------------------\n // Hatching: forty-five degrees, sliding slowly, clipped to the inside of the\n // outline so the stroke always sits on clean ground.\n let spacing = max(4.0, res.y * 0.014 * clamp(params.b, 0.0, 3.0));\n let hatchPos = (px.x + px.y) * 0.70710678 + t * 6.0;\n let hatchD = abs(fract(hatchPos / spacing) - 0.5) * spacing;\n let hatchLine = bp_hairline(hatchD, max(1.2, spacing * 0.18));\n let interior = 1.0 - smoothstep(-weight * 0.5 - 1.2, -weight * 0.5 + 0.2, sd);\n let hatched = hatchLine * interior * step(0.001, params.b);\n col = mix(col, pencil, hatched * 0.85);\n\n // The outline: a stroke of `weight` pixels centred on the edge of the ink.\n let stroke = bp_hairline(abs(sd), weight);\n col = mix(col, ink, stroke);\n\n // --- the dimensions -------------------------------------------------------------\n let gap = res.y * 0.055;\n let tick = res.y * 0.012;\n var dims = 0.0;\n var faint = 0.0;\n\n // Width, along the bottom.\n let wy = fitMax.y + gap;\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMin.x, wy), vec2f(fitMax.x, wy)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMin.x, wy - tick), vec2f(fitMin.x, wy + tick)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMax.x, wy - tick), vec2f(fitMax.x, wy + tick)), hair));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMin.x, fitMax.y + 3.0), vec2f(fitMin.x, wy - tick)), 1.0));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x, fitMax.y + 3.0), vec2f(fitMax.x, wy - tick)), 1.0));\n\n // Height, up the right-hand side.\n let hx = fitMax.x + gap;\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx, fitMin.y), vec2f(hx, fitMax.y)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx - tick, fitMin.y), vec2f(hx + tick, fitMin.y)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx - tick, fitMax.y), vec2f(hx + tick, fitMax.y)), hair));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x + 3.0, fitMin.y), vec2f(hx - tick, fitMin.y)), 1.0));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x + 3.0, fitMax.y), vec2f(hx - tick, fitMax.y)), 1.0));\n\n // The labels give the size of the part as drawn, in device pixels, set at\n // a glyph height that follows the canvas but never drops below legible.\n let size = fitMax - fitMin;\n let widthPx = u32(round(size.x));\n let heightPx = u32(round(size.y));\n let em = max(12.0, res.y / 44.0);\n\n // \"W: … px\" reads left to right, centred under the width line.\n let wLabelW = st_labelWidth(widthPx) * em;\n let wOrigin = vec2f((fitMin.x + fitMax.x) * 0.5 - wLabelW * 0.5, wy + tick + em * 0.15);\n dims = max(dims, st_label((px - wOrigin) / em, ST_GLYPH_W, widthPx));\n\n // \"H: … px\" is turned a quarter clockwise to read down the height line: its\n // baseline runs down the canvas and the tops of its letters face right.\n let hLabelW = st_labelWidth(heightPx) * em;\n let hTop = hx + tick + em * 0.15 + em;\n let hStart = (fitMin.y + fitMax.y) * 0.5 - hLabelW * 0.5;\n dims = max(dims, st_label(vec2f(px.y - hStart, hTop - px.x) / em, ST_GLYPH_H, heightPx));\n\n col = mix(col, ink, faint * 0.45);\n col = mix(col, ink, dims);\n\n // What the bloom may pick up: the drawn lines, never the mat or the hatch.\n let lit = clamp(stroke + dims + hairCross * 0.8, 0.0, 1.0);\n return vec4f(col * params.intensity, lit);\n}\n",
17
18
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
18
- "caldera": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// caldera\n// ---------------------------------------------------------------------------\n\n// Caldera — a volcanic island at night, lit by its own veins.\n//\n// The island is the usual fbm-minus-falloff, but the light source is inside\n// it: the valleys of a ridged field read as lava channels radiating from the\n// crater, brightest at the pool and cooling as they run out. Everything else\n// — basalt, the sea, the surf line that draws the coast — is lit by that\n// glow and a little moonlight.\n\nfn terrain(p : vec2f) -> f32 {\n return st_fbm2(p * 2.1 + vec2f(3.1, 7.7), 4) - smoothstep(0.1, 1.1, length(p)) * 0.9;\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 * 2.2;\n\n let h = terrain(p) - 0.14;\n let r = length(p);\n let craterR = 0.16 * params.a;\n\n // Moonlit sea: dark, but alive, and paler near the coast so the island\n // silhouette always reads.\n let swell = st_fbm2(p * 5.0 + vec2f(t * 0.1, -t * 0.07), 3);\n var col = vec3f(0.05, 0.07, 0.13) * (0.7 + swell * 0.6);\n col = col + vec3f(0.10, 0.13, 0.20) * exp(min(h, 0.0) * 9.0);\n\n let land = smoothstep(0.0, 0.015, h);\n\n // A thin surf line draws the coast in the dark.\n let coast = smoothstep(0.02, 0.0, abs(h)) * smoothstep(0.4, 0.8, st_fbm2(p * 12.0 + t * 0.25, 3));\n col = mix(col, vec3f(0.35, 0.42, 0.50), coast * 0.6);\n\n // Basalt: moon-grey shoulders, brighter with height so the cone shows.\n let rock = mix(vec3f(0.10, 0.10, 0.14), vec3f(0.24, 0.22, 0.27), smoothstep(0.0, 0.45, h))\n * (0.7 + 0.5 * st_fbm2(p * 7.0, 3));\n col = mix(col, rock, land);\n\n // Lava rivers: thin runs at fixed angles off the pool, meandering more\n // the further they fall, each pulsing outward — downhill — as it goes.\n // The angular multiplier is an integer, so the runs close over the seam.\n let ang = atan2(p.y, p.x);\n let meander = (st_fbm2(p * 2.6 + vec2f(7.7, 1.3), 3) - 0.5) * (2.2 + r * 4.5) * params.warp;\n let runs = abs(sin(ang * 3.0 + meander));\n let width = 0.22 * (0.4 + clamp(params.b, 0.0, 1.5)) / (1.0 + r * 2.5);\n let vein = smoothstep(width, width * 0.2, runs);\n let reach = smoothstep(craterR + mix(0.35, 1.0, clamp(params.b, 0.0, 1.5) / 1.5), craterR * 0.5, r);\n let pulse = 0.85 + 0.15 * sin(t * 1.1 + r * 5.0);\n let downhill = 0.7 + 0.3 * sin(r * 15.0 - t * 2.2 + meander);\n var lava = vein * reach * land * downhill;\n\n // The crater pool: a dark cooling crust floating on the bright melt.\n let crust = smoothstep(0.45, 0.75, st_fbm2(p * 9.0 + vec2f(0.0, t * 0.12), 3));\n let pool = smoothstep(craterR, craterR * 0.5, r) * (1.0 - crust * 0.55);\n lava = max(lava, pool);\n\n // Heat colours from the shared ramp: colorB the running orange, colorC\n // the white heart.\n let heat = st_palette(clamp(lava * 1.3, 0.0, 1.0));\n col = mix(col, heat, smoothstep(0.10, 0.45, lava));\n\n // The glow the mountain throws: veins light their own banks, the crater\n // lights the summit, and both tint the smoke of the night air.\n col = col + params.colorB.rgb * lava * lava * 1.4;\n col = col + params.colorB.rgb * exp(-abs(r - craterR * 0.7) * 9.0) * 0.25 * params.c * pulse;\n col = col + params.colorB.rgb * vein * reach * 0.22 * params.c;\n\n // Embers: a handful of soft sparks climbing off the crater, not pixels.\n for (var i = 0; i < 5; i = i + 1) {\n let fi = f32(i);\n let cycle = fract(t * (0.11 + st_hash11(fi) * 0.07) + st_hash11(fi * 3.7));\n let emberPos = vec2f(\n (st_hash11(fi * 7.3) - 0.5) * 0.5 + sin(t * 0.8 + fi * 2.2) * 0.06 + cycle * 0.3,\n -cycle * 0.9 + 0.1,\n );\n let fadeIn = smoothstep(0.0, 0.1, cycle) * smoothstep(1.0, 0.55, cycle);\n col = col + params.colorC.rgb * exp(-length(p - emberPos) * 90.0) * fadeIn * params.d * 0.8;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
19
+ "caldera": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// caldera\n// ---------------------------------------------------------------------------\n\nfn volcano(p : vec2f) -> f32 {\n let r = length(p);\n let noise = st_fbm2(p * 5.0 + vec2f(3.1, 7.7), 3);\n let crater = 0.23 * params.a;\n let cone = 0.78 * exp(-r * r * 2.6);\n let bowl = 0.28 * (1.0 - smoothstep(crater * 0.60, crater * 1.35, r));\n let coast = 1.0 - smoothstep(0.82, 1.2, r + (noise - 0.45) * 0.22);\n return max((cone - bowl + (noise - 0.45) * 0.075 * smoothstep(crater * 0.8, crater * 1.5, r)) * coast, 0.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let screen = st_centered(uv) * params.scale;\n let eye = vec3f(1.4, 3.4, 2.8);\n let forward = normalize(vec3f(0.0, 0.18, 0.0) - eye);\n let right = normalize(cross(forward, vec3f(0.0, 1.0, 0.0)));\n let up = cross(right, forward);\n let ray = normalize(forward * 1.7 + right * screen.x - up * screen.y);\n var distance = 0.6;\n var pos = eye;\n for (var i = 0; i < st_steps(140); i += 1) {\n pos = eye + ray * distance;\n let gap = pos.y - volcano(pos.xz);\n if (gap < 0.0015 || distance > 12.0) { break; }\n distance += max(gap * 0.25, 0.002);\n }\n let p = pos.xz;\n let r = length(p);\n let h = volcano(p);\n let crater = 0.23 * params.a;\n let eps = 0.003;\n let normal = normalize(vec3f(volcano(p - vec2f(eps, 0.0)) - volcano(p + vec2f(eps, 0.0)), 2.0 * eps,\n volcano(p - vec2f(0.0, eps)) - volcano(p + vec2f(0.0, eps))));\n let moon = normalize(vec3f(-0.6, 1.0, 0.2));\n let rough = st_fbm2(p * 32.0, 3);\n var col = params.colorA.rgb * 0.7;\n if (h > 0.012) {\n col = mix(vec3f(0.032, 0.037, 0.047), vec3f(0.16, 0.18, 0.21), rough)\n * (0.28 + 0.95 * max(dot(normal, moon), 0.0));\n let ang = atan2(p.y, p.x);\n let meander = (st_fbm2(p * 3.0 + 7.1, 3) - 0.45) * params.warp * 3.2;\n let channel = abs(sin(ang * 3.0 + meander + r * 1.2));\n let width = 0.018 + params.b * 0.055;\n let vein = (1.0 - smoothstep(width * 0.3, width, channel)) * smoothstep(0.0, 0.15, params.b);\n let reach = (1.0 - smoothstep(0.45, 0.55 + params.b * 0.37, r)) * smoothstep(crater * 0.8, crater * 1.4, r);\n let flow = st_fbm2(vec2f(ang * 4.0, r * 20.0 - t * 0.6), 3);\n let crust = st_fbm2(p * 25.0 + vec2f(t * 0.08, -t * 0.05), 3);\n let pool = (1.0 - smoothstep(crater * 0.65, crater * 0.95, r));\n let melt = pool * (0.35 + 0.65 * (1.0 - smoothstep(0.40, 0.59, crust)));\n let lava = max(melt, vein * reach * (0.4 + flow));\n let heat = mix(params.colorB.rgb, params.colorC.rgb, pow(clamp(lava, 0.0, 1.0), 2.0));\n col = mix(col, heat * 2.3, smoothstep(0.1, 0.65, lava));\n let bankGlow = exp(-channel * 16.0) * reach * smoothstep(0.0, 0.15, params.b);\n let rimGlow = exp(-abs(r - crater * 0.85) * 18.0);\n col += params.colorB.rgb * (bankGlow * 0.22 + rimGlow * 0.20) * params.c;\n } else {\n let wave = st_fbm2(p * vec2f(8.0, 24.0) + vec2f(t * 0.04, t * 0.10), 3);\n col *= 0.8 + wave * 0.6;\n let reflection = exp(-p.x * p.x * 7.0) * exp(-abs(p.y - 0.8) * 1.3);\n col += params.colorB.rgb * reflection * pow(wave, 5.0) * params.c * 0.8;\n col += vec3f(0.10, 0.16, 0.19) * exp(-abs(r - 1.12) * 35.0) * smoothstep(0.48, 0.65, wave);\n }\n // Thin, drifting haze catches the crater light above the summit.\n let haze = st_fbm2(screen * 8.0 + vec2f(t * 0.035, t * 0.09), 3);\n col += params.colorB.rgb * exp(-dot(screen + vec2f(0.0, 0.09), screen + vec2f(0.0, 0.09)) * 35.0)\n * haze * 0.10 * params.c;\n for (var i = 0; i < 12; i += 1) {\n let rnd = st_hash22(vec2f(f32(i) + 1.0, 13.7));\n let cycle = fract(t * (0.04 + rnd.x * 0.035) + rnd.y);\n let spark = vec2f((rnd.x - 0.5) * 0.15 + cycle * 0.09, -0.025 - cycle * 0.3);\n let delta = screen - spark;\n col += params.colorC.rgb * exp(-dot(delta, delta) * 180000.0) * sin(cycle * PI) * params.d;\n }\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
19
20
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
20
21
  "chladni": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// chladni\n// ---------------------------------------------------------------------------\n\n// Chladni — a singing plate, and the sand that maps its silence.\n//\n// The height is a pair of square-plate standing waves, the classic\n// cos(mπx)cos(nπy) figures, beating in time. The aux channel is the inverse:\n// where the figure crosses zero the plate never moves, and that is where the\n// accent colour gathers — the same reason sand walks to the nodal lines of a\n// real vibrating plate. Sweep drifts the mode numbers continuously, so the\n// figure re-solves itself into the next one instead of cutting.\n\nfn chladniFigure(q : vec2f, m : f32, n : f32) -> f32 {\n // The difference of the two orderings is what draws the diagonals; the sum\n // alone would be a plain checkerboard of humps.\n return cos(m * PI * q.x) * cos(n * PI * q.y) - cos(n * PI * q.x) * cos(m * PI * q.y);\n}\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans roughly ±1 across the plate; scale zooms the figure.\n let q = p * (params.scale / (gu.world.x * 0.5));\n\n // Sweep walks both mode numbers on incommensurate clocks, so the figure\n // never repeats a loop the eye can catch.\n let m = params.a + params.d * (0.5 + 0.5 * sin(t * 0.21));\n let n = params.b + params.d * (0.5 + 0.5 * sin(t * 0.13 + 2.1));\n let figure = chladniFigure(q, m, n);\n\n // The plate rings: the whole figure oscillates through zero, which is what\n // separates a vibration from a relief map of one.\n var h = figure * 0.4 * cos(t * 2.6);\n\n // A travelling ripple from the driver at the centre, fading toward the rim.\n let r = length(q);\n h = h + params.warp * 0.18 * sin(r * 22.0 - t * 3.0) * exp(-r * 1.6);\n\n // Sand: bright exactly where the figure is silent. The vibration term does\n // not gate it — nodes hold still at every phase, so the lines hold steady\n // while the plate rings around them.\n let node = 1.0 - smoothstep(0.0, 0.22, abs(figure));\n let aux = clamp(node * params.c * 0.85, 0.0, 1.0);\n\n return vec4f(0.0, h, 0.0, aux);\n}\n",
21
22
  "chromatic-city": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// @standalone\n// Render the city once, then apply tilt-shift blur in screen space.\n@group(0) @binding(1) var t_city : texture_2d<f32>;\n@group(0) @binding(2) var citySampler : sampler;\n\nstruct CityVertex {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n@vertex fn vs_fullscreen(@builtin(vertex_index) index : u32) -> CityVertex {\n let positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n let uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n return CityVertex(vec4f(positions[index], 0.0, 1.0), uvs[index]);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let band = params.k * 0.5;\n let falloff = smoothstep(band, 0.5, abs(uv.y - 0.5));\n let radius = params.j * falloff * 0.018;\n let center = textureSampleLevel(t_city, citySampler, uv, 0.0).rgb;\n if (radius * params.resolution.y < 0.3) { return st_post(center, uv); }\n var sum = center;\n var weights = 1.0;\n let count = st_steps(48);\n for (var i = 0; i < count; i += 1) {\n let fraction = (f32(i) + 0.5) / f32(count);\n let angle = f32(i) * 2.399963;\n let offset = vec2f(cos(angle) / st_aspect(), sin(angle)) * sqrt(fraction) * radius;\n let weight = exp(-fraction * 2.0);\n sum += textureSampleLevel(t_city, citySampler, uv + offset, 0.0).rgb * weight;\n weights += weight;\n }\n return st_post(sum / weights, uv);\n}\n\n// ---------------------------------------------------------------------------\n// chromatic-city\n// ---------------------------------------------------------------------------\n\nfn cityInset(cell : vec2f) -> f32 {\n return 0.5 * (params.h + params.i * st_hash21(cell + vec2f(params.a, 71.3)));\n}\n\nfn cityHeight(cell : vec2f) -> f32 {\n let seed = st_hash22(vec2f(params.a, params.a + 17.0)) * 200.0;\n let q = cell * params.c + seed;\n let drift = vec2f(sin(st_time() * 0.12), cos(st_time() * 0.09)) * 0.35;\n let broad = st_noise2(q + drift);\n let detail = st_noise2(q * 2.13 + vec2f(31.7, 9.2));\n let field = clamp((broad * 0.78 + detail * 0.22 - 0.23) * 1.7, 0.0, 1.0);\n let level = field * params.d;\n // Hold each terrace, then ease to the next before the integer boundary.\n let terraces = (floor(level) + smoothstep(0.75, 1.0, fract(level))) / params.d;\n let tower = pow(st_hash21(cell + seed), 12.0) * params.e;\n return 0.3 + (terraces + tower) * params.b;\n}\n\n// Follow the light through grid cells, including the gaps between columns.\nfn cityShadow(p : vec3f, light : vec3f) -> f32 {\n var cell = floor(p.xz);\n let direction = sign(light.xz);\n let delta = 1.0 / abs(light.xz);\n var boundary = (select(cell, cell + 1.0, light.xz > vec2f(0.0)) - p.xz) / light.xz;\n var shadow = 1.0;\n var travel = 0.0;\n let ceiling = 0.3 + (1.0 + params.e) * params.b;\n for (var i = 0; i < st_steps(48); i += 1) {\n let gap = cityInset(cell);\n let a = (cell + gap - p.xz) / light.xz;\n let b = (cell + 1.0 - gap - p.xz) / light.xz;\n let near = min(a, b);\n let far = max(a, b);\n let enter = max(0.02, max(near.x, near.y));\n let leave = min(far.x, far.y);\n if (enter < leave) {\n let clearance = p.y + light.y * enter - cityHeight(cell);\n if (clearance < 0.0) { return 0.0; }\n shadow = min(shadow, clamp(clearance * 5.0 / max(enter, 0.1), 0.0, 1.0));\n }\n if (boundary.x < boundary.y) {\n travel = boundary.x;\n boundary.x += delta.x;\n cell.x += direction.x;\n } else {\n travel = boundary.y;\n boundary.y += delta.y;\n cell.y += direction.y;\n }\n if (p.y + travel * light.y > ceiling) { break; }\n }\n return shadow;\n}\n\nfn cityOcclusion(p : vec3f, cell : vec2f) -> f32 {\n let local = p.xz - cell;\n let distances = vec4f(local.x, 1.0 - local.x, local.y, 1.0 - local.y);\n let heights = vec4f(\n cityHeight(cell - vec2f(1.0, 0.0)), cityHeight(cell + vec2f(1.0, 0.0)),\n cityHeight(cell - vec2f(0.0, 1.0)), cityHeight(cell + vec2f(0.0, 1.0)),\n );\n let walls = max(heights - p.y, vec4f(0.0));\n let shade = walls / (walls + 0.7) * exp(-max(distances, vec4f(0.0)) * 2.5);\n return exp(-dot(shade, vec4f(0.65)));\n}\n\nfn cityColor(height : f32) -> vec3f {\n let t = clamp((height - 0.3) / params.b, 0.0, 1.0);\n let violet = mix(params.colorA.rgb, params.colorB.rgb, 0.65) * vec3f(0.65, 0.4, 1.0);\n var col = mix(params.colorA.rgb, violet, smoothstep(0.05, 0.35, t));\n col = mix(col, params.colorB.rgb, smoothstep(0.32, 0.6, t));\n return mix(col, params.colorC.rgb, smoothstep(0.6, 0.95, t));\n}\n\n@fragment fn fs_scene(@location(0) uv : vec2f) -> @location(0) vec4f {\n let yaw = params.f;\n let pitch = params.g;\n let rd = vec3f(sin(yaw) * cos(pitch), -sin(pitch), cos(yaw) * cos(pitch));\n let right = vec3f(cos(yaw), 0.0, -sin(yaw));\n let up = cross(rd, right);\n let screen = st_centered(uv) * params.scale;\n let focus = vec3f(params.pointer.x - 0.5, 0.0, params.pointer.y - 0.5) * 2.0;\n let origin = focus - rd * 40.0 + right * screen.x - up * screen.y;\n let ceiling = 0.3 + (1.0 + params.e) * params.b + 0.01;\n var travel = (ceiling - origin.y) / rd.y;\n let start = origin + rd * travel;\n var cell = floor(start.xz);\n let direction = sign(rd.xz);\n let delta = 1.0 / max(abs(rd.xz), vec2f(0.00001));\n var boundary = travel + (select(cell, cell + 1.0, rd.xz > vec2f(0.0)) - start.xz) / rd.xz;\n var col = params.colorA.rgb * 0.12;\n\n // Visit cells in ray order; intersect each box exactly so thin tops cannot be skipped.\n for (var i = 0; i < st_steps(128); i = i + 1) {\n let height = cityHeight(cell);\n let gap = cityInset(cell);\n let lo = vec3f(cell.x + gap, -0.5, cell.y + gap);\n let hi = vec3f(cell.x + 1.0 - gap, height, cell.y + 1.0 - gap);\n let a = (lo - origin) / rd;\n let b = (hi - origin) / rd;\n let near = min(a, b);\n let far = max(a, b);\n let enter = max(near.x, max(near.y, near.z));\n let leave = min(far.x, min(far.y, far.z));\n if (enter <= leave && leave >= travel - 0.001) {\n let p = origin + rd * enter;\n var normal = vec3f(0.0, 1.0, 0.0);\n if (near.x > near.y && near.x > near.z) { normal = vec3f(-sign(rd.x), 0.0, 0.0); }\n if (near.z > near.y && near.z >= near.x) { normal = vec3f(0.0, 0.0, -sign(rd.z)); }\n let light = normalize(vec3f(-0.8, 1.0, 0.3));\n let diffuse = max(dot(normal, light), 0.0);\n var shadow = 0.0;\n if (diffuse > 0.0) { shadow = cityShadow(p + normal * 0.025, light); }\n let occlusion = cityOcclusion(p, cell);\n let rise = clamp(p.y / height, 0.0, 1.0);\n let sideShade = select(0.5 + 0.5 * smoothstep(0.0, 1.0, rise), 1.0, normal.y > 0.5);\n let local = p.xz - cell;\n let edge = min(min(local.x - gap, 1.0 - gap - local.x), min(local.y - gap, 1.0 - gap - local.y));\n let rim = (1.0 - smoothstep(0.0, 0.035, edge)) * max(normal.y, 0.0);\n let ambient = vec3f(0.34, 0.4, 0.58) * occlusion;\n let direct = vec3f(1.0, 0.91, 0.78) * diffuse * shadow;\n col = cityColor(height) * (ambient + direct) * sideShade;\n col = col * (0.92 + 0.08 * st_hash21(cell + params.a));\n col = col + vec3f(0.06, 0.055, 0.05) * rim * shadow;\n break;\n }\n if (boundary.x < boundary.y) {\n travel = boundary.x;\n boundary.x += delta.x;\n cell.x += direction.x;\n } else {\n travel = boundary.y;\n boundary.y += delta.y;\n cell.y += direction.y;\n }\n if ((origin + rd * travel).y < -0.5) { break; }\n }\n return vec4f(col * params.intensity, 1.0);\n}\n",
@@ -58,7 +59,7 @@ export const shaderSources = {
58
59
  "particles": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// particles\n// ---------------------------------------------------------------------------\n\n// Particles — a hundred thousand embers riding a wind you can bend.\n//\n// The wind is curl noise (Bridson 2007): the perpendicular gradient of a\n// scalar fbm field. Taking the curl makes the flow divergence-free, which is\n// why the streams braid and circulate instead of piling up in the sinks a raw\n// noise field would have. Each particle splats its passage into a fixed-point\n// accumulation buffer that decays a little every frame — the picture is a\n// long-exposure photograph of the swarm, not a frame of dots. Two channels\n// ride the buffer: energy, and energy weighted by speed, so a pixel remembers\n// not just that particles passed but how fast they were going, and the\n// palette reads that ratio. The pointer is a vortex: it pulls the swarm in\n// and swings it sideways, and the same knob below zero blows it apart.\n\nstruct SimUniforms {\n // Seconds this step advances the simulation. 0 redraws without moving.\n dt : f32,\n // Canvas aspect, so forces and distances are round in screen space.\n aspect : f32,\n // How far the pointer moved this frame, in uv per second.\n pointerVel : vec2f,\n // Accumulation buffer size in cells.\n accumW : f32,\n accumH : f32,\n // Particles alive this frame. The buffer always holds the maximum.\n count : f32,\n}\n\n@group(0) @binding(1) var<uniform> sim : SimUniforms;\n\nstruct Particle {\n pos : vec2f,\n vel : vec2f,\n}\n\n// The compute side of the state: particles integrate, the trail accumulates.\n@group(0) @binding(2) var<storage, read_write> parts : array<Particle>;\n@group(0) @binding(3) var<storage, read_write> accum : array<atomic<u32>>;\n\n// The fragment side reads the same trail buffer through a read-only binding:\n// layouts are built per entry point, so each pipeline sees the buffer once.\n@group(0) @binding(4) var<storage, read> trail : array<u32>;\n\n// Fixed-point scale for the accumulation buffer. Energy per splat is well\n// under 1, and a pixel can hold thousands of passages before the u32 clips.\nconst FIXED : f32 = 256.0;\n\n// --- the wind ----------------------------------------------------------------\n\n// Curl of a two-octave fbm: sample the field twice per axis and take the\n// perpendicular gradient. Divergence-free by construction.\nfn windAt(p : vec2f, t : f32) -> vec2f {\n let q = p * (2.4 / max(params.scale, 0.2));\n let e = 0.09;\n let z = t * 0.32;\n let nx0 = st_fbm3(vec3f(q.x - e, q.y, z), 2);\n let nx1 = st_fbm3(vec3f(q.x + e, q.y, z), 2);\n let ny0 = st_fbm3(vec3f(q.x, q.y - e, z), 2);\n let ny1 = st_fbm3(vec3f(q.x, q.y + e, z), 2);\n return vec2f(ny1 - ny0, -(nx1 - nx0)) * (1.0 / (2.0 * e));\n}\n\n// --- the splat ---------------------------------------------------------------\n\nfn splat(pos : vec2f, energy : f32, heat : f32) {\n let size = vec2f(sim.accumW, sim.accumH);\n let px = pos * size - 0.5;\n let base = floor(px);\n let f = px - base;\n // Four-cell bilinear deposit, so a slow particle draws a smooth line\n // rather than snapping from cell to cell.\n for (var dy = 0; dy < 2; dy = dy + 1) {\n for (var dx = 0; dx < 2; dx = dx + 1) {\n let cell = base + vec2f(f32(dx), f32(dy));\n if (cell.x < 0.0 || cell.y < 0.0 || cell.x >= size.x || cell.y >= size.y) { continue; }\n let w = abs(1.0 - f32(dx) - f.x) * abs(1.0 - f32(dy) - f.y);\n let i = (u32(cell.y) * u32(size.x) + u32(cell.x)) * 2u;\n atomicAdd(&accum[i], u32(energy * w * FIXED));\n atomicAdd(&accum[i + 1u], u32(energy * heat * w * FIXED));\n }\n }\n}\n\n// --- passes ------------------------------------------------------------------\n\n// The exposure fades: one thread per cell, both channels.\n@compute @workgroup_size(256)\nfn cs_fade(@builtin(global_invocation_id) gid : vec3u) {\n let cells = u32(sim.accumW) * u32(sim.accumH);\n if (gid.x >= cells) { return; }\n // params.b is Trail: 0 forgets almost instantly, 1 holds for seconds.\n let keep = exp(-sim.dt * mix(14.0, 0.6, clamp(params.b, 0.0, 1.0)));\n let i = gid.x * 2u;\n atomicStore(&accum[i], u32(f32(atomicLoad(&accum[i])) * keep));\n atomicStore(&accum[i + 1u], u32(f32(atomicLoad(&accum[i + 1u])) * keep));\n}\n\n@compute @workgroup_size(256)\nfn cs_move(@builtin(global_invocation_id) gid : vec3u) {\n if (gid.x >= u32(sim.count)) { return; }\n let t = params.time * params.speed;\n let dt = sim.dt * params.speed;\n var p = parts[gid.x];\n\n // Positions live in uv space; distances are measured with x stretched by\n // aspect so a circle of influence is a circle on screen.\n let scr = vec2f(p.pos.x * sim.aspect, p.pos.y);\n\n // Follow the wind with a little inertia — the lag is what draws the\n // ribbons out instead of stamping the noise field itself.\n let wind = windAt(scr, t) * (0.10 + 0.22 * params.warp);\n p.vel = mix(p.vel, wind, 1.0 - exp(-dt * 3.0));\n\n // The pointer: a pull toward it and a swing around it, so the swarm\n // orbits rather than collapsing onto the cursor. Below zero it blows away.\n let cursor = vec2f(params.pointer.x * sim.aspect, params.pointer.y);\n let toCursor = cursor - scr;\n let d = length(toCursor);\n if (params.c != 0.0 && d > 0.0001) {\n let dir = toCursor / d;\n let swirl = vec2f(-dir.y, dir.x);\n let falloff = 1.0 / (1.0 + d * d * 26.0);\n let pull = params.c * falloff;\n p.vel = p.vel + (dir * pull * 0.55 + swirl * abs(pull) * 0.4) * dt * 3.0;\n // The pointer's own motion stirs the swarm as it passes through.\n p.vel = p.vel + sim.pointerVel * falloff * dt * 0.9;\n }\n\n p.vel = p.vel * exp(-dt * 0.6);\n var next = p.pos + vec2f(p.vel.x / sim.aspect, p.vel.y) * dt;\n\n // A particle lives one cycle and is reborn somewhere new, staggered by its\n // index, so the field never settles into the wind's fixed orbits.\n let cycle = 7.0;\n let phase = st_hash11(f32(gid.x) * 0.7213);\n let age = fract(t / cycle + phase);\n let out = next.x < -0.02 || next.x > 1.02 || next.y < -0.02 || next.y > 1.02;\n if (out || age < dt / cycle) {\n let r = st_hash22(vec2f(f32(gid.x) * 0.911, floor(t / cycle + phase) + 3.7));\n next = r;\n p.vel = vec2f(0.0);\n }\n\n // Fast movers cross several cells a frame; splitting the deposit along the\n // segment keeps the trail a stroke instead of a dotted line.\n let speed = length(p.vel);\n let moved = length((next - p.pos) * vec2f(sim.accumW, sim.accumH));\n let steps = clamp(i32(moved), 1, 6);\n let heat = clamp(speed * 3.2, 0.0, 1.0);\n // Deposit scales with the trail buffer's area: the same swarm spread over\n // four times the cells would read four times dimmer otherwise.\n let area = clamp(sim.accumW * sim.accumH / 320000.0, 0.5, 4.0);\n let energy = (0.55 * params.d * area) / f32(steps);\n for (var s = 0; s < steps; s = s + 1) {\n let q = mix(p.pos, next, (f32(s) + 0.5) / f32(steps));\n splat(q, energy, heat);\n }\n\n p.pos = next;\n parts[gid.x] = p;\n}\n\n// --- the picture -------------------------------------------------------------\n\nfn readTrail(cell : vec2i) -> vec2f {\n let w = i32(sim.accumW);\n let h = i32(sim.accumH);\n let c = vec2i(clamp(cell.x, 0, w - 1), clamp(cell.y, 0, h - 1));\n let i = u32(c.y * w + c.x) * 2u;\n return vec2f(f32(trail[i]), f32(trail[i + 1u])) / FIXED;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n // Bilinear read of the trail, since the buffer may be coarser than the canvas.\n let px = uv * vec2f(sim.accumW, sim.accumH) - 0.5;\n let base = floor(px);\n let f = px - base;\n let b = vec2i(base);\n let e00 = readTrail(b);\n let e10 = readTrail(b + vec2i(1, 0));\n let e01 = readTrail(b + vec2i(0, 1));\n let e11 = readTrail(b + vec2i(1, 1));\n let e = mix(mix(e00, e10, f.x), mix(e01, e11, f.x), f.y);\n\n let energy = e.x;\n let heat = e.y / max(e.x, 0.0001);\n\n // A soft-knee exposure: dense crossings saturate instead of clipping.\n let lum = 1.0 - exp(-energy * 1.6 * params.intensity);\n\n // Slow passages sit at the bottom of the ramp, fast ones at the top, and\n // where trails pile up the colour climbs on its own.\n var col = st_palette(clamp(heat * 0.8 + lum * 0.35, 0.0, 1.0)) * lum;\n\n // The faintest wash of the low colour behind everything, so an empty\n // corner is a night sky rather than a dead screen.\n col = col + params.colorA.rgb * 0.05 * (1.0 - uv.y * 0.5);\n\n return st_post(st_tonemap(col), uv);\n}\n",
59
60
  "pixel-clouds": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-clouds\n// ---------------------------------------------------------------------------\n\n// Pixel Clouds — sprite clouds on a flat blue sky.\n//\n// Not a noise field wearing a threshold: each cloud is a drawn sprite, built\n// the way a pixel artist builds one — a row of round bumps riding a flat\n// baseline, tall in the middle and tapering to the ends, over a low slab\n// that closes the gaps. Clouds live on a lattice of rows, every row drifting\n// at its own whole-cell speed, and the shading is one rule: a cell whose\n// neighbour a step down is open air takes the shade stop.\n\n// Is this point (in cells, y down, origin at the cloud's baseline centre)\n// inside the cloud that `rnd` describes? Flat bottom, bumpy top.\nfn cloudSprite(d : vec2f, rnd : vec2f, size : f32) -> bool {\n // Cloud class: squaring the roll keeps giants rarer than puffs. Derived\n // straight from the lattice hash — re-hashing a hash collapsed on f32.\n let cls = fract(rnd.x * 7.13 + rnd.y * 3.71);\n let w = mix(4.5, 19.0, cls * cls) * size; // half-width, in cells\n if (d.y > 0.0 || d.y < -w * 1.4 || abs(d.x) > w * 1.25) {\n return false;\n }\n\n // The slab: a low ellipse along the baseline that closes the gaps between\n // bump seats, cut dead flat by the d.y clip above. Squared by hand: WGSL's\n // pow is undefined for negative bases, and d.y always is.\n let sx = d.x / (w * 0.92);\n let sy = d.y / (w * 0.24);\n var inside = sx * sx + sy * sy < 1.0;\n\n // The bumps: tall in the middle, tapering to the ends, every radius and\n // seat hashed off the cloud's own id so no two clouds repeat.\n let count = 4.0 + floor(cls * 4.0);\n for (var i = 0; i < 7; i = i + 1) {\n if (f32(i) >= count) { break; }\n let fi = f32(i);\n let h = st_hash22(rnd * 37.1 + vec2f(fi * 5.3, fi * 2.9));\n let xi = (fi / (count - 1.0) - 0.5) * 2.0 * w * 0.8 + (h.x - 0.5) * w * 0.2;\n let taper = 1.0 - abs(xi) / (w * 1.15);\n let ri = w * (0.28 + h.y * 0.22) * (0.45 + 0.55 * taper);\n let yi = -ri * (0.55 + h.x * 0.5);\n let q = (d - vec2f(xi, yi)) * vec2f(1.0, 1.12);\n inside = inside || dot(q, q) < ri * ri;\n }\n return inside;\n}\n\n// The whole sky at one point, in cells. Rows of clouds, each row scrolled by\n// whole cells at its own speed — the parallax is the animation. A cloud can\n// be wider than its lattice slot, so the two neighbouring slots get a say.\nfn cloudAt(cell : vec2f, tt : f32, cover : f32, size : f32) -> bool {\n let rowH = 30.0 * size;\n let colW = 40.0 * size;\n\n let row = floor(cell.y / rowH);\n let drift = floor(tt * (2.0 + st_hash11(row * 3.7) * 3.0));\n let x = cell.x + drift;\n let column = floor(x / colW);\n\n for (var k = -1; k <= 1; k = k + 1) {\n let c = column + f32(k);\n let rnd = st_hash22(vec2f(c * 1.31, row * 2.17));\n if (rnd.x > cover) { continue; }\n let anchor = vec2f(\n (c + 0.5) * colW + (rnd.x - 0.5) * colW * 0.7,\n (row + 0.74 + (rnd.y - 0.5) * 0.25) * rowH,\n );\n if (cloudSprite(vec2f(x, cell.y) - anchor, rnd, size)) {\n return true;\n }\n }\n return false;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n\n let tt = floor(st_time() * 8.0) / 8.0;\n\n // The sky is one flat colour. The reference for this whole shader is a\n // sprite sheet, and sprite sheets do not own gradients.\n var col = params.colorA.rgb;\n\n let cover = mix(0.35, 1.0, clamp(params.b, 0.0, 1.5) / 1.5);\n let size = clamp(params.c, 0.4, 2.0);\n\n if (cloudAt(cell, tt, cover, size)) {\n col = params.colorB.rgb;\n // The shade band: open air a step down means this cell is a bottom\n // edge — of the slab, or of a bump overhanging a crevice.\n if (!cloudAt(cell + vec2f(0.0, max(2.2 * size, 1.0)), tt, cover, size)) {\n col = params.colorC.rgb;\n } else if (!cloudAt(cell + vec2f(-1.8 * size, 1.2 * size), tt, cover, size) && params.d > 0.5) {\n // A softer second rule catches the west-facing hollows.\n col = mix(params.colorB.rgb, params.colorC.rgb, 0.5);\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n",
60
61
  "pixel-fire": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-fire\n// ---------------------------------------------------------------------------\n\n// Pixel Fire — the Doom fire look, computed stateless.\n//\n// The 1996 original was a feedback automaton: heat copied upward, minus a\n// random decay. One pass has no history, so this reproduces its statistics\n// instead: a heat ramp from the bottom edge, turbulence that grows with\n// height (solid slab below, shredded tongues above), and per-cell decay\n// jitter re-rolled at a low frame rate — that flicker grain is the\n// automaton's signature.\n\nfn bayer4(cell : vec2f) -> f32 {\n let x = i32(cell.x) & 3;\n let y = i32(cell.y) & 3;\n var index = 0;\n for (var i = 0u; i < 2u; i = i + 1u) {\n let shift = 1u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return (f32(index) + 0.5) / 16.0;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n\n let tt = floor(st_time() * 9.0) / 9.0;\n\n // Hot at the bottom edge (uv.y grows downward), tapering into tongues.\n let rise = 1.0 - puv.y;\n var heat = (puv.y * 1.3 - 0.22) * params.b;\n heat = heat - abs(puv.x - 0.5) * st_aspect() * 0.4 * rise;\n\n // Upward advection: time is added to the noise's y, so features climb.\n // Fire never scrolls sideways — the only x motion is a gentle lick.\n let q = vec2f(puv.x * 6.0 * st_aspect(), puv.y * 8.0) * params.scale;\n let lick = st_fbm2(q * 1.6 + vec2f(0.0, tt * 2.0), 3);\n let w = st_fbm2(q + vec2f((lick - 0.5) * 2.4, tt * 3.2), 4);\n heat = heat + (w - 0.5) * (0.3 + 1.1 * rise);\n\n // The per-cell decay the automaton had, re-rolled ~9 times a second.\n heat = heat - st_hash21(cell + floor(tt * 9.0)) * 0.13 * params.d;\n\n // Six palette stops, dithered at the seams. Everything below the lowest\n // stop is the room the fire lights, not black.\n let stops = 6.0;\n let idx = clamp(floor(heat * stops + (bayer4(cell) - 0.5)), 0.0, stops - 1.0);\n var col = st_palette(idx / (stops - 1.0));\n if (idx < 0.5) {\n col = params.colorA.rgb * 0.4;\n }\n\n // Embers: rare lit cells detaching above the flame top, one cell at a time.\n let ember = st_hash21(vec2f(cell.x, cell.y + floor(tt * 9.0)) * 1.37);\n if (heat > -0.3 && heat < 0.02 && ember > 1.0 - 0.01 * params.c) {\n col = st_palette(0.85);\n }\n\n return st_post(col * params.intensity, uv);\n}\n",
61
- "pixel-isle": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-isle\n// ---------------------------------------------------------------------------\n\n// Pixel Isle — a procedural island drawn as pixel art, one biome per cell.\n//\n// Height is fbm minus a radial falloff. Subtracting rather than multiplying\n// lets noise push land right up to the rim, which is what keeps the\n// coastline ragged. Every cell then makes a hard choice of biome — the only\n// softness anywhere is the Bayer screen jittering each threshold, which is\n// exactly how hand-made pixel maps dither their borders.\n//\n// Nine biome stops are derived from the three colour wells, hue-shifted the\n// way a pixel artist would shift them: darker steps lean cold, lighter steps\n// lean warm. The registry ships curated preset trios so Randomise stays on\n// dry land, and every well can still be overridden.\n\nfn bayer4(cell : vec2f) -> f32 {\n let x = i32(cell.x) & 3;\n let y = i32(cell.y) & 3;\n var index = 0;\n for (var i = 0u; i < 2u; i = i + 1u) {\n let shift = 1u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return (f32(index) + 0.5) / 16.0;\n}\n\n// Stops: 0 deep, 1 mid, 2 shallow, 3 sand, 4 wet sand, 5 grass, 6 forest,\n// 7 rock, 8 snow. A function-scope var so the stop index can be dynamic.\nfn isleRamp() -> array<vec3f, 9> {\n let water = params.colorA.rgb;\n let green = params.colorB.rgb;\n let sand = params.colorC.rgb;\n var c : array<vec3f, 9>;\n c[0] = water * vec3f(0.30, 0.36, 0.54);\n c[1] = water * vec3f(0.62, 0.68, 0.84);\n c[2] = mix(water, sand, 0.38) * 1.1;\n c[3] = sand;\n c[4] = sand * vec3f(0.72, 0.71, 0.66);\n c[5] = green;\n c[6] = green * vec3f(0.46, 0.56, 0.52);\n c[7] = mix(green, vec3f(st_luma(green) + 0.28), 0.78);\n c[8] = mix(vec3f(0.93, 0.92, 0.88), sand, 0.18);\n return c;\n}\n\nfn isleHeight(p : vec2f, seed : vec2f) -> f32 {\n let drift = vec2f(\n st_fbm2(p * 1.7 + seed, 3),\n st_fbm2(p * 1.7 + seed + vec2f(4.7, 9.2), 3),\n );\n let warped = p + (drift - vec2f(0.48)) * params.warp;\n\n // The seed also decides how many islands there are — one big one, or an\n // archipelago of two or three. Land grows wherever the nearest falloff\n // allows it, so neighbours sometimes fuse into one ragged landmass.\n let count = 1 + i32(floor(st_hash11(seed.x * 0.173 + 0.31) * 3.0));\n let extent = vec2f(st_aspect(), 1.0) * params.scale * 1.2;\n var fall = 1.0;\n for (var i = 0; i < 3; i = i + 1) {\n if (i >= count) { break; }\n let rc = st_hash22(seed + vec2f(f32(i) * 7.7, f32(i) * 3.1));\n // Centres stay well inside the frame — the falloff needs room to land.\n let spread = select(vec2f(0.25), vec2f(0.72), count > 1);\n let center = (rc - vec2f(0.5)) * extent * spread;\n let radius = mix(0.8, 1.1, st_hash11(seed.y + f32(i) * 5.9)) * mix(1.0, 0.68, f32(count - 1) * 0.5);\n fall = min(fall, smoothstep(0.1, 1.0, length(p - center) / radius));\n }\n // The noise fades with the falloff: raggedness belongs to coastlines, not\n // to a field pretending to be shoals in the middle of nowhere.\n return st_fbm2(warped * 1.5 + seed, 5) * (1.0 - fall * 0.5) - fall * 0.72;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n let cellsY = params.resolution.y / px;\n\n let tt = floor(st_time() * 8.0) / 8.0;\n\n let ramp = isleRamp();\n\n let seed = vec2f(floor(params.d) * 13.71, floor(params.d) * 7.31);\n let p = (puv - vec2f(0.5)) * vec2f(st_aspect(), 1.0) * params.scale * 2.4;\n\n let bias = mix(-0.02, 0.28, params.b);\n let h = isleHeight(p, seed) - bias;\n let e = h + (bayer4(cell) - 0.5) * 0.04;\n\n // One extra height read buys sun-from-the-northwest relief: a cell whose\n // up-left neighbour sits higher is in that neighbour's shade.\n let step2 = params.scale * 2.4 / cellsY;\n let upLeft = isleHeight(p - vec2f(step2, step2), seed) - bias;\n\n var col : vec3f;\n if (e < 0.0) {\n // Three water stops, darkest furthest out.\n col = ramp[2];\n if (e < -0.26) {\n col = ramp[0];\n } else if (e < -0.11) {\n col = ramp[1];\n }\n\n // Waves march on the coast: h is a distance proxy, so bands of constant\n // h are rings around the island, and adding time walks them shoreward.\n // Each ring thins as it dies against deeper water.\n let churn = st_noise2(p * 6.0 + vec2f(tt * 0.7, 0.0));\n let ring = fract(h * 16.0 + churn * 0.8 - tt * 0.45);\n let due = smoothstep(-0.17, -0.02, e);\n if (e > -0.17 && ring > 1.0 - 0.28 * due * min(params.c, 1.4)) {\n col = ramp[2];\n }\n\n // The foam line itself, breathing on the sprite clock.\n let band = 0.028 * params.c * (0.6 + 0.4 * sin(tt * 1.6 + churn * 4.0));\n if (e > -band) {\n col = vec3f(0.93, 0.95, 0.92);\n }\n } else {\n // Land bands: sand, grass, forest, rock, snow.\n col = ramp[3];\n if (e > 0.40) {\n col = ramp[8];\n } else if (e > 0.29) {\n col = ramp[7];\n } else if (e > 0.16) {\n col = ramp[6];\n } else if (e > 0.035) {\n col = ramp[5];\n } else if (e < 0.012 * (0.5 + 0.5 * sin(tt * 1.6))) {\n // The tide wets the lowest row of sand as the foam band swells.\n col = ramp[4];\n }\n if (upLeft > h + 0.015 && e > 0.035) {\n col = col * 0.8;\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n",
62
+ "pixel-isle": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-isle\n// ---------------------------------------------------------------------------\n\n// All shapes and textures share one pixel grid; only the surf and sea move.\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n // Keep geography round while using the available width for the layout.\n let extent = params.resolution / min(params.resolution.x, params.resolution.y);\n let p = (puv - 0.5) * extent * params.scale * 2.4;\n let seed = vec2f(floor(params.d) * 13.71, floor(params.d) * 7.31);\n let t = floor(st_time() * 8.0) / 8.0;\n let terrain = isleTerrain(p, seed, extent);\n let h = terrain.x - mix(-0.08, 0.22, params.b);\n let fleck = st_hash21(cell + seed);\n let edge = h + (fleck - 0.5) * 0.009;\n let water = params.colorA.rgb;\n let grass = params.colorB.rgb;\n let sand = params.colorC.rgb;\n let foam = mix(sand, vec3f(1.0), 0.85);\n let lagoon = mix(water * vec3f(0.9, 1.9, 1.15), sand, 0.28);\n var col = water;\n\n if (edge < 0.0) {\n if (edge > -0.065) {\n col = mix(lagoon, sand, 0.16);\n } else if (edge > -0.13) {\n col = lagoon;\n } else if (edge > -0.21) {\n col = mix(water, lagoon, 0.52);\n }\n // Short staggered dashes give open water a scale without full-screen noise.\n let wavePos = p * vec2f(65.0, 95.0) + vec2f(t * 0.55, 0.0);\n let row = floor(wavePos.y / 5.0);\n let waveCell = floor(vec2f((wavePos.x + row * 3.7) / 9.0, row));\n let wave = fract(vec2f((wavePos.x + row * 3.7) / 9.0, wavePos.y / 5.0));\n let chance = st_hash21(waveCell + seed);\n if (edge < -0.21 && chance > 0.60 && wave.x < 0.20 + chance * 0.20 && wave.y < 0.14) {\n col = mix(water, lagoon, 0.22 + 0.24 * chance);\n }\n let churn = st_noise2(p * 14.0 + seed + vec2f(t * 0.15, 0.0));\n let swell = sin(t * 1.5 + churn * 4.0);\n if (h > -0.014 * params.c * (1.0 + 0.3 * swell)) {\n col = foam;\n }\n let ring = fract(-h * 11.0 - t * 0.23);\n if (h < -0.035 && h > -0.22 && ring < 0.065 * params.c && churn > 0.48) {\n col = mix(foam, lagoon, 0.15);\n }\n } else {\n col = sand;\n if (edge < 0.013) {\n col = mix(sand, lagoon, 0.18);\n } else if (edge < 0.082) {\n if (fleck > 0.88) { col = sand * vec3f(0.89, 0.87, 0.79); }\n } else {\n let mottle = st_fbm2(p * 6.0 + seed + 21.0, 3);\n col = grass;\n if (mottle + h * 0.28 > 0.60) { col = grass * vec3f(0.77, 0.88, 0.72); }\n if (mottle + h * 0.24 > 0.72) { col = grass * vec3f(0.58, 0.74, 0.64); }\n let tufts = st_noise2(p * 42.0 + seed);\n if (tufts > 0.70 && fleck > 0.48) {\n col = col * vec3f(0.68, 0.80, 0.65);\n } else if (fleck > 0.96) {\n col = mix(col, sand, 0.22);\n }\n }\n }\n\n // Boulders sit in the grass and shallows, with larger groups inland.\n if (h > -0.14) {\n col = isleRocks(terrain.yz, seed + terrain.w * 19.7, h, col);\n }\n return st_post(col * params.intensity, uv);\n}\n\n// The closest coast owns the rock field too, so each island has its own ridge.\nfn isleTerrain(p : vec2f, seed : vec2f, extent : vec2f) -> vec4f {\n let count = 1 + i32(floor(params.d)) % 4;\n let phase = st_hash21(seed + 3.1) * TAU;\n let spread = max(extent - 0.4, vec2f(0.6)) * 0.86;\n var terrain = vec4f(-100.0, 0.0, 0.0, 0.0);\n for (var i = 0; i < 4; i = i + 1) {\n if (i >= count) { break; }\n let key = seed + f32(i) * 19.7;\n let random = st_hash22(key + 5.3);\n let angle = phase + f32(i) * TAU / f32(count);\n var center = vec2f(cos(angle), sin(angle)) * spread;\n center += (random - 0.5) * 0.28;\n var radius = mix(0.33, 0.56, random.x);\n if (count == 1) {\n radius = mix(0.60, 0.85, random.x);\n center = (random - 0.5) * max(extent * 1.8 - 1.1, vec2f(0.3));\n } else if (count == 2) {\n radius += 0.10;\n }\n let local = p - center;\n let q = st_rot(random.y * TAU) * local;\n let shape = q * vec2f(mix(0.8, 1.35, random.y), mix(0.85, 1.15, random.x));\n let a = atan2(shape.y, shape.x);\n let lobes = sin(a * 3.0 + phase + f32(i)) * 0.13\n + sin(a * 5.0 - phase) * 0.08;\n let coast = (st_fbm2(shape * 4.0 + key, 4) - 0.45) * 0.24;\n let h = radius - length(shape)\n + (lobes * radius + coast) * (0.45 + params.warp);\n if (h > terrain.x) { terrain = vec4f(h, local, f32(i)); }\n }\n return terrain;\n}\n\n// Nine nearby cells cover the largest rock and its shadow.\n// The same polygon, shifted southeast, casts its shadow before the stone.\nfn isleRocks(p : vec2f, seed : vec2f, h : f32, ground : vec3f) -> vec3f {\n let grid = p * 10.0;\n let baseCell = floor(grid);\n let local = fract(grid);\n let stone = mix(params.colorB.rgb, vec3f(st_luma(params.colorC.rgb) * 0.79), 0.82);\n var col = ground;\n var top = -10.0;\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let offset = vec2f(f32(x), f32(y));\n let id = baseCell + offset;\n let random = st_hash22(id + seed);\n let center = offset + 0.25 + random * 0.5;\n let rockPos = (baseCell + center) / 10.0;\n let ridge = length((rockPos - vec2f(-0.04, 0.02)) * vec2f(1.3, 0.85));\n let cluster = 1.0 - smoothstep(0.09, 0.32, ridge);\n let keep = st_hash21(id + seed + 41.0);\n if (keep > mix(0.055, 0.88, cluster)) { continue; }\n let radius = mix(0.15, 0.29, random.x) + cluster * 0.38;\n let q = (local - center) / vec2f(radius, radius * 0.85);\n let shadow = q - vec2f(0.45, 0.55);\n let shadowDist = max(max(abs(shadow.x), abs(shadow.y)), (abs(shadow.x) + abs(shadow.y)) * 0.72);\n if (shadowDist < 1.05 && top < -9.0) { col = ground * 0.72; }\n let dist = max(max(abs(q.x), abs(q.y)), (abs(q.x) + abs(q.y)) * 0.72);\n let height = (1.0 - dist) * radius;\n if (dist < 1.0 && height > top) {\n top = height;\n let light = -q.x * 0.45 - q.y * 0.55;\n col = stone * 0.67;\n if (light > -0.32) { col = stone * 0.87; }\n if (light > 0.12) { col = stone * 1.08; }\n if (light > 0.48) { col = mix(stone, params.colorC.rgb, 0.34); }\n if (st_hash21(floor(grid * 5.0) + seed) > 0.80) { col *= 0.82; }\n if (h < 0.0) { col = mix(col, ground, 0.44); }\n }\n }\n }\n return col;\n}\n",
62
63
  "pixel-rain": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-rain\n// ---------------------------------------------------------------------------\n\n// Pixel Rain — rain over a sleeping skyline, drawn on a virtual pixel grid.\n//\n// Three flat bands of night sky under a moon and a few fixed stars, two\n// ranks of hashed buildings — the far rank hazed, the near rank black with\n// a sparse lattice of windows whose lights toggle rarely — and per-column\n// raindrops that fall by whole cells per tick onto a shimmering street.\n// Nothing is blended; every pixel is one of a dozen stops.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n let cells = params.resolution / px;\n\n let tt = floor(st_time() * 10.0) / 10.0;\n\n // Night sky: three flat bands, faintest at the top.\n var col = params.colorA.rgb * 0.5;\n if (puv.y > 0.55) {\n col = params.colorA.rgb;\n } else if (puv.y > 0.3) {\n col = params.colorA.rgb * 0.72;\n }\n\n // A few fixed stars, blinking very slowly. Hash on the cell, so they sit\n // still while the rain falls through them.\n let star = st_hash21(cell * 0.73);\n if (star > 0.9975 && puv.y < 0.45) {\n col = mix(params.colorA.rgb, vec3f(0.9), 0.35 + 0.3 * step(0.5, fract(star * 100.0 + tt * 0.2)));\n }\n\n // The moon: a hard disc, one shade, one shadow cell ring.\n let moonPos = vec2f(0.30 * st_aspect(), -0.30);\n let moonDist = length((puv - vec2f(0.5)) * vec2f(st_aspect(), 1.0) - moonPos);\n if (moonDist < 0.055) {\n col = vec3f(0.87, 0.89, 0.90);\n // A crescent: the shadow side takes the sky back.\n if (length((puv - vec2f(0.5)) * vec2f(st_aspect(), 1.0) - moonPos - vec2f(0.022, -0.012)) < 0.048) {\n col = params.colorA.rgb * 0.72;\n }\n }\n\n // Two ranks of buildings. Blocks are hashed by column group; the far rank\n // is drawn first and hazed towards the sky.\n var nearTop = 2.0; // the near roofline this pixel falls under, for antennas\n for (var layer = 0; layer < 2; layer = layer + 1) {\n let fl = f32(layer); // 0 far, 1 near\n let blockW = floor(mix(5.0, 8.0, fl));\n let block = floor((cell.x + fl * 37.0) / blockW);\n let rand = st_hash11(block + fl * 101.0);\n let topRow = cells.y * (mix(0.34, 0.52, fl) + rand * 0.28);\n\n if (cell.y > topRow) {\n col = mix(params.colorA.rgb * 0.38, params.colorA.rgb * 0.10, fl);\n\n // Windows: a sparse lattice, dim on the far rank, lit on the near.\n let wx = i32(cell.x) % 3;\n let wy = i32(cell.y) % 4;\n if (wx == 1 && wy == 2 && cell.y > topRow + 2.0) {\n let room = vec2f(floor(cell.x / 3.0), floor(cell.y / 4.0) + fl * 53.0);\n // A room's light toggles rarely — someone getting up, not a strobe.\n let awake = st_hash21(room + floor(tt * 0.25) * 5.0);\n if (awake > 1.0 - 0.3 * params.c) {\n col = mix(params.colorC.rgb * 0.4, params.colorC.rgb, fl);\n }\n }\n }\n if (layer == 1) {\n nearTop = topRow;\n\n // An antenna on the taller blocks: a one-cell mast with a slow red\n // blink at the tip.\n let mastX = block * blockW + floor(blockW * 0.5);\n if (rand > 0.62 && cell.x == mastX && cell.y <= topRow && cell.y > topRow - 4.0) {\n col = params.colorA.rgb * 0.16;\n if (cell.y > topRow - 2.0 && fract(tt * 0.5 + rand) < 0.25) {\n col = vec3f(0.85, 0.25, 0.25);\n }\n }\n }\n }\n\n // Rain: each sheared column owns a few drops, falling whole cells per\n // tick — a bright head cell trailing a dimmer one. The shear is the wind.\n let column = floor(cell.x + cell.y * params.d);\n for (var drop = 0; drop < 2; drop = drop + 1) {\n let rand = st_hash11(column * 2.7 + f32(drop) * 17.9);\n if (rand < clamp(params.b, 0.0, 1.0)) {\n let speed = 1.6 + rand * 1.4;\n let head = fract(rand * 7.13 + f32(drop) * 0.41 - tt * speed);\n let rows = fract(puv.y - head);\n if (rows < 1.2 / cells.y) {\n col = mix(params.colorB.rgb, vec3f(1.0), 0.25);\n } else if (rows < 3.0 / cells.y) {\n col = mix(col, params.colorB.rgb, 0.75);\n }\n }\n }\n\n // The street: a wet band at the foot of the frame, shimmering where drops\n // land. The shimmer re-rolls per tick, which reads as splashes.\n if (cell.y > cells.y - 3.0) {\n col = params.colorA.rgb * 0.55;\n let splash = st_hash21(vec2f(cell.x, floor(tt * 10.0)));\n if (splash > 1.0 - 0.14 * params.b) {\n col = mix(params.colorB.rgb, vec3f(1.0), 0.35);\n }\n // The city's light pools faintly on the wet asphalt.\n let room = st_hash11(floor(cell.x / 3.0));\n if (room > 0.55 && i32(cell.x) % 3 == 1) {\n col = mix(col, params.colorC.rgb, 0.22 * params.c);\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n",
63
64
  "pixel-sea": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// pixel-sea\n// ---------------------------------------------------------------------------\n\n// Pixel Sea — a 16-bit sea: dithered sky, low sun, and its path on the water.\n//\n// A proper seascape rather than abstract bands: horizon in the upper third,\n// perspective rows that pack tighter as they recede, and the sun's\n// reflection breaking into dashes on the swell. Everything is sampled at\n// cell centres and quantised to stops derived from the colour wells; the\n// clock ticks at a sprite-sheet rate so nothing moves by a fraction of a cell.\n\nfn bayer4(cell : vec2f) -> f32 {\n let x = i32(cell.x) & 3;\n let y = i32(cell.y) & 3;\n var index = 0;\n for (var i = 0u; i < 2u; i = i + 1u) {\n let shift = 1u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return (f32(index) + 0.5) / 16.0;\n}\n\n// Stops: 0 sky top, 1 sky horizon, 2 sun, 3 deep, 4 mid, 5 light, 6 glint —\n// derived from the three wells: colorA the sky, colorB the water, colorC the\n// sun, with the horizon warmed toward the sun the way haze warms it.\nfn seaRamp() -> array<vec3f, 7> {\n let sky = params.colorA.rgb;\n let water = params.colorB.rgb;\n let sun = params.colorC.rgb;\n var c : array<vec3f, 7>;\n c[0] = sky;\n c[1] = mix(sky, sun, 0.5) * 1.08;\n c[2] = sun;\n c[3] = water * vec3f(0.40, 0.44, 0.60);\n c[4] = water * vec3f(0.76, 0.79, 0.94);\n c[5] = mix(water, c[1], 0.42);\n c[6] = mix(sun, vec3f(1.0), 0.55);\n return c;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n\n let tt = floor(st_time() * 8.0) / 8.0;\n let ramp = seaRamp();\n\n let horizon = 0.34;\n let aspect = st_aspect();\n let sunX = 0.5;\n let threshold = bayer4(cell) - 0.5;\n\n var col : vec3f;\n if (puv.y < horizon) {\n // Sky: dithered bands, brightest at the horizon.\n let skyT = puv.y / horizon;\n let band = clamp(floor(skyT * 4.0 + 0.5 + threshold * params.d), 0.0, 4.0);\n col = mix(ramp[0], ramp[1], band / 4.0);\n\n // The sun sits just above the water, a hard disc with a dithered halo.\n let sd = length((puv - vec2f(sunX, horizon - 0.09)) * vec2f(aspect, 1.0));\n if (sd < 0.055 * params.c) {\n col = ramp[2];\n } else if (sd < 0.055 * params.c * (1.0 + bayer4(cell + vec2f(1.0, 3.0)))) {\n col = mix(col, ramp[2], 0.8);\n }\n } else {\n // The sea. Distance from the horizon is the perspective proxy: rows\n // pack tighter and waves get finer as dist shrinks.\n let dist = puv.y - horizon;\n let z = 0.05 / max(dist, 0.004);\n let xw = (puv.x - 0.5) * aspect * z;\n\n // Each row of cells shears on its own phase — the displaced-scanline\n // trick — and a wave field decides which cells catch light.\n let shear = sin(cell.y * 0.9 + tt * 1.2) * (1.5 * px / params.resolution.x);\n let wave = st_noise2(vec2f((xw + shear * z * 40.0) * 2.4 * params.b, z * 1.7 * params.b - tt * 0.55));\n\n // Base water: three stops, lighter toward the horizon.\n let depthBand = clamp(floor(smoothstep(0.66, 0.0, dist) * 2.0 + 0.5 + threshold * params.d), 0.0, 2.0);\n col = ramp[u32(depthBand) + 3u];\n\n // Wave rows: cells the swell lifts take the next stop up.\n if (wave > 0.62 + threshold * 0.14) {\n col = ramp[u32(min(depthBand + 1.0, 2.0)) + 3u];\n }\n\n // The sun's path: a column that widens toward the viewer, its light\n // broken into dashes by the same swell.\n let halfWidth = 0.015 + 0.06 * dist;\n let inPath = abs(puv.x - sunX) / aspect < halfWidth * (0.7 + 0.6 * bayer4(cell + vec2f(2.0, 1.0)));\n if (inPath && wave > 0.52 + dist * 0.45 + threshold * 0.1) {\n col = ramp[2];\n }\n\n // Rare single-cell glints outside the path, re-rolled slowly.\n let glint = st_hash21(cell + floor(tt * 5.0) * 7.0);\n if (glint > 0.9965 && wave > 0.55) {\n col = ramp[6];\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n",
64
65
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
@@ -69,10 +70,10 @@ export const shaderSources = {
69
70
  "shafts": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// shadertown glow prelude — prepended (after the shared and graph preludes)\n// to every shader in src/shaders/graph/glow/.\n//\n// The simplest flavour: one full-screen scene pass into the HDR target, then\n// the shared bloom chain and the present pass. It exists for the shaders that\n// are light on dark — threads, beams, scanned bands — where a real bloom is\n// the difference between a drawing of light and light. The variant file below\n// this prelude supplies exactly one function:\n//\n// fn st_scene(uv : vec2f) -> vec4f\n//\n// `uv` is the canvas uv; the return value is linear-light colour in rgb, and\n// in w how much of the pixel is light the chain may bleed. The present pass\n// converts to sRGB on the way out, so wells go through `st_toLinear` first.\n\n@fragment fn fs_scene(@location(0) uv : vec2f) -> @location(0) vec4f {\n return st_scene(uv);\n}\n\n// ---------------------------------------------------------------------------\n// shafts\n// ---------------------------------------------------------------------------\n\n// Shafts — god rays from a source just above the frame, and the hand in them.\n//\n// Crepuscular rays are a pattern in angle, not in position: every shaft is a\n// line from the source, and what varies is how much light each bearing\n// carries. So the light is three octaves of noise over the angle from the\n// source, each drifting at its own rate, carved by a soft threshold into a\n// handful of broad beams with finer rays between. A cone about the axis\n// keeps the light where the source points, the shafts fade with distance and\n// fall to dark at the foot of the frame, and dust in the air breaks them up a\n// little. The pointer is a thing held in the light: a disc that blocks the\n// beams and throws a shadow away from the source, its edge softening the\n// further the shadow falls. Bloomed, so the beams bleed into each other the\n// way light through haze does.\n// colorA the light near the source, colorB what it fades to, colorC the sky\n// a spread, b length, c density, d pulse; warp is the size of the occluder\n\n/**\n * Light carried on one bearing, in glow-friendly units.\n *\n * `density` scales the angular frequency: at 1 a cone of light holds a\n * handful of broad beams, the way a stage light through haze does, and finer\n * rays only show faintly between them.\n */\nfn shaftBeams(a : f32, t : f32, density : f32) -> vec2f {\n let k = density;\n let n1 = st_noise2(vec2f(a * 5.5 * k, t * 0.08));\n let n2 = st_noise2(vec2f(a * 13.0 * k + 7.3, t * 0.13 + 3.1));\n let n3 = st_noise2(vec2f(a * 40.0 * k + 19.0, t * 0.20 + 9.7));\n // Broad beams: soft-edged, few. Fine rays: the texture between them.\n let broad = smoothstep(0.50, 0.92, n1 * 0.60 + n2 * 0.30 + n3 * 0.10);\n let fine = smoothstep(0.45, 0.88, n2 * 0.55 + n3 * 0.45);\n return vec2f(broad, fine);\n}\n\nfn st_scene(uv : vec2f) -> vec4f {\n let res = params.resolution;\n let t = st_time();\n let coord = uv * res;\n\n // The source sits just above the top edge, in the middle, pointing down.\n let source = vec2f(res.x * 0.5, -res.y * 0.08);\n let axis = vec2f(0.0, 1.0);\n\n let spread = clamp(params.a, 0.2, 3.0);\n let reach = clamp(params.b, 0.5, 4.0);\n let density = clamp(params.c, 0.4, 2.5);\n let pulse = clamp(params.d, 0.0, 1.0);\n\n let toCoord = coord - source;\n let dist = length(toCoord);\n let dir = toCoord / max(dist, 1e-4);\n\n // The bearing of this pixel from the source, measured from the axis, bent a\n // little along the ray so the shafts sway rather than point.\n var a = atan2(axis.x * dir.y - axis.y * dir.x, dot(axis, dir));\n a = a + 0.02 * sin(t * 0.7 + dist * 0.004);\n\n // The beams: a handful of broad soft shafts, and the finer rays between.\n let beams = shaftBeams(a, t, density);\n let streaks = beams.x * 0.85 + beams.y * 0.12;\n\n // The cone the source lights, and how far along it the light carries.\n let cone = pow(max(cos(a), 0.0), 1.0 / max(spread, 0.05));\n let reachPx = res.x * reach;\n let carry = smoothstep(reachPx, reachPx * 0.08, dist);\n let core = 1.0 + 1.1 * exp(-dist / (res.y * 0.4));\n let beat = 1.0 - pulse * 0.25 * (0.5 - 0.5 * sin(t * 2.4));\n\n // The hand in the light: a disc hung from the pointer. Its near edge sits\n // on the pointer itself, so the shadow begins at the cursor's tip rather\n // than above it, and the disc's body trails away from the source. Everything\n // on a bearing through the disc and further from the source than it is in\n // shadow, with a penumbra that widens the further the shadow falls. The cap\n // fades in over most of the disc, so what the eye reads is the shadow\n // condensing under the cursor, not a shape that fails to match it. A\n // pointer that has never moved sits at the exact centre, and that one is\n // left out, so a card with no hand on it shows the light unblocked.\n let pointer = params.pointer;\n let moved = step(1e-5, length(pointer - 0.5));\n let tip = pointer * res - source;\n let tipDist = length(tip);\n let tipDir = tip / max(tipDist, 1e-4);\n let radius = res.y * 0.05 * clamp(params.warp, 0.0, 2.0);\n let handDist = tipDist + radius;\n let handA = atan2(axis.x * tipDir.y - axis.y * tipDir.x, dot(axis, tipDir));\n let halfWidth = atan(radius / max(handDist, 1.0));\n let penumbra = halfWidth * (0.3 + 0.6 * max(dist - handDist, 0.0) / max(handDist, 1.0));\n let inCone = 1.0 - smoothstep(halfWidth - penumbra, halfWidth + penumbra, abs(a - handA));\n let offset = handDist * tan(clamp(a - handA, -1.2, 1.2));\n let chord = sqrt(max(radius * radius - offset * offset, 0.0));\n let entry = handDist - chord;\n let behind = smoothstep(entry - radius * 0.1, entry + radius * 1.6, dist);\n let shadow = inCone * behind * moved;\n\n // Dust in the air: a slow, coarse mottle that the shafts pick out.\n let dust = 0.82 + 0.18 * st_noise2(coord * 0.0035 + vec2f(t * 0.03, -t * 0.02));\n\n // Light is strongest where it enters and thins toward the foot of the frame.\n let height = 1.0 - uv.y;\n let fall = 0.03 + 0.97 * height * height;\n\n let nearCol = st_toLinear(params.colorA.rgb);\n let farCol = st_toLinear(params.colorB.rgb);\n let sky = st_toLinear(params.colorC.rgb);\n let tint = mix(nearCol, farCol, smoothstep(0.0, reachPx * 0.7, dist));\n\n let blocked = 1.0 - shadow * 0.92;\n let shafts = streaks * cone * carry * core * beat * dust * fall * blocked;\n // The haze the source throws around itself, before the beams resolve.\n let haze = cone * exp(-dist / (res.y * 0.45)) * 0.16 * fall * blocked;\n\n let ground = sky * (0.5 + 0.5 * height * height);\n let col = (ground + tint * (shafts * 0.55 + haze)) * params.intensity;\n let lit = clamp(shafts * 0.5, 0.0, 1.0);\n return vec4f(col, lit);\n}\n",
70
71
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let taps = st_steps(12);\n var sum = 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;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / f32(taps + 1);\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",
71
72
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let taps = st_steps(12);\n var sum = 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;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / f32(taps + 1);\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",
72
- "shore": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// shore\n// ---------------------------------------------------------------------------\n\n// Shore — the swash zone from above: run-up, foam, and sand that remembers.\n//\n// One animated line drives everything. The waterline breathes in and out on\n// a cosine and is ragged from fbm — real swash is never straight — and the\n// wet sand is a second envelope lagging behind it, darkest at the water and\n// drying to nothing, which is as close to memory as a single pass gets.\n//\n// The wells are the hour: colorA the deep water, colorB the shallows, colorC\n// the sand. The registry ships preset trios — daylight, golden hour,\n// overcast, moonlit — so Randomise never turns the sand green.\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 let yUp = -p.y;\n\n // The shoreline runs along the frame; sand below, water above.\n let ragged = (st_fbm2(vec2f(p.x * 2.6, t * 0.22), 3) - 0.5) * 0.3 * params.warp;\n let breath = cos(t * 0.9) * 0.06 * params.a;\n let edge = -0.06 + breath + ragged;\n\n // d grows seaward: negative on the beach, positive in the water.\n let d = yUp - edge;\n\n // Dry sand, grained twice so it never reads as a fill.\n let grain = st_fbm2(p * 22.0, 3) * 0.5 + st_noise2(p * 90.0) * 0.5;\n var col = params.colorC.rgb * (0.9 + grain * 0.2);\n\n // Wet sand: a lagging envelope shoreward of the water. Darker, a little\n // saturated, with a whisper of sky sheen — wet, not shadowed.\n let wetEdge = edge - 0.02 - 0.05 * params.a * (0.5 + 0.5 * cos(t * 0.9 + 1.2));\n let wet = smoothstep(0.02, -0.02, yUp - wetEdge) * smoothstep(-0.3, -0.02, yUp - edge);\n let dry = smoothstep(0.0, 0.22, edge - yUp);\n let wetness = clamp(wet * (1.0 - dry * 0.8), 0.0, 1.0) * params.d;\n col = mix(col, params.colorC.rgb * 0.58 + params.colorB.rgb * 0.08, wetness);\n\n if (d > 0.0) {\n // Water deepens seaward on a Beer ramp.\n var sea = mix(params.colorB.rgb, params.colorA.rgb, 1.0 - exp(-d * 3.4));\n\n // Wave fronts ride toward the shore: a sawtooth raised to a power is a\n // sharp crest with an exponential wake. Two trains at different angles\n // interfere so no front arrives alone.\n let wob = st_fbm2(p * vec2f(3.0, 1.0) + vec2f(0.0, t * 0.1), 3);\n let phaseA = d * 5.0 * params.b + t * 0.55 + wob * 0.7;\n let phaseB = d * 3.4 * params.b + t * 0.4 + p.x * 0.5 + wob * 0.5;\n let front = pow(fract(-phaseA), 8.0) + 0.6 * pow(fract(-phaseB), 8.0);\n let approach = smoothstep(0.65, 0.1, d);\n sea = sea + vec3f(0.9, 0.96, 0.95) * front * approach * params.c * 0.7;\n\n // The foam arc at the waterline, eaten by noise into a granular lace.\n let lace = smoothstep(0.35, 0.75, st_fbm2(p * 18.0 + vec2f(t * 0.3, -t * 0.2), 3));\n let arc = smoothstep(0.05, 0.0, d) * lace;\n sea = mix(sea, vec3f(0.94, 0.97, 0.96), clamp(arc * params.c, 0.0, 1.0));\n\n // In the first hand's-width of water the sand still shows through.\n col = mix(col * 0.92, sea, smoothstep(0.0, 0.06, d) * 0.8 + 0.2);\n } else {\n // Dissolving foam patches left on the sand as the water retreats.\n let retreat = smoothstep(0.0, 0.6, sin(t * 0.9));\n let patches = smoothstep(0.68, 0.8, st_fbm2(p * 24.0 + vec2f(1.7, 4.2), 3));\n let strand = smoothstep(-0.05, 0.0, d) * patches * retreat;\n col = mix(col, vec3f(0.93, 0.95, 0.93), strand * params.c * 0.8);\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
73
+ "shore": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// shore\n// ---------------------------------------------------------------------------\n\n// Cellular gaps form foam bubbles; warped coordinates break the regular cells.\nfn foamCells(p : vec2f) -> f32 {\n let cell = floor(p);\n let local = fract(p);\n var first = 10.0;\n var second = 10.0;\n for (var y = -1; y <= 1; y += 1) {\n for (var x = -1; x <= 1; x += 1) {\n let offset = vec2f(f32(x), f32(y));\n let delta = offset + st_hash22(cell + offset) - local;\n let d = dot(delta, delta);\n if (d < first) { second = first; first = d; }\n else if (d < second) { second = d; }\n }\n }\n return sqrt(second) - sqrt(first);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_rot(-0.22) * st_centered(uv) * params.scale;\n let aa = max(params.scale / params.resolution.y, 0.0004);\n let coast = sin(p.x * 2.4 + 0.7) * 0.055 + (st_noise2(vec2f(p.x * 3.5, 3.1)) - 0.5) * 0.10 * params.warp;\n let runup = sin(t * 0.65) * 0.045 * params.a;\n let ragged = (st_fbm2(vec2f(p.x * 13.0, t * 0.12), 3) - 0.45) * 0.035 * params.warp;\n let d = -p.y - coast + 0.025 - runup - ragged;\n let sandNoise = st_noise2(p * 340.0);\n let sandRidges = sin(p.y * 155.0 + st_noise2(p * 12.0) * 7.0);\n let sand = params.colorC.rgb * (0.82 + sandNoise * 0.13 + sandRidges * 0.014);\n // The wet envelope reaches past the moving swash, keeping a drying margin.\n let wet = (1.0 - smoothstep(0.015, 0.13, -d)) * min(params.d, 1.6);\n var col = mix(sand, sand * 0.57 + params.colorB.rgb * 0.035, clamp(wet, 0.0, 1.0));\n let distort = vec2f(st_noise2(p * 17.0 + t * 0.06), st_noise2(p * 19.0 - t * 0.04));\n let bubbles = foamCells(p * 105.0 + distort * 2.2 + vec2f(0.0, t * 0.2));\n let lace = 1.0 - smoothstep(0.025, 0.10, bubbles);\n let holes = st_fbm2(p * 45.0 + vec2f(t * 0.08, 0.0), 3);\n\n if (d > -aa) {\n let depth = max(d, 0.0);\n let ripple = st_fbm2(p * vec2f(30.0, 65.0) + vec2f(t * 0.10, t * 0.14), 3);\n var sea = mix(params.colorB.rgb * 0.74, params.colorA.rgb * 0.63, 1.0 - exp(-depth * 3.0));\n sea *= 0.91 + ripple * 0.17;\n let caustic = pow(1.0 - abs(sin(p.x * 75.0 + distort.y * 12.0) * sin(p.y * 90.0 + distort.x * 9.0)), 18.0);\n sea += params.colorB.rgb * caustic * exp(-depth * 13.0) * 0.075;\n let phase = depth * 5.2 * params.b + t * 0.20 + st_noise2(p * 5.0) * 0.12;\n let wave = fract(phase);\n let breaker = exp(-pow((wave - 0.78) / 0.13, 2.0));\n let lip = exp(-pow((wave - 0.85) / 0.021, 2.0));\n let approach = 1.0 - smoothstep(0.30, 0.85, depth);\n // A darker trough and translucent shoulder give each front thickness.\n sea *= 1.0 - exp(-pow((wave - 0.90) / 0.042, 2.0)) * approach * 0.16;\n sea += params.colorB.rgb * breaker * approach * 0.09;\n let wash = exp(-depth * 38.0);\n let broken = smoothstep(0.28, 0.57, holes);\n let foam = clamp((breaker * (lace * 0.7 + broken * 0.35) + lip * 0.60) * approach\n + wash * (0.32 + lace * 0.5 + broken * 0.2), 0.0, 1.0) * params.c;\n sea = mix(sea, vec3f(0.90, 0.94, 0.90), clamp(foam, 0.0, 0.95));\n col = mix(col, sea, smoothstep(-aa, 0.014, d));\n } else {\n let remnant = exp(d * 70.0) * lace * smoothstep(0.46, 0.62, holes)\n * (0.5 + 0.5 * cos(t * 0.65)) * params.c;\n col = mix(col, vec3f(0.86, 0.88, 0.82), clamp(remnant, 0.0, 0.8));\n }\n return st_post(col * params.intensity, uv);\n}\n",
73
74
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
74
75
  "spectra": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the functions the flavour asks for:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n// glow one scene pass of light on dark, bloomed\n// studio sculpted meshes, ray-traced glass, depth of field and bloom\n// masonry a floor of slabs as instanced boxes, depth of field and bloom\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn st_toLinear(c : vec3f) -> vec3f {\n return pow(max(c, vec3f(0.0)), vec3f(2.2));\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n // The generator supplies an identity hook unless the flavour opts into\n // tone mapping. Existing emissive graphs retain their original response.\n let linear = max(st_graphTone(scene.rgb + bloom.rgb), vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\n/**\n * The bloom gate for a scene that says itself what is light: its alpha. A\n * pale mat or a bright ground would otherwise bloom as a whole.\n */\n@fragment fn fs_brightAlpha(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\nfn st_graphTone(c : vec3f) -> vec3f { return c; }\n\n// shadertown glow prelude — prepended (after the shared and graph preludes)\n// to every shader in src/shaders/graph/glow/.\n//\n// The simplest flavour: one full-screen scene pass into the HDR target, then\n// the shared bloom chain and the present pass. It exists for the shaders that\n// are light on dark — threads, beams, scanned bands — where a real bloom is\n// the difference between a drawing of light and light. The variant file below\n// this prelude supplies exactly one function:\n//\n// fn st_scene(uv : vec2f) -> vec4f\n//\n// `uv` is the canvas uv; the return value is linear-light colour in rgb, and\n// in w how much of the pixel is light the chain may bleed. The present pass\n// converts to sRGB on the way out, so wells go through `st_toLinear` first.\n\n@fragment fn fs_scene(@location(0) uv : vec2f) -> @location(0) vec4f {\n return st_scene(uv);\n}\n\n// ---------------------------------------------------------------------------\n// spectra\n// ---------------------------------------------------------------------------\n\n// Spectra — colour-split bands over a rippling field, swept by a slow pulse.\n//\n// A field of crossed sines bends a stack of horizontal bands, a pulse travels\n// down them and lights the ones it passes, and each colour channel reads the\n// band pattern a little offset from the others, so the edges fringe into\n// spectra the way a cheap prism does. Scanlines over the top; the pulse\n// tightens and brightens under the pointer.\n// colorA the dark end of the ramp, colorB the mid, colorC the peak\n// a band count, b sweep width, c sharpness, d split; warp is the ripple\n\nfn spectraField(p : vec2f, t : f32) -> f32 {\n var w = sin(p.x * 1.3 + t * 0.7);\n w = w + sin(p.y * 1.7 - t * 0.52) * 0.8;\n w = w + sin((p.x + p.y) * 0.9 + t * 0.91) * 0.6;\n w = w + sin((p.x - p.y) * 1.53 - t * 0.63) * 0.42;\n return w * 0.35;\n}\n\n/** The three-well ramp, with a little contrast pushed into it. */\nfn spectraRamp(f : f32, low : vec3f, mid : vec3f, peak : vec3f) -> vec3f {\n let x = pow(clamp(f, 0.0, 1.0), 1.15);\n let c = mix(low, mid, smoothstep(0.08, 0.6, x));\n return mix(c, peak, smoothstep(0.68, 1.0, x));\n}\n\n/** One band: a raised cosine sharpened by a power, softened by `aa`. */\nfn spectraBand(x : f32, aa : f32, sharp : f32) -> f32 {\n let v = mix(0.5, 0.5 + 0.5 * cos(x * TAU), aa);\n return pow(v, sharp);\n}\n\nfn st_scene(uv : vec2f) -> vec4f {\n let res = params.resolution;\n let aspect = st_aspect();\n // Centred, aspect-corrected, y up: the pulse travels down the frame.\n let uv0 = vec2f((uv.x * 2.0 - 1.0) * aspect, 1.0 - uv.y * 2.0);\n let p = uv0 / max(clamp(params.scale, 0.4, 2.5) * 1.5, 0.001);\n let t = st_time() * 0.5;\n\n // The hand: a soft boost of contrast and light around it.\n let pointer = params.pointer;\n let moved = step(1e-5, length(pointer - 0.5));\n let hand = vec2f((pointer.x * 2.0 - 1.0) * aspect, 1.0 - pointer.y * 2.0);\n let toHand = uv0 - hand;\n let boost = exp(-dot(toHand, toHand) / 0.25) * 0.5 * moved;\n\n let ripple = clamp(params.warp, 0.0, 1.0);\n let sig = spectraField(p * 2.0, t);\n let coord = p.y + sig * ripple;\n\n // The pulse lights what it passes; the rest of the stack keeps a low glow,\n // so a frame the pulse has left is a dim pattern rather than a black one.\n let sweepWidth = max(params.b, 0.05);\n let phase = coord / sweepWidth - t * 0.25;\n let pulse = pow(0.5 + 0.5 * cos(phase * TAU), 6.0);\n let sweep = mix(0.32, 1.0, pulse);\n\n let lc = coord * clamp(params.a, 3.0, 24.0);\n var aa = 1.0 / (1.0 + 1.4 * fwidth(lc) * 3.0);\n aa = clamp(aa * (1.0 + boost * 0.6), 0.0, 1.0);\n\n let bodyBase = clamp(0.5 + 0.5 * sig, 0.0, 1.0);\n let body = bodyBase * bodyBase * 0.22 * sweep;\n\n let sharp = max(params.c, 0.1);\n let split = clamp(params.d, 0.0, 1.5) * 0.16;\n let fr = clamp(spectraBand(lc + split, aa, sharp) * sweep + body, 0.0, 1.0);\n let fg = clamp(spectraBand(lc, aa, sharp) * sweep + body, 0.0, 1.0);\n let fb = clamp(spectraBand(lc - split, aa, sharp) * sweep + body, 0.0, 1.0);\n\n let low = st_toLinear(params.colorA.rgb);\n let mid = st_toLinear(params.colorB.rgb);\n let peak = st_toLinear(params.colorC.rgb);\n let col = vec3f(\n spectraRamp(fr, low, mid, peak).r,\n spectraRamp(fg, low, mid, peak).g,\n spectraRamp(fb, low, mid, peak).b,\n );\n\n var lit = (fr + fg + fb) * 0.3333333 * params.intensity * (1.0 + boost * 0.9);\n // Scanlines, one every few device pixels.\n lit = lit * (1.0 - 0.18 * (0.5 + 0.5 * cos(uv.y * res.y * 1.7)));\n lit = clamp(lit, 0.0, 1.0);\n\n return vec4f(col * lit, lit);\n}\n",
75
- "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
76
+ "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_rot(t * 0.05 * params.d) * st_centered(uv) * params.scale;\n var col = vec3f(0.0015, 0.0025, 0.007);\n\n // The distant sky turns with the camera but scarcely expands.\n col += distantStars(p, t);\n for (var i = 0; i < 6; i = i + 1) {\n col += starLayer(p, f32(i), t);\n }\n\n let core = exp(-dot(p, p) * 7.0) * params.c * 0.075;\n col += mix(params.colorA.rgb, params.colorB.rgb, 0.65) * core;\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n\n// Independent hashes keep adjacent depth slices from forming diagonal patterns.\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 starLayer(p : vec2f, index : f32, t : f32) -> vec3f {\n // Increasing depth expands the field toward the camera. Both ends fade\n // before the slice wraps, avoiding a flash when it returns to the distance.\n let depth = fract(index / 6.0 + t * 0.085);\n let scale = mix(22.0, 1.5, depth);\n let fade = smoothstep(0.0, 0.15, depth) * (1.0 - smoothstep(0.82, 1.0, depth));\n if (fade <= 0.0) { return vec3f(0.0); }\n let grid = st_rot(index * 1.37) * p * scale;\n let base = floor(grid);\n let local = fract(grid);\n var col = vec3f(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 = cellRandom(base + neighbour, index + 1.0);\n if (rnd.z < 0.77) { continue; }\n let point = neighbour + rnd.xy;\n let offset = local - point;\n // Offset and direction must be in the same rotated grid space.\n let position = base + point;\n let radial = position / max(length(position), 0.0001);\n let along = dot(offset, radial);\n let across = offset - radial * along;\n // Stars keep a round core. Only high Streak values build a long,\n // tapered wake behind it, toward the vanishing point.\n let radius = max(0.009 * params.a, scale * params.scale / params.resolution.y * 0.65);\n let d2 = dot(offset, offset);\n let core = exp(-d2 / (radius * radius));\n let halo = exp(-d2 / (radius * radius * 18.0)) * 0.10;\n let streak = params.b * params.b * params.b;\n let trailLength = min(0.65, streak * depth * depth * length(position) * 0.20);\n let behind = max(-along, 0.0);\n let taper = max(1.0 - behind / max(trailLength, 0.0001), 0.0);\n let trailWidth = radius * (0.35 + 0.4 * taper);\n let trail = exp(-dot(across, across) / (trailWidth * trailWidth))\n * taper * taper * smoothstep(0.0, radius, behind)\n * smoothstep(0.15, 0.85, params.b) * 0.65;\n let twinkle = 0.90 + 0.10 * sin(t * (0.6 + rnd.x) + rnd.y * TAU);\n let brightness = mix(0.35, 2.8, depth * depth) * mix(0.6, 1.4, rnd.x);\n let tint = mix(st_palette(rnd.y), vec3f(1.0), 0.48);\n col += tint * (core + halo + trail) * brightness * fade * twinkle;\n }\n }\n return col;\n}\n\nfn distantStars(p : vec2f, t : f32) -> vec3f {\n let grid = p * 95.0;\n let cell = floor(grid);\n let rnd = cellRandom(cell, 17.0);\n if (rnd.z < 0.86) { return vec3f(0.0); }\n // Keep points clear of cell borders so the small kernel never gets clipped.\n let offset = fract(grid) - mix(vec2f(0.2), vec2f(0.8), rnd.xy);\n let radius = max(0.022 * params.a, 95.0 * params.scale / params.resolution.y * 0.5);\n let star = exp(-dot(offset, offset) / (radius * radius));\n let shimmer = 0.8 + 0.2 * sin(t * 0.45 + rnd.x * TAU);\n return mix(params.colorA.rgb, params.colorB.rgb, rnd.y) * star * 0.24 * shimmer;\n}\n",
76
77
  "strata": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// strata\n// ---------------------------------------------------------------------------\n\n// Nested contour shelves share one field, so they cannot cross as they move.\nfn strata_field(p: vec2f, t: f32) -> f32 {\n let phase = params.d * 2.399963;\n let bend = vec2f(\n sin(p.y * 2.9 + t * 0.23 + phase) * 0.24 + sin(p.y * 5.1 - t * 0.13 + phase * 0.7) * 0.08,\n sin(p.x * 2.6 - t * 0.19 + phase * 1.3) * 0.22 + cos(p.x * 4.3 + t * 0.17 + phase * 0.9) * 0.09,\n );\n let q = p + bend * params.warp - vec2f(0.18, -0.08);\n return length(q * vec2f(0.85 / params.h, 1.12 * params.h))\n + sin(q.x * 3.4 + q.y * 2.1 + t * 0.11 + phase) * 0.075 * params.warp;\n}\n\n@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n let centered = st_centered(uv) - vec2f(params.e * st_aspect(), params.f);\n let p = st_rot(params.g * PI / 180.0) * centered * params.scale * 2.4;\n let field = strata_field(p, st_time());\n let gradient = vec2f(dpdx(field), dpdy(field));\n let normal = gradient / max(length(gradient), 0.00001);\n let light = clamp(dot(normal, normalize(vec2f(-0.6, -0.8))), -1.0, 1.0);\n let aa = max(fwidth(field), 0.0001);\n let spacing = 1.65 / params.a;\n let warm = 1.0 - smoothstep(-0.85, 0.65, p.x + p.y * 0.85);\n var col = params.colorA.rgb * 0.13;\n\n for (var i = 0; i < 12; i = i + 1) {\n if (f32(i) >= params.a) { break; }\n let level = f32(i);\n let edge = 0.24 + level * spacing;\n let d = field - edge;\n let mask = smoothstep(-aa, aa, d);\n let rise = smoothstep(0.0, spacing * 0.6, max(d, 0.0));\n let depth = 0.32 + 0.68 * (1.0 - exp(-level * 0.62));\n let cool = mix(params.colorA.rgb, params.colorB.rgb, 0.5 + 0.5 * sin(level * 0.95 - 1.1));\n let pink = vec3f(params.colorC.r, params.colorA.g * 0.7, params.colorA.b * 0.85);\n let innerWarm = mix(params.colorA.rgb, pink, smoothstep(0.0, 3.0, level));\n let hot = mix(innerWarm, params.colorC.rgb, smoothstep(3.0, 8.0, level));\n // Keep the palette's brightness through blends between opposing hues.\n let blend = mix(cool, hot, warm);\n let peak = max(blend.r, max(blend.g, blend.b));\n let brightness = mix(max(cool.r, max(cool.g, cool.b)), max(hot.r, max(hot.g, hot.b)), warm);\n let pigment = st_saturation(blend * brightness / max(peak, 0.001), 1.12) * depth;\n let shade = mix(1.0, 0.46 + 0.62 * rise + 0.12 * light, mix(0.65, 1.0, params.b));\n var shelf = pigment * shade;\n // A narrow lip and broad shoulder suggest a bevel without a ray march.\n let lip = exp(-max(d, 0.0) / max(aa, spacing * 0.025));\n let shoulder = exp(-max(d, 0.0) / (spacing * 0.22));\n shelf = shelf + (pigment + vec3f(0.15)) * (lip * 0.85 + shoulder * 0.32)\n * (0.7 + 0.3 * max(light, 0.0)) * params.c;\n col = mix(col, shelf, mask);\n }\n return st_post(col * params.intensity, uv);\n}\n",
77
78
  "swell": "// 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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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// swell\n// ---------------------------------------------------------------------------\n\n// Swell — open ocean marched as a heightfield, so waves hide waves.\n//\n// The surface is real: eight steps of interpolation search land the ray on a\n// displaced heightfield, which is what buys actual silhouettes — a crest in\n// front occluding the trough behind it — instead of a flat plane wearing\n// normal-map make-up. Each octave is a product of the two axes of a\n// noise-warped sine, sharpened by a choppiness power: sharp crests, round\n// troughs, and no criss-cross lattice. The march reads three octaves, the\n// normal five; nobody can tell.\n\nfn waveOctave(uv : vec2f, choppy : f32) -> f32 {\n let n = st_noise2(uv) * 2.0 - 1.0;\n let q = uv + vec2f(n, n);\n let wv = vec2f(1.0) - abs(sin(q));\n let sw = abs(cos(q));\n let m = mix(wv, sw, wv);\n return pow(1.0 - pow(m.x * m.y, 0.65), choppy);\n}\n\nfn seaHeight(p2 : vec2f, t : f32, octaves : i32) -> f32 {\n var freq = 0.16;\n var amp = 0.62 * params.b;\n var choppy = mix(1.2, 4.5, clamp(params.d, 0.0, 2.0) * 0.5);\n var uv = vec2f(p2.x * 0.75, p2.y);\n var h = 0.0;\n let count = st_steps(octaves);\n for (var i = 0; i < 5; i = i + 1) {\n if (i >= count) { break; }\n var d = waveOctave((uv + vec2f(t, t)) * freq, choppy);\n d = d + waveOctave((uv - vec2f(t, t)) * freq, choppy);\n h = h + d * amp;\n uv = mat2x2f(vec2f(1.6, -1.2), vec2f(1.2, 1.6)) * uv;\n freq = freq * 1.9;\n amp = amp * 0.22;\n choppy = mix(choppy, 1.0, 0.2);\n }\n return h;\n}\n\nfn skyColor(dir : f32, sunAmt : f32) -> vec3f {\n var sky = mix(mix(params.colorA.rgb, vec3f(0.9, 0.93, 0.95), 0.75), params.colorA.rgb * 1.15, smoothstep(-0.02, 0.5, dir));\n sky = mix(sky, params.colorC.rgb, pow(sunAmt, 4.0) * 0.5);\n return sky;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time() * 0.8;\n let p = st_centered(uv);\n\n let look = (params.pointer - vec2f(0.5)) * 0.15;\n\n let maxAmp = 1.35 * params.b;\n let ro = vec3f(0.0, maxAmp + 2.2, t * 1.2);\n let rd = normalize(vec3f(p.x + look.x * 0.2, -p.y * 0.8 - 0.14 + look.y * 0.15, 1.15));\n\n let sunDir = normalize(vec3f(0.15 - look.x * 0.2, 0.03 + 0.3 * params.a, 0.9));\n let sunAmt = clamp(dot(rd, sunDir), 0.0, 1.0);\n\n var sky = skyColor(rd.y, sunAmt);\n sky = sky + params.colorC.rgb * pow(sunAmt, 1400.0) * 4.0;\n sky = sky + params.colorC.rgb * pow(sunAmt, 32.0) * 0.5;\n\n var col = sky;\n if (rd.y < -0.01) {\n // Bracket the crossing: the ray is above every wave at tm and below sea\n // floor level at tx, then eight secant steps pin the surface.\n var tm = (maxAmp - ro.y) / rd.y;\n var tx = -ro.y / rd.y * 1.6;\n var hm = ro.y + rd.y * tm - seaHeight((ro + rd * tm).xz, t, 3);\n var hx = ro.y + rd.y * tx - seaHeight((ro + rd * tx).xz, t, 3);\n var travelled = tx;\n let refine = st_steps(8);\n for (var i = 0; i < refine; i = i + 1) {\n travelled = mix(tm, tx, hm / max(hm - hx, 1e-4));\n let pos = ro + rd * travelled;\n let h = pos.y - seaHeight(pos.xz, t, 3);\n if (h < 0.0) {\n tx = travelled;\n hx = h;\n } else {\n tm = travelled;\n hm = h;\n }\n }\n let pos = ro + rd * travelled;\n\n // Normal from central differences of the five-octave field, the step\n // widening with distance so far water stays calm instead of fizzing.\n let e = max(0.008 * travelled, 0.01);\n let h0 = seaHeight(pos.xz, t, 5);\n let n = normalize(vec3f(\n seaHeight(pos.xz - vec2f(e, 0.0), t, 5) - seaHeight(pos.xz + vec2f(e, 0.0), t, 5),\n 2.0 * e,\n seaHeight(pos.xz - vec2f(0.0, e), t, 5) - seaHeight(pos.xz + vec2f(0.0, e), t, 5),\n ));\n\n // Fresnel decides sky against body.\n let fres = 0.03 + 0.5 * pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0);\n let refl = skyColor(reflect(rd, n).y, sunAmt);\n\n // The body: deep colour lifted toward the crest colour with height —\n // the wrapped light that reads as the sun inside the wave.\n let lift = clamp(h0 / maxAmp, 0.0, 1.0);\n var body = params.colorA.rgb * 0.38;\n body = body + params.colorB.rgb * pow(clamp(dot(n, sunDir), 0.0, 1.0) * 0.35 + 0.65, 3.0) * lift * 0.75;\n\n col = mix(body, refl, fres);\n\n // Whitecaps where a crest peaks, eaten by noise into streaks.\n let cap = smoothstep(0.72, 0.95, lift) * smoothstep(0.35, 0.75, st_noise2(pos.xz * 1.6 + t * 0.8));\n col = mix(col, vec3f(0.93, 0.96, 0.96), cap * 0.85);\n\n // Glitter: tight and sharp nearby, a broad streak at distance.\n let spec = pow(clamp(dot(reflect(rd, n), sunDir), 0.0, 1.0), 260.0 / (1.0 + travelled * travelled * 0.02));\n col = col + params.colorC.rgb * spec * params.c * 2.0;\n\n // Haze folds sea into sky at the horizon.\n col = mix(col, sky, 1.0 - exp(-max(travelled - 6.0, 0.0) * 0.022));\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n",
78
79
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let taps = st_steps(12);\n var sum = 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;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / f32(taps + 1);\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",
@@ -82,186 +83,4 @@ export const shaderSources = {
82
83
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 // On Low the march takes half the steps and trusts the distance more, so\n // it still reaches the fogged far end of the corridor.\n var travelled = 0.05;\n var hit = false;\n let steps = st_steps(96);\n let relax = select(0.7, 0.9, st_low());\n for (var i = 0; i < steps; 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 * relax, 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",
83
84
  "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 // 1 on the High tier, 0 on Low. Read it through st_steps, not directly.\n quality : f32,\n e : f32,\n f : f32,\n g : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n // Appended so existing controls and colors keep their byte offsets.\n h : f32,\n i : f32,\n j : f32,\n k : f32,\n l : f32,\n m : f32,\n n : f32,\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// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- quality ---------------------------------------------------------------\n// The runtime draws Low at a device pixel ratio of 1, which is most of the\n// saving. These are how a shader halves its own work per pixel as well, so\n// that the shaders whose cost is a loop — a march, a fan of rays, a spiral of\n// taps — cost half on Low and not merely a quarter of the pixels.\n\n// Whether the runtime asked for the cheaper picture.\nfn st_low() -> bool {\n return params.quality < 0.5;\n}\n\n// An iteration count for the current tier: n on High, about half on Low, and\n// never under three unless n itself is. Give it the bound of every loop whose\n// length is cost rather than look. A loop of three curtains or five layers is\n// the picture; a loop of ninety-six march steps is the price of it.\nfn st_steps(n : i32) -> i32 {\n if (!st_low()) { return n; }\n return max(i32(ceil(f32(n) * 0.5)), min(n, 3));\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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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 let count = st_steps(octaves);\n for (var i = 0; i < count; 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",
84
85
  };
85
- /** Whether a shader invents its picture or reads one from a bound texture. */
86
- export const shaderKinds = {
87
- "alloy": "graph",
88
- "archipelago": "generative",
89
- "ascii": "source",
90
- "aurora": "generative",
91
- "basalt": "graph",
92
- "beacon": "generative",
93
- "billow": "graph",
94
- "bloom": "source",
95
- "blossom": "generative",
96
- "blueprint": "graph",
97
- "bokeh": "generative",
98
- "caldera": "generative",
99
- "caustics": "generative",
100
- "chladni": "graph",
101
- "chromatic-city": "graph",
102
- "cloth": "compute",
103
- "cumulus": "generative",
104
- "dither": "generative",
105
- "dunes": "graph",
106
- "duotone": "source",
107
- "edges": "source",
108
- "eyes": "generative",
109
- "ferro": "graph",
110
- "filament": "graph",
111
- "film-grain": "generative",
112
- "flow": "source",
113
- "fluid": "compute",
114
- "glass": "source",
115
- "gossamer": "graph",
116
- "gravity": "generative",
117
- "halftone": "generative",
118
- "hills": "generative",
119
- "island": "generative",
120
- "julia": "generative",
121
- "kaleidoscope": "generative",
122
- "lagoon": "generative",
123
- "lens": "source",
124
- "liquid-light": "graph",
125
- "liquid-metal": "generative",
126
- "louvres": "generative",
127
- "lumen": "generative",
128
- "maelstrom": "graph",
129
- "magnet": "generative",
130
- "meadow": "graph",
131
- "melt": "source",
132
- "mesh-gradient": "generative",
133
- "mosaic": "source",
134
- "neon-grid": "generative",
135
- "neon-sign": "source",
136
- "orb": "generative",
137
- "parallax": "generative",
138
- "particles": "compute",
139
- "pixel-clouds": "generative",
140
- "pixel-fire": "generative",
141
- "pixel-isle": "generative",
142
- "pixel-rain": "generative",
143
- "pixel-sea": "generative",
144
- "plasma": "generative",
145
- "pulsar": "graph",
146
- "radiance": "generative",
147
- "reveal": "source",
148
- "ripple": "generative",
149
- "shafts": "graph",
150
- "shatter": "source",
151
- "shockwave": "source",
152
- "shore": "generative",
153
- "silk": "generative",
154
- "spectra": "graph",
155
- "starfield": "generative",
156
- "strata": "generative",
157
- "swell": "generative",
158
- "swirl": "source",
159
- "tesla": "generative",
160
- "torch": "generative",
161
- "truchet": "generative",
162
- "tunnel": "generative",
163
- "voronoi": "generative",
164
- };
165
- /**
166
- * Whether a shader binds a source layer: every source-kind shader, and the
167
- * graph shaders whose flavour prelude asked for one.
168
- */
169
- export const shaderReadsSource = {
170
- "alloy": false,
171
- "archipelago": false,
172
- "ascii": true,
173
- "aurora": false,
174
- "basalt": false,
175
- "beacon": false,
176
- "billow": false,
177
- "bloom": true,
178
- "blossom": false,
179
- "blueprint": true,
180
- "bokeh": false,
181
- "caldera": false,
182
- "caustics": false,
183
- "chladni": false,
184
- "chromatic-city": false,
185
- "cloth": false,
186
- "cumulus": false,
187
- "dither": false,
188
- "dunes": false,
189
- "duotone": true,
190
- "edges": true,
191
- "eyes": false,
192
- "ferro": false,
193
- "filament": false,
194
- "film-grain": false,
195
- "flow": true,
196
- "fluid": false,
197
- "glass": true,
198
- "gossamer": false,
199
- "gravity": false,
200
- "halftone": false,
201
- "hills": false,
202
- "island": false,
203
- "julia": false,
204
- "kaleidoscope": false,
205
- "lagoon": false,
206
- "lens": true,
207
- "liquid-light": false,
208
- "liquid-metal": false,
209
- "louvres": false,
210
- "lumen": false,
211
- "maelstrom": false,
212
- "magnet": false,
213
- "meadow": false,
214
- "melt": true,
215
- "mesh-gradient": false,
216
- "mosaic": true,
217
- "neon-grid": false,
218
- "neon-sign": true,
219
- "orb": false,
220
- "parallax": false,
221
- "particles": false,
222
- "pixel-clouds": false,
223
- "pixel-fire": false,
224
- "pixel-isle": false,
225
- "pixel-rain": false,
226
- "pixel-sea": false,
227
- "plasma": false,
228
- "pulsar": false,
229
- "radiance": false,
230
- "reveal": true,
231
- "ripple": false,
232
- "shafts": false,
233
- "shatter": true,
234
- "shockwave": true,
235
- "shore": false,
236
- "silk": false,
237
- "spectra": false,
238
- "starfield": false,
239
- "strata": false,
240
- "swell": false,
241
- "swirl": true,
242
- "tesla": false,
243
- "torch": false,
244
- "truchet": false,
245
- "tunnel": false,
246
- "voronoi": false,
247
- };
248
- /** Which render graph drives a graph shader; absent for the other kinds. */
249
- export const shaderGraphs = {
250
- "alloy": "studio",
251
- "basalt": "masonry",
252
- "billow": "glow",
253
- "blueprint": "drafting",
254
- "chladni": "particles",
255
- "chromatic-city": "miniature",
256
- "dunes": "particles",
257
- "ferro": "particles",
258
- "filament": "glow",
259
- "gossamer": "glow",
260
- "liquid-light": "dielectric",
261
- "maelstrom": "particles",
262
- "meadow": "landscape",
263
- "pulsar": "particles",
264
- "shafts": "glow",
265
- "spectra": "glow",
266
- };
267
86
  //# sourceMappingURL=sources.js.map