@mkbabb/glass-ui 10.1.4 → 11.0.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 (214) hide show
  1. package/MIGRATION.md +1024 -1461
  2. package/README.md +2 -2
  3. package/dist/DialogContent-DG5kseRQ.js +159 -0
  4. package/dist/DropdownMenuTrigger-uhSXgEfK.js +76 -0
  5. package/dist/{Surface-1nPoabc3.js → Surface-C7wq2j0A.js} +3 -0
  6. package/dist/aurora.css +1 -0
  7. package/dist/aurora.js +246 -2472
  8. package/dist/auroraImageSource-xNLDUjcy.js +30 -0
  9. package/dist/axes.js +8 -8
  10. package/dist/blob.css +1 -0
  11. package/dist/blob.js +57 -54
  12. package/dist/budget-CJfhLIIV.js +13 -0
  13. package/dist/button-CAKhiOZy.js +113 -0
  14. package/dist/button.js +1 -1
  15. package/dist/{card-ByyqRBvK.js → card-D_uXphvL.js} +4 -1
  16. package/dist/card.js +1 -1
  17. package/dist/carousel.js +3 -2
  18. package/dist/{checkbox-B1qn65se.js → checkbox-COLnvULu.js} +1 -0
  19. package/dist/checkbox.css +1 -0
  20. package/dist/checkbox.js +1 -1
  21. package/dist/{collapsible-DFzK29Oh.js → collapsible-Bd4Et730.js} +1 -0
  22. package/dist/collapsible.css +1 -0
  23. package/dist/collapsible.js +1 -1
  24. package/dist/color-HkUaTAJj.js +270 -0
  25. package/dist/{color.glsl-DTJhdekG.js → color.glsl-bFJm8k5R.js} +4 -16
  26. package/dist/color.wgsl-B-qDJ_OM.js +4 -0
  27. package/dist/{command-CatrAsrl.js → command-BsFOGZwB.js} +3 -2
  28. package/dist/command.css +1 -0
  29. package/dist/command.js +1 -1
  30. package/dist/component-styles.css +0 -1
  31. package/dist/components/_shared/axes.d.ts +7 -0
  32. package/dist/components/_shared/feedback/dot-ring.css +1 -1
  33. package/dist/components/_shared/overlay/focus-return.d.ts +9 -0
  34. package/dist/components/_shared/overlay/index.d.ts +3 -0
  35. package/dist/components/_shared/overlay/root-scroll-lock.d.ts +4 -0
  36. package/dist/components/_shared/overlay/sheet-width.d.ts +2 -0
  37. package/dist/components/_shared/shape/useBarShape.d.ts +4 -0
  38. package/dist/components/aurora/composables/auroraFallbackGround.d.ts +12 -0
  39. package/dist/components/aurora/constants/presets.d.ts +21 -0
  40. package/dist/components/aurora/index.d.ts +2 -2
  41. package/dist/components/button/styles.css +1 -1
  42. package/dist/components/dark-mode-toggle/dark-mode-toggle.css +1 -1
  43. package/dist/components/dialog/DialogContent.vue.d.ts +2 -2
  44. package/dist/components/dialog/ModalOverlay.vue.d.ts +2 -2
  45. package/dist/components/dialog/ModalScrim.vue.d.ts +26 -0
  46. package/dist/components/dialog/styles.css +1 -1
  47. package/dist/components/dock/GlassDock.vue.d.ts +4 -1
  48. package/dist/components/dock/composables/useDockClickIntegrity.d.ts +4 -0
  49. package/dist/components/dock/composables/useDockShellProps.d.ts +34 -2
  50. package/dist/components/dock/composables/useDockState.d.ts +5 -0
  51. package/dist/components/dock/index.d.ts +1 -2
  52. package/dist/components/dock/styles/controls/icon-button.css +1 -1
  53. package/dist/components/dock/styles/controls/tab-button.css +1 -1
  54. package/dist/components/dock/styles/morph.css +1 -1
  55. package/dist/components/dock/styles/run.css +1 -1
  56. package/dist/components/dock/trigger.d.ts +1 -0
  57. package/dist/components/expandable-container/ExpandableContainer.vue.d.ts +30 -29
  58. package/dist/components/menu/ContextMenu.vue.d.ts +27 -0
  59. package/dist/components/menu/DropdownMenu.vue.d.ts +5 -23
  60. package/dist/components/menu/context.d.ts +18 -5
  61. package/dist/components/menu/family-click.d.ts +2 -0
  62. package/dist/components/menu/family-context.d.ts +2 -0
  63. package/dist/components/menu/index.d.ts +1 -1
  64. package/dist/components/sheet/SheetContent.vue.d.ts +2 -2
  65. package/dist/components/sortable-list/styles.css +1 -0
  66. package/dist/components/surface/Surface.vue.d.ts +4 -1
  67. package/dist/components/tabs/composables/useTabDragMorph.d.ts +4 -7
  68. package/dist/components/tabs/styles/segmented.css +1 -1
  69. package/dist/components/tabs/types.d.ts +7 -8
  70. package/dist/composables/glass/fieldPaint.d.ts +4 -0
  71. package/dist/composables/glass/useGlassBackdropLuminance.d.ts +7 -6
  72. package/dist/composables/glass/webgpu/useGpuSubstrate.d.ts +8 -0
  73. package/dist/composables/motion/core/motionTempo.d.ts +2 -1
  74. package/dist/composables/motion/morph/useDragMorph.d.ts +2 -2
  75. package/dist/composables/motion/morph/useSelectionGroup.d.ts +6 -25
  76. package/dist/composables/motion/morph/useSelectionIndicator.d.ts +8 -54
  77. package/dist/composables/motion/spring/useLiquidFlex.d.ts +3 -5
  78. package/dist/composables/motion/spring/useSpringMount.d.ts +0 -9
  79. package/dist/{configurator-Dl10mc3L.js → configurator-C1ZKEf1b.js} +7 -5
  80. package/dist/configurator.css +1 -0
  81. package/dist/configurator.js +1 -1
  82. package/dist/constants-DfrO-IaH.js +8 -0
  83. package/dist/constellation.css +1 -0
  84. package/dist/constellation.js +2 -1
  85. package/dist/control.css +1 -0
  86. package/dist/{control.css_vue_type_style_index_0_src_true_lang-BIdufaDG.js → control.css_vue_type_style_index_0_src_true_lang-D2hdtjs4.js} +1 -0
  87. package/dist/{createCanvasLifecycle-hJHobVTn.js → createCanvasLifecycle-DS0vEs_G.js} +76 -75
  88. package/dist/dark-mode-toggle.js +1 -1
  89. package/dist/{data-table-DQuWkiwL.js → data-table-lDykVYuo.js} +1 -0
  90. package/dist/data-table.css +1 -0
  91. package/dist/data-table.js +1 -1
  92. package/dist/{deck-Cm_VmyNL.js → deck-Bc-2mEKE.js} +1 -0
  93. package/dist/deck.js +1 -1
  94. package/dist/dialog.js +1 -1
  95. package/dist/dock-trigger.d.ts +1 -0
  96. package/dist/dock-trigger.js +88 -0
  97. package/dist/dock.js +677 -689
  98. package/dist/easing.js +4 -4
  99. package/dist/expandable-container-wS1awXGd.js +135 -0
  100. package/dist/expandable-container.css +1 -0
  101. package/dist/expandable-container.js +1 -1
  102. package/dist/{fading-scroll-ClCPooXb.js → fading-scroll-D3Y20ibL.js} +1 -1
  103. package/dist/fading-scroll.js +1 -1
  104. package/dist/fieldPaint-Ci77oajZ.js +7 -0
  105. package/dist/fourier-field.css +1 -0
  106. package/dist/fourier-field.js +15 -13
  107. package/dist/glSetup-DMaDgrEj.js +577 -0
  108. package/dist/glass-ui.js +66 -65
  109. package/dist/handmark.css +1 -0
  110. package/dist/handmark.js +82 -71
  111. package/dist/index.css +1 -0
  112. package/dist/index.d.ts +1 -1
  113. package/dist/{input-eLZ_AU7l.js → input-BLF2pMHE.js} +1 -1
  114. package/dist/input.js +1 -1
  115. package/dist/{label-CE03H1KL.js → label-ryNPL7e6.js} +1 -0
  116. package/dist/label.css +1 -0
  117. package/dist/label.js +1 -1
  118. package/dist/labeled-field.css +1 -0
  119. package/dist/labeled-field.js +5 -4
  120. package/dist/{menu-DaYAZiVW.js → menu-C_l2Sslf.js} +162 -112
  121. package/dist/menu.js +3 -3
  122. package/dist/metric.js +1 -0
  123. package/dist/motion-core.js +138 -44
  124. package/dist/motion-lHDAQHEE.js +91 -0
  125. package/dist/motion.js +5 -5
  126. package/dist/motionTempo-DXdVbCWr.js +13 -0
  127. package/dist/music-staff.js +1 -0
  128. package/dist/{number-field-RP89046a.js → number-field-BXFUP49Z.js} +3 -2
  129. package/dist/number-field.css +1 -0
  130. package/dist/number-field.js +1 -1
  131. package/dist/pager-dots.css +1 -0
  132. package/dist/pager-dots.js +3 -2
  133. package/dist/participation-C5y5VZTN.js +52 -0
  134. package/dist/{popover-BdpoE4Ar.js → popover-DQUmwuAa.js} +3 -2
  135. package/dist/popover.js +1 -1
  136. package/dist/{progress-eVa-mc7J.js → progress-BUn0Bw7Z.js} +1 -0
  137. package/dist/progress.css +1 -0
  138. package/dist/progress.js +1 -1
  139. package/dist/{radio-group-B58Bz16F.js → radio-group-xdEy2tdi.js} +1 -0
  140. package/dist/radio-group.css +1 -0
  141. package/dist/radio-group.js +1 -1
  142. package/dist/root-scroll-lock-DJFrQhMa.js +22 -0
  143. package/dist/select-Dt5dXm87.js +309 -0
  144. package/dist/select.js +1 -1
  145. package/dist/{separator-D4EZM10u.js → separator-C4y9p_ky.js} +1 -0
  146. package/dist/separator.css +1 -0
  147. package/dist/separator.js +1 -1
  148. package/dist/sheet-DLqNUIZl.js +347 -0
  149. package/dist/sheet-width-DgXgJNa5.js +18 -0
  150. package/dist/sheet.js +2 -2
  151. package/dist/{slider-5uoY-cLG.js → slider-CZiKrWGg.js} +8 -8
  152. package/dist/slider.js +1 -1
  153. package/dist/sortable-list.js +2 -1
  154. package/dist/{status-dot-W4LFiJps.js → status-dot-BDZKcRxy.js} +2 -1
  155. package/dist/status-dot.css +1 -0
  156. package/dist/status-dot.js +1 -1
  157. package/dist/styles/components.css +1 -1
  158. package/dist/styles/glass/glass-capsule.css +1 -1
  159. package/dist/styles/glass/glass-chip.css +1 -1
  160. package/dist/styles/glass/mark.css +1 -1
  161. package/dist/styles/glass/overlay-plate.css +1 -1
  162. package/dist/styles/glass/surface-axis.css +1 -1
  163. package/dist/styles/index.css +1 -1
  164. package/dist/styles/theme/radius.css +1 -1
  165. package/dist/styles/tokens/color-radius.css +1 -1
  166. package/dist/styles/tokens/manifest.d.ts +1 -1
  167. package/dist/styles/tokens/scale-paper.css +1 -1
  168. package/dist/styles/utilities/base-misc.css +1 -1
  169. package/dist/surface.js +1 -1
  170. package/dist/{switch-BYUZ6g9G.js → switch-DLfGYU35.js} +1 -0
  171. package/dist/switch.css +1 -0
  172. package/dist/switch.js +1 -1
  173. package/dist/tabs-NgTRPGZx.js +229 -0
  174. package/dist/tabs.js +1 -1
  175. package/dist/textarea.js +1 -1
  176. package/dist/timeline.css +1 -0
  177. package/dist/timeline.js +4 -3
  178. package/dist/toggle-group-DgXcJGD6.js +129 -0
  179. package/dist/toggle-group.css +1 -0
  180. package/dist/toggle-group.js +1 -1
  181. package/dist/tokens.js +2 -0
  182. package/dist/{tooltip-CZVubnJf.js → tooltip-Do3qCw0r.js} +1 -1
  183. package/dist/tooltip.js +1 -1
  184. package/dist/typewriter.css +1 -0
  185. package/dist/typewriter.js +1 -0
  186. package/dist/uniformBridge-CJyy7NOn.js +150 -0
  187. package/dist/useBarShape-CmP35Z8o.js +24 -0
  188. package/dist/{useCanvas2D-CQRElMyE.js → useCanvas2D-Bhpwua98.js} +1 -1
  189. package/dist/{useDockCtaReceive-BSSgD6mu.js → useDockCtaReceive-Dn_-paqP.js} +1 -1
  190. package/dist/{useDragMorph-B363RUbF.js → useDragMorph-BEI74U0t.js} +51 -40
  191. package/dist/{color.wgsl-BTszLY39.js → useGpuSubstrate-Y5XAdU8d.js} +7 -7
  192. package/dist/{useLiquidPress-h1scrPtP.js → useLiquidPress-D5mjZC6U.js} +2 -2
  193. package/dist/useScrollChrome-DMP6kDI9.js +67 -0
  194. package/dist/useSelectionGroup-DAQv5Lb5.js +186 -0
  195. package/dist/{useSpring-9u2_shxV.js → useSpring-Aof0nTRt.js} +1 -1
  196. package/dist/useSpringMount-jgNkymRl.js +73 -0
  197. package/dist/wgpuSetup-CFuqd_RP.js +1278 -0
  198. package/package.json +8 -1
  199. package/dist/DialogContent-CwIBvou_.js +0 -157
  200. package/dist/DropdownMenuTrigger-CKRnhDSQ.js +0 -93
  201. package/dist/button-BOFTUPyU.js +0 -110
  202. package/dist/composables/motion/morph/eyeglass.d.ts +0 -141
  203. package/dist/expandable-container-CZUj9BzY.js +0 -142
  204. package/dist/glass-ui.css +0 -2
  205. package/dist/motion-BvV1_24p.js +0 -61
  206. package/dist/motionTempo-tscp92Nm.js +0 -7
  207. package/dist/participation-B7UFdgXB.js +0 -55
  208. package/dist/select-BiF0FCZf.js +0 -292
  209. package/dist/sheet-DmJxgAHc.js +0 -345
  210. package/dist/tabs-C5sC0fBE.js +0 -269
  211. package/dist/toggle-group-CC5b3o1R.js +0 -125
  212. package/dist/useScrollChrome-BJbWV32F.js +0 -162
  213. package/dist/useSelectionGroup-zsEwitF6.js +0 -291
  214. package/dist/useSpringMount-OA-0mTNv.js +0 -73
@@ -0,0 +1,1278 @@
1
+ import { h as e, m as t, n, v as r } from "./color-HkUaTAJj.js";
2
+ import { i, n as a, r as o, t as s } from "./color.wgsl-B-qDJ_OM.js";
3
+ import { a as c, n as l, r as u, s as d, t as f } from "./uniformBridge-CJyy7NOn.js";
4
+ //#endregion
5
+ //#region src/components/aurora/constants/shaders/aurora.wgsl.ts
6
+ var p = `
7
+ const MAX_NUCLEI: i32 = 8;
8
+ const MAX_STOPS: i32 = 8;
9
+ const PI: f32 = 3.141592653589793;
10
+
11
+ // ── Uniforms (the typed-struct source-of-truth — see aurora.wgsl.uniforms.ts) ──
12
+ // Scalars are packed into vec4 lanes to keep the std140/WGSL 16-byte struct alignment
13
+ // trivial. nuc0[i] = (pos.x, pos.y, radius, paletteBias); nuc1[i] = (valueBias,
14
+ // driftRadius, driftPhase, elong); nuc2[i] = (angle, _, _, _). palette[i].rgb is the
15
+ // CPU-baked linear-sRGB stop.
16
+ struct Uniforms {
17
+ // scalars0: (uTime, uSoftmaxBeta, uValueVariance, uWarpAmount)
18
+ scalars0: vec4<f32>,
19
+ // scalars1: (uWarpScale, uWarpDrift, uSaturation, uPaperGrain)
20
+ scalars1: vec4<f32>,
21
+ // scalars2: (uAlpha, uNucleiDrift, uPaletteDrift, uBreathDepth)
22
+ scalars2: vec4<f32>,
23
+ // scalars3: (uBreathPeriod, uCursorStrength, uCursorRadius, uVividness)
24
+ // The .w pad lane now carries the chroma-floor strength.
25
+ scalars3: vec4<f32>,
26
+ // cursor: (uCursor.x, uCursor.y, uMetalPolish, uMetalHeightScale)
27
+ // The two free pad lanes carry the metal-medium knobs (the
28
+ // metal light is cursor-synthesized so it reads in-struct on the WGSL primary too).
29
+ cursor: vec4<f32>,
30
+ // ints: (uStopCount, uNucleiCount, uWarpMode, uNoiseOctaves)
31
+ ints0: vec4<i32>,
32
+ // ints1: (uMedium, uHuePath, _, _)
33
+ ints1: vec4<i32>,
34
+ palette: array<vec4<f32>, 8>,
35
+ nuc0: array<vec4<f32>, 8>,
36
+ nuc1: array<vec4<f32>, 8>,
37
+ nuc2: array<vec4<f32>, 8>,
38
+ // The painterly-medium scalar lanes are appended after the
39
+ // arrays so EVERY existing offset (and the smooth-default parity capture) is
40
+ // byte-identical (these lanes are written 0 on a smooth config). scalars4:
41
+ // (uStrokeAmount, uStrokeScale, uStrokeAnisotropy, uCanvasGrain). scalars5:
42
+ // (uWetEdge, uGranulation, uBrokenColor, uKuwaharaQ). kuwahara: (uKuwaharaRadius,
43
+ // uKuwaharaSectors, _, _) — the Kuwahara radius/sectors/q in lockstep with
44
+ // packAuroraWGPUUniforms (the typed-struct discipline).
45
+ scalars4: vec4<f32>,
46
+ scalars5: vec4<f32>,
47
+ kuwahara: vec4<f32>,
48
+ };
49
+
50
+ @group(0) @binding(0) var<uniform> u: Uniforms;
51
+
52
+ // ── Authoring-scale → rad/sec lifts (byte-identical to aurora.frag.ts) ──
53
+ const K_NUCLEI: f32 = 14.0;
54
+ const K_PAL: f32 = 24.0;
55
+ const K_WARP: f32 = 5.0;
56
+
57
+ // ── Noise (Aurora-local hash21/vnoise/fbm, deliberately distinct) ──────────
58
+ fn hash21(p0: vec2<f32>) -> f32 {
59
+ var p = fract(p0 * vec2<f32>(123.34, 456.21));
60
+ p = p + dot(p, p + 45.32);
61
+ return fract(p.x * p.y);
62
+ }
63
+
64
+ fn hash22(p0: vec2<f32>) -> vec2<f32> {
65
+ let p = vec2<f32>(dot(p0, vec2<f32>(127.1, 311.7)),
66
+ dot(p0, vec2<f32>(269.5, 183.3)));
67
+ return fract(sin(p) * 43758.5453);
68
+ }
69
+
70
+ fn vnoise(p: vec2<f32>) -> f32 {
71
+ let i = floor(p);
72
+ let f = fract(p);
73
+ let uu = f * f * (3.0 - 2.0 * f);
74
+ let a = hash21(i);
75
+ let b = hash21(i + vec2<f32>(1.0, 0.0));
76
+ let c = hash21(i + vec2<f32>(0.0, 1.0));
77
+ let d = hash21(i + vec2<f32>(1.0, 1.0));
78
+ return mix(mix(a, b, uu.x), mix(c, d, uu.x), uu.y);
79
+ }
80
+
81
+ ${s}
82
+
83
+ fn fbm(p0: vec2<f32>) -> f32 {
84
+ var v = 0.0;
85
+ var a = 0.5;
86
+ var p = p0;
87
+ let octaves = u.ints0.w;
88
+ for (var i = 0; i < 5; i = i + 1) {
89
+ if (i >= octaves) { break; }
90
+ v = v + a * vnoise(p);
91
+ p = FBM_ROT * p * 2.02;
92
+ a = a * 0.5;
93
+ }
94
+ return v;
95
+ }
96
+
97
+ fn potentialFBM(p: vec2f) -> f32 { return fbm(p); }
98
+
99
+ const CURL_EPS: f32 = 0.012;
100
+
101
+ // ∇×ψ in 2D — the divergence-free flow direction. Central finite differences of the
102
+ // scalar potential give the partials; the cross-pairing (∂y, −∂x) is the 2D curl.
103
+ fn curlFBM(p: vec2f) -> vec2f {
104
+ let dx = potentialFBM(p + vec2f(CURL_EPS, 0.0)) - potentialFBM(p - vec2f(CURL_EPS, 0.0));
105
+ let dy = potentialFBM(p + vec2f(0.0, CURL_EPS)) - potentialFBM(p - vec2f(0.0, CURL_EPS));
106
+ // ψ-gradient is (dx, dy)/(2·eps); the curl rotates it 90°: (∂ψ/∂y, −∂ψ/∂x).
107
+ let g = vec2f(dx, dy) / (2.0 * CURL_EPS);
108
+ return vec2f(g.y, -g.x);
109
+ }
110
+
111
+
112
+ ${a}
113
+ ${o}
114
+ ${i}
115
+
116
+ // Interleaved Gradient Noise (Jimenez) — display-space 1-LSB dither.
117
+ fn ign(p: vec2<f32>) -> f32 {
118
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
119
+ }
120
+
121
+ // Cellular, Worley f1.
122
+ fn cellular(p: vec2<f32>) -> f32 {
123
+ let i = floor(p);
124
+ let f = fract(p);
125
+ var m = 1e9;
126
+ for (var y = -1; y <= 1; y = y + 1) {
127
+ for (var x = -1; x <= 1; x = x + 1) {
128
+ let g = vec2<f32>(f32(x), f32(y));
129
+ let o = hash22(i + g);
130
+ let r = g + o - f;
131
+ m = min(m, dot(r, r));
132
+ }
133
+ }
134
+ return sqrt(m);
135
+ }
136
+
137
+ // ── Warp (Quilez double warp + the four warp modes + the cursor swirl) ──
138
+ fn domainWarp(p: vec2<f32>, t: f32) -> vec2<f32> {
139
+ let warpScale = u.scalars1.x;
140
+ let warpDrift = u.scalars1.y;
141
+ let warpAmount = u.scalars0.w;
142
+ let warpMode = u.ints0.z;
143
+ let cursorStrength = u.scalars3.y;
144
+ let cursorRadius = u.scalars3.z;
145
+ let cursor = u.cursor.xy;
146
+
147
+ let q = vec2<f32>(fbm(p * warpScale + vec2<f32>(0.0, 0.0) + t * warpDrift * K_WARP),
148
+ fbm(p * warpScale + vec2<f32>(5.2, 1.3) + t * warpDrift * K_WARP));
149
+ let r = vec2<f32>(fbm(p * warpScale + 4.0 * q + vec2<f32>(1.7, 9.2)),
150
+ fbm(p * warpScale + 4.0 * q + vec2<f32>(8.3, 2.8)));
151
+
152
+ var warp = r;
153
+ if (warpMode == 1) {
154
+ // cellular — chunky territories
155
+ let c1 = cellular(p * warpScale * 1.5 + vec2<f32>(t * warpDrift * K_WARP * 2.0, 0.0));
156
+ let c2 = cellular(p * warpScale * 1.5 + vec2<f32>(11.0, 7.0 + t * warpDrift * K_WARP * 2.0));
157
+ warp = vec2<f32>(c1, c2);
158
+ } else if (warpMode == 2) {
159
+ // hybrid — fbm + cellular averaged
160
+ let c1 = cellular(p * warpScale * 1.2);
161
+ let c2 = cellular(p * warpScale * 1.2 + vec2<f32>(11.0, 7.0));
162
+ warp = mix(r, vec2<f32>(c1, c2), 0.5);
163
+ } else if (warpMode == 3) {
164
+ let fp = p * warpScale + vec2<f32>(t * warpDrift * K_WARP);
165
+ warp = curlFBM(fp);
166
+ }
167
+ var warped = p + warpAmount * warp;
168
+
169
+ // Cursor swirl — rotate p around the cursor with radial falloff.
170
+ if (cursorStrength > 0.001) {
171
+ let toP = p - cursor;
172
+ let d = length(toP);
173
+ let rr = max(cursorRadius, 0.01);
174
+ let w = exp(-(d * d) / (rr * rr * 0.45));
175
+ let ang = w * cursorStrength * 2.1;
176
+ let ca = cos(ang);
177
+ let sa = sin(ang);
178
+ var rotated = vec2<f32>(ca * toP.x - sa * toP.y, sa * toP.x + ca * toP.y) + cursor;
179
+ let pinch = w * cursorStrength * 0.08;
180
+ rotated = mix(rotated, cursor, pinch);
181
+ warped = mix(warped, rotated + warpAmount * warp * 0.7, w * cursorStrength);
182
+ }
183
+ return warped;
184
+ }
185
+
186
+ // ── Palette LUT ──
187
+ fn samplePalette(id0: f32) -> vec3<f32> {
188
+ let id = clamp(id0, 0.0, 1.0);
189
+ let stopCount = u.ints0.x;
190
+ let scaled = id * f32(stopCount - 1);
191
+ let i0 = i32(floor(scaled));
192
+ let i1 = min(i0 + 1, stopCount - 1);
193
+ let t = fract(scaled);
194
+ return samplePaletteRamp(u.palette[i0].rgb, u.palette[i1].rgb, t, u.ints1.y);
195
+ }
196
+
197
+ // ── Nuclei field (anisotropic Gaussian softmax + drift + palette drift) ──
198
+ struct NucleiResult { paletteId: f32, valueMod: f32 };
199
+
200
+ fn nucleiField(p: vec2<f32>, t: f32) -> NucleiResult {
201
+ let nucleiCount = u.ints0.y;
202
+ let nucleiDrift = u.scalars2.y;
203
+ let paletteDrift = u.scalars2.z;
204
+ let softmaxBeta = u.scalars0.y;
205
+
206
+ var accumBias = 0.0;
207
+ var accumValue = 0.0;
208
+ var accumW = 0.0;
209
+ for (var i = 0; i < MAX_NUCLEI; i = i + 1) {
210
+ if (i >= nucleiCount) { break; }
211
+ let n0 = u.nuc0[i]; // pos.x, pos.y, radius, paletteBias
212
+ let n1 = u.nuc1[i]; // valueBias, driftRadius, driftPhase, elong
213
+ let angle = u.nuc2[i].x;
214
+ let driftRadius = n1.y;
215
+ let driftPhase = n1.z;
216
+ let posI = n0.xy + driftRadius * vec2<f32>(
217
+ cos(t * nucleiDrift * K_NUCLEI + driftPhase),
218
+ sin(t * nucleiDrift * K_NUCLEI + driftPhase * 1.13)
219
+ );
220
+ let diff = p - posI;
221
+ let ca = cos(angle);
222
+ let sa = sin(angle);
223
+ let local = vec2<f32>(ca * diff.x + sa * diff.y, -sa * diff.x + ca * diff.y);
224
+ let along = local.x / max(n1.w, 0.01);
225
+ let across = local.y;
226
+ let d2 = along * along + across * across;
227
+ let r = max(n0.z, 0.01);
228
+ let w = exp(-softmaxBeta * d2 / (r * r));
229
+ accumBias = accumBias + w * n0.w;
230
+ accumValue = accumValue + w * n1.x;
231
+ accumW = accumW + w;
232
+ }
233
+ var paletteId = accumBias / max(accumW, 1e-4);
234
+ let valueMod = accumValue / max(accumW, 1e-4);
235
+
236
+ let palAmp = clamp(paletteDrift * 6.0, 0.0, 0.06);
237
+ paletteId = paletteId + palAmp * sin(t * paletteDrift * K_PAL);
238
+ paletteId = clamp(paletteId, 0.0, 1.0);
239
+ return NucleiResult(paletteId, valueMod);
240
+ }
241
+
242
+ // ── Color utils (OKLCh saturation) ──
243
+ fn linOklab(lin: vec3<f32>) -> vec3<f32> {
244
+ let lms = LINEAR_SRGB_TO_LMS * lin;
245
+ return LMS_TO_OKLAB * cbrt3(lms);
246
+ }
247
+
248
+ fn saturate3(c: vec3<f32>, amt: f32) -> vec3<f32> {
249
+ var lch = oklabToOklch(linOklab(c));
250
+ lch.y = max(lch.y * amt, 0.0);
251
+ return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3<f32>(0.0));
252
+ }
253
+
254
+ // The chroma floor, the WGSL twin of the GLSL vividnessFloor.
255
+ // OKLab chroma lifted toward the floor, hue + L preserved; below VIVID_EPS the (a,b)
256
+ // direction is precision noise so the lift synthesizes along the WARM anchor (amber,
257
+ // both components positive) — never teal/navy. vividness (scalars3.w) == 0 is a no-op
258
+ // (the byte-identity opt-out). Mode factor lifts a dim field a touch (read through glass).
259
+ const VIVID_TARGET: f32 = 0.115;
260
+ const VIVID_EPS: f32 = 0.012;
261
+ const VIVID_WARM_ANCHOR: vec2<f32> = vec2<f32>(0.34202, 0.93969);
262
+ fn vividnessFloor(c: vec3<f32>, vividness: f32) -> vec3<f32> {
263
+ if (vividness <= 0.0001) { return c; }
264
+ var lab = linOklab(c);
265
+ let C = length(lab.yz);
266
+ let modeLift = mix(1.18, 1.0, clamp(lab.x * 1.4, 0.0, 1.0));
267
+ let Cmin = vividness * VIVID_TARGET * modeLift;
268
+ var hueDir = VIVID_WARM_ANCHOR;
269
+ if (C > VIVID_EPS) { hueDir = lab.yz / C; }
270
+ let yz = hueDir * max(C, Cmin);
271
+ lab = vec3<f32>(lab.x, yz.x, yz.y);
272
+ return max(oklabToLinearSrgb(lab), vec3<f32>(0.0));
273
+ }
274
+
275
+ // ── PBR-Neutral tonemap (named aces() in the GLSL — the tonemap slot) ──
276
+ fn aces(color0: vec3<f32>) -> vec3<f32> {
277
+ let startCompression = 0.8 - 0.04;
278
+ let desaturation = 0.15;
279
+ var color = color0;
280
+ let x = min(color.r, min(color.g, color.b));
281
+ var offset = 0.04;
282
+ if (x < 0.08) { offset = x - 6.25 * x * x; }
283
+ color = color - offset;
284
+ let peak = max(color.r, max(color.g, color.b));
285
+ if (peak < startCompression) { return clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)); }
286
+ let d = 1.0 - startCompression;
287
+ let newPeak = 1.0 - d * d / (peak + d - startCompression);
288
+ color = color * (newPeak / peak);
289
+ let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
290
+ return clamp(mix(color, newPeak * vec3<f32>(1.0), g), vec3<f32>(0.0), vec3<f32>(1.0));
291
+ }
292
+
293
+ // ── Painterly-medium bodies (the keystone WGSL port) ───────────────────────
294
+ // Spliced HERE, after every helper the mediums consume is defined (domainWarp,
295
+ // nucleiField, samplePalette, linOklab, saturate3, oklabToOklch/oklchToOklab,
296
+ // hash21/vnoise/fbm) and before the vertex/fragment stages. applyMedium(col,p,t)
297
+ // is the dispatch the fragment stage calls.
298
+
299
+ // ── Painterly luma + medium scalar accessors ───────────────────────────────
300
+ const W_LUMA: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
301
+
302
+ fn mStrokeAmount() -> f32 { return u.scalars4.x; }
303
+ fn mStrokeScale() -> f32 { return u.scalars4.y; }
304
+ fn mStrokeAnisotropy() -> f32 { return u.scalars4.z; }
305
+ fn mCanvasGrain() -> f32 { return u.scalars4.w; }
306
+ fn mWetEdge() -> f32 { return u.scalars5.x; }
307
+ fn mGranulation() -> f32 { return u.scalars5.y; }
308
+ fn mBrokenColor() -> f32 { return u.scalars5.z; }
309
+ fn mKuwaharaQ() -> f32 { return u.scalars5.w; }
310
+ fn mKuwaharaRadius() -> f32 { return u.kuwahara.x; }
311
+ // The metal-medium knobs ride the free cursor.z/.w pad lanes
312
+ // (uniformBridgeWGPU packs them; written 0 on a non-metal config → no-op). These read
313
+ // the IN-STRUCT cursor — the metal light is cursor-synthesized so it crosses to WGSL
314
+ // (a phantom uLightDir read would be flat on the primary; uLightDir is fragment-only).
315
+ fn mMetalPolish() -> f32 { return u.cursor.z; }
316
+ fn mMetalHeightScale() -> f32 { return u.cursor.w; }
317
+
318
+ // ── Gradient pack (the WGSL twin) ──────────────────────────────────────────
319
+ // structureTensorField re-plumbs its already-computed luma gradient (Gx,Gy) out
320
+ // through the return value's .w lane so the metal medium pays ZERO extra taps. 12 bits per
321
+ // bounded component ([-4,4]) fit into one f32; the crayon/kuwahara callers read only .xy/.z.
322
+ fn packGrad(gx: f32, gy: f32) -> f32 {
323
+ let qx = floor(clamp(gx * 0.125 + 0.5, 0.0, 1.0) * 4095.0);
324
+ let qy = floor(clamp(gy * 0.125 + 0.5, 0.0, 1.0) * 4095.0);
325
+ return qx + qy * 4096.0;
326
+ }
327
+ fn unpackGrad(packed: f32) -> vec2<f32> {
328
+ let qy = floor(packed * (1.0 / 4096.0));
329
+ let qx = packed - qy * 4096.0;
330
+ return (vec2<f32>(qx, qy) * (1.0 / 4095.0) - 0.5) * 8.0;
331
+ }
332
+
333
+ // ── Base color re-sample (the edge-mask operator the mediums Sobel/sector-sample) ──
334
+ // The WGSL twin of mediums.glsl.ts sampleBase: warp -> nuclei field -> palette -> value.
335
+ fn sampleBase(p: vec2<f32>, t: f32) -> vec3<f32> {
336
+ let pw = domainWarp(p, t);
337
+ let nf = nucleiField(pw, t);
338
+ var c = samplePalette(nf.paletteId);
339
+ c = c * (1.0 + u.scalars0.z * nf.valueMod);
340
+ return c;
341
+ }
342
+
343
+ // ── Broken color (hue + chroma jitter at constant OKLCh L) ──────────────────
344
+ fn brokenColorJitter(c: vec3<f32>, hueSeed: f32, valueSeed: f32, strength: f32) -> vec3<f32> {
345
+ let amt = clamp(mBrokenColor() * strength, 0.0, 1.0);
346
+ if (amt <= 0.001) { return c; }
347
+ var lch = oklabToOklch(linOklab(c));
348
+ lch.z = lch.z + (hueSeed - 0.5) * (32.0 * PI / 180.0) * amt;
349
+ lch.y = max(lch.y * (1.0 + (valueSeed - 0.5) * 0.28 * amt), 0.0);
350
+ return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3<f32>(0.0));
351
+ }
352
+
353
+ // ── Structure tensor / edge-tangent flow (the WGSL twin) ───────────────────
354
+ // Returns vec4(tangent.x, tangent.y, A, packGrad(Gx,Gy)) — the minor-eigenvector
355
+ // edge-tangent + coherence + the metal medium's packed gradient. The .xy/.z
356
+ // crayon/kuwahara callers are byte-unchanged.
357
+ fn structureTensorField(p: vec2<f32>, t: f32, fallbackDir: vec2<f32>) -> vec4<f32> {
358
+ let e = 0.0035;
359
+ let l00 = dot(sampleBase(p + vec2<f32>(-e, -e), t), W_LUMA);
360
+ let l10 = dot(sampleBase(p + vec2<f32>(0.0, -e), t), W_LUMA);
361
+ let l20 = dot(sampleBase(p + vec2<f32>( e, -e), t), W_LUMA);
362
+ let l01 = dot(sampleBase(p + vec2<f32>(-e, 0.0), t), W_LUMA);
363
+ let l21 = dot(sampleBase(p + vec2<f32>( e, 0.0), t), W_LUMA);
364
+ let l02 = dot(sampleBase(p + vec2<f32>(-e, e), t), W_LUMA);
365
+ let l12 = dot(sampleBase(p + vec2<f32>(0.0, e), t), W_LUMA);
366
+ let l22 = dot(sampleBase(p + vec2<f32>( e, e), t), W_LUMA);
367
+ let Gx = (l20 + 2.0 * l21 + l22) - (l00 + 2.0 * l01 + l02);
368
+ let Gy = (l02 + 2.0 * l12 + l22) - (l00 + 2.0 * l10 + l20);
369
+ let Jxx = Gx * Gx;
370
+ let Jyy = Gy * Gy;
371
+ let Jxy = Gx * Gy;
372
+ let tr = Jxx + Jyy;
373
+ let disc = sqrt(max((Jxx - Jyy) * (Jxx - Jyy) + 4.0 * Jxy * Jxy, 0.0));
374
+ let lambdaMaj = 0.5 * (tr + disc);
375
+ let lambdaMin = 0.5 * (tr - disc);
376
+ var tangent: vec2<f32>;
377
+ if (disc < 1e-7) {
378
+ tangent = fallbackDir;
379
+ } else {
380
+ let theta = 0.5 * atan2(2.0 * Jxy, Jxx - Jyy);
381
+ tangent = vec2<f32>(-sin(theta), cos(theta));
382
+ }
383
+ var A = (lambdaMaj - lambdaMin) / (lambdaMaj + lambdaMin + 1e-6);
384
+ A = clamp(A, 0.0, 1.0);
385
+ if (dot(tangent, fallbackDir) < 0.0) { tangent = -tangent; }
386
+ let blendW = pow(A, 0.28);
387
+ let dir = normalize(mix(fallbackDir, tangent, blendW) + vec2<f32>(1e-6, 1e-6));
388
+ return vec4<f32>(dir, A, packGrad(Gx, Gy));
389
+ }
390
+
391
+ // ── Flow field (the WGSL twin — the simple flow seed the medium fallback dirs use) ──
392
+ fn flowField(p: vec2<f32>, t: f32) -> vec2<f32> {
393
+ // Curl-noise-driven swirl (the medium-default dir; the full radial/swirl/diagonal
394
+ // dispatch rides uniforms the WGSL struct does not carry — the procedural mediums
395
+ // use this multi/curl seed as the fallback the structure tensor relaxes toward).
396
+ let n = fbm(p * 2.0 + vec2<f32>(t * 0.02, t * 0.02));
397
+ let a = n * 6.2831853;
398
+ var dir = vec2<f32>(cos(a), sin(a));
399
+ // Cursor swirl bias (matches the GLSL flow cursor term — the steady attraction).
400
+ let cursorStrength = u.scalars3.y;
401
+ let cursorRadius = u.scalars3.z;
402
+ let cursor = u.cursor.xy;
403
+ if (cursorStrength > 0.001) {
404
+ let toCur = cursor - p;
405
+ let d = length(toCur);
406
+ let r = max(cursorRadius, 0.01);
407
+ let w = exp(-(d * d) / (r * r * 0.5));
408
+ let tangent = vec2<f32>(-toCur.y, toCur.x) / max(d, 1e-4);
409
+ let a0 = atan2(dir.y, dir.x);
410
+ let a1 = atan2(tangent.y, tangent.x);
411
+ var da = a1 - a0;
412
+ da = atan2(sin(da), cos(da));
413
+ let aa = a0 + da * w * cursorStrength;
414
+ dir = vec2<f32>(cos(aa), sin(aa));
415
+ }
416
+ return dir;
417
+ }
418
+
419
+ // ── Watercolor — wet-edge cauliflowers + granulation + wash banding ─────────
420
+ fn mediumWatercolor(col0: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
421
+ var col = col0;
422
+ let eps = 0.004;
423
+ let cx1 = sampleBase(p + vec2<f32>(eps, 0.0), t);
424
+ let cx2 = sampleBase(p - vec2<f32>(eps, 0.0), t);
425
+ let cy1 = sampleBase(p + vec2<f32>(0.0, eps), t);
426
+ let cy2 = sampleBase(p - vec2<f32>(0.0, eps), t);
427
+ let gx = dot(cx1 - cx2, W_LUMA);
428
+ let gy = dot(cy1 - cy2, W_LUMA);
429
+ let edge = sqrt(gx * gx + gy * gy) / (2.0 * eps);
430
+ let mask = smoothstep(0.0, 2.5, edge);
431
+ col = col * mix(1.0, 0.78, mask * mWetEdge());
432
+
433
+ let paper = 0.5 * vnoise(p * 160.0) + 0.5 * vnoise(p * 360.0);
434
+ let pigLoad = 1.0 - dot(col, W_LUMA);
435
+ col = col * (1.0 - mGranulation() * pigLoad * (paper - 0.5));
436
+
437
+ let band = fbm(vec2<f32>(p.x * 1.5, p.y * 0.4));
438
+ col = col * (1.0 + 0.04 * (band - 0.5));
439
+ return col;
440
+ }
441
+
442
+ // ── Pastel — anisotropic stroke noise along the flow + tooth grain ──────────
443
+ fn mediumPastel(col0: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
444
+ var col = col0;
445
+ let flow = flowField(p, t);
446
+ let perp = vec2<f32>(-flow.y, flow.x);
447
+ let strokeScale = mStrokeScale();
448
+ let along = dot(p, flow) * strokeScale;
449
+ var across = dot(p, perp) * strokeScale;
450
+ across = across + 0.03 * (vnoise(p * 260.0) - 0.5);
451
+
452
+ let aniso = mix(1.0, 0.18, mStrokeAnisotropy());
453
+ let stroke = fbm(vec2<f32>(along * aniso, across));
454
+ col = col * mix(1.0, 0.82 + 0.32 * stroke, mStrokeAmount());
455
+
456
+ let tooth = vnoise(p * 800.0);
457
+ col = col * (1.0 - 0.08 * mStrokeAmount() * (tooth - 0.5));
458
+ return col;
459
+ }
460
+
461
+ // ── Crayon — DRY wax pigment on paper tooth (the tooth-multiply + scumble) ──
462
+ fn mediumCrayon(col0: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
463
+ let flow = structureTensorField(p, t, flowField(p, t)).xy;
464
+ let ang = atan2(flow.y, flow.x);
465
+ let ca = cos(-ang);
466
+ let sa = sin(-ang);
467
+ let pr = vec2<f32>(p.x * ca - p.y * sa, p.x * sa + p.y * ca);
468
+
469
+ let aniso = mix(0.45, 0.95, mStrokeAnisotropy());
470
+ let scale = max(mStrokeScale() * 1.6, 180.0);
471
+
472
+ let t1 = vnoise(vec2<f32>(pr.x * scale * aniso, pr.y * scale));
473
+ let t2 = vnoise(vec2<f32>(pr.x * scale * aniso * 0.4, pr.y * scale * 0.4) + vec2<f32>(11.0, 11.0));
474
+ let t3 = vnoise(vec2<f32>(pr.x * scale * aniso * 2.1, pr.y * scale * 2.1) + vec2<f32>(23.0, 23.0));
475
+ let tooth = 0.55 * t1 + 0.30 * t2 + 0.15 * t3;
476
+
477
+ let pressure = clamp(mStrokeAmount(), 0.0, 1.0);
478
+ let bite = mix(0.18, 0.46, pressure);
479
+ var result = col0 * (1.0 - bite * (0.5 - tooth));
480
+
481
+ let scumbleMask = smoothstep(0.42, 0.78, vnoise(vec2<f32>(pr.x * scale * 0.55, pr.y * scale * 0.22) + vec2<f32>(7.0, 7.0)));
482
+ let scumbleCoverage = scumbleMask * pressure * 0.7;
483
+ let paper = col0 * mix(0.80, 0.94, pressure);
484
+ result = mix(paper, result, scumbleCoverage);
485
+
486
+ let pigmentCell = floor(pr * max(scale * 0.18, 32.0));
487
+ let pigmentMask = smoothstep(0.28, 0.82, vnoise(pr * scale * 0.21 + vec2<f32>(19.0, 19.0)));
488
+ result = brokenColorJitter(
489
+ result,
490
+ hash21(pigmentCell + vec2<f32>(17.0, 17.0)),
491
+ hash21(pigmentCell * 2.1 + vec2<f32>(31.0, 31.0)),
492
+ 0.45 + 0.55 * pigmentMask
493
+ );
494
+
495
+ result = saturate3(result, 1.08);
496
+ return result;
497
+ }
498
+
499
+ // ── Kuwahara — the SOFT anisotropic generalized Kuwahara painterly finish ────
500
+ // THE KEYSTONE (Kyprianidis 2010). Aurora is a PROCEDURAL field (no input texture) so the
501
+ // operator runs over sampleBase directly: an elliptical kernel oriented along the structure
502
+ // tensor (elongated along the edge-tangent, narrowed across by the anisotropy), 8 OVERLAPPING
503
+ // sectors via SMOOTH gaussian-angular weights, blended by 1/(1+var^q) (the SOFT criterion →
504
+ // no 8-spoke pinwheel by construction). 4 rings × 8 angular taps = 32 procedural samples.
505
+ fn mediumKuwahara(col0: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
506
+ let stf = structureTensorField(p, t, flowField(p, t));
507
+ let tangent = stf.xy;
508
+ let A = stf.z;
509
+ // The studio knob drives the patch radius; clamp into the procedural patch band.
510
+ let radius = clamp(mKuwaharaRadius() + mStrokeScale() * 0.00018, 0.006, 0.024);
511
+ let aniso = mix(1.0, 0.34, clamp(A, 0.0, 1.0));
512
+ let ax = tangent;
513
+ let ay = vec2<f32>(-tangent.y, tangent.x);
514
+
515
+ // Per-sector weighted sums (8 sectors). WGSL has no runtime-indexed local arrays of
516
+ // vectors as cleanly as GLSL, so unroll the sector accumulators explicitly.
517
+ var mSum: array<vec3<f32>, 8>;
518
+ var sSum: array<vec3<f32>, 8>;
519
+ var wSum: array<f32, 8>;
520
+ for (var i = 0; i < 8; i = i + 1) {
521
+ mSum[i] = vec3<f32>(0.0);
522
+ sSum[i] = vec3<f32>(0.0);
523
+ wSum[i] = 0.0;
524
+ }
525
+
526
+ let RINGS = 4;
527
+ let TAPS = 8;
528
+ for (var r = 1; r <= RINGS; r = r + 1) {
529
+ let rr = f32(r) / f32(RINGS);
530
+ let radial = 1.0 - 0.6 * rr;
531
+ for (var a = 0; a < TAPS; a = a + 1) {
532
+ let ang = 6.2831853 * f32(a) / f32(TAPS);
533
+ let dirE = ax * cos(ang) + ay * (sin(ang) * aniso);
534
+ let off = dirE * (radius * rr);
535
+ let c = sampleBase(p + off, t);
536
+ for (var k = 0; k < 8; k = k + 1) {
537
+ let sectorAng = 6.2831853 * f32(k) / 8.0;
538
+ var d = ang - sectorAng;
539
+ d = atan2(sin(d), cos(d));
540
+ let sw = exp(-d * d * 2.2) * radial;
541
+ mSum[k] = mSum[k] + c * sw;
542
+ sSum[k] = sSum[k] + c * c * sw;
543
+ wSum[k] = wSum[k] + sw;
544
+ }
545
+ }
546
+ }
547
+
548
+ let q = max(mKuwaharaQ(), 1.0);
549
+ var accum = vec3<f32>(0.0);
550
+ var accumW = 0.0;
551
+ for (var k = 0; k < 8; k = k + 1) {
552
+ let wk = max(wSum[k], 1e-4);
553
+ let mean = mSum[k] / wk;
554
+ let var3 = max(sSum[k] / wk - mean * mean, vec3<f32>(0.0));
555
+ let variance = dot(var3, W_LUMA);
556
+ let sectorW = 1.0 / (1.0 + pow(variance * 320.0, q));
557
+ accum = accum + mean * sectorW;
558
+ accumW = accumW + sectorW;
559
+ }
560
+ var result = accum / max(accumW, 1e-4);
561
+
562
+ // Anchor in near-flat zones so the smoothing only bites at edges.
563
+ let center = sampleBase(p, t);
564
+ result = mix(center, result, clamp(0.35 + 0.65 * A, 0.0, 1.0));
565
+
566
+ // Faint canvas tooth + pigment-saturation so the flattened patches read as oil paint.
567
+ let tooth = vnoise(p * 300.0) - 0.5;
568
+ result = result * (1.0 + tooth * 0.06 * mCanvasGrain());
569
+ result = saturate3(result, 1.04);
570
+
571
+ return mix(col0, result, clamp(mStrokeAmount(), 0.0, 1.0));
572
+ }
573
+
574
+ // ── Metal — the two-term anisotropic BRDF (uMedium==8/9; the WGSL twin) ──────
575
+ // The field re-lights as warm folded metal: the luma HEIGHT field (the packed
576
+ // structure-tensor gradient → the surface normal) catches an anisotropic Blinn streak
577
+ // (brushed metal, running ALONG the coherent edge-tangent) plus a sharp crest, both
578
+ // raked by the IN-STRUCT cursor-synth light (crosses to WGSL — never uLightDir). The
579
+ // catch is the achromatic-warm anchor (never a cold chrome-blue). ZERO extra taps —
580
+ // the gradient is unpacked from the tensor's .w lane the caller already computed.
581
+ const METAL_CATCH_WARM: vec3<f32> = vec3<f32>(1.0, 0.97, 0.90);
582
+ const METAL_HEIGHT_SCALE: f32 = 2.2;
583
+ const METAL_LIGHT_Z: f32 = 0.55;
584
+ const METAL_IDLE_RAKE: vec2<f32> = vec2<f32>(0.32, 0.34);
585
+ const METAL_SHININESS_ANISO: f32 = 6.0;
586
+ const METAL_SHININESS_CREST: f32 = 40.0;
587
+ const METAL_COHERENCE_FLOOR: f32 = 0.22;
588
+ const METAL_BODY_FLOOR: f32 = 0.42;
589
+ const METAL_GRADIENT_FLATTEN: f32 = 0.55;
590
+ const METAL_SPARKLE_DENSITY: f32 = 240.0;
591
+ const METAL_SPARKLE_THRESH: f32 = 0.72;
592
+ const METAL_SPARKLE_AMP: f32 = 0.6;
593
+
594
+ fn metalShade(baseCol: vec3<f32>, p: vec2<f32>, stf: vec4<f32>) -> vec3<f32> {
595
+ let tangent = stf.xy;
596
+ let A = stf.z;
597
+ let grad = unpackGrad(stf.w);
598
+ let N = normalize(vec3<f32>(grad * (METAL_HEIGHT_SCALE * mMetalHeightScale()), 1.0));
599
+ let V = vec3<f32>(0.0, 0.0, 1.0);
600
+ // Cursor-synth rake light (u.cursor is in-struct — crosses to WGSL).
601
+ let L = normalize(vec3<f32>((u.cursor.xy - p) + METAL_IDLE_RAKE, METAL_LIGHT_Z));
602
+ let H = normalize(L + V);
603
+ // Anisotropic streak — brushed metal along the edge-tangent.
604
+ let tH = dot(normalize(tangent), H.xy);
605
+ let sinTH = sqrt(clamp(1.0 - tH * tH, 0.0, 1.0));
606
+ let streak = pow(clamp(sinTH, 0.0, 1.0), METAL_SHININESS_ANISO);
607
+ let crest = pow(clamp(dot(N, H), 0.0, 1.0), METAL_SHININESS_CREST);
608
+ var spec = streak * crest;
609
+ spec = spec * smoothstep(0.0, METAL_COHERENCE_FLOOR, A);
610
+ // Technicolor valley base — the body keeps the FIELD hue; luma rides the relief.
611
+ let diff = clamp(dot(N, L), 0.0, 1.0);
612
+ let body = baseCol * (METAL_BODY_FLOOR + (1.0 - METAL_BODY_FLOOR) * diff);
613
+ return body + METAL_CATCH_WARM * (spec * clamp(mMetalPolish(), 0.0, 4.0));
614
+ }
615
+
616
+ fn mediumMetal(col: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
617
+ let stf = structureTensorField(p, t, flowField(p, t));
618
+ return metalShade(col, p, stf);
619
+ }
620
+
621
+ // metal-gradient (uMedium==9) — the SAME BRDF over a pre-flattened base + the
622
+ // twinkle-in-place flake sparkle (position FIXED per cell, phase-animated, facing-gated).
623
+ fn mediumMetalGradient(col: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
624
+ let stf = structureTensorField(p, t, flowField(p, t));
625
+ let flat = mix(col, vec3<f32>(dot(col, W_LUMA)), METAL_GRADIENT_FLATTEN);
626
+ var metal = metalShade(flat, p, stf);
627
+ let grad = unpackGrad(stf.w);
628
+ let facing = normalize(vec3<f32>(grad * (METAL_HEIGHT_SCALE * mMetalHeightScale()), 1.0)).z;
629
+ let cell = floor(p * METAL_SPARKLE_DENSITY);
630
+ let seed = hash21(cell);
631
+ let tw = pow(clamp(sin(t + seed * 6.2831853), 0.0, 1.0), 40.0);
632
+ let flakeGate = step(METAL_SPARKLE_THRESH, seed);
633
+ metal = metal + METAL_CATCH_WARM * (tw * flakeGate * clamp(facing, 0.0, 1.0) * METAL_SPARKLE_AMP);
634
+ return metal;
635
+ }
636
+
637
+ // ── Medium dispatch (the WGSL twin of the GLSL main() if-ladder) ────────────
638
+ // uMedium: 0 smooth · 1 pastel · 2 watercolor · 3 oil · 4 crayon · 5 vangogh ·
639
+ // 6 oil-pastel · 7 kuwahara · 8 metal · 9 metal-gradient. The cheap peer mediums
640
+ // (pastel/watercolor/crayon) + kuwahara + metal port their own bodies; the
641
+ // oil/vangogh/oil-pastel STROKE cascade renders the anisotropic-Kuwahara PAINTERLY
642
+ // finish here (a real oil-paint read, never the smooth core — the "NO FALLBACKS on
643
+ // Safari" mandate; the full per-dab Starry-Night stroke WGSL remains a separate
644
+ // full-fidelity port).
645
+ fn applyMedium(col: vec3<f32>, p: vec2<f32>, t: f32) -> vec3<f32> {
646
+ let medium = u.ints1.x;
647
+ if (medium == 1) { return mediumPastel(col, p, t); }
648
+ if (medium == 2) { return mediumWatercolor(col, p, t); }
649
+ if (medium == 4) { return mediumCrayon(col, p, t); }
650
+ // Metal uses both renderer paths; a fragment-only implementation is invisible on the
651
+ // WGSL primary. IDs 8 and 9 select metal and metal-gradient respectively.
652
+ if (medium == 8) { return mediumMetal(col, p, t); }
653
+ if (medium == 9) { return mediumMetalGradient(col, p, t); }
654
+ // oil(3), vangogh(5), oil-pastel(6), kuwahara(7) all render the painterly
655
+ // anisotropic-Kuwahara finish on the WGSL primary (the WebGL2 fallback carries the
656
+ // full per-dab stroke cascade for oil/vangogh/oil-pastel).
657
+ if (medium == 3 || medium == 5 || medium == 6 || medium == 7) {
658
+ return mediumKuwahara(col, p, t);
659
+ }
660
+ return col;
661
+ }
662
+
663
+
664
+ // ── Full-screen triangle vertex stage (the pilot idiom — no vertex buffer) ──
665
+ struct VSOut {
666
+ @builtin(position) pos: vec4<f32>,
667
+ @location(0) uv: vec2<f32>,
668
+ };
669
+
670
+ @vertex
671
+ fn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {
672
+ // NDC corners of the covering triangle (-1,-1), (3,-1), (-1,3).
673
+ var p = vec2<f32>(-1.0, -1.0);
674
+ if (vi == 1u) { p = vec2<f32>(3.0, -1.0); }
675
+ else if (vi == 2u) { p = vec2<f32>(-1.0, 3.0); }
676
+ var out: VSOut;
677
+ out.pos = vec4<f32>(p, 0.0, 1.0);
678
+ // vUv = aPos * 0.5 + 0.5 (the aurora.vert convention).
679
+ out.uv = p * 0.5 + 0.5;
680
+ return out;
681
+ }
682
+
683
+ // ── Fragment stage (the main() transcription) ──
684
+ @fragment
685
+ fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
686
+ let t = u.scalars0.x;
687
+ let valueVariance = u.scalars0.z;
688
+ let breathDepth = u.scalars2.w;
689
+ let breathPeriod = u.scalars3.x;
690
+ let saturation = u.scalars1.z;
691
+ let paperGrain = u.scalars1.w;
692
+ let alpha = u.scalars2.x;
693
+ let vividness = u.scalars3.w;
694
+
695
+ let uv = in.uv;
696
+ let pN = uv;
697
+
698
+ let p_warp = domainWarp(pN, t);
699
+
700
+ let nf = nucleiField(p_warp, t);
701
+ var col = samplePalette(nf.paletteId);
702
+ col = col * (1.0 + valueVariance * nf.valueMod);
703
+
704
+ // Breath — slow global luminance wobble.
705
+ let breath = sin(t * 6.2831 / max(breathPeriod, 1.0));
706
+ col = col * (1.0 + breathDepth * breath * 0.5);
707
+
708
+ // Painterly-medium dispatch. uMedium 0 (smooth) is a
709
+ // no-op pass-through so the DEFAULT smooth config renders unchanged
710
+ // (the parity capture); uMedium 1-7 render the painterly
711
+ // bodies (pastel/watercolor/crayon/kuwahara) or the anisotropic-Kuwahara finish
712
+ // (oil/vangogh/oil-pastel) — never a silent smooth degrade on Safari 26.
713
+ col = applyMedium(col, pN, t);
714
+
715
+ // Cursor-local luminance lean, byte-parallel with the GLSL path.
716
+ let cursorDelta = pN - u.cursor.xy;
717
+ let cursorRadius = max(u.scalars3.z, 0.01);
718
+ let cursorLean = exp(-dot(cursorDelta, cursorDelta) / (cursorRadius * cursorRadius * 0.5));
719
+ col = col * (1.0 + 0.12 * cursorLean * u.scalars3.y);
720
+
721
+ // Saturation trim.
722
+ col = saturate3(col, saturation);
723
+
724
+ // The chroma floor is warm-anchored and hue-preserving. The WGSL
725
+ // twin of the GLSL call; vividness:0 is a no-op so the smooth/pale default stays gated.
726
+ col = vividnessFloor(col, vividness);
727
+
728
+ // Tonemap + film grain.
729
+ col = aces(col);
730
+ // gl_FragCoord.xy → the device-pixel position (the WGSL @builtin(position)).
731
+ let fragCoord = in.pos.xy;
732
+ let grain = hash21(fragCoord + t * 17.0);
733
+ col = col + (grain - 0.5) * paperGrain;
734
+
735
+ col = clamp(col * 0.985 + 0.008, vec3<f32>(0.0), vec3<f32>(1.0));
736
+
737
+ // MANDATORY OETF — closes the seam (the whole pipeline above runs in LINEAR).
738
+ col = linearToSrgb(col);
739
+
740
+ // 1-LSB triangular IGN dither in DISPLAY space, AFTER the OETF.
741
+ col = col + (1.0 / 255.0) * (ign(fragCoord) - 0.5);
742
+
743
+ // Premultiply on DISPLAY-space color (matches the WebGL2 path: OETF, then premul).
744
+ return vec4<f32>(col * alpha, alpha);
745
+ }
746
+ `, m = `
747
+ const MAX_NUCLEI: i32 = 8;
748
+ const PI: f32 = 3.141592653589793;
749
+ const BLUR_RINGS: i32 = 3;
750
+ const BLUR_SECTORS: i32 = 8;
751
+
752
+ // The image program's OWN uniform struct (packed vec4 lanes — trivial std140/WGSL
753
+ // alignment). DISTINCT from the palette program's 672-byte struct (a separate program).
754
+ struct ImageUniforms {
755
+ // scalars0: (uTime, uWarpAmount, uWarpScale, uWarpDrift)
756
+ scalars0: vec4<f32>,
757
+ // scalars1: (uSoftmaxBeta, uNucleiDrift, uCursorStrength, uCursorRadius)
758
+ scalars1: vec4<f32>,
759
+ // scalars2: (uSaturation, uPaperGrain, uAlpha, uVividness)
760
+ scalars2: vec4<f32>,
761
+ // image: (uBlurMin, uBlurMax, uImageAspect, _)
762
+ image: vec4<f32>,
763
+ // cursor: (uCursor.x, uCursor.y, _, _)
764
+ cursor: vec4<f32>,
765
+ // ints: (uNucleiCount, uNoiseOctaves, _, _)
766
+ ints: vec4<i32>,
767
+ // nuc0[i] = (pos.x, pos.y, radius, valueBias); nuc1[i] = (driftRadius, driftPhase, elong, angle)
768
+ nuc0: array<vec4<f32>, 8>,
769
+ nuc1: array<vec4<f32>, 8>,
770
+ };
771
+
772
+ @group(0) @binding(0) var<uniform> u: ImageUniforms;
773
+ @group(0) @binding(1) var imgTex: texture_2d<f32>;
774
+ @group(0) @binding(2) var imgSampler: sampler;
775
+
776
+ const K_NUCLEI: f32 = 14.0;
777
+ const K_WARP: f32 = 5.0;
778
+
779
+ // ── Noise (aurora-local hash21/vnoise/fbm — the same compact foundation) ──
780
+ fn hash21(p0: vec2<f32>) -> f32 {
781
+ var p = fract(p0 * vec2<f32>(123.34, 456.21));
782
+ p = p + dot(p, p + 45.32);
783
+ return fract(p.x * p.y);
784
+ }
785
+ fn vnoise(p: vec2<f32>) -> f32 {
786
+ let i = floor(p);
787
+ let f = fract(p);
788
+ let uu = f * f * (3.0 - 2.0 * f);
789
+ let a = hash21(i);
790
+ let b = hash21(i + vec2<f32>(1.0, 0.0));
791
+ let c = hash21(i + vec2<f32>(0.0, 1.0));
792
+ let d = hash21(i + vec2<f32>(1.0, 1.0));
793
+ return mix(mix(a, b, uu.x), mix(c, d, uu.x), uu.y);
794
+ }
795
+ ${s}
796
+ fn fbm(p0: vec2<f32>) -> f32 {
797
+ var v = 0.0;
798
+ var a = 0.5;
799
+ var p = p0;
800
+ let octaves = u.ints.y;
801
+ for (var i = 0; i < 5; i = i + 1) {
802
+ if (i >= octaves) { break; }
803
+ v = v + a * vnoise(p);
804
+ p = FBM_ROT * p * 2.02;
805
+ a = a * 0.5;
806
+ }
807
+ return v;
808
+ }
809
+ fn ign(p: vec2<f32>) -> f32 {
810
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
811
+ }
812
+
813
+ ${a}
814
+ ${o}
815
+
816
+ fn linOklab(lin: vec3<f32>) -> vec3<f32> {
817
+ let lms = LINEAR_SRGB_TO_LMS * lin;
818
+ return LMS_TO_OKLAB * cbrt3(lms);
819
+ }
820
+
821
+ // The Quilez double-warp (fbm) + the cursor swirl — the drift that moves the blur zone.
822
+ fn domainWarp(p: vec2<f32>, t: f32) -> vec2<f32> {
823
+ let warpAmount = u.scalars0.y;
824
+ let warpScale = u.scalars0.z;
825
+ let warpDrift = u.scalars0.w;
826
+ let cursorStrength = u.scalars1.z;
827
+ let cursorRadius = u.scalars1.w;
828
+ let cursor = u.cursor.xy;
829
+ let q = vec2<f32>(fbm(p * warpScale + vec2<f32>(0.0, 0.0) + t * warpDrift * K_WARP),
830
+ fbm(p * warpScale + vec2<f32>(5.2, 1.3) + t * warpDrift * K_WARP));
831
+ let r = vec2<f32>(fbm(p * warpScale + 4.0 * q + vec2<f32>(1.7, 9.2)),
832
+ fbm(p * warpScale + 4.0 * q + vec2<f32>(8.3, 2.8)));
833
+ var warped = p + warpAmount * r;
834
+ if (cursorStrength > 0.001) {
835
+ let toP = p - cursor;
836
+ let d = length(toP);
837
+ let rr = max(cursorRadius, 0.01);
838
+ let w = exp(-(d * d) / (rr * rr * 0.45));
839
+ let ang = w * cursorStrength * 2.1;
840
+ let ca = cos(ang);
841
+ let sa = sin(ang);
842
+ let rotated = vec2<f32>(ca * toP.x - sa * toP.y, sa * toP.x + ca * toP.y) + cursor;
843
+ warped = mix(warped, rotated + warpAmount * r * 0.7, w * cursorStrength);
844
+ }
845
+ return warped;
846
+ }
847
+
848
+ // The drifting BLUR ZONE — the anisotropic-Gaussian nuclei softmax (the SAME field the
849
+ // palette program reads), drift-coupled.
850
+ fn zoneField(p: vec2<f32>, t: f32) -> f32 {
851
+ let nucleiCount = u.ints.x;
852
+ let nucleiDrift = u.scalars1.y;
853
+ let softmaxBeta = u.scalars1.x;
854
+ var accum = 0.0;
855
+ var accumW = 0.0;
856
+ for (var i = 0; i < MAX_NUCLEI; i = i + 1) {
857
+ if (i >= nucleiCount) { break; }
858
+ let n0 = u.nuc0[i]; // pos.x, pos.y, radius, valueBias
859
+ let n1 = u.nuc1[i]; // driftRadius, driftPhase, elong, angle
860
+ let posI = n0.xy + n1.x * vec2<f32>(
861
+ cos(t * nucleiDrift * K_NUCLEI + n1.y),
862
+ sin(t * nucleiDrift * K_NUCLEI + n1.y * 1.13));
863
+ let diff = p - posI;
864
+ let ca = cos(n1.w);
865
+ let sa = sin(n1.w);
866
+ let local = vec2<f32>(ca * diff.x + sa * diff.y, -sa * diff.x + ca * diff.y);
867
+ let along = local.x / max(n1.z, 0.01);
868
+ let d2 = along * along + local.y * local.y;
869
+ let rad = max(n0.z, 0.01);
870
+ let w = exp(-softmaxBeta * d2 / (rad * rad));
871
+ accum = accum + w * (0.5 + 0.5 * n0.w);
872
+ accumW = accumW + w;
873
+ }
874
+ return clamp(accum / max(accumW, 1e-4), 0.0, 1.0);
875
+ }
876
+
877
+ fn coverUv(uv: vec2<f32>) -> vec2<f32> {
878
+ let aspect = max(u.image.z, 0.0001);
879
+ var c = uv - vec2<f32>(0.5);
880
+ c.x = c.x * aspect;
881
+ let s = 1.0 / max(aspect, 1.0);
882
+ return c * s + vec2<f32>(0.5);
883
+ }
884
+
885
+ // The bounded 24-tap zone blur — a FIXED 3-ring × 8-sector stratified kernel, radius
886
+ // per-fragment, sampled in LINEAR light. textureSampleLevel (explicit LOD 0) — WGSL has
887
+ // no implicit derivative in a varying-radius loop.
888
+ fn zoneBlur(uv: vec2<f32>, radius: f32) -> vec3<f32> {
889
+ let aspect = max(u.image.z, 0.0001);
890
+ var sum = srgbToLinear(textureSampleLevel(imgTex, imgSampler, coverUv(uv), 0.0).rgb);
891
+ var wsum = 1.0;
892
+ for (var ring = 1; ring <= BLUR_RINGS; ring = ring + 1) {
893
+ let rr = radius * (f32(ring) / f32(BLUR_RINGS));
894
+ for (var s = 0; s < BLUR_SECTORS; s = s + 1) {
895
+ let a = (f32(s) + 0.5 * f32(ring)) / f32(BLUR_SECTORS) * 2.0 * PI;
896
+ let off = vec2<f32>(cos(a) / aspect, sin(a)) * rr;
897
+ sum = sum + srgbToLinear(textureSampleLevel(imgTex, imgSampler, coverUv(uv + off), 0.0).rgb);
898
+ wsum = wsum + 1.0;
899
+ }
900
+ }
901
+ return sum / wsum;
902
+ }
903
+
904
+ const VIVID_TARGET: f32 = 0.115;
905
+ const VIVID_EPS: f32 = 0.012;
906
+ const VIVID_WARM_ANCHOR: vec2<f32> = vec2<f32>(0.34202, 0.93969);
907
+ fn vividnessFloor(c: vec3<f32>, vividness: f32) -> vec3<f32> {
908
+ if (vividness <= 0.0001) { return c; }
909
+ var lab = linOklab(c);
910
+ let C = length(lab.yz);
911
+ let modeLift = mix(1.18, 1.0, clamp(lab.x * 1.4, 0.0, 1.0));
912
+ let Cmin = vividness * VIVID_TARGET * modeLift;
913
+ var hueDir = VIVID_WARM_ANCHOR;
914
+ if (C > VIVID_EPS) { hueDir = lab.yz / C; }
915
+ let yz = hueDir * max(C, Cmin);
916
+ lab = vec3<f32>(lab.x, yz.x, yz.y);
917
+ return max(oklabToLinearSrgb(lab), vec3<f32>(0.0));
918
+ }
919
+
920
+ fn saturate3(c: vec3<f32>, amt: f32) -> vec3<f32> {
921
+ var lch = oklabToOklch(linOklab(c));
922
+ lch.y = max(lch.y * amt, 0.0);
923
+ return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3<f32>(0.0));
924
+ }
925
+
926
+ fn aces(color0: vec3<f32>) -> vec3<f32> {
927
+ let startCompression = 0.8 - 0.04;
928
+ let desaturation = 0.15;
929
+ var color = color0;
930
+ let x = min(color.r, min(color.g, color.b));
931
+ var offset = 0.04;
932
+ if (x < 0.08) { offset = x - 6.25 * x * x; }
933
+ color = color - offset;
934
+ let peak = max(color.r, max(color.g, color.b));
935
+ if (peak < startCompression) { return clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)); }
936
+ let d = 1.0 - startCompression;
937
+ let newPeak = 1.0 - d * d / (peak + d - startCompression);
938
+ color = color * (newPeak / peak);
939
+ let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
940
+ return clamp(mix(color, newPeak * vec3<f32>(1.0), g), vec3<f32>(0.0), vec3<f32>(1.0));
941
+ }
942
+
943
+ struct VSOut {
944
+ @builtin(position) pos: vec4<f32>,
945
+ @location(0) uv: vec2<f32>,
946
+ };
947
+
948
+ @vertex
949
+ fn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {
950
+ var p = vec2<f32>(-1.0, -1.0);
951
+ if (vi == 1u) { p = vec2<f32>(3.0, -1.0); }
952
+ else if (vi == 2u) { p = vec2<f32>(-1.0, 3.0); }
953
+ var out: VSOut;
954
+ out.pos = vec4<f32>(p, 0.0, 1.0);
955
+ out.uv = p * 0.5 + 0.5;
956
+ return out;
957
+ }
958
+
959
+ @fragment
960
+ fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
961
+ let t = u.scalars0.x;
962
+ let saturation = u.scalars2.x;
963
+ let paperGrain = u.scalars2.y;
964
+ let alpha = u.scalars2.z;
965
+ let vividness = u.scalars2.w;
966
+ let blurMin = u.image.x;
967
+ let blurMax = u.image.y;
968
+
969
+ let uv = in.uv;
970
+ let p_warp = domainWarp(uv, t);
971
+ let zone = zoneField(p_warp, t);
972
+
973
+ let radius = mix(blurMin, blurMax, zone);
974
+ var col = zoneBlur(uv, radius);
975
+
976
+ let cursorDelta = uv - u.cursor.xy;
977
+ let cursorRadius = max(u.scalars1.w, 0.01);
978
+ let cursorLean = exp(-dot(cursorDelta, cursorDelta) / (cursorRadius * cursorRadius * 0.5));
979
+ col = col * (1.0 + 0.12 * cursorLean * u.scalars1.z);
980
+
981
+ col = saturate3(col, saturation);
982
+ col = vividnessFloor(col, vividness);
983
+
984
+ col = aces(col);
985
+ let fragCoord = in.pos.xy;
986
+ let grain = hash21(fragCoord + t * 17.0);
987
+ col = col + (grain - 0.5) * paperGrain;
988
+ col = clamp(col * 0.985 + 0.008, vec3<f32>(0.0), vec3<f32>(1.0));
989
+
990
+ col = linearToSrgb(col);
991
+ col = col + (1.0 / 255.0) * (ign(fragCoord) - 0.5);
992
+
993
+ return vec4<f32>(col * alpha, alpha);
994
+ }
995
+ `, h = {
996
+ scalars0: 0,
997
+ scalars1: 4,
998
+ scalars2: 8,
999
+ scalars3: 12,
1000
+ cursor: 16,
1001
+ ints0: 20,
1002
+ ints1: 24,
1003
+ palette: 28,
1004
+ nuc0: 60,
1005
+ nuc1: 92,
1006
+ nuc2: 124,
1007
+ scalars4: 156,
1008
+ scalars5: 160,
1009
+ kuwahara: 164
1010
+ }, g = .01, _ = 8, v = 4, y = (e) => 1 - e;
1011
+ function b() {
1012
+ let e = /* @__PURE__ */ new ArrayBuffer(672);
1013
+ return {
1014
+ buffer: e,
1015
+ f32: new Float32Array(e),
1016
+ i32: new Int32Array(e),
1017
+ paletteScratch: /* @__PURE__ */ new Float32Array(24)
1018
+ };
1019
+ }
1020
+ function x(e, t, r, i, a = t.alpha) {
1021
+ let { f32: o, i32: s, paletteScratch: c } = e;
1022
+ o[h.scalars0 + 0] = i, o[h.scalars0 + 1] = t.softmaxBeta, o[h.scalars0 + 2] = t.valueVariance, o[h.scalars0 + 3] = t.warpAmount, o[h.scalars1 + 0] = t.warpScale, o[h.scalars1 + 1] = t.warpDrift, o[h.scalars1 + 2] = t.saturation, o[h.scalars1 + 3] = t.paperGrain, o[h.scalars2 + 0] = a, o[h.scalars2 + 1] = t.nucleiDrift, o[h.scalars2 + 2] = t.paletteDrift, o[h.scalars2 + 3] = t.breathDepth, o[h.scalars3 + 0] = t.breathPeriod, o[h.scalars3 + 1] = r.strength, o[h.scalars3 + 2] = r.radius, o[h.scalars3 + 3] = t.vividness ?? 1, o[h.cursor + 0] = r.x, o[h.cursor + 1] = y(r.y), o[h.cursor + 2] = t.metalPolish ?? 1, o[h.cursor + 3] = t.metalHeightScale ?? 1, s[h.ints0 + 0] = Math.min(t.palette.length, 8);
1023
+ let d = Math.min(t.nuclei.length, 8);
1024
+ s[h.ints0 + 1] = d, s[h.ints0 + 2] = u[t.warpMode], s[h.ints0 + 3] = t.noiseOctaves, s[h.ints1 + 0] = l[t.medium], s[h.ints1 + 1] = f[t.huePath ?? "shorter"], s[h.ints1 + 2] = 0, s[h.ints1 + 3] = 0, n(t.palette, 8, c);
1025
+ for (let e = 0; e < 8; e++) {
1026
+ let t = h.palette + e * 4;
1027
+ o[t + 0] = c[e * 3 + 0], o[t + 1] = c[e * 3 + 1], o[t + 2] = c[e * 3 + 2], o[t + 3] = 0;
1028
+ }
1029
+ for (let e = 0; e < 8; e++) {
1030
+ let n = h.nuc0 + e * 4, r = h.nuc1 + e * 4, i = h.nuc2 + e * 4;
1031
+ if (e < d) {
1032
+ let a = t.nuclei[e];
1033
+ o[n + 0] = a.x, o[n + 1] = y(a.y), o[n + 2] = a.radius, o[n + 3] = a.paletteBias, o[r + 0] = a.valueBias, o[r + 1] = a.driftRadius, o[r + 2] = a.driftPhase, o[r + 3] = a.elongation ?? 1, o[i + 0] = -(a.angle ?? 0) * Math.PI / 180, o[i + 1] = 0, o[i + 2] = 0, o[i + 3] = 0;
1034
+ } else o[n + 0] = 0, o[n + 1] = 0, o[n + 2] = 0, o[n + 3] = 0, o[r + 0] = 0, o[r + 1] = 0, o[r + 2] = 0, o[r + 3] = 1, o[i + 0] = 0, o[i + 1] = 0, o[i + 2] = 0, o[i + 3] = 0;
1035
+ }
1036
+ return o[h.scalars4 + 0] = t.strokeAmount, o[h.scalars4 + 1] = t.strokeScale, o[h.scalars4 + 2] = t.strokeAnisotropy, o[h.scalars4 + 3] = t.canvasGrain, o[h.scalars5 + 0] = t.wetEdge, o[h.scalars5 + 1] = t.granulation, o[h.scalars5 + 2] = t.brokenColor, o[h.scalars5 + 3] = t.kuwaharaQ ?? v, o[h.kuwahara + 0] = t.kuwaharaRadius ?? g, o[h.kuwahara + 1] = _, o[h.kuwahara + 2] = 0, o[h.kuwahara + 3] = 0, e;
1037
+ }
1038
+ var S = {
1039
+ scalars0: 0,
1040
+ scalars1: 4,
1041
+ scalars2: 8,
1042
+ image: 12,
1043
+ cursor: 16,
1044
+ ints: 20,
1045
+ nuc0: 24,
1046
+ nuc1: 56
1047
+ }, C = (e) => 1 - e;
1048
+ function w() {
1049
+ let e = /* @__PURE__ */ new ArrayBuffer(352);
1050
+ return {
1051
+ buffer: e,
1052
+ f32: new Float32Array(e),
1053
+ i32: new Int32Array(e)
1054
+ };
1055
+ }
1056
+ function T(e, t, n, r, i, a = t.alpha) {
1057
+ let { f32: o, i32: s } = e;
1058
+ o[S.scalars0 + 0] = r, o[S.scalars0 + 1] = t.warpAmount, o[S.scalars0 + 2] = t.warpScale, o[S.scalars0 + 3] = t.warpDrift, o[S.scalars1 + 0] = t.softmaxBeta, o[S.scalars1 + 1] = t.nucleiDrift, o[S.scalars1 + 2] = n.strength, o[S.scalars1 + 3] = n.radius, o[S.scalars2 + 0] = t.saturation, o[S.scalars2 + 1] = t.paperGrain, o[S.scalars2 + 2] = a, o[S.scalars2 + 3] = t.vividness ?? 1, o[S.image + 0] = t.imageBlur?.min ?? .004, o[S.image + 1] = t.imageBlur?.max ?? .06, o[S.image + 2] = i, o[S.image + 3] = 0, o[S.cursor + 0] = n.x, o[S.cursor + 1] = C(n.y), o[S.cursor + 2] = 0, o[S.cursor + 3] = 0;
1059
+ let c = Math.min(t.nuclei.length, 8);
1060
+ s[S.ints + 0] = c, s[S.ints + 1] = t.noiseOctaves, s[S.ints + 2] = 0, s[S.ints + 3] = 0;
1061
+ for (let e = 0; e < 8; e++) {
1062
+ let n = S.nuc0 + e * 4, r = S.nuc1 + e * 4;
1063
+ if (e < c) {
1064
+ let i = t.nuclei[e];
1065
+ o[n + 0] = i.x, o[n + 1] = C(i.y), o[n + 2] = i.radius, o[n + 3] = i.valueBias, o[r + 0] = i.driftRadius, o[r + 1] = i.driftPhase, o[r + 2] = i.elongation ?? 1, o[r + 3] = -(i.angle ?? 0) * Math.PI / 180;
1066
+ } else o[n + 0] = 0, o[n + 1] = 0, o[n + 2] = 0, o[n + 3] = 0, o[r + 0] = 0, o[r + 1] = 0, o[r + 2] = 1, o[r + 3] = 0;
1067
+ }
1068
+ return e;
1069
+ }
1070
+ //#endregion
1071
+ //#region src/components/aurora/composables/wgpuSetup.ts
1072
+ var E = 2, D = {
1073
+ color: {
1074
+ srcFactor: "one",
1075
+ dstFactor: "one-minus-src-alpha",
1076
+ operation: "add"
1077
+ },
1078
+ alpha: {
1079
+ srcFactor: "one",
1080
+ dstFactor: "one-minus-src-alpha",
1081
+ operation: "add"
1082
+ }
1083
+ };
1084
+ function O(e) {
1085
+ let { canvas: n, opaquePresentation: r, getCursorScalars: i, getConfig: a, getReducedMotion: o, getRenderTime: s, pointerField: l, getDecodedImage: u, registerImageUploader: f } = e, m = null;
1086
+ return function(e, h, g) {
1087
+ if (a().source === "image") return k(e, h, g, {
1088
+ canvas: n,
1089
+ opaquePresentation: r,
1090
+ getCursorScalars: i,
1091
+ getConfig: a,
1092
+ getReducedMotion: o,
1093
+ getRenderTime: s,
1094
+ pointerField: l,
1095
+ getDecodedImage: u,
1096
+ registerImageUploader: f
1097
+ });
1098
+ let _ = e.createShaderModule({
1099
+ label: "[Aurora] aurora.wgsl",
1100
+ code: p
1101
+ }), v = e.createBuffer({
1102
+ label: "[Aurora] uniforms",
1103
+ size: 672,
1104
+ usage: 72
1105
+ }), y = e.createBindGroupLayout({
1106
+ label: "[Aurora] bind-group-0",
1107
+ entries: [{
1108
+ binding: 0,
1109
+ visibility: E,
1110
+ buffer: { type: "uniform" }
1111
+ }]
1112
+ }), S = e.createRenderPipeline({
1113
+ label: "[Aurora] pipeline",
1114
+ layout: e.createPipelineLayout({ bindGroupLayouts: [y] }),
1115
+ vertex: {
1116
+ module: _,
1117
+ entryPoint: "vs_main"
1118
+ },
1119
+ fragment: {
1120
+ module: _,
1121
+ entryPoint: "fs_main",
1122
+ targets: [{
1123
+ format: g,
1124
+ blend: r ? void 0 : D
1125
+ }]
1126
+ },
1127
+ primitive: { topology: "triangle-list" }
1128
+ }), C = e.createBindGroup({
1129
+ label: "[Aurora] bind-group",
1130
+ layout: y,
1131
+ entries: [{
1132
+ binding: 0,
1133
+ resource: { buffer: v }
1134
+ }]
1135
+ }), w = b();
1136
+ function T(e) {}
1137
+ function O(t) {
1138
+ let n = +!o(), c = m === null ? 0 : Math.max(0, (t - m) * 1e3);
1139
+ m = t, l.tick(n === 0 ? 0 : c);
1140
+ let u = d(l, i(), n), f = a();
1141
+ x(w, f, u, s(t), r ? 1 : f.alpha), e.queue.writeBuffer(v, 0, w.buffer);
1142
+ let p = h.getCurrentTexture().createView(), g = e.createCommandEncoder({ label: "[Aurora] frame" }), _ = g.beginRenderPass({ colorAttachments: [{
1143
+ view: p,
1144
+ clearValue: {
1145
+ r: 0,
1146
+ g: 0,
1147
+ b: 0,
1148
+ a: +!!r
1149
+ },
1150
+ loadOp: "clear",
1151
+ storeOp: "store"
1152
+ }] });
1153
+ _.setPipeline(S), _.setBindGroup(0, C), _.draw(3, 1, 0, 0), _.end(), e.queue.submit([g.finish()]);
1154
+ }
1155
+ function A() {
1156
+ return o() ? !1 : t(a()) ? !0 : c(l);
1157
+ }
1158
+ return {
1159
+ frame: O,
1160
+ shouldContinue: A,
1161
+ resize: T,
1162
+ teardown: () => {
1163
+ v.destroy();
1164
+ }
1165
+ };
1166
+ };
1167
+ }
1168
+ function k(n, i, a, o) {
1169
+ let { canvas: s, opaquePresentation: l, getCursorScalars: u, getConfig: f, getReducedMotion: p, getRenderTime: h, pointerField: g, getDecodedImage: _, registerImageUploader: v } = o, y = null, b = n.createShaderModule({
1170
+ label: "[Aurora] aurora-image.wgsl",
1171
+ code: m
1172
+ }), x = n.createBuffer({
1173
+ label: "[Aurora] image uniforms",
1174
+ size: 352,
1175
+ usage: 72
1176
+ }), S = n.createSampler({
1177
+ label: "[Aurora] image sampler",
1178
+ magFilter: "linear",
1179
+ minFilter: "linear",
1180
+ addressModeU: "clamp-to-edge",
1181
+ addressModeV: "clamp-to-edge"
1182
+ }), C = n.createBindGroupLayout({
1183
+ label: "[Aurora] image bind-group-0",
1184
+ entries: [
1185
+ {
1186
+ binding: 0,
1187
+ visibility: E,
1188
+ buffer: { type: "uniform" }
1189
+ },
1190
+ {
1191
+ binding: 1,
1192
+ visibility: E,
1193
+ texture: {
1194
+ sampleType: "float",
1195
+ viewDimension: "2d"
1196
+ }
1197
+ },
1198
+ {
1199
+ binding: 2,
1200
+ visibility: E,
1201
+ sampler: { type: "filtering" }
1202
+ }
1203
+ ]
1204
+ }), O = n.createRenderPipeline({
1205
+ label: "[Aurora] image pipeline",
1206
+ layout: n.createPipelineLayout({ bindGroupLayouts: [C] }),
1207
+ vertex: {
1208
+ module: b,
1209
+ entryPoint: "vs_main"
1210
+ },
1211
+ fragment: {
1212
+ module: b,
1213
+ entryPoint: "fs_main",
1214
+ targets: [{
1215
+ format: a,
1216
+ blend: l ? void 0 : D
1217
+ }]
1218
+ },
1219
+ primitive: { topology: "triangle-list" }
1220
+ }), k = r(n, _() ?? e()).texture;
1221
+ function A() {
1222
+ return n.createBindGroup({
1223
+ label: "[Aurora] image bind-group",
1224
+ layout: C,
1225
+ entries: [
1226
+ {
1227
+ binding: 0,
1228
+ resource: { buffer: x }
1229
+ },
1230
+ {
1231
+ binding: 1,
1232
+ resource: k.createView()
1233
+ },
1234
+ {
1235
+ binding: 2,
1236
+ resource: S
1237
+ }
1238
+ ]
1239
+ });
1240
+ }
1241
+ let j = A();
1242
+ v((e) => {
1243
+ k.destroy(), k = r(n, e).texture, j = A();
1244
+ });
1245
+ let M = w();
1246
+ function N(e) {}
1247
+ function P(e) {
1248
+ let t = +!p(), r = y === null ? 0 : Math.max(0, (e - y) * 1e3);
1249
+ y = e, g.tick(t === 0 ? 0 : r);
1250
+ let a = d(g, u(), t), o = s.height > 0 ? s.width / s.height : 1, c = f();
1251
+ T(M, c, a, h(e), o, l ? 1 : c.alpha), n.queue.writeBuffer(x, 0, M.buffer);
1252
+ let m = i.getCurrentTexture().createView(), _ = n.createCommandEncoder({ label: "[Aurora] image frame" }), v = _.beginRenderPass({ colorAttachments: [{
1253
+ view: m,
1254
+ clearValue: {
1255
+ r: 0,
1256
+ g: 0,
1257
+ b: 0,
1258
+ a: +!!l
1259
+ },
1260
+ loadOp: "clear",
1261
+ storeOp: "store"
1262
+ }] });
1263
+ v.setPipeline(O), v.setBindGroup(0, j), v.draw(3, 1, 0, 0), v.end(), n.queue.submit([_.finish()]);
1264
+ }
1265
+ function F() {
1266
+ return p() ? !1 : t(f()) ? !0 : c(g);
1267
+ }
1268
+ return {
1269
+ frame: P,
1270
+ shouldContinue: F,
1271
+ resize: N,
1272
+ teardown: () => {
1273
+ v(null), k.destroy(), x.destroy();
1274
+ }
1275
+ };
1276
+ }
1277
+ //#endregion
1278
+ export { O as createAuroraWGPUSetup };