shadertown 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/cli/cli.mjs +341 -0
  4. package/cli/shadertown.mjs +8 -0
  5. package/dist/config.d.ts +29 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +8 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/export.d.ts +30 -0
  10. package/dist/export.d.ts.map +1 -0
  11. package/dist/export.js +234 -0
  12. package/dist/export.js.map +1 -0
  13. package/dist/generated/sources.d.ts +22 -0
  14. package/dist/generated/sources.d.ts.map +1 -0
  15. package/dist/generated/sources.js +24 -0
  16. package/dist/generated/sources.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/params.d.ts +24 -0
  22. package/dist/params.d.ts.map +1 -0
  23. package/dist/params.js +105 -0
  24. package/dist/params.js.map +1 -0
  25. package/dist/react.d.ts +41 -0
  26. package/dist/react.d.ts.map +1 -0
  27. package/dist/react.js +127 -0
  28. package/dist/react.js.map +1 -0
  29. package/dist/registry.d.ts +1930 -0
  30. package/dist/registry.d.ts.map +1 -0
  31. package/dist/registry.js +1067 -0
  32. package/dist/registry.js.map +1 -0
  33. package/dist/renderer.d.ts +31 -0
  34. package/dist/renderer.d.ts.map +1 -0
  35. package/dist/renderer.js +144 -0
  36. package/dist/renderer.js.map +1 -0
  37. package/dist/stage.d.ts +39 -0
  38. package/dist/stage.d.ts.map +1 -0
  39. package/dist/stage.js +79 -0
  40. package/dist/stage.js.map +1 -0
  41. package/dist/types.d.ts +71 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +85 -0
package/dist/export.js ADDED
@@ -0,0 +1,234 @@
1
+ import { shaderSources } from "./generated/sources.js";
2
+ import { defaultParams } from "./params.js";
3
+ import { shaderRegistry } from "./registry.js";
4
+ /**
5
+ * Only the values that differ from the registry defaults. Exported snippets
6
+ * stay short, and a shader's defaults can improve later without silently
7
+ * changing anyone's pinned look.
8
+ */
9
+ export function diffParams(name, params) {
10
+ const defaults = defaultParams(name);
11
+ const diff = {};
12
+ for (const [key, value] of Object.entries(params)) {
13
+ if (value === undefined || value === defaults[key])
14
+ continue;
15
+ diff[key] = typeof value === "number" ? Number(value.toFixed(4)) : value;
16
+ }
17
+ return diff;
18
+ }
19
+ function literal(value) {
20
+ return typeof value === "number" ? String(value) : JSON.stringify(value);
21
+ }
22
+ function paramsBlock(diff, indent) {
23
+ const keys = Object.keys(diff);
24
+ if (keys.length === 0)
25
+ return "";
26
+ const lines = keys.map((key) => {
27
+ const safeKey = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
28
+ return `${indent} ${safeKey}: ${literal(diff[key])},`;
29
+ });
30
+ return `${indent}params: {\n${lines.join("\n")}\n${indent}},\n`;
31
+ }
32
+ /** A `shader.config.ts` module: the tuned preset, typed against the registry. */
33
+ export function toConfigSource(name, params) {
34
+ const diff = diffParams(name, params);
35
+ const definition = shaderRegistry[name];
36
+ return `import { defineShader } from "shadertown";
37
+
38
+ // ${definition.name} — ${definition.tagline}
39
+ export const ${toIdentifier(name)} = defineShader({
40
+ shader: ${JSON.stringify(name)},
41
+ ${paramsBlock(diff, " ")} play: "visible",
42
+ });
43
+ `;
44
+ }
45
+ /** A drop-in React usage snippet for the current settings. */
46
+ export function toReactSource(name, params) {
47
+ const diff = diffParams(name, params);
48
+ const entries = Object.entries(diff).map(([key, value]) => ` ${/^[A-Za-z_$][\w$]*$/.test(key) ? key : `["${key}"]`}: ${literal(value)},`);
49
+ const paramsProp = entries.length > 0 ? `\n params={{\n${entries.join("\n")}\n }}` : "";
50
+ return `import { ShaderCanvas } from "shadertown/react";
51
+
52
+ export function Background() {
53
+ return (
54
+ <ShaderCanvas
55
+ shader="${name}"${paramsProp}
56
+ play="visible"
57
+ style={{ position: "absolute", inset: 0 }}
58
+ />
59
+ );
60
+ }
61
+ `;
62
+ }
63
+ /** The imperative form, for codebases that are not React. */
64
+ export function toVanillaSource(name, params) {
65
+ const diff = diffParams(name, params);
66
+ const entries = Object.entries(diff).map(([key, value]) => ` ${key}: ${literal(value)},`);
67
+ const paramsArg = entries.length > 0 ? `\n params: {\n${entries.join("\n")}\n },` : "";
68
+ return `import { createShaderRenderer } from "shadertown";
69
+
70
+ const canvas = document.querySelector("canvas")!;
71
+
72
+ const renderer = createShaderRenderer(canvas, {
73
+ shader: ${JSON.stringify(name)},${paramsArg}
74
+ dpr: [1, 2],
75
+ });
76
+
77
+ // renderer.setParams({ speed: 0.2 }) // live updates, no recompile
78
+ // renderer.setPaused(true) // stop drawing, keep the last frame
79
+ // renderer.destroy() // release the surface
80
+ `;
81
+ }
82
+ /** The complete, self-contained WGSL for this shader. */
83
+ export function toWgslSource(name) {
84
+ return shaderSources[name];
85
+ }
86
+ export function toInstallCommand(name) {
87
+ return `npx shadertown@latest add ${name}`;
88
+ }
89
+ function toIdentifier(name) {
90
+ const camel = name.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
91
+ return /^[a-z]/.test(camel) ? camel : `shader${camel}`;
92
+ }
93
+ /** A paste-ready component wired with the tuned parameters. */
94
+ export function toExampleSource(name, params) {
95
+ const definition = shaderRegistry[name];
96
+ const identifier = toIdentifier(name);
97
+ // Heavy shaders get a lower resolution ceiling by default; a fullscreen
98
+ // raymarch at DPR 2 is the most common way to make one feel slow.
99
+ const dpr = definition.cost === "heavy" ? "[1, 1.25]" : definition.cost === "medium" ? "[1, 1.5]" : "[1, 2]";
100
+ return `import { ShaderCanvas } from "shadertown/react";
101
+ import { ${identifier} } from "./${name}";
102
+
103
+ // ${definition.name} — ${definition.tagline}
104
+ export function Background() {
105
+ return (
106
+ <div style={{ position: "relative", minHeight: "70vh" }}>
107
+ <ShaderCanvas
108
+ preset={${identifier}}
109
+ quality={{ dpr: ${dpr} }}
110
+ style={{ position: "absolute", inset: 0 }}
111
+ />
112
+ <div style={{ position: "relative" }}>{/* your content */}</div>
113
+ </div>
114
+ );
115
+ }
116
+ `;
117
+ }
118
+ /**
119
+ * Per-shader documentation, generated from the registry rather than written by
120
+ * hand — the control table cannot drift out of date with the shader.
121
+ */
122
+ export function toReadme(name, params) {
123
+ const definition = shaderRegistry[name];
124
+ const diff = diffParams(name, params);
125
+ const identifier = toIdentifier(name);
126
+ const controlRows = Object.entries(definition.controls)
127
+ .map(([key, control]) => {
128
+ const c = control;
129
+ return `| \`${key}\` | ${c.label} | ${c.min} – ${c.max} | ${c.default} | ${c.hint ?? ""} |`;
130
+ })
131
+ .join("\n");
132
+ const colourRows = ["colorA", "colorB", "colorC"]
133
+ .map((key) => {
134
+ const c = definition.colors[key];
135
+ return `| \`${key}\` | ${c.label} | \`${c.default}\` | ${c.hint ?? ""} |`;
136
+ })
137
+ .join("\n");
138
+ const tuned = Object.keys(diff).length
139
+ ? Object.entries(diff)
140
+ .map(([key, value]) => `- \`${key}\`: ${typeof value === "number" ? value : JSON.stringify(value)}`)
141
+ .join("\n")
142
+ : "_None — this preset uses the catalogue defaults._";
143
+ const costNote = {
144
+ light: "Light. A few samples per pixel — fine fullscreen at full pixel ratio, including on integrated graphics.",
145
+ medium: "Medium. A few dozen samples per pixel — comfortable fullscreen on a discrete GPU, but cap the pixel ratio for a retina hero.",
146
+ heavy: "Heavy. A raymarch or escape-time loop per pixel — cap the pixel ratio, or keep it to a card rather than a fullscreen background.",
147
+ }[definition.cost];
148
+ return `# ${definition.name}
149
+
150
+ ${definition.tagline}.
151
+
152
+ ${definition.description}
153
+
154
+ ## Files
155
+
156
+ | File | What it is |
157
+ | --- | --- |
158
+ | \`${name}.wgsl\` | The complete shader. The shared prelude is inlined, so it has no imports and compiles on its own with \`vgpu check\` or any other WGSL toolchain. |
159
+ | \`${name}.ts\` | Your tuned preset, typed against this shader's controls. |
160
+ | \`example.tsx\` | A working component using the preset. |
161
+ | \`LICENSE\` | Your licence for this shader. |
162
+
163
+ ## Use it
164
+
165
+ \`\`\`tsx
166
+ import { ShaderCanvas } from "shadertown/react";
167
+ import { ${identifier} } from "./${name}";
168
+
169
+ <ShaderCanvas preset={${identifier}} />;
170
+ \`\`\`
171
+
172
+ The canvas fills its parent, so give the parent a size.
173
+
174
+ ## This preset
175
+
176
+ ${tuned}
177
+
178
+ ## Controls
179
+
180
+ | Name | Label | Range | Default | What it does |
181
+ | --- | --- | --- | --- | --- |
182
+ ${controlRows}
183
+
184
+ ## Colours
185
+
186
+ | Name | Label | Default | What it is |
187
+ | --- | --- | --- | --- |
188
+ ${colourRows}
189
+
190
+ ## Rendering cost
191
+
192
+ ${costNote}
193
+
194
+ The runtime shares one GPU context and one frame loop across every canvas on a
195
+ page, and stops drawing when nothing is animating. Use \`play="visible"\` (the
196
+ default) so this shader only runs while it is on screen.
197
+
198
+ ## Requirements
199
+
200
+ WebGPU: Chrome and Edge 113+, Safari 26+, Firefox behind a flag. \`ShaderCanvas\`
201
+ reports an \`unsupported\` status so you can render a fallback.
202
+
203
+ ---
204
+
205
+ ${definition.name} is part of the [shadertown](https://www.shadertown.com) catalogue,
206
+ drawn by [vgpu](https://vgpu.sh).
207
+ `;
208
+ }
209
+ /** The licence that ships with a purchased shader, issued to the buyer. */
210
+ export function toLicense(name, holder = {}) {
211
+ const definition = shaderRegistry[name];
212
+ const issuedTo = holder.name ?? "the licensee";
213
+ const orderLine = holder.order ? `\nOrder: ${holder.order}` : "";
214
+ return `shadertown — ${definition.name}
215
+
216
+ Licensed to: ${issuedTo}${orderLine}
217
+ Issued: ${new Date().toISOString().slice(0, 10)}
218
+
219
+ You may use, modify and embed this shader in unlimited personal and commercial
220
+ projects, including projects you deliver to clients. No attribution is required.
221
+
222
+ You may not resell or redistribute the shader source itself, or include it in a
223
+ product whose main purpose is to distribute shaders, assets or templates.
224
+
225
+ This licence does not expire. If your subscription lapses you keep every shader
226
+ you have already downloaded; you simply stop receiving new ones and updates.
227
+
228
+ The shader is provided as is, without warranty of any kind. The authors are not
229
+ liable for any claim or damage arising from its use.
230
+
231
+ The shadertown runtime itself is MIT licensed and is not covered by this file.
232
+ `;
233
+ }
234
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAsC,MAAM,eAAe,CAAC;AAEnF;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,IAAO,EACP,MAAuB;IAEvB,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAoC,CAAC;IACxE,MAAM,IAAI,GAAoC,EAAE,CAAC;IACjD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAyC,CAAC,EAAE,CAAC;QACrF,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7D,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,KAAsB;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,WAAW,CAAC,IAAqC,EAAE,MAAc;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC3E,OAAO,GAAG,MAAM,KAAK,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,CAAoB,CAAC,GAAG,CAAC;IAC5E,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,MAAM,cAAc,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,MAAM,CAAC;AAClE,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAuB,IAAO,EAAE,MAAuB;IACnF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO;;KAEJ,UAAU,CAAC,IAAI,MAAM,UAAU,CAAC,OAAO;eAC7B,YAAY,CAAC,IAAI,CAAC;YACrB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;EAC9B,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC;;CAExB,CAAC;AACF,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAAuB,IAAO,EAAE,MAAuB;IAClF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CACtC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,WAAW,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,KAAK,CAAC,GAAG,CACvG,CAAC;IACF,MAAM,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,sBAAsB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjF,OAAO;;;;;gBAKO,IAAI,IAAI,UAAU;;;;;;CAMjC,CAAC;AACF,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,eAAe,CAAuB,IAAO,EAAE,MAAuB;IACpF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7F,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,kBAAkB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IAEzF,OAAO;;;;;YAKG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS;;;;;;;CAO5C,CAAC;AACF,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,YAAY,CAAuB,IAAO;IACxD,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAuB,IAAO;IAC5D,OAAO,6BAA6B,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACpF,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC;AACzD,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAuB,IAAO,EAAE,MAAuB;IACpF,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE7G,OAAO;WACE,UAAU,cAAc,IAAI;;KAElC,UAAU,CAAC,IAAI,MAAM,UAAU,CAAC,OAAO;;;;;kBAK1B,UAAU;0BACF,GAAG;;;;;;;CAO5B,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAuB,IAAO,EAAE,MAAuB;IAC7E,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEtC,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE;QACtB,MAAM,CAAC,GAAG,OAAsF,CAAC;QACjG,OAAO,OAAO,GAAG,QAAQ,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,IAAI,IAAI,EAAE,IAAI,CAAC;IAC9F,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,UAAU,GAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAW;SACzD,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjC,OAAO,OAAO,GAAG,QAAQ,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI,EAAE,IAAI,CAAC;IAC5E,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM;QACpC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;aACjB,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;aACnG,IAAI,CAAC,IAAI,CAAC;QACf,CAAC,CAAC,mDAAmD,CAAC;IAExD,MAAM,QAAQ,GAAG;QACf,KAAK,EAAE,yGAAyG;QAChH,MAAM,EACJ,8HAA8H;QAChI,KAAK,EACH,kIAAkI;KACrI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAEnB,OAAO,KAAK,UAAU,CAAC,IAAI;;EAE3B,UAAU,CAAC,OAAO;;EAElB,UAAU,CAAC,WAAW;;;;;;MAMlB,IAAI;MACJ,IAAI;;;;;;;;WAQC,UAAU,cAAc,IAAI;;wBAEf,UAAU;;;;;;;EAOhC,KAAK;;;;;;EAML,WAAW;;;;;;EAMX,UAAU;;;;EAIV,QAAQ;;;;;;;;;;;;;EAaR,UAAU,CAAC,IAAI;;CAEhB,CAAC;AACF,CAAC;AAOD,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAuB,IAAO,EAAE,SAAwB,EAAE;IACjF,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,IAAI,cAAc,CAAC;IAC/C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjE,OAAO,gBAAgB,UAAU,CAAC,IAAI;;eAEzB,QAAQ,GAAG,SAAS;UACzB,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;;;;;;;;;;;;;;;CAe9C,CAAC;AACF,CAAC"}
@@ -0,0 +1,22 @@
1
+ export declare const shaderSources: {
2
+ readonly aurora: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// aurora\n// ---------------------------------------------------------------------------\n\n// Aurora — layered curtains of domain-warped noise over a night sky.\n\nfn curtain(p : vec2f, offset : f32, t : f32) -> f32 {\n var q = p;\n q.x = q.x + st_fbm2(p * vec2f(1.4, 0.7) + vec2f(offset, t * 0.25), 4) * params.warp;\n let band = st_fbm2(vec2f(q.x * 2.2 + offset * 3.0, q.y * 0.6 - t * 0.4), 5);\n let ribbon = pow(clamp(band, 0.0, 1.0), 2.2);\n let falloff = smoothstep(0.62, -0.12, p.y);\n let top = smoothstep(-0.55, -0.05, p.y);\n return ribbon * falloff * top;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n // Night sky gradient, darkest at the horizon.\n var col = mix(vec3f(0.004, 0.008, 0.028), vec3f(0.02, 0.03, 0.07), smoothstep(1.0, -0.2, uv.y));\n\n // Stars, thinned out towards the aurora.\n let starCell = floor(uv * params.resolution / 3.0);\n let star = st_hash21(starCell);\n let twinkle = 0.6 + 0.4 * sin(t * 2.0 + star * TAU);\n col = col + vec3f(step(0.9985, star) * twinkle * params.d);\n\n // Three curtains at different depths.\n let layers = 3.0;\n var glow = 0.0;\n var tint = vec3f(0.0);\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let depth = 1.0 - fi / layers * 0.55;\n let amount = curtain(p * (1.0 + fi * 0.35), fi * 7.31, t) * depth;\n glow = glow + amount;\n tint = tint + st_palette(fi / (layers - 1.0)) * amount;\n }\n\n let energy = glow * params.intensity;\n col = col + tint * params.intensity;\n col = col + st_palette(0.5) * pow(energy, 3.0) * 0.35 * params.b;\n\n return st_post(st_tonemap(col * params.a), uv);\n}\n";
3
+ readonly bokeh: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// bokeh\n// ---------------------------------------------------------------------------\n\n// Bokeh — out-of-focus lights drifting through depth. Additive, soft-edged.\n\nfn bokehLayer(p : vec2f, depth : f32, t : f32) -> vec3f {\n let density = mix(2.5, 7.0, depth);\n let drift = vec2f(t * 0.06 * (1.0 - depth * 0.7), -t * 0.09 * (1.0 - depth * 0.5));\n let grid = p * density + drift * density;\n let base = floor(grid);\n let frac = fract(grid) - 0.5;\n\n var acc = vec3f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = st_hash22(base + neighbour + depth * 91.7);\n if (rnd.x < 0.35) { continue; }\n\n let wobble = vec2f(sin(t * 0.5 + rnd.y * TAU), cos(t * 0.4 + rnd.x * TAU)) * 0.12;\n let centre = neighbour + rnd - 0.5 + wobble;\n let d = length(frac - centre);\n\n // A disc with a bright rim — how a real lens renders defocused points.\n let radius = (0.10 + rnd.y * 0.22) * params.a * mix(1.6, 0.5, depth);\n let disc = smoothstep(radius, radius * (1.0 - params.b), d);\n let rim = smoothstep(radius * (1.0 - params.b * 0.8), radius, d) * disc;\n\n let tint = st_palette(rnd.y);\n acc = acc + tint * (disc * 0.55 + rim * params.c) * mix(0.35, 1.0, 1.0 - depth);\n }\n }\n return acc;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n var col = mix(params.colorA.rgb * 0.10, vec3f(0.008, 0.008, 0.02), uv.y);\n for (var i = 0; i < 4; i = i + 1) {\n col = col + bokehLayer(p, f32(i) / 3.0, t) * params.d;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
4
+ readonly caustics: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// caustics\n// ---------------------------------------------------------------------------\n\n// Caustics — light focused through a moving water surface onto a pool floor.\n\nfn causticField(p : vec2f, t : f32, sharpness : f32) -> f32 {\n var acc = 0.0;\n var q = p;\n // Stacked, counter-rotating cell fields approximate the focusing pattern.\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let rotated = st_rot(fi * 1.1 + t * 0.05 * (fi + 1.0)) * q;\n let wave = sin(rotated.x * 6.0 + t * (1.1 + fi * 0.3))\n + sin(rotated.y * 6.0 - t * (0.9 + fi * 0.2))\n + sin((rotated.x + rotated.y) * 4.0 + t * 0.7);\n acc = acc + abs(wave) / 3.0;\n q = q * 1.35;\n }\n let v = 1.0 - clamp(acc / 3.0, 0.0, 1.0);\n return pow(v, 1.0 + sharpness * 14.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 2.0;\n\n // Surface ripple displaces the floor lookup.\n let ripple = vec2f(st_fbm2(p * 1.5 + t * 0.1, 3), st_fbm2(p * 1.5 + 4.2 - t * 0.08, 3));\n p = p + (ripple - 0.5) * params.warp;\n\n let light = causticField(p, t, params.a);\n\n // Chromatic split — the three wavelengths focus at slightly different depths.\n let split = params.b * 0.03;\n let r = causticField(p + vec2f(split, 0.0), t, params.a);\n let b = causticField(p - vec2f(split, 0.0), t, params.a);\n\n let floorTone = mix(params.colorA.rgb, params.colorB.rgb, clamp(st_fbm2(p * 0.8, 3), 0.0, 1.0)) * 0.35;\n var col = floorTone;\n col = col + params.colorC.rgb * vec3f(r, light, b) * params.intensity * 2.2;\n\n // Depth falloff towards the pool edges.\n col = col * (0.55 + 0.45 * smoothstep(1.1, 0.1, length(st_centered(uv)) * params.c));\n\n return st_post(st_tonemap(col), uv);\n}\n";
5
+ readonly dither: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// dither\n// ---------------------------------------------------------------------------\n\n// Dither — a smooth field quantised through an 8x8 Bayer matrix.\n// Deliberately low-fi: flat colour, a visible pattern, no antialiasing.\n\n// Ordered-dither threshold, built by interleaving the bits of y and x^y.\nfn bayer8(coord : vec2f) -> f32 {\n let x = i32(coord.x) & 7;\n let y = i32(coord.y) & 7;\n var index = 0;\n for (var i = 0u; i < 3u; i = i + 1u) {\n let shift = 2u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return f32(index) / 64.0;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Centred on 0.5 so every quantisation level gets used. Biasing the source\n // upwards instead just crushes the image to the top level.\n let field = st_fbm2(p * 2.0 + vec2f(t * 0.10, t * 0.06), 5);\n let sweep = clamp((field - 0.5) * 1.7 + (0.5 - uv.y) * params.c + 0.5, 0.0, 1.0);\n\n // Pixelate first, so the pattern reads the same at any resolution.\n let pixel = max(params.a, 1.0);\n let screenPos = floor(uv * params.resolution / pixel);\n let threshold = (bayer8(screenPos) - 0.5) / max(floor(params.b), 2.0);\n\n let levels = max(floor(params.b), 2.0);\n let stepped = min(floor(clamp(sweep + threshold * params.d * 2.0, 0.0, 1.0) * levels), levels - 1.0);\n let quantised = stepped / (levels - 1.0);\n\n let col = st_palette(quantised) * params.intensity;\n return st_post(col, uv);\n}\n";
6
+ readonly "film-grain": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// film-grain\n// ---------------------------------------------------------------------------\n\n// Film grain — a texture pass. Dark wash, heavy stock grain, halation, scanlines.\n\nfn wash(p : vec2f, t : f32) -> f32 {\n return st_fbm2(p * 1.2 + vec2f(t * 0.05, t * 0.03), 4);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Chromatic aberration: read the same wash at three lateral offsets.\n let ca = params.c * 0.02;\n let wr = wash(p + vec2f(ca, 0.0), t);\n let wg = wash(p, t);\n let wb = wash(p - vec2f(ca, 0.0), t);\n\n var col = vec3f(\n st_palette(clamp(wr * 1.3 - 0.1, 0.0, 1.0)).r,\n st_palette(clamp(wg * 1.3 - 0.1, 0.0, 1.0)).g,\n st_palette(clamp(wb * 1.3 - 0.1, 0.0, 1.0)).b\n ) * params.intensity;\n\n // Stock grain, sampled per cell so it stays the same size at any DPR.\n let cell = floor(uv * params.resolution / max(params.d, 0.5));\n let n1 = st_hash21(cell + floor(t * 24.0) * 17.13);\n let n2 = st_hash21(cell.yx + floor(t * 24.0) * 3.71);\n col = col + (n1 * 0.65 + n2 * 0.35 - 0.5) * params.a;\n\n // Scanlines.\n let lines = sin(uv.y * params.resolution.y * 1.5) * 0.5 + 0.5;\n col = col * (1.0 - lines * params.b);\n\n // Halation blooming out of the brightest areas.\n col = col + st_palette(1.0) * pow(clamp(st_luma(col), 0.0, 1.0), 4.0) * 0.4;\n\n return st_post(col, uv);\n}\n";
7
+ readonly halftone: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// halftone\n// ---------------------------------------------------------------------------\n\n// Halftone — a duotone image reproduced as a rotated dot screen.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // The continuous-tone image being screened. Centred on 0.5 so the screen\n // actually has a range of dot sizes to work with instead of saturating.\n let field = st_fbm2(p * 1.8 + vec2f(t * 0.12, t * 0.07), 4);\n let sweep = clamp((field - 0.5) * 1.6 + (p.x + p.y) * 0.35 + 0.5, 0.0, 1.0);\n\n // Dot screen, rotated the way a real halftone plate would be.\n let angle = params.c * PI;\n let grid = st_rot(angle) * uv * params.resolution / max(params.a, 2.0);\n let cellPos = fract(grid) - 0.5;\n\n let ink = pow(sweep, 1.0 / max(params.d, 0.05));\n // 0.62 lets neighbouring dots touch at full ink while leaving the corners\n // open — past ~0.71 the screen fills solid and stops reading as halftone.\n let radius = sqrt(ink) * 0.62;\n let coverage = 1.0 - smoothstep(radius - params.b, radius + params.b, length(cellPos));\n\n let paper = params.colorA.rgb;\n let inkColor = mix(params.colorB.rgb, params.colorC.rgb, sweep);\n\n let col = mix(paper, inkColor, coverage * params.intensity);\n return st_post(col, uv);\n}\n";
8
+ readonly julia: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// julia\n// ---------------------------------------------------------------------------\n\n// Julia — escape-time fractal with orbit-trap colouring. A moving c parameter\n// walks the set through connected and dust-like shapes.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var z = st_centered(uv) * (3.4 / max(params.scale, 0.05));\n z = st_rot(t * 0.03 * params.d) * z;\n\n // The seed orbits a cardioid edge, which is where the interesting shapes live.\n let angle = t * 0.12;\n let c = vec2f(\n 0.7885 * cos(angle) * params.a,\n 0.7885 * sin(angle) * params.a\n ) + vec2f(params.b - 0.5, 0.0) * 0.6;\n\n let maxIter = 96;\n var i = 0;\n var trap = 1e9;\n var escaped = false;\n\n for (; i < maxIter; i = i + 1) {\n z = vec2f(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;\n trap = min(trap, abs(length(z) - 1.0));\n if (dot(z, z) > 64.0) { escaped = true; break; }\n }\n\n if (!escaped) {\n // Interior: shade by how close the orbit came to the trap circle.\n let inner = st_palette(clamp(trap * 4.0, 0.0, 1.0)) * 0.5;\n return st_post(inner * params.intensity, uv);\n }\n\n // Smooth iteration count removes the banding of a raw integer count.\n let smoothed = f32(i) - log2(max(log2(length(z)), 1.0)) + 4.0;\n let shade = fract(smoothed * 0.09 * params.c + t * 0.04);\n\n // Points that escape immediately are the empty outer plane; letting them\n // take a full palette colour washes the whole frame out.\n let escapeDepth = clamp(smoothed / 18.0, 0.0, 1.0);\n\n var col = st_paletteCyclic(shade) * pow(escapeDepth, 1.15);\n col = col * (0.25 + 0.9 * clamp(1.0 - trap * 1.1, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
9
+ readonly kaleidoscope: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// kaleidoscope\n// ---------------------------------------------------------------------------\n\n// Kaleidoscope — polar mirror folding over a drifting noise field.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n\n let segments = max(floor(params.a), 2.0);\n var angle = atan2(p.y, p.x);\n let radius = length(p);\n\n // Fold the angle into one wedge, then mirror it.\n let wedge = TAU / segments;\n angle = angle + t * 0.15 * params.d;\n angle = angle - wedge * floor(angle / wedge);\n angle = abs(angle - wedge * 0.5);\n\n var q = vec2f(cos(angle), sin(angle)) * radius;\n q = q + vec2f(st_fbm2(q * 3.0 + t * 0.2, 4) - 0.5) * params.warp;\n\n let rings = sin(radius * 22.0 * params.b - t * 2.0) * 0.5 + 0.5;\n let field = st_fbm2(q * 4.0 + vec2f(0.0, t * 0.3), 5);\n\n var col = st_palette(fract(field * 1.6 + rings * params.c));\n\n // Bright core, dark rim — reads like looking down a tube.\n col = col * (0.4 + 0.9 * smoothstep(0.75, 0.0, radius));\n col = col + st_palette(1.0) * pow(clamp(1.0 - radius * 2.2, 0.0, 1.0), 3.0) * 0.6;\n\n return st_post(col * params.intensity, uv);\n}\n";
10
+ readonly "liquid-metal": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// liquid-metal\n// ---------------------------------------------------------------------------\n\n// Liquid metal — a warped height field lit like polished chrome.\n// The look lives in the banded environment reflection, not in the noise: keep\n// the field low-frequency, and let the bands do the work.\n\nfn height(p : vec2f, t : f32) -> f32 {\n var q = p;\n q = q + vec2f(st_fbm2(q * 0.9 + vec2f(t * 0.18, 0.0), 3),\n st_fbm2(q * 0.9 + vec2f(0.0, t * 0.15), 3)) * params.warp * 1.6;\n return st_fbm2(q * 1.1 + vec2f(0.0, t * 0.08), 3);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 2.2;\n let t = st_time();\n\n // Fixed sampling offset in field units: a resolution-derived epsilon makes\n // the surface look completely different in a card and in a hero.\n let eps = 0.012;\n let h = height(p, t);\n let hx = height(p + vec2f(eps, 0.0), t);\n let hy = height(p + vec2f(0.0, eps), t);\n\n let slope = params.a * 0.55;\n let n = normalize(vec3f((h - hx) / eps * slope, (h - hy) / eps * slope, 1.0));\n\n let lightDir = normalize(vec3f(cos(t * 0.35) * 0.7, sin(t * 0.35) * 0.7, 0.9));\n let viewDir = vec3f(0.0, 0.0, 1.0);\n let halfway = normalize(lightDir + viewDir);\n\n let diffuse = clamp(dot(n, lightDir), 0.0, 1.0);\n let spec = pow(clamp(dot(n, halfway), 0.0, 1.0), 16.0 + params.b * 180.0);\n let fresnel = pow(1.0 - clamp(dot(n, viewDir), 0.0, 1.0), 4.0);\n\n // Wide, smooth reflection bands. Reflecting the view around the normal and\n // reading a ramp off it is what separates chrome from rough plastic.\n let reflected = reflect(-viewDir, n);\n let band = reflected.y * 1.6 + reflected.x * 0.5 + t * 0.05;\n let env = st_palette(clamp(band * 0.5 + 0.5, 0.0, 1.0));\n\n // A second, tighter band set gives the hard highlight streaks of polish.\n let streak = pow(abs(sin(band * PI * 1.5)), 8.0 + params.b * 40.0);\n\n var col = env * (0.35 + 0.75 * diffuse) * params.c;\n col = col + st_palette(1.0) * streak * 0.6 * params.c;\n col = col + vec3f(1.0) * spec * params.intensity * 1.4;\n col = col + st_palette(0.9) * fresnel * params.d;\n\n return st_post(st_tonemap(col), uv);\n}\n";
11
+ readonly "mesh-gradient": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// mesh-gradient\n// ---------------------------------------------------------------------------\n\n// Mesh gradient — soft colour centres orbiting behind frosted glass.\n\n// A gaussian well, raised to a power so nearby colours dominate rather than\n// averaging. Without the exponent every centre contributes everywhere and the\n// middle of the frame turns grey.\nfn blob(p : vec2f, centre : vec2f, radius : f32) -> f32 {\n let d = length(p - centre);\n let g = exp(-d * d / max(radius * radius, 0.0001));\n return g * g * g;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n\n // Gentle domain warp keeps the blend from looking like plain radial gradients.\n let warpNoise = vec2f(\n st_fbm2(p * 1.6 + vec2f(0.0, t * 0.12), 3),\n st_fbm2(p * 1.6 + vec2f(5.2, t * 0.12 + 1.3), 3)\n );\n p = p + (warpNoise - 0.5) * params.warp;\n\n let r = params.a;\n let spread = params.b;\n\n let c0 = vec2f(cos(t * 0.31) * spread, sin(t * 0.24) * spread * 0.6);\n let c1 = vec2f(cos(t * 0.19 + 2.1) * spread * 0.9, sin(t * 0.37 + 1.1) * spread * 0.7);\n let c2 = vec2f(cos(t * 0.27 + 4.3) * spread * 0.8, sin(t * 0.21 + 3.7) * spread * 0.75);\n let c3 = vec2f(cos(t * 0.15 + 5.6) * spread, sin(t * 0.33 + 0.4) * spread * 0.5);\n\n // Sharpening the weights before normalising is what keeps the colours\n // distinct. Plain gaussian weights average towards grey in the middle.\n let sharpen = 2.6;\n let w0 = pow(blob(p, c0, r), sharpen);\n let w1 = pow(blob(p, c1, r * 0.9), sharpen);\n let w2 = pow(blob(p, c2, r * 1.15), sharpen);\n let w3 = pow(blob(p, c3, r * 0.8), sharpen);\n let total = w0 + w1 + w2 + w3 + 1e-6;\n\n let base = mix(params.colorA.rgb, params.colorB.rgb, 0.5) * 0.25;\n var col = (params.colorA.rgb * w0\n + params.colorB.rgb * w1\n + params.colorC.rgb * w2\n + mix(params.colorA.rgb, params.colorC.rgb, 0.5) * w3) / total;\n\n // Coverage decides how much of the dark backdrop shows through.\n let coverage = clamp(pow(total, 1.0 / sharpen) * params.c, 0.0, 1.0);\n col = mix(base, col, coverage) * params.intensity;\n\n return st_post(col, uv);\n}\n";
12
+ readonly "neon-grid": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// neon-grid\n// ---------------------------------------------------------------------------\n\n// Neon grid — perspective floor, banded sun, horizon haze. Synthwave by the book.\n\nfn gridLines(coord : vec2f, weight : f32) -> f32 {\n let width = max(fwidth(coord), vec2f(0.0001));\n let g = abs(fract(coord - 0.5) - 0.5) / width;\n return 1.0 - smoothstep(0.0, weight, min(g.x, g.y));\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv);\n let horizon = 0.02;\n\n // Floor is evaluated unconditionally: fwidth needs uniform control flow.\n let depth = 1.0 / max(p.y - horizon, 0.004);\n let floorCoord = vec2f(p.x * depth * 0.35, depth * 0.35 - t * params.b);\n let line = gridLines(floorCoord * params.a, 1.8);\n let below = step(horizon, p.y);\n let fade = smoothstep(0.0, 0.09, p.y - horizon) * smoothstep(1.4, 0.12, depth * 0.05);\n\n // Sky above the horizon.\n let skyT = clamp((horizon - p.y) * 2.4, 0.0, 1.0);\n var col = mix(params.colorA.rgb * 0.32, vec3f(0.015, 0.008, 0.05), skyT);\n\n // Sun: a disc sliced by horizontal bands, resting on the horizon.\n let sunCentre = vec2f(0.0, horizon - 0.17);\n let sunDist = length((p - sunCentre) * vec2f(1.0, 1.25));\n let sunMask = smoothstep(0.20, 0.19, sunDist);\n let bands = step(0.38, fract((p.y - sunCentre.y) * 26.0 + t * 0.4))\n * step(sunCentre.y + 0.015, p.y);\n let sunColor = mix(params.colorC.rgb, params.colorB.rgb,\n clamp((p.y - sunCentre.y + 0.2) * 2.5, 0.0, 1.0));\n col = mix(col, sunColor, sunMask * (1.0 - bands));\n col = col + sunColor * smoothstep(0.55, 0.0, sunDist) * 0.3 * params.d;\n\n // Floor: darken the plate, then lay the neon lines over it.\n col = mix(col, col * 0.3, below);\n col = col + params.colorB.rgb * line * fade * below * params.intensity * 1.8;\n\n // Horizon haze.\n col = col + params.colorB.rgb * exp(-abs(p.y - horizon) * 80.0) * params.c;\n\n return st_post(st_tonemap(col), uv);\n}\n";
13
+ readonly plasma: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// plasma\n// ---------------------------------------------------------------------------\n\n// Plasma — stacked sine fields, the oldest trick in the demoscene.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 6.0;\n let t = st_time();\n\n var v = 0.0;\n v = v + sin(p.x * 1.1 + t);\n v = v + sin(p.y * 1.3 + t * 1.17);\n v = v + sin((p.x + p.y) * 0.8 + t * 0.83);\n v = v + sin(length(p * params.a) * 1.4 - t * 1.6);\n\n // Rotating secondary field adds the interference bloom.\n let q = st_rot(t * 0.15 * params.b) * p;\n v = v + sin(q.x * 0.9 - q.y * 1.4 + t * 0.6) * params.c;\n\n let band = v * 0.125 + 0.5 + t * 0.05;\n var col = st_paletteCyclic(band * params.d);\n\n // Contour lines pick out the field's iso-levels.\n let contour = abs(fract(v * 0.9) - 0.5) * 2.0;\n col = col * (1.0 - smoothstep(0.85, 1.0, contour) * 0.55);\n\n return st_post(col * params.intensity, uv);\n}\n";
14
+ readonly ripple: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// ripple\n// ---------------------------------------------------------------------------\n\n// Ripple — interference between travelling circular waves.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n let sourceCount = 5;\n var wave = 0.0;\n var gradient = vec2f(0.0);\n\n for (var i = 0; i < sourceCount; i = i + 1) {\n let fi = f32(i);\n let phase = fi * 2.399963;\n let orbit = 0.28 * params.b;\n let src = vec2f(cos(t * 0.23 + phase) * orbit, sin(t * 0.31 + phase * 1.7) * orbit);\n let delta = p - src;\n let d = length(delta) + 0.0001;\n let k = 40.0 * params.a;\n let decay = 1.0 / (1.0 + d * 6.0);\n let s = sin(d * k - t * 4.0 + phase) * decay;\n wave = wave + s;\n gradient = gradient + (delta / d) * cos(d * k - t * 4.0 + phase) * decay * k;\n }\n\n wave = wave / f32(sourceCount);\n gradient = gradient / f32(sourceCount);\n\n // Treat the wave slope as a refracting surface and read the palette through it.\n let refracted = clamp(0.5 + wave * 1.4 + gradient.x * 0.02 * params.c, 0.0, 1.0);\n var col = st_palette(refracted);\n\n // Caustic highlights where the wavefronts focus.\n let caustic = pow(clamp(1.0 - abs(wave) * 3.2, 0.0, 1.0), 6.0);\n col = col + vec3f(1.0) * caustic * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
15
+ readonly silk: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// silk\n// ---------------------------------------------------------------------------\n\n// Silk — anisotropic sheen across warped fibre lines. Soft, cloth-like.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale;\n\n // Two-stage domain warp gives the fabric its folds.\n let w1 = vec2f(st_fbm2(p * 1.1 + vec2f(0.0, t * 0.08), 4),\n st_fbm2(p * 1.1 + vec2f(3.7, t * 0.06), 4));\n p = p + (w1 - 0.5) * params.warp * 2.0;\n let w2 = st_fbm2(p * 2.6 + vec2f(t * 0.05, 0.0), 5);\n\n // Fibre direction: dense parallel lines bent by the warp.\n let fibres = sin((p.x * 18.0 + p.y * 6.0) * params.a + w2 * 12.0 * params.b);\n let sheen = pow(clamp(fibres * 0.5 + 0.5, 0.0, 1.0), 3.0);\n\n // The base tone comes from the fold height, the highlight from the fibres.\n var col = st_palette(clamp(w2 * 1.4 - 0.1, 0.0, 1.0));\n col = col + st_palette(1.0) * sheen * params.c;\n\n // Slight darkening in the creases sells the depth.\n let crease = smoothstep(0.55, 0.15, w2);\n col = col * (1.0 - crease * params.d * 0.7);\n\n return st_post(col * params.intensity, uv);\n}\n";
16
+ readonly starfield: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// starfield\n// ---------------------------------------------------------------------------\n\n// Starfield — a warp corridor. Layered depth slices flying at the camera.\n\n// Two independent hashes per cell; sharing one seed across layers is what\n// produces the diagonal moiré you get from adding a scalar to both components.\nfn cellRandom(cell : vec2f, layerSeed : f32) -> vec3f {\n let a = st_hash22(cell + vec2f(layerSeed * 37.19, layerSeed * 11.73));\n let b = st_hash21(cell.yx + layerSeed * 91.31);\n return vec3f(a, b);\n}\n\nfn layer(p : vec2f, layerIndex : f32, t : f32) -> vec3f {\n let z = fract(layerIndex * 0.1667 - t * 0.12);\n let scaleByDepth = mix(16.0, 2.0, z);\n let fade = smoothstep(0.0, 0.22, z) * smoothstep(1.0, 0.7, z);\n if (fade <= 0.0) { return vec3f(0.0); }\n\n // Rotating each slice breaks the alignment between neighbouring layers.\n let grid = st_rot(layerIndex * 1.37) * p * scaleByDepth;\n let base = floor(grid);\n let frac = fract(grid) - 0.5;\n\n var acc = vec3f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = cellRandom(base + neighbour, layerIndex);\n if (rnd.z < 0.72) { continue; }\n\n let point = neighbour + rnd.xy - 0.5;\n var offset = frac - point;\n // Motion blur: stretch each star along its radial direction.\n let radial = normalize(p + vec2f(1e-5));\n let along = dot(offset, radial);\n let across = offset - radial * along;\n offset = radial * along / (1.0 + params.b * 8.0 * (1.0 - z)) + across;\n\n // A tight inverse-square core keeps stars as points. Widen the epsilon\n // and six layers of them blur into fog.\n let d = length(offset);\n let brightness = 0.00022 * params.a * (0.35 + rnd.z);\n acc = acc + st_palette(rnd.y) * (brightness / (d * d + 0.000018)) * fade;\n }\n }\n return acc;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n var p = st_centered(uv) * params.scale;\n let t = st_time();\n p = st_rot(t * 0.05 * params.d) * p;\n\n var col = vec3f(0.004, 0.005, 0.012);\n for (var i = 0; i < 6; i = i + 1) {\n col = col + layer(p, f32(i), t);\n }\n\n // Core glow at the vanishing point.\n let core = params.c * 0.05 / (length(p) + 0.06);\n col = col + st_palette(1.0) * core;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
17
+ readonly truchet: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// truchet\n// ---------------------------------------------------------------------------\n\n// Truchet — quarter-arc tiles that flip in place. Hard, graphic, no noise wash.\n\nfn arcTile(f : vec2f, flipped : bool, weight : f32) -> f32 {\n var q = f;\n if (flipped) { q.x = 1.0 - q.x; }\n let d1 = abs(length(q) - 0.5);\n let d2 = abs(length(q - vec2f(1.0, 1.0)) - 0.5);\n let d = min(d1, d2);\n return 1.0 - smoothstep(weight, weight + 0.02, d);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 6.0;\n p = st_rot(t * 0.04 * params.d) * p;\n p = p + vec2f(t * 0.25 * params.c, 0.0);\n\n let base = floor(p);\n let f = fract(p);\n\n // Each tile flips on its own clock.\n let rnd = st_hash21(base);\n let phase = sin(t * 0.9 + rnd * TAU) * 0.5 + 0.5;\n let flipped = (rnd + phase * params.b) > 1.0;\n\n let line = arcTile(f, flipped, params.a * 0.12);\n\n let tint = st_palette(fract(rnd + t * 0.05));\n let bg = params.colorA.rgb * 0.14;\n var col = mix(bg, tint * params.intensity, line);\n\n // A soft bloom off the strokes keeps it from looking flat.\n col = col + tint * line * 0.25;\n\n return st_post(col, uv);\n}\n";
18
+ readonly tunnel: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// tunnel\n// ---------------------------------------------------------------------------\n\n// Tunnel — a raymarched corridor with emissive rings and distance fog.\n\n// The corridor centre wanders as you fly down it. Keep the amplitude well\n// under the radius, or the ray leaves the tube before it has travelled.\nfn tunnelPath(z : f32) -> vec2f {\n return vec2f(sin(z * 0.28) * 0.42, cos(z * 0.21) * 0.34);\n}\n\n// Distance from a point to the wall, measured from inside the tube.\nfn sceneSdf(pos : vec3f, radius : f32, ribs : f32) -> f32 {\n let q = pos.xy - tunnelPath(pos.z);\n let r = length(q);\n let theta = atan2(q.y, q.x);\n let corrugation = sin(pos.z * 2.4) * 0.05 * ribs + sin(theta * 6.0 + pos.z * 0.5) * 0.05 * ribs;\n return radius + corrugation - r;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * 2.0;\n let t = st_time() * 2.0;\n\n let ro = vec3f(tunnelPath(t), t);\n // Aim slightly along the corridor so the walls sweep past rather than\n // sitting still around a fixed vanishing point.\n let ahead = tunnelPath(t + 2.0) - tunnelPath(t);\n let rd = normalize(vec3f(p / max(params.scale, 0.2) + ahead * 0.45, 1.6));\n\n var travelled = 0.05;\n var hit = false;\n for (var i = 0; i < 96; i = i + 1) {\n let d = sceneSdf(ro + rd * travelled, params.a, params.b);\n if (d < 0.004 * travelled) {\n hit = true;\n break;\n }\n travelled = travelled + max(d * 0.7, 0.01);\n if (travelled > 30.0) { break; }\n }\n\n if (!hit) {\n return st_post(vec3f(0.0), uv);\n }\n\n let pos = ro + rd * travelled;\n let q = pos.xy - tunnelPath(pos.z);\n let theta = atan2(q.y, q.x);\n\n // Wall texture in tunnel space: seams around, plates along. Both are thin\n // lines on a dark wall — wide bands read as a kaleidoscope, not a corridor.\n let seams = 1.0 - smoothstep(0.0, 0.16, abs(fract(theta / TAU * 8.0) - 0.5) * 2.0);\n let plates = 1.0 - smoothstep(0.0, 0.2, abs(fract(pos.z * 0.9) - 0.5) * 2.0);\n let grime = st_fbm2(vec2f(theta * 2.4, pos.z * 0.7), 3);\n let surface = clamp(0.12 + grime * 0.35 + max(seams, plates) * 0.55, 0.0, 1.0);\n\n // Cheap normal from the SDF gradient, lit from a lamp at the camera.\n let e = vec2f(0.01, 0.0);\n let n = normalize(vec3f(\n sceneSdf(pos + e.xyy, params.a, params.b) - sceneSdf(pos - e.xyy, params.a, params.b),\n sceneSdf(pos + e.yxy, params.a, params.b) - sceneSdf(pos - e.yxy, params.a, params.b),\n sceneSdf(pos + e.yyx, params.a, params.b) - sceneSdf(pos - e.yyx, params.a, params.b),\n ));\n let lamp = clamp(dot(n, -rd), 0.0, 1.0);\n\n // Falling off with distance as well as with the lamp angle is what makes\n // the near wall read as near.\n let nearness = 1.0 / (1.0 + travelled * 0.35);\n var col = st_palette(clamp(surface, 0.0, 1.0))\n * (0.10 + 1.15 * lamp * nearness) * params.intensity;\n\n // Emissive rings at fixed intervals down the corridor.\n let ring = pow(1.0 - abs(fract(pos.z * 0.5) * 2.0 - 1.0), 20.0);\n col = col + st_palette(1.0) * ring * params.c * 5.0;\n\n // Fog swallows the far end.\n col = col * exp(-travelled * 0.10 * (0.4 + params.d * 2.4));\n\n return st_post(st_tonemap(col), uv);\n}\n";
19
+ readonly voronoi: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// voronoi\n// ---------------------------------------------------------------------------\n\n// Voronoi — animated cells with lit edges. Crystalline, hard-edged.\n\nstruct Cell {\n f1 : f32,\n f2 : f32,\n id : vec2f,\n}\n\nfn voronoi(p : vec2f, t : f32, jitter : f32) -> Cell {\n let base = floor(p);\n let frac = fract(p);\n var f1 = 8.0;\n var f2 = 8.0;\n var id = vec2f(0.0);\n\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = st_hash22(base + neighbour);\n let point = neighbour + 0.5 + (sin(t + rnd * TAU) * 0.5) * jitter;\n let d = length(point - frac);\n if (d < f1) {\n f2 = f1;\n f1 = d;\n id = base + neighbour;\n } else if (d < f2) {\n f2 = d;\n }\n }\n }\n return Cell(f1, f2, id);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 8.0;\n let t = st_time();\n\n let cell = voronoi(p, t, params.a);\n let edge = cell.f2 - cell.f1;\n\n let tone = st_hash21(cell.id);\n var col = st_palette(fract(tone + t * 0.03 * params.d)) * (0.25 + 0.75 * tone);\n\n // Depth: darken the cell interior, then draw the ridge between cells.\n col = col * mix(1.0, smoothstep(0.0, 0.55, cell.f1), params.c);\n\n let ridge = 1.0 - smoothstep(0.0, params.b, edge);\n col = col + st_palette(1.0) * ridge * params.intensity;\n\n return st_post(col, uv);\n}\n";
20
+ };
21
+ export type GeneratedShaderName = keyof typeof shaderSources;
22
+ //# sourceMappingURL=sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../../src/generated/sources.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;CAmBhB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,aAAa,CAAC"}