@mkbabb/glass-ui 3.7.0 → 3.8.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 (213) hide show
  1. package/dist/CardAction-q2qHCJFG.js +157 -0
  2. package/dist/{CommandShortcut-CY0aQTz5.js → CommandShortcut-SzWRhROC.js} +2 -2
  3. package/dist/{ContextMenuSubContent-BeDz7G6f.js → ContextMenuSubContent-Do977Cbf.js} +1 -1
  4. package/dist/{DataTable-gtPOLZvl.js → DataTable-CvlWXz65.js} +3 -3
  5. package/dist/DeckProgress-AVgbIULY.js +23 -0
  6. package/dist/{DialogContent-DG1YMR-P.js → DialogContent-DjZWT3lK.js} +2 -2
  7. package/dist/{DialogFooter-BCIaDP15.js → DialogFooter-Gprw8H_A.js} +1 -1
  8. package/dist/{DiscoGlyph-ZoBYpoxI.js → DiscoGlyph-CG0F410s.js} +1 -1
  9. package/dist/{GlyphFace-Ce_oe3tM.js → GlyphFace-DRjDkB0s.js} +1 -1
  10. package/dist/{HoverPopover-DXpOEw1P.js → HoverPopover-D56O6Ylp.js} +1 -1
  11. package/dist/{IconTooltip-Cu86JR4e.js → IconTooltip-D2T5nYOM.js} +1 -1
  12. package/dist/{MetricBadge-D-bRaQUD.js → MetricBadge-lTc002u2.js} +1 -1
  13. package/dist/{Notification-B5WHP3_1.js → Notification-BxK8LZMw.js} +1 -1
  14. package/dist/{PopoverContent-F1bWnYoh.js → PopoverContent-BId6ggf_.js} +1 -1
  15. package/dist/{Progress-c4vo4ALi.js → Progress-iEuD9pxG.js} +4 -4
  16. package/dist/{ScrollingText-BEp377U5.js → ScrollingText-CVyqxvfv.js} +2 -2
  17. package/dist/{SelectScrollDownButton-DAo5NmY_.js → SelectScrollDownButton-C7TRe-ep.js} +2 -2
  18. package/dist/{ToggleGroupItem-C-AIkBUw.js → ToggleGroupItem-CkXUcBlt.js} +1 -1
  19. package/dist/{UnderlineTabs-D_c1iYo-.js → UnderlineTabs-Uxn-R3MT.js} +2 -2
  20. package/dist/animated-digit.js +2 -2
  21. package/dist/api/index.d.ts +4 -2
  22. package/dist/api.js +1 -1
  23. package/dist/aurora.js +336 -405
  24. package/dist/canvas.d.ts +1 -0
  25. package/dist/canvas.js +3 -0
  26. package/dist/card.js +1 -1
  27. package/dist/carousel.js +105 -104
  28. package/dist/collapsible.js +1 -1
  29. package/dist/{color-DjJCAQ6f.js → color-DMLTunJK.js} +22 -14
  30. package/dist/color.js +2 -2
  31. package/dist/command.js +1 -1
  32. package/dist/compile-BBTRjvQS.js +158 -0
  33. package/dist/components/custom/aurora/composables/atoms.d.ts +90 -25
  34. package/dist/components/custom/aurora/composables/color.d.ts +0 -12
  35. package/dist/components/custom/aurora/composables/uniformBridge.d.ts +34 -23
  36. package/dist/components/custom/aurora/constants/presets.d.ts +10 -7
  37. package/dist/components/custom/aurora/constants/renderMode.d.ts +22 -0
  38. package/dist/components/custom/aurora/constants/shaders/aurora.wgsl.d.ts +1 -1
  39. package/dist/components/custom/aurora/constants/shaders/brush.glsl.d.ts +1 -1
  40. package/dist/components/custom/aurora/constants/shaders/composition.glsl.d.ts +1 -1
  41. package/dist/components/custom/aurora/constants/shaders/mediums.glsl.d.ts +1 -1
  42. package/dist/components/custom/aurora/index.d.ts +3 -3
  43. package/dist/components/custom/constellation/Constellation.vue.d.ts +40 -5
  44. package/dist/components/custom/constellation/constellationField.d.ts +110 -5
  45. package/dist/components/custom/constellation/index.d.ts +2 -2
  46. package/dist/components/custom/dock/GlassDock.vue.d.ts +12 -4
  47. package/dist/components/custom/dock/composables/dockMorphContext.d.ts +57 -0
  48. package/dist/components/custom/dock/composables/index.d.ts +3 -0
  49. package/dist/components/custom/dock/composables/useDockHold.d.ts +61 -0
  50. package/dist/components/custom/dock/composables/useLayerTransition.d.ts +13 -59
  51. package/dist/components/custom/dock/index.d.ts +2 -0
  52. package/dist/components/custom/goo-blob/GooBlob.vue.d.ts +13 -0
  53. package/dist/components/custom/goo-blob/composables/useBlobMood.d.ts +4 -0
  54. package/dist/components/custom/goo-blob/composables/useBlobPointer.d.ts +2 -0
  55. package/dist/components/custom/goo-blob/composables/useBlobSatellites.d.ts +2 -0
  56. package/dist/components/custom/goo-blob/composables/useMetaballRenderer.d.ts +20 -5
  57. package/dist/components/custom/goo-blob/index.d.ts +1 -1
  58. package/dist/components/custom/goo-blob/shaders/sdf-body.glsl.d.ts +1 -1
  59. package/dist/components/custom/goo-blob/shaders/watercolor-edges.glsl.d.ts +2 -2
  60. package/dist/components/custom/goo-blob/types.d.ts +8 -0
  61. package/dist/components/ui/card/Card.vue.d.ts +23 -7
  62. package/dist/components/ui/card/index.d.ts +1 -1
  63. package/dist/components/ui/progress/ProgressDefault.vue.d.ts +14 -4
  64. package/dist/components/ui/slider/Slider.vue.d.ts +3 -1
  65. package/dist/composables/color/index.d.ts +19 -0
  66. package/dist/composables/dom/index.d.ts +1 -1
  67. package/dist/composables/dom/useResolveTokenColor.d.ts +27 -0
  68. package/dist/composables/glass/canvas2d/index.d.ts +2 -1
  69. package/dist/composables/glass/canvas2d/resolveCanvasColor.d.ts +20 -0
  70. package/dist/composables/glass/canvas2d/useCanvas2D.d.ts +15 -1
  71. package/dist/composables/glass/index.d.ts +3 -1
  72. package/dist/composables/glass/useSpecularTracking.d.ts +33 -0
  73. package/dist/composables/glass/webgl/createCanvasLifecycle.d.ts +1 -1
  74. package/dist/composables/glass/webgl/shaders/procedural-color.glsl.d.ts +4 -0
  75. package/dist/composables/glass/webgl/useWebGLCanvas.d.ts +1 -1
  76. package/dist/composables/motion/core/index.d.ts +1 -0
  77. package/dist/configurator.js +1 -1
  78. package/dist/confirm-dialog.js +1 -1
  79. package/dist/constellation.js +144 -75
  80. package/dist/context-menu.js +2 -2
  81. package/dist/controls.js +2 -2
  82. package/dist/dark.js +1 -1
  83. package/dist/data-table.js +1 -1
  84. package/dist/deck-progress.d.ts +1 -0
  85. package/dist/deck-progress.js +2 -0
  86. package/dist/dialog.js +2 -2
  87. package/dist/disco-glyph.js +1 -1
  88. package/dist/dock.js +386 -285
  89. package/dist/{dockContext-DLgrVWsU.js → dockContext-BKeIMmNk.js} +1 -1
  90. package/dist/dom.js +6 -6
  91. package/dist/{dropdown-menu-BWpwutt0.js → dropdown-menu-B_4bWyyG.js} +2 -2
  92. package/dist/dropdown-menu.js +1 -1
  93. package/dist/expandable-container.js +1 -1
  94. package/dist/fonts/README.md +26 -18
  95. package/dist/forms.js +3 -3
  96. package/dist/fourier-field.js +37 -37
  97. package/dist/glass-carousel.js +12 -7
  98. package/dist/glass-panel.js +7 -7
  99. package/dist/glass-ui.css +1 -1
  100. package/dist/glass-ui.js +150 -162
  101. package/dist/glyph-face.js +2 -2
  102. package/dist/goo-blob.js +353 -270
  103. package/dist/header-ribbon.js +1 -1
  104. package/dist/hover-card.js +1 -1
  105. package/dist/hover-popover.js +1 -1
  106. package/dist/icon-tooltip.js +1 -1
  107. package/dist/index.d.ts +1 -0
  108. package/dist/instrument-chassis.js +1 -1
  109. package/dist/instrument-rail.js +1 -1
  110. package/dist/keyboard.js +1 -1
  111. package/dist/{keys-Dyf_uTs5.js → keys-BsYjEFOW.js} +1 -1
  112. package/dist/label.js +1 -1
  113. package/dist/labeled-field.js +6 -6
  114. package/dist/metric-badge.js +1 -1
  115. package/dist/metric-stack.js +1 -1
  116. package/dist/motion-core.js +65 -65
  117. package/dist/motion.js +3 -3
  118. package/dist/notification.js +1 -1
  119. package/dist/paper-backdrop.js +1 -1
  120. package/dist/popover.js +1 -1
  121. package/dist/progress.js +1 -1
  122. package/dist/pulse.js +1 -1
  123. package/dist/reactive.js +2 -2
  124. package/dist/resolveCanvasColor-BBiYfp8g.js +11 -0
  125. package/dist/responsive-tabs.js +2 -2
  126. package/dist/scrolling-text.js +1 -1
  127. package/dist/search.js +7 -7
  128. package/dist/select.js +3 -3
  129. package/dist/separator.js +1 -1
  130. package/dist/{sheet-Cv1uO-Gq.js → sheet-VZEtNFvO.js} +2 -2
  131. package/dist/sheet.js +1 -1
  132. package/dist/slider-D9puf-Ko.js +142 -0
  133. package/dist/slider.js +1 -1
  134. package/dist/sortable-list.js +3 -3
  135. package/dist/styles/components.css +1 -1
  136. package/dist/styles/dock-controls.css +21 -18
  137. package/dist/styles/dock.css +415 -224
  138. package/dist/styles/fonts.css +0 -25
  139. package/dist/styles/glass.css +100 -41
  140. package/dist/styles/theme.css +9 -2
  141. package/dist/styles/tokens.css +139 -32
  142. package/dist/styles/typography.css +52 -69
  143. package/dist/styles/utilities.css +2 -1
  144. package/dist/styles/view-transition.css +12 -40
  145. package/dist/switch.js +1 -1
  146. package/dist/tabs.js +5 -5
  147. package/dist/timeline.js +3 -3
  148. package/dist/toast.js +1 -1
  149. package/dist/toggle-group.js +1 -1
  150. package/dist/tooltip.js +1 -1
  151. package/dist/typewriter.js +1 -1
  152. package/dist/{useAnimatedNumber-C5I3wWd8.js → useAnimatedNumber-C_3wZLx4.js} +1 -1
  153. package/dist/{useCanvas2D-C31yfUGY.js → useCanvas2D-ClrQuZtp.js} +2 -1
  154. package/dist/{useConfiguratorState-B24TcgIn.js → useConfiguratorState-CMff4GzH.js} +3 -3
  155. package/dist/useResolveTokenColor-r9pnNjqK.js +24 -0
  156. package/dist/useSpecularTracking-BVRZsRHA.js +26 -0
  157. package/dist/{useTextHighlight-aMPdEpkq.js → useTextHighlight-9nMW23Qb.js} +1 -1
  158. package/dist/{useTouchGate-YQRwyUGU.js → useTouchGate-WFyJYsth.js} +1 -1
  159. package/dist/vReveal-CqaCz_mC.js +44 -0
  160. package/dist/watercolor-dot.js +2 -2
  161. package/package.json +36 -4
  162. package/src/fonts/README.md +26 -18
  163. package/src/styles/dock-controls.css +21 -18
  164. package/src/styles/dock.css +415 -224
  165. package/src/styles/fonts.css +0 -25
  166. package/src/styles/glass.css +100 -41
  167. package/src/styles/theme.css +9 -2
  168. package/src/styles/tokens.css +139 -32
  169. package/src/styles/typography.css +52 -69
  170. package/src/styles/utilities.css +2 -1
  171. package/src/styles/view-transition.css +12 -40
  172. package/dist/CardAction-D8nSE62a.js +0 -163
  173. package/dist/compile-iM4IUB66.js +0 -158
  174. package/dist/fonts/fraunces/fraunces-latin.woff2 +0 -0
  175. package/dist/slider-BA9wIs5e.js +0 -130
  176. package/dist/useViewTransition-DaN5Zrox.js +0 -21
  177. package/dist/vReveal-BE2TsPoR.js +0 -24
  178. package/src/fonts/fraunces/fraunces-latin.woff2 +0 -0
  179. /package/dist/{CollapsibleContent-D7UNsXQr.js → CollapsibleContent-CP9qF1v4.js} +0 -0
  180. /package/dist/{ContextMenuContent-DGKZX1Du.js → ContextMenuContent-DDa0v27s.js} +0 -0
  181. /package/dist/{HoverCardContent-Bn0UBMY9.js → HoverCardContent-B6CcQ8He.js} +0 -0
  182. /package/dist/{Input-DWQQIOC4.js → Input-DVG_J0ne.js} +0 -0
  183. /package/dist/{InstrumentChassis-BLJBl1BW.js → InstrumentChassis-C2f6cBaQ.js} +0 -0
  184. /package/dist/{InstrumentRail-BElFwKyt.js → InstrumentRail-CUJ05YVG.js} +0 -0
  185. /package/dist/{Label-DBc7bKzZ.js → Label-zRfbQdCn.js} +0 -0
  186. /package/dist/{ModalOverlay-D1VrWyoi.js → ModalOverlay-B1GkAjNb.js} +0 -0
  187. /package/dist/{PaperBackdrop-DCGoGatv.js → PaperBackdrop-CqwjnfLv.js} +0 -0
  188. /package/dist/{SelectGroup-BcaFjvP4.js → SelectGroup-BFiod7X2.js} +0 -0
  189. /package/dist/{SelectSeparator-COFMJGng.js → SelectSeparator-L8pPEn4L.js} +0 -0
  190. /package/dist/{Separator-DZyqsCum.js → Separator-D7sD7RKj.js} +0 -0
  191. /package/dist/{Switch-2rsAtDfA.js → Switch-LDKLmmgx.js} +0 -0
  192. /package/dist/{TabsList-COqIdW7i.js → TabsList-qyHZ2lKY.js} +0 -0
  193. /package/dist/{Toaster-BMaeziJJ.js → Toaster-CabYEEF_.js} +0 -0
  194. /package/dist/{TooltipProvider-CD2IXfRv.js → TooltipProvider-CmZzCuaG.js} +0 -0
  195. /package/dist/{_plugin-vue_export-helper-gLzyemvl.js → _plugin-vue_export-helper-ok8pgSl5.js} +0 -0
  196. /package/dist/composables/{dom → motion}/useTextHighlight.d.ts +0 -0
  197. /package/dist/{constants-CS75AWHG.js → constants-BPOqYJAm.js} +0 -0
  198. /package/dist/{createContext-CvNyBk4o.js → createContext-B44Sah_H.js} +0 -0
  199. /package/dist/{menuItemVariants-B4eFR5Ix.js → menuItemVariants-DKM85SNE.js} +0 -0
  200. /package/dist/{presets-BKVyQfbt.js → presets-D1XPuTj-.js} +0 -0
  201. /package/dist/{prng-wHuHlNHE.js → prng-COGhh02-.js} +0 -0
  202. /package/dist/{useGlassRenderer-HebgSEgN.js → useGlassRenderer-5I0YkI4t.js} +0 -0
  203. /package/dist/{useGlobalDark-yhL4Wp-s.js → useGlobalDark-D3EBWzXK.js} +0 -0
  204. /package/dist/{useIntersectionPause-Cc-H9ees.js → useIntersectionPause-Bj4sPkMh.js} +0 -0
  205. /package/dist/{useInterval-D3USn0yc.js → useInterval-CcLfq2WU.js} +0 -0
  206. /package/dist/{useKeyboardShortcuts-Be3w_Rwk.js → useKeyboardShortcuts-BXWXOZ-9.js} +0 -0
  207. /package/dist/{useResizeObserver-CSyxGxIX.js → useResizeObserver-CWCbk5Px.js} +0 -0
  208. /package/dist/{useScrollProgress-CAH8Ofwt.js → useScrollProgress-C9ysDic5.js} +0 -0
  209. /package/dist/{useSortable-C8ReQ50P.js → useSortable-MGAbCGVJ.js} +0 -0
  210. /package/dist/{useSpringMount-LRZqJTaF.js → useSpringMount-BJSHcLl3.js} +0 -0
  211. /package/dist/{useTimer-DEJonOZa.js → useTimer-3xqf_54b.js} +0 -0
  212. /package/dist/{useUserInvalidAria-CQKjEfwP.js → useUserInvalidAria-DoJKMZ3s.js} +0 -0
  213. /package/dist/{useViewportReady-K96m-Mdv.js → useViewportReady-RIr5Wgkh.js} +0 -0
package/dist/aurora.js CHANGED
@@ -1,13 +1,13 @@
1
- import { t as e } from "./_plugin-vue_export-helper-gLzyemvl.js";
2
- import { a as t, c as n, d as r, f as i, i as a, l as o, n as s, o as c, r as l, s as u, t as d, u as f } from "./compile-iM4IUB66.js";
3
- import { n as p, r as m, t as h } from "./presets-BKVyQfbt.js";
4
- import { a as g, c as _, i as v, o as y, t as b } from "./color-DjJCAQ6f.js";
5
- import { t as x } from "./useIntersectionPause-Cc-H9ees.js";
6
- import { t as ee } from "./useScrollProgress-CAH8Ofwt.js";
7
- import { computed as S, createElementBlock as C, createElementVNode as w, defineComponent as te, normalizeClass as ne, normalizeStyle as T, onBeforeUnmount as E, onMounted as D, openBlock as re, readonly as ie, ref as O, unref as ae, watch as k } from "vue";
8
- import { rawOklabToOklch as oe, srgbToOKLab as se } from "@mkbabb/value.js";
1
+ import { t as e } from "./_plugin-vue_export-helper-ok8pgSl5.js";
2
+ import { a as t, c as n, d as r, f as i, g as a, h as o, i as s, l as c, m as l, n as u, o as d, p as f, r as p, s as m, t as h, u as g } from "./compile-BBTRjvQS.js";
3
+ import { n as _, r as v, t as y } from "./presets-D1XPuTj-.js";
4
+ import { a as ee, c as b, i as te, l as x, o as S, t as C } from "./color-DMLTunJK.js";
5
+ import { t as ne } from "./useIntersectionPause-Bj4sPkMh.js";
6
+ import { t as re } from "./useScrollProgress-C9ysDic5.js";
7
+ import { computed as w, createElementBlock as ie, createElementVNode as T, defineComponent as ae, normalizeClass as oe, normalizeStyle as E, onBeforeUnmount as D, onMounted as O, openBlock as se, readonly as ce, ref as k, unref as le, watch as A } from "vue";
8
+ import { interpolateHue as ue, rawOklabToOklch as de, srgbToOKLab as fe } from "@mkbabb/value.js";
9
9
  //#region src/components/custom/aurora/constants/shaders/aurora.vert.ts
10
- var ce = "#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() {\n vUv = aPos * 0.5 + 0.5;\n gl_Position = vec4(aPos, 0.0, 1.0);\n}", le = "// ── Palette LUT (W5 — OKLCh/OKLab interpolation) ──────────────────────────────\n// The endpoints stay CPU-baked to LINEAR sRGB (the Aras precompute pattern); only\n// the BETWEEN-endpoint interpolation moves to perceptual space. OKLab-rectangular\n// for the default ramp (holds chroma, no grey midpoint); the OKLCh hue-arc only on\n// uHuePath increasing/decreasing (deliberate rainbow travel). The prior linear\n// mix() (the muddy-midtone source) is GONE.\nvec3 samplePalette(float id) {\n id = clamp(id, 0.0, 1.0);\n float scaled = id * float(uStopCount - 1);\n int i0 = int(floor(scaled));\n int i1 = min(i0 + 1, uStopCount - 1);\n float t = fract(scaled);\n t = smoothstep(0.0, 1.0, t);\n vec3 a = uPalette[i0];\n vec3 b = uPalette[i1];\n // increasing(2)/decreasing(3) request the OKLCh hue-arc (rainbow sweep); every\n // other method takes the OKLab-rectangular straight line (no hue detour).\n if (uHuePath == 2 || uHuePath == 3) {\n return mixPaletteOklchArc(a, b, t, uHuePath);\n }\n return mixPaletteOklab(a, b, t);\n}\n\n// ── Nuclei field ──────────────────────────────────────────────────────────\nvoid nucleiField(vec2 p, float t, out float paletteId, out float valueMod) {\n float accumBias = 0.0;\n float accumValue = 0.0;\n float accumW = 0.0;\n for (int i = 0; i < MAX_NUCLEI; i++) {\n if (i >= uNucleiCount) break;\n vec2 posI = uNucleiPos[i]\n + uNucleiDriftRadius[i] * vec2(\n cos(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i]),\n sin(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i] * 1.13)\n );\n vec2 diff = p - posI;\n // Anisotropic Gaussian: rotate diff into the nucleus's local frame\n // (major axis along uNucleiAngle), then scale the major-axis component by\n // 1/elongation so the squared distance describes an ellipse. Defaults\n // 1.0/0.0 reduce to the isotropic dot(diff, diff).\n float ca = cos(uNucleiAngle[i]);\n float sa = sin(uNucleiAngle[i]);\n vec2 local = vec2( ca * diff.x + sa * diff.y,\n -sa * diff.x + ca * diff.y);\n float along = local.x / max(uNucleiElong[i], 0.01);\n float across = local.y;\n float d2 = along * along + across * across;\n float r = max(uNucleiRadius[i], 0.01);\n float w = exp(-uSoftmaxBeta * d2 / (r * r));\n accumBias += w * uNucleiPaletteBias[i];\n accumValue += w * uNucleiValueBias[i];\n accumW += w;\n }\n paletteId = accumBias / max(accumW, 1e-4);\n valueMod = accumValue / max(accumW, 1e-4);\n\n // Palette drift — slow global paletteId breathe between adjacent stops. The\n // rate rides K_PAL (perceptible ~30–60s hue cycle at the default coefficient);\n // the amplitude lifts the authored coefficient into a 0.03..0.06 paletteId band\n // so the palette visibly travels rather than dithering within one stop.\n float palAmp = clamp(uPaletteDrift * 6.0, 0.0, 0.06);\n paletteId += palAmp * sin(t * uPaletteDrift * K_PAL);\n paletteId = clamp(paletteId, 0.0, 1.0);\n}\n", ue = "// ── Flow field ────────────────────────────────────────────────────────────\n// Structure-tensor prototype (body defined post-sampleBase in the mediums block).\n// Returns vec3(dir.x, dir.y, coherence) — the minor-eigenvector tangent + A.\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir);\n\nvec2 flowField(vec2 p, float t) {\n vec2 dir = vec2(1.0, 0.0);\n if (uFlowPattern == 1) {\n // radial\n dir = normalize(p - uFlowFocal + 1e-4);\n } else if (uFlowPattern == 2) {\n // swirl — tangent to radial\n vec2 rad = normalize(p - uFlowFocal + 1e-4);\n dir = vec2(-rad.y, rad.x);\n } else if (uFlowPattern == 3) {\n // diagonal\n float a = radians(uFlowAngle);\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 4) {\n // multi — curl-noise driven\n float n = fbm(p * 2.0 + t * 0.02);\n float a = n * 6.2831;\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 5) {\n // tensor / ETF — the color field's OWN edge-tangent flow (AW.W4.1). The\n // diagonal angle seeds the smooth fallback the coherence-weighted blend\n // relaxes toward in low-structure zones. Returns directly (no curl/cursor\n // post-mix — the tensor field already carries the structure orientation;\n // the cursor warp still drives the underlying color field in domainWarp).\n float a = radians(uFlowAngle);\n vec2 fallback = vec2(cos(a), sin(a));\n return structureTensorField(p, t, fallback).xy;\n }\n // curl — perturb by noise. Radial gets much less curl so rays stay clean.\n if (uFlowCurl > 0.0) {\n float n = fbm(p * 3.0) - 0.5;\n float curlAmt = uFlowCurl;\n if (uFlowPattern == 1) curlAmt *= 0.25; // radial: preserve ray clarity\n if (uFlowPattern == 2) curlAmt *= 0.55; // swirl: allow moderate curl\n float a = atan(dir.y, dir.x) + n * 3.14159 * curlAmt;\n dir = vec2(cos(a), sin(a));\n }\n // cursor influence — swirl around the cursor position\n if (uCursorStrength > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.5));\n // swirl tangent\n vec2 tangent = vec2(-toCur.y, toCur.x) / max(d, 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(tangent.y, tangent.x);\n // blend angle\n float da = a1 - a0;\n da = atan(sin(da), cos(da)); // wrap to [-pi, pi]\n float a = a0 + da * w * uCursorStrength;\n dir = vec2(cos(a), sin(a));\n }\n // AW.W8.1 — velocity-reactive swirl-BURST: a fast flick (uCursorBurst, decaying\n // over ~1s) injects a transient flow bias along the pointer velocity, distinct from\n // the steady attraction above (the stateless swirl is SUPERSEDED by this form). The\n // burst is gated by the master tempo scalar on the CPU side (it decays to 0 under\n // PRM/pause), so this term cannot leak motion under reduced-motion.\n if (uCursorBurst > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.7));\n // Bias the flow toward the velocity direction, scaled by the decaying burst.\n vec2 velDir = uCursorVelocity / max(length(uCursorVelocity), 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(velDir.y, velDir.x);\n float da = atan(sin(a1 - a0), cos(a1 - a0));\n float a = a0 + da * w * uCursorBurst * 0.8;\n dir = vec2(cos(a), sin(a));\n }\n return dir;\n}\n", de = "// ACES approximation\nvec3 aces(vec3 x) {\n float a = 2.51;\n float b = 0.03;\n float c = 2.43;\n float d = 0.59;\n float e = 0.14;\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);\n}\n", fe = "// ── Curved swept brushstroke primitive ────────────────────────────────────\n// A stroke is a curved spine from A to B with quadratic bulge K (sideways offset\n// at midpoint). Width varies along length via a shape profile. Edge is ragged\n// (bristle-modulated), ends are rounded blobs, inside has streak modulation.\n//\n// For pixel p, we invert an approximate projection onto the curved spine:\n// 1) project p onto straight AB axis -> along0, cross0\n// 2) at parameter along0, spine offset = 4·K·along0·(1-along0) perpendicular\n// 3) cross = cross0 - spineOffset, refine along one iteration\n// Coverage blends paint atop col based on per-stroke color sampled from the\n// underlying base at spine midpoint — so overlapping strokes from different\n// palette regions meet at hard, ragged demarcations.\n\nstruct StrokeHit {\n float coverage; // 0..1\n vec3 color;\n float alongT; // 0..1 along spine, for internal modulation\n float crossN; // cross distance / current half-width, signed\n float edgeN; // distance to edge in half-widths (0 = at edge, 1 = at spine)\n};\n\nStrokeHit noHit() { return StrokeHit(0.0, vec3(0.0), 0.0, 0.0, 0.0); }\n\nvec2 rotateDir(vec2 dir, float angle) {\n float ca = cos(angle), sa = sin(angle);\n return vec2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);\n}\n\nvec2 safeDir(vec2 dir) {\n float len = length(dir);\n return len > 1e-4 ? dir / len : vec2(1.0, 0.0);\n}\n\n// Shape profile along the stroke. type:\n// 0 tapered — thin-fat-thin (classic brush)\n// 1 load-drag — fat start, tapers to a point\n// 2 dab — ellipse-like, round center\n// 3 even — near-constant, slight end taper\nfloat strokeShape(float t, int type) {\n t = clamp(t, 0.0, 1.0);\n if (type == 1) {\n // loaded at t=0, tapering\n return pow(1.0 - t, 0.55) * smoothstep(0.0, 0.08, t);\n } else if (type == 2) {\n // dab / blob\n float d = t - 0.5;\n return exp(-d * d * 12.0);\n } else if (type == 3) {\n // mostly even, slight end softening\n return smoothstep(0.0, 0.08, t) * smoothstep(1.0, 0.92, t) * 0.95 + 0.05;\n }\n // 0 tapered\n return smoothstep(0.0, 0.22, t) * smoothstep(1.0, 0.78, t);\n}\n\n// A single curved stroke.\n// a, b — endpoints\n// halfW — base half-width (world units)\n// bulge — signed perpendicular midpoint offset, in world units\n// shapeType — 0..3\n// bristleFreq — spatial frequency of edge raggedness\n// bristleAmp — 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed — uniqueness seed for internal streaks\n// colAtMid — pre-sampled color at midpoint\nStrokeHit curvedStroke(vec2 p, vec2 a, vec2 b, float halfW,\n float bulge, int shapeType,\n float bristleFreq, float bristleAmp,\n float streakSeed, vec3 colAtMid) {\n vec2 ab = b - a;\n float L = length(ab);\n if (L < 1e-5) return noHit();\n vec2 tang = ab / L;\n vec2 norm = vec2(-tang.y, tang.x);\n\n vec2 rel = p - a;\n float along0 = dot(rel, tang) / L; // straight projection 0..1 along AB\n float cross0 = dot(rel, norm); // signed cross\n\n // Spine sideways offset due to bulge (parabolic)\n float bend = 4.0 * bulge * along0 * (1.0 - along0);\n float cross1 = cross0 - bend;\n\n // One refinement step: as the spine bends, the closest-point along shifts.\n // Spine tangent differs from straight tangent; first-order correction:\n float dBend = 4.0 * bulge * (1.0 - 2.0 * along0); // d(bend)/d(along)\n float along1 = along0 + (cross1 * dBend) / (L * (1.0 + dBend * dBend / (L * L)));\n\n // Bristle-ragged edge: half-width gets chewed by 1D noise along the spine\n float edgeNoise = fbm(vec2(along1 * bristleFreq, streakSeed * 7.3)) - 0.5;\n float edgeNoise2 = fbm(vec2(along1 * bristleFreq * 2.3, streakSeed * 3.1 + 17.0)) - 0.5;\n float edgeMod = 1.0 - bristleAmp * (0.6 + 0.4 * edgeNoise) * (0.5 + edgeNoise2);\n\n // Width profile along the stroke\n float shape = strokeShape(along1, shapeType);\n float halfWNow = halfW * shape * edgeMod;\n\n // Inside-segment coverage\n float cov = 0.0;\n if (along1 >= 0.0 && along1 <= 1.0 && halfWNow > 1e-6) {\n float cn = abs(cross1) / halfWNow;\n // fwidth-derived AA: half-width from the field's screen-space gradient so the\n // edge holds ~1px regardless of zoom/DPR (mirrors metaball.frag.ts:252-255).\n float aaC = max(fwidth(cn), 1e-6);\n cov = 1.0 - smoothstep(1.0 - aaC, 1.0 + aaC, cn);\n }\n\n // End-cap blobs (rounded tips, not perpendicular cuts)\n float capA = 0.0, capB = 0.0;\n {\n vec2 capCenterA = a + norm * (bulge * 0.0); // at a\n vec2 capCenterB = a + tang * L + norm * bend * 0.0 + norm * 0.0; // at b; bend is 0 at endpoints\n capCenterB = b;\n float endShapeA = strokeShape(0.02, shapeType);\n float endShapeB = strokeShape(0.98, shapeType);\n float rA = halfW * endShapeA * (0.9 + 0.2 * fbm(vec2(streakSeed * 13.0)));\n float rB = halfW * endShapeB * (0.9 + 0.2 * fbm(vec2(streakSeed * 19.0 + 5.0)));\n float dA = length(p - capCenterA);\n float dB = length(p - capCenterB);\n // fwidth-derived AA on the clean Euclidean cap distances (mirrors metaball.frag.ts:252-255).\n float aaA = max(fwidth(dA), 1e-6);\n float aaB = max(fwidth(dB), 1e-6);\n capA = 1.0 - smoothstep(rA - aaA, rA + aaA, dA);\n capB = 1.0 - smoothstep(rB - aaB, rB + aaB, dB);\n // Only apply caps if we're BEYOND the segment; otherwise the segment wins.\n capA *= (along1 < 0.05) ? 1.0 : 0.0;\n capB *= (along1 > 0.95) ? 1.0 : 0.0;\n }\n\n float coverage = max(cov, max(capA, capB));\n if (coverage < 0.002) return noHit();\n\n // Edge distance (for impasto later): 0 at edge, 1 at spine\n float edgeDist = halfWNow > 1e-6 ? clamp(1.0 - abs(cross1) / halfWNow, 0.0, 1.0) : 0.0;\n\n return StrokeHit(\n coverage,\n colAtMid,\n clamp(along1, 0.0, 1.0),\n halfWNow > 1e-6 ? cross1 / halfWNow : 0.0,\n edgeDist\n );\n}\n\n// Composite stroke over col with internal streaking + ACCUMULATE paint HEIGHT.\n// streakAmp — 0..0.2 how much internal streaks darken/lighten\n// impastoAmp — 0..1 paint-thickness contribution (the height the relight reads)\n// hardness — 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\n//\n// AW.W4.2 — the fixed-RGB edge rim (the phantom upper-left light) is RETIRED. Each\n// stroke now deposits PAINT HEIGHT into the height accumulator (coverage x per-stroke\n// thickness, perturbed by the bristle/streak fbm for ridges/grooves); mediumOil\n// derives a normal from the accumulated height gradient (dFdx/dFdy) and relights it\n// once with the movable uLightDir source (relightImpasto), in linear before aces().\nvoid paintOver(inout vec3 col, inout float height, StrokeHit s,\n float streakFreq, float streakAmp,\n float impastoAmp, float hardness, float streakSeed) {\n if (s.coverage < 0.002) return;\n float strokeOpacity = clamp(uStrokeAmount, 0.0, 1.0);\n if (strokeOpacity <= 0.001) return;\n vec3 c = s.color;\n\n // Internal streaks — fbm along spine, modulated by cross position. Gives\n // the loaded-brush look: some bristles carry more pigment than others.\n float streakA = fbm(vec2(s.alongT * streakFreq, s.crossN * 2.7 + streakSeed));\n float streakB = fbm(vec2(s.alongT * streakFreq * 0.6 + streakSeed * 3.7, s.crossN * 4.1));\n float streak = 0.6 * (streakA - 0.5) + 0.4 * (streakB - 0.5);\n c *= 1.0 + streak * streakAmp * 2.0;\n\n // Subtle value variance across width (hollow-center catch-light)\n float crossShade = smoothstep(0.0, 0.4, s.edgeN) * (1.0 - smoothstep(0.65, 1.0, s.edgeN));\n c *= 1.0 + crossShade * 0.05;\n\n float softLimit = mix(0.35, 0.98, hardness);\n float alpha = smoothstep(0.0, 1.0 - softLimit, s.coverage) * strokeOpacity;\n col = mix(col, c, alpha);\n\n // ── Accumulate paint HEIGHT (AW.W4.2). Thickness peaks at the stroke spine and\n // falls to zero at the edge (edgeN: 0=edge, 1=spine), perturbed by the bristle\n // streak so ridges/grooves form. The relight reads the gradient of this field.\n float ridge = 0.7 + 0.3 * (streak + 0.5);\n height += s.coverage * impastoAmp * (0.4 + 0.6 * s.edgeN) * ridge * alpha;\n}\n\n// AW.W4.2 — relight the accumulated paint height. Derive a normal from the height\n// gradient (dFdx/dFdy — derivatives are in-pattern; fwidth is already used for AA),\n// then apply diffuse + Blinn specular from the movable uLightDir/uLightColor source.\n// LINEAR-space lighting BEFORE aces() (the tonemap/OETF stay locked); thin strokes\n// inherit a low shininess, thick impasto raises it (high-height ridges catch a sharp\n// glint). canvasBase is the tooth height so the canvas weave also catches the light.\nvec3 relightImpasto(vec3 col, float height, float canvasBase) {\n float h = height + canvasBase;\n // Height-gradient normal. heightScale trades relief depth for surface flatness.\n float heightScale = 6.0;\n vec3 N = normalize(vec3(-dFdx(h) * 40.0, -dFdy(h) * 40.0, 1.0 / heightScale));\n vec3 L = normalize(uLightDir);\n vec3 V = vec3(0.0, 0.0, 1.0); // view straight on\n vec3 H = normalize(L + V);\n float diff = max(dot(N, L), 0.0);\n // Thick impasto → high shininess (sharp glint); thin paint → broad soft sheen.\n float shininess = mix(8.0, 64.0, clamp(height * 2.0, 0.0, 1.0));\n float spec = pow(max(dot(N, H), 0.0), shininess);\n // Modulate the lit terms by how much paint sits here (no relief on bare ground).\n float relief = clamp(h * 1.5, 0.0, 1.0);\n vec3 lit = col * (1.0 + uImpasto * relief * (diff - 0.5) * 0.5 * uLightColor);\n lit += uImpasto * relief * spec * 0.6 * uLightColor;\n return lit;\n}\n\n// Best-of-9-neighbor placement: sample 3x3 surrounding cells and take the\n// stroke that covers this pixel most. Breaks the grid by per-cell jitter and\n// sparse density via noise thresholding.\nStrokeHit bestOil(vec2 p, float cellSize, float lenMul, float halfWMul,\n float jitterAmt, float density, int shapeType,\n float bristleAmp, vec2 flow, float t, float seed) {\n vec2 cell = floor(p / cellSize);\n\n StrokeHit best = noHit();\n // 3x3 neighborhood\n for (int dy = -1; dy <= 1; dy++) {\n for (int dx = -1; dx <= 1; dx++) {\n vec2 cc = cell + vec2(float(dx), float(dy));\n vec2 hh = hash22(cc + seed) - 0.5;\n // Density gate — noise threshold; sparse placement.\n float gate = hash21(cc * 1.7 + seed * 0.3);\n if (gate > density) continue;\n\n vec2 center = (cc + 0.5 + hh * jitterAmt) * cellSize;\n\n // Per-stroke direction source — the AW.W4.1 strokeOrient switch:\n // uStrokeOrient==0 (flow) — the layer-provided hand-authored flowField.\n // uStrokeOrient==1 (tensor) — the structure-tensor minor eigenvector at the\n // cell center (the color field's edge-tangent),\n // so strokes hug the color zones (van Gogh).\n // coh carries the per-cell coherence A for the W4.3 energy grading (long\n // confident strokes where structure is coherent, stubby dabs in flat zones).\n vec2 f;\n float coh = 1.0;\n if (uStrokeOrient == 1) {\n vec3 tf = structureTensorField(center, t, safeDir(flow));\n f = safeDir(tf.xy);\n coh = tf.z;\n } else {\n f = safeDir(flow);\n }\n // Per-stroke deterministic local perturbation so alternate stroke layers stay\n // live. Tensor strokes get LESS angular jitter (the field already orients them).\n float jScale = (uStrokeOrient == 1) ? 0.4 : 1.0;\n float angJ = (hash21(cc + seed + 11.0) - 0.5) * 0.9 * jScale; // +/- 0.45/0.18 rad\n float localCurl = (fbm(center * (2.6 + seed * 0.11) + seed * 1.9) - 0.5) * 0.55 * uFlowCurl;\n vec2 dir = rotateDir(f, angJ + localCurl);\n\n // AW.W4.3 — energy-graded length/density for the van-Gogh cascade: in vangogh\n // mode (uMedium==5) modulate the stroke length by local luminance (bright →\n // long confident strokes, dark → short dabs, the Starry-Night Kolmogorov/\n // Batchelor congruence) AND by coherence (coherent zones → long, flat → stubby).\n float energy = 1.0;\n if (uMedium == 5) {\n float luma = clamp(dot(sampleBase(center, t), W_LUMA), 0.0, 1.0);\n // long in the lights + coherent zones; short dabs in the darks + flat zones.\n energy = mix(0.5, 1.5, luma) * mix(0.6, 1.0, coh);\n }\n\n float lenV = cellSize * lenMul * (0.65 + 0.55 * hash21(cc + seed + 23.0)) * energy;\n float halfW = cellSize * halfWMul * (0.70 + 0.55 * hash21(cc + seed + 41.0));\n float bulge = (hash21(cc + seed + 53.0) - 0.5) * lenV * 0.35;\n\n vec2 a = center - dir * (lenV * 0.5);\n vec2 b = center + dir * (lenV * 0.5);\n\n vec3 colMid = brokenColorJitter(\n sampleBase(center, t),\n hash21(cc + seed + 89.0),\n hash21(cc * 2.3 + seed + 97.0),\n 1.0\n );\n\n StrokeHit h = curvedStroke(p, a, b, halfW, bulge, shapeType,\n 7.0, bristleAmp,\n hash21(cc + seed + 67.0), colMid);\n if (h.coverage > best.coverage) best = h;\n }\n }\n return best;\n}\n", pe = "// ── Medium overlays ───────────────────────────────────────────────────────\n\n// A quick re-computation of base color for edge-mask sampling\nvec3 sampleBase(vec2 p, float t) {\n vec2 pw = domainWarp(p, t);\n float id; float vm;\n nucleiField(pw, t, id, vm);\n vec3 c = samplePalette(id);\n c *= 1.0 + uValueVariance * vm;\n return c;\n}\n\n// ── Structure-tensor / edge-tangent-flow (AW.W4.1) ─────────────────────────\n// The keystone of the painterly engine: derive stroke orientation from the COLOR\n// FIELD's OWN structure, not a hand-authored flow pattern. A 3x3 Sobel over\n// luma(sampleBase) yields the gradient (Gx,Gy); the 2x2 structure tensor\n// J = [[Gx², GxGy],[GxGy, Gy²]] eigen-decomposes closed-form into the MINOR\n// eigenvector (the edge-TANGENT — least color change, the stroke direction; the\n// MAJOR eigenvector is the gradient/edge-NORMAL and would make strokes cross the\n// bands) and the coherence A = (λ1-λ2)/(λ1+λ2). Low-coherence (flat) zones relax\n// toward the smooth fallbackDir by (1-A) so tensor noise never reads as jitter.\n// Single-pass small-tap WebGL2 form; the Gaussian-smoothed multi-tap + LIC smear\n// is the AW.W7 WebGPU multi-pass fold. Returns vec3(tangent.x, tangent.y, A).\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir) {\n float e = 0.0035; // small-tap neighborhood (edge-mask scale)\n // 3x3 luma samples.\n float l00 = dot(sampleBase(p + vec2(-e, -e), t), W_LUMA);\n float l10 = dot(sampleBase(p + vec2( 0.0, -e), t), W_LUMA);\n float l20 = dot(sampleBase(p + vec2( e, -e), t), W_LUMA);\n float l01 = dot(sampleBase(p + vec2(-e, 0.0), t), W_LUMA);\n float l21 = dot(sampleBase(p + vec2( e, 0.0), t), W_LUMA);\n float l02 = dot(sampleBase(p + vec2(-e, e), t), W_LUMA);\n float l12 = dot(sampleBase(p + vec2( 0.0, e), t), W_LUMA);\n float l22 = dot(sampleBase(p + vec2( e, e), t), W_LUMA);\n // Sobel derivatives.\n float Gx = (l20 + 2.0 * l21 + l22) - (l00 + 2.0 * l01 + l02);\n float Gy = (l02 + 2.0 * l12 + l22) - (l00 + 2.0 * l10 + l20);\n // Structure tensor components.\n float Jxx = Gx * Gx;\n float Jyy = Gy * Gy;\n float Jxy = Gx * Gy;\n // Closed-form 2x2 eigenvalues: λ = 0.5(tr ± sqrt((Jxx-Jyy)² + 4Jxy²)).\n float tr = Jxx + Jyy;\n float disc = sqrt(max((Jxx - Jyy) * (Jxx - Jyy) + 4.0 * Jxy * Jxy, 0.0));\n float lambdaMaj = 0.5 * (tr + disc);\n float lambdaMin = 0.5 * (tr - disc);\n // The MAJOR eigenvector points along the gradient (the edge NORMAL); the MINOR\n // is its perpendicular — the edge TANGENT (the stroke direction). The principal\n // angle is θ = 0.5·atan2(2·Jxy, Jxx-Jyy); this form is robust at Jxy≈0 (where the\n // (λ-Jyy, Jxy) eigenvector formula collapses to a zero vector). The tangent is at\n // θ + 90°. Guard the fully-isotropic (no-gradient) degenerate case → fallback dir.\n vec2 tangent;\n if (disc < 1e-7) {\n tangent = fallbackDir;\n } else {\n float theta = 0.5 * atan(2.0 * Jxy, Jxx - Jyy); // major (gradient) angle\n tangent = vec2(-sin(theta), cos(theta)); // perpendicular = tangent\n }\n // Coherence A in [0,1] — high where one eigenvalue dominates (a strong edge),\n // ~0 in flat zones. Relax toward the smooth fallback by (1 - A).\n float A = (lambdaMaj - lambdaMin) / (lambdaMaj + lambdaMin + 1e-6);\n A = clamp(A, 0.0, 1.0);\n // Coherence-weighted blend: full tensor in coherent zones, smooth in flat ones.\n // Resolve the 180° sign ambiguity toward the fallback so the blend is continuous.\n if (dot(tangent, fallbackDir) < 0.0) tangent = -tangent;\n vec2 dir = normalize(mix(fallbackDir, tangent, A) + 1e-6);\n return vec3(dir, A);\n}\n\nvec3 mediumWatercolor(vec3 col, vec2 p, float t) {\n // Wet-edge cauliflowers via luma-gradient magnitude\n float eps = 0.004;\n vec3 cx1 = sampleBase(p + vec2(eps, 0.0), t);\n vec3 cx2 = sampleBase(p - vec2(eps, 0.0), t);\n vec3 cy1 = sampleBase(p + vec2(0.0, eps), t);\n vec3 cy2 = sampleBase(p - vec2(0.0, eps), t);\n float gx = dot(cx1 - cx2, W_LUMA);\n float gy = dot(cy1 - cy2, W_LUMA);\n float edge = sqrt(gx * gx + gy * gy) / (2.0 * eps);\n float mask = smoothstep(0.0, 2.5, edge);\n col *= mix(1.0, 0.78, mask * uWetEdge);\n\n // Granulation — pigment settles in paper tooth\n float paper = 0.5 * vnoise(p * 160.0) + 0.5 * vnoise(p * 360.0);\n float pigLoad = 1.0 - dot(col, W_LUMA);\n col *= 1.0 - uGranulation * pigLoad * (paper - 0.5);\n\n // Wash banding — faint horizontal wet gradient\n float band = fbm(vec2(p.x * 1.5, p.y * 0.4));\n col *= 1.0 + 0.04 * (band - 0.5);\n return col;\n}\n\nvec3 mediumPastel(vec3 col, vec2 p, float t) {\n vec2 flow = flowField(p, t);\n vec2 perp = vec2(-flow.y, flow.x);\n float along = dot(p, flow) * uStrokeScale;\n float across = dot(p, perp) * uStrokeScale;\n across += 0.03 * (vnoise(p * 260.0) - 0.5);\n\n float aniso = mix(1.0, 0.18, uStrokeAnisotropy);\n float stroke = fbm(vec2(along * aniso, across));\n col *= mix(1.0, 0.82 + 0.32 * stroke, uStrokeAmount);\n\n // Pastel tooth — tiny high-frequency grain\n float tooth = vnoise(p * 800.0);\n col *= 1.0 - 0.08 * uStrokeAmount * (tooth - 0.5);\n return col;\n}\n", me = "// ── Crayon / oil-pastel — paper tooth × wax pigment (PEER medium) ─────────\n// Crayon is not strokes. It's pigment crumbs dragged across paper tooth.\n// Model: heavy 2D tooth noise at multiple scales, anisotropically stretched\n// along flow direction, multiplied into the base color. Add a slow \"waxy\n// film\" that slightly unifies hues, and occasional darker \"pressed\" spots\n// where the crayon dug in. NO straight segments. Dispatched at main() level as\n// a peer of pastel/watercolor/oil — never a branch inside mediumOil().\n// AW.W4.4 — GENUINE oil pastel: pigment DEPOSITED on the paper tooth, a broken\n// SCUMBLE upper layer letting the paper/lower color show through, and a waxy BURNISH\n// film whose sheen grows with layer count. Reworked from the old tooth-multiply\n// gradient into a material-truth deposition model. Oriented along the ETF field\n// (W4.1); OKLCh broken color (W5). The oil-pastel first-class medium (uMedium==6)\n// AND the legacy crayon peer (uMedium==4) both dispatch this single body.\nvec3 mediumCrayon(vec3 col, vec2 p, float t) {\n // Orient the deposition streaks along the structure-tensor edge-tangent. The\n // painterly mediums force uStrokeOrient==tensor, so read the tensor field directly;\n // fall back to flowField for the legacy crayon strokeMode route.\n vec2 flow = (uStrokeOrient == 1)\n ? structureTensorField(p, t, flowField(p, t)).xy\n : flowField(p, t);\n float ang = atan(flow.y, flow.x);\n float ca = cos(-ang), sa = sin(-ang);\n vec2 pr = vec2(p.x * ca - p.y * sa, p.x * sa + p.y * ca);\n\n float aniso = mix(0.45, 0.95, uStrokeAnisotropy);\n float scale = max(uStrokeScale * 1.6, 180.0);\n\n // ── Paper TOOTH height field — the relief the pigment rides. Anisotropic\n // (squished along flow, coarse cross-flow). Peaks = ridges, valleys = paper pits.\n float t1 = vnoise(vec2(pr.x * scale * aniso, pr.y * scale));\n float t2 = vnoise(vec2(pr.x * scale * aniso * 0.4, pr.y * scale * 0.4) + 11.0);\n float t3 = vnoise(vec2(pr.x * scale * aniso * 2.1, pr.y * scale * 2.1) + 23.0);\n float paperHeight = 0.55 * t1 + 0.30 * t2 + 0.15 * t3;\n\n // ── DEPOSITION — pigment lands on the tooth PEAKS and skips the valleys. Light\n // pressure (low uStrokeAmount) shows paper through the valleys; heavy pressure\n // fills them. deposit in [0,1] is the per-pixel pigment coverage.\n float pressure = clamp(uStrokeAmount, 0.0, 1.0);\n float toothFloor = mix(0.62, 0.18, pressure); // heavy pressure lowers the floor\n float deposit = smoothstep(toothFloor, toothFloor + 0.35, paperHeight);\n\n // ── SCUMBLE — a broken UPPER layer at coverage < 1 (the signature oil-pastel\n // move). A coarse mask gates a lighter dragged stroke over the deposition so the\n // LOWER color shows through the gaps. Drag direction follows the tensor flow.\n float scumbleMask = smoothstep(0.35, 0.85, vnoise(vec2(pr.x * scale * 0.55, pr.y * scale * 0.22) + 7.0));\n float scumbleCoverage = scumbleMask * pressure * 0.6; // < 1 — paper/lower shows\n\n // The lower color is the base; the deposited pigment is the base lifted into the\n // tooth. Mix the base toward the deposited layer by the deposition coverage, then\n // let the scumble break the upper layer so the lower reads through the gaps.\n vec3 paper = col * 0.92; // the paper-showing-through tone\n vec3 deposited = col; // the pigment-on-tooth tone\n vec3 result = mix(paper, deposited, deposit);\n // Scumble: a broken pass of the deposited color at < 1 coverage.\n result = mix(result, deposited * (0.96 + 0.08 * t1), scumbleCoverage);\n\n // ── WAXY BURNISH FILM — a low-roughness BROAD specular lobe whose sheen grows\n // with the pigment build-up (burnish), distinct from oil's sharp glint. Reads the\n // paper-height gradient as a soft normal and lights it with the movable uLightDir.\n float waxNormalZ = 2.4;\n vec3 N = normalize(vec3(-dFdx(paperHeight) * 30.0, -dFdy(paperHeight) * 30.0, waxNormalZ));\n vec3 L = normalize(uLightDir);\n vec3 V = vec3(0.0, 0.0, 1.0);\n vec3 H = normalize(L + V);\n float burnish = deposit * (0.5 + 0.5 * scumbleMask); // sheen grows with build-up\n float sheen = pow(max(dot(N, H), 0.0), 6.0); // broad waxy lobe\n result += burnish * sheen * 0.10 * uLightColor;\n\n // ── Broken-color pigment: stable wax/pigment patches in OKLCh (W5), not flicker.\n vec2 pigmentCell = floor(pr * max(scale * 0.18, 32.0));\n float pigmentMask = smoothstep(0.28, 0.82, vnoise(pr * scale * 0.21 + 19.0));\n result = brokenColorJitter(\n result,\n hash21(pigmentCell + 17.0),\n hash21(pigmentCell * 2.1 + 31.0),\n 0.45 + 0.55 * pigmentMask\n );\n\n // Oil pastel is saturation-amplified (the waxy chroma punch).\n result = saturate3(result, 1.12);\n\n return result;\n}\n\nvec3 mediumOil(vec3 col, vec2 p, float t) {\n // Mode knobs (uStrokeMode) — oil-stroke modes ONLY (crayon is a peer medium,\n // uMedium==4, dispatched at main()):\n // 0 oil — balanced modern-abstract/palette-knife hybrid\n // 1 knife — palette-knife impasto: razor edges, heavy bristle/shadow\n // 3 brushwork — thick bristle brush\n int mode = uStrokeMode;\n\n // Per-mode parameters\n int shapeType = 0; // tapered\n float bristleAmp = 0.25; // 0..0.5\n float streakFreq = 9.0;\n float streakAmp = 0.09;\n float impastoAmp = 0.9;\n float hardness = 0.80; // edge compositing\n float toothScale = 240.0;\n float toothAmp = 0.09;\n float pigmentSat = 1.03;\n float densityBig = 0.65;\n float densityMed = 0.78;\n float densitySml = 0.90;\n\n if (mode == 1) { // palette knife\n shapeType = 3; // flat, even\n bristleAmp = 0.12;\n streakFreq = 4.0; streakAmp = 0.05;\n impastoAmp = 1.6;\n hardness = 0.95;\n toothAmp = 0.04;\n densityBig = 0.80; densityMed = 0.88; densitySml = 0.70;\n } else if (mode == 3) { // thick brushwork\n shapeType = 0; // tapered\n bristleAmp = 0.32;\n streakFreq = 14.0; streakAmp = 0.14;\n impastoAmp = 1.2;\n hardness = 0.85;\n toothAmp = 0.07;\n }\n\n // Scales & multipliers from uniforms\n float baseScale = max(uStrokeScale * 0.006, 0.008);\n // Three layers: big gestural, medium body, small dabs\n float sBig = baseScale * 2.4;\n float sMed = baseScale * 1.1;\n float sSml = baseScale * 0.45;\n\n float lenMulBig = mix(2.2, 3.8, uStrokeAnisotropy);\n float widMulBig = mix(0.55, 0.32, uStrokeAnisotropy);\n float lenMulMed = mix(2.0, 3.4, uStrokeAnisotropy);\n float widMulMed = mix(0.50, 0.30, uStrokeAnisotropy);\n float lenMulSml = mix(1.6, 2.6, uStrokeAnisotropy);\n float widMulSml = mix(0.45, 0.32, uStrokeAnisotropy);\n\n float jitterAmt = 0.75; // large jitter — no grid\n vec2 flow = flowField(p, t);\n\n vec3 result = col;\n // AW.W4.2 — accumulated paint HEIGHT across the stroke layers. The relight reads\n // its gradient for the normal; the canvas tooth seeds the base relief.\n float height = 0.0;\n\n // Layer 1 — big gestural strokes (sparse, shaping)\n StrokeHit hBig = bestOil(p, sBig, lenMulBig, widMulBig, jitterAmt * 0.55,\n densityBig, shapeType, bristleAmp, flow, t, 1.3);\n paintOver(result, height, hBig, streakFreq * 0.7, streakAmp,\n uImpasto * impastoAmp * uStrokeAmount, hardness, 1.3);\n\n // Layer 2 — medium body strokes\n StrokeHit hMed = bestOil(p + vec2(11.3, 3.7), sMed, lenMulMed, widMulMed,\n jitterAmt, densityMed, shapeType, bristleAmp, flow, t, 2.7);\n paintOver(result, height, hMed, streakFreq, streakAmp,\n uImpasto * impastoAmp * uStrokeAmount, hardness, 2.7);\n\n // Layer 3 — small dabs (more frequent, smaller)\n int smlShape = (mode == 1) ? 2 : shapeType; // knife uses dabs for sparkle\n StrokeHit hSml = bestOil(p + vec2(-5.1, 8.4), sSml, lenMulSml, widMulSml,\n jitterAmt * 1.3, densitySml, smlShape,\n bristleAmp * 0.85, flow, t, 4.1);\n paintOver(result, height, hSml, streakFreq * 1.4, streakAmp * 0.8,\n uImpasto * impastoAmp * 0.65 * uStrokeAmount, hardness, 4.1);\n\n // Layer 4 — fill dabs (very dense, very small) — covers bald spots\n float sFill = baseScale * 0.22;\n float lenMulFill = mix(1.4, 2.0, uStrokeAnisotropy);\n float widMulFill = mix(0.50, 0.38, uStrokeAnisotropy);\n int fillShape = (mode == 1) ? 3 : 2; // knife=even, others=dab (round fills)\n StrokeHit hFill = bestOil(p + vec2(3.9, -6.2), sFill, lenMulFill, widMulFill,\n jitterAmt * 1.5, 0.95, fillShape,\n bristleAmp * 0.6, flow, t, 8.9);\n paintOver(result, height, hFill, streakFreq * 1.8, streakAmp * 0.6,\n uImpasto * impastoAmp * 0.4 * uStrokeAmount, hardness * 0.9, 8.9);\n\n // Optional crosshatch layer\n if (uStrokeLayers == 2) {\n vec2 flow2 = vec2(-flow.y, flow.x);\n StrokeHit hX = bestOil(p + vec2(7.3, -2.1), sMed, lenMulMed * 0.9, widMulMed,\n jitterAmt, densityMed * 0.7, shapeType, bristleAmp, flow2, t, 6.5);\n paintOver(result, height, hX, streakFreq, streakAmp * 0.85,\n uImpasto * impastoAmp * 0.55 * uStrokeAmount, hardness, 6.5);\n }\n\n // Canvas tooth — linen weave\n float tooth1 = vnoise(p * toothScale);\n float tooth2 = vnoise(p * toothScale * vec2(0.6, 2.4) + 37.0);\n float tooth = (0.6 * tooth1 + 0.4 * tooth2) - 0.5;\n result *= 1.0 + tooth * toothAmp * uCanvasGrain;\n\n // AW.W4.2 — relight the accumulated paint height with the movable uLightDir\n // source (diffuse + Blinn specular, in LINEAR before aces()). The canvas tooth\n // is the base relief term so the weave also catches the raking light.\n float canvasBase = tooth * toothAmp * 0.5;\n result = relightImpasto(result, height, canvasBase);\n\n // Pigment saturation boost\n result = saturate3(result, pigmentSat);\n\n return result;\n}\n\n// AW.W4.3 — the van-Gogh atomic-stroke medium. Composes the oil stroke engine\n// (mediumOil) with the W4.1 ETF orientation (forced tensor via the bridge), the\n// W4.3 energy grading (gated by uMedium==5 inside bestOil — long confident strokes\n// in the lights/coherent zones, short dabs in the darks/flat zones, the Starry-Night\n// Kolmogorov/Batchelor cascade), the W5 OKLCh per-stroke pigment jitter (already in\n// bestOil's brokenColorJitter), and the W4.2 real impasto relight. No subject matter\n// — the \"source image\" is the generated nuclei field, so strokes trace its iso-bands.\nvec3 mediumVangogh(vec3 col, vec2 p, float t) {\n return mediumOil(col, p, t);\n}", A = "\n", he = `#version 300 es
10
+ var pe = "#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() {\n vUv = aPos * 0.5 + 0.5;\n gl_Position = vec4(aPos, 0.0, 1.0);\n}", me = "// ── Palette LUT (W5 — OKLCh/OKLab interpolation) ──────────────────────────────\n// The endpoints stay CPU-baked to LINEAR sRGB (the Aras precompute pattern); only\n// the BETWEEN-endpoint interpolation runs in perceptual space. AX.W11 — the ramp\n// itself (the smoothstep ease + the OKLab-rectangular-vs-OKLCh-hue-arc dispatch on\n// uHuePath) is the shared samplePaletteRamp from procedural-color.glsl, the SAME\n// source the WebGPU twin splices; samplePalette here only selects the bracketing\n// stop pair + the raw inter-stop t and hands them to the shared ramp.\nvec3 samplePalette(float id) {\n id = clamp(id, 0.0, 1.0);\n float scaled = id * float(uStopCount - 1);\n int i0 = int(floor(scaled));\n int i1 = min(i0 + 1, uStopCount - 1);\n float t = fract(scaled);\n return samplePaletteRamp(uPalette[i0], uPalette[i1], t, uHuePath);\n}\n\n// ── Nuclei field ──────────────────────────────────────────────────────────\nvoid nucleiField(vec2 p, float t, out float paletteId, out float valueMod) {\n float accumBias = 0.0;\n float accumValue = 0.0;\n float accumW = 0.0;\n for (int i = 0; i < MAX_NUCLEI; i++) {\n if (i >= uNucleiCount) break;\n vec2 posI = uNucleiPos[i]\n + uNucleiDriftRadius[i] * vec2(\n cos(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i]),\n sin(t * uNucleiDrift * K_NUCLEI + uNucleiDriftPhase[i] * 1.13)\n );\n vec2 diff = p - posI;\n // Anisotropic Gaussian: rotate diff into the nucleus's local frame\n // (major axis along uNucleiAngle), then scale the major-axis component by\n // 1/elongation so the squared distance describes an ellipse. Defaults\n // 1.0/0.0 reduce to the isotropic dot(diff, diff).\n float ca = cos(uNucleiAngle[i]);\n float sa = sin(uNucleiAngle[i]);\n vec2 local = vec2( ca * diff.x + sa * diff.y,\n -sa * diff.x + ca * diff.y);\n float along = local.x / max(uNucleiElong[i], 0.01);\n float across = local.y;\n float d2 = along * along + across * across;\n float r = max(uNucleiRadius[i], 0.01);\n float w = exp(-uSoftmaxBeta * d2 / (r * r));\n accumBias += w * uNucleiPaletteBias[i];\n accumValue += w * uNucleiValueBias[i];\n accumW += w;\n }\n paletteId = accumBias / max(accumW, 1e-4);\n valueMod = accumValue / max(accumW, 1e-4);\n\n // Palette drift — slow global paletteId breathe between adjacent stops. The\n // rate rides K_PAL (perceptible ~30–60s hue cycle at the default coefficient);\n // the amplitude lifts the authored coefficient into a 0.03..0.06 paletteId band\n // so the palette visibly travels rather than dithering within one stop.\n float palAmp = clamp(uPaletteDrift * 6.0, 0.0, 0.06);\n paletteId += palAmp * sin(t * uPaletteDrift * K_PAL);\n paletteId = clamp(paletteId, 0.0, 1.0);\n}\n", he = "// ── Flow field ────────────────────────────────────────────────────────────\n// Structure-tensor prototype (body defined post-sampleBase in the mediums block).\n// Returns vec3(dir.x, dir.y, coherence) — the minor-eigenvector tangent + A.\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir);\n\nvec2 flowField(vec2 p, float t) {\n vec2 dir = vec2(1.0, 0.0);\n if (uFlowPattern == 1) {\n // radial\n dir = normalize(p - uFlowFocal + 1e-4);\n } else if (uFlowPattern == 2) {\n // swirl — tangent to radial\n vec2 rad = normalize(p - uFlowFocal + 1e-4);\n dir = vec2(-rad.y, rad.x);\n } else if (uFlowPattern == 3) {\n // diagonal\n float a = radians(uFlowAngle);\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 4) {\n // multi — curl-noise driven\n float n = fbm(p * 2.0 + t * 0.02);\n float a = n * 6.2831;\n dir = vec2(cos(a), sin(a));\n } else if (uFlowPattern == 5) {\n // tensor / ETF — the color field's OWN edge-tangent flow (AW.W4.1). The\n // diagonal angle seeds the smooth fallback the coherence-weighted blend\n // relaxes toward in low-structure zones. Returns directly (no curl/cursor\n // post-mix — the tensor field already carries the structure orientation;\n // the cursor warp still drives the underlying color field in domainWarp).\n float a = radians(uFlowAngle);\n vec2 fallback = vec2(cos(a), sin(a));\n return structureTensorField(p, t, fallback).xy;\n }\n // curl — perturb by noise. Radial gets much less curl so rays stay clean.\n if (uFlowCurl > 0.0) {\n float n = fbm(p * 3.0) - 0.5;\n float curlAmt = uFlowCurl;\n if (uFlowPattern == 1) curlAmt *= 0.25; // radial: preserve ray clarity\n if (uFlowPattern == 2) curlAmt *= 0.55; // swirl: allow moderate curl\n float a = atan(dir.y, dir.x) + n * 3.14159 * curlAmt;\n dir = vec2(cos(a), sin(a));\n }\n // cursor influence — swirl around the cursor position\n if (uCursorStrength > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.5));\n // swirl tangent\n vec2 tangent = vec2(-toCur.y, toCur.x) / max(d, 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(tangent.y, tangent.x);\n // blend angle\n float da = a1 - a0;\n da = atan(sin(da), cos(da)); // wrap to [-pi, pi]\n float a = a0 + da * w * uCursorStrength;\n dir = vec2(cos(a), sin(a));\n }\n // AW.W8.1 — velocity-reactive swirl-BURST: a fast flick (uCursorBurst, decaying\n // over ~1s) injects a transient flow bias along the pointer velocity, distinct from\n // the steady attraction above (the stateless swirl is SUPERSEDED by this form). The\n // burst is gated by the master tempo scalar on the CPU side (it decays to 0 under\n // PRM/pause), so this term cannot leak motion under reduced-motion.\n if (uCursorBurst > 0.001) {\n vec2 toCur = uCursor - p;\n float d = length(toCur);\n float r = max(uCursorRadius, 0.01);\n float w = exp(-(d * d) / (r * r * 0.7));\n // Bias the flow toward the velocity direction, scaled by the decaying burst.\n vec2 velDir = uCursorVelocity / max(length(uCursorVelocity), 1e-4);\n float a0 = atan(dir.y, dir.x);\n float a1 = atan(velDir.y, velDir.x);\n float da = atan(sin(a1 - a0), cos(a1 - a0));\n float a = a0 + da * w * uCursorBurst * 0.8;\n dir = vec2(cos(a), sin(a));\n }\n return dir;\n}\n", ge = "// ACES approximation\nvec3 aces(vec3 x) {\n float a = 2.51;\n float b = 0.03;\n float c = 2.43;\n float d = 0.59;\n float e = 0.14;\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);\n}\n", _e = "// ── Curved swept brushstroke primitive ────────────────────────────────────\n// A stroke is a curved spine from A to B with quadratic bulge K (sideways offset\n// at midpoint). Width varies along length via a shape profile. Edge is ragged\n// (bristle-modulated), ends are rounded blobs, inside has streak modulation.\n//\n// For pixel p, we invert an approximate projection onto the curved spine:\n// 1) project p onto straight AB axis -> along0, cross0\n// 2) at parameter along0, spine offset = 4·K·along0·(1-along0) perpendicular\n// 3) cross = cross0 - spineOffset, refine along one iteration\n// Coverage blends paint atop col based on per-stroke color sampled from the\n// underlying base at spine midpoint — so overlapping strokes from different\n// palette regions meet at hard, ragged demarcations.\n\nstruct StrokeHit {\n float coverage; // 0..1\n vec3 color;\n float alongT; // 0..1 along spine, for internal modulation\n float crossN; // cross distance / current half-width, signed\n float edgeN; // distance to edge in half-widths (0 = at edge, 1 = at spine)\n};\n\nStrokeHit noHit() { return StrokeHit(0.0, vec3(0.0), 0.0, 0.0, 0.0); }\n\nvec2 rotateDir(vec2 dir, float angle) {\n float ca = cos(angle), sa = sin(angle);\n return vec2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);\n}\n\nvec2 safeDir(vec2 dir) {\n float len = length(dir);\n return len > 1e-4 ? dir / len : vec2(1.0, 0.0);\n}\n\n// Shape profile along the stroke. type:\n// 0 tapered — thin-fat-thin (classic brush)\n// 1 load-drag — fat start, tapers to a point\n// 2 dab — ellipse-like, round center\n// 3 even — near-constant, slight end taper\n// 4 comma — van-Gogh atomic dab: ASYMMETRIC taper, fat near one end and\n// drawn to a fine point at the other (the divisionist crescent\n// mark). The width peak sits off-centre (~0.32) so the two ends\n// taper at DIFFERENT rates — a loaded round head + a dragged tail.\nfloat strokeShape(float t, int type) {\n t = clamp(t, 0.0, 1.0);\n if (type == 1) {\n // loaded at t=0, tapering\n return pow(1.0 - t, 0.55) * smoothstep(0.0, 0.08, t);\n } else if (type == 2) {\n // dab / blob\n float d = t - 0.5;\n return exp(-d * d * 12.0);\n } else if (type == 3) {\n // mostly even, slight end softening\n return smoothstep(0.0, 0.08, t) * smoothstep(1.0, 0.92, t) * 0.95 + 0.05;\n } else if (type == 4) {\n // comma / crescent — width peaks at ~0.32 then drags to a fine tail. The two\n // sides of the peak fall at different exponents (a fat round head, a long thin\n // tail), so the mark reads as a directional van-Gogh comma, not a symmetric oval.\n float peak = 0.32;\n float head = smoothstep(0.0, peak, t); // rounded loaded head\n float tail = pow(clamp((1.0 - t) / (1.0 - peak), 0.0, 1.0), 1.6); // dragged thin tail\n return clamp(min(head, tail) * 1.04, 0.0, 1.0);\n }\n // 0 tapered\n return smoothstep(0.0, 0.22, t) * smoothstep(1.0, 0.78, t);\n}\n\n// A single curved stroke.\n// a, b — endpoints\n// halfW — base half-width (world units)\n// bulge — signed perpendicular midpoint offset, in world units\n// shapeType — 0..3\n// bristleFreq — spatial frequency of edge raggedness\n// bristleAmp — 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed — uniqueness seed for internal streaks\n// colAtMid — pre-sampled color at midpoint\nStrokeHit curvedStroke(vec2 p, vec2 a, vec2 b, float halfW,\n float bulge, int shapeType,\n float bristleFreq, float bristleAmp,\n float streakSeed, vec3 colAtMid) {\n vec2 ab = b - a;\n float L = length(ab);\n if (L < 1e-5) return noHit();\n vec2 tang = ab / L;\n vec2 norm = vec2(-tang.y, tang.x);\n\n vec2 rel = p - a;\n float along0 = dot(rel, tang) / L; // straight projection 0..1 along AB\n float cross0 = dot(rel, norm); // signed cross\n\n // Spine sideways offset due to bulge (parabolic)\n float bend = 4.0 * bulge * along0 * (1.0 - along0);\n float cross1 = cross0 - bend;\n\n // One refinement step: as the spine bends, the closest-point along shifts.\n // Spine tangent differs from straight tangent; first-order correction:\n float dBend = 4.0 * bulge * (1.0 - 2.0 * along0); // d(bend)/d(along)\n float along1 = along0 + (cross1 * dBend) / (L * (1.0 + dBend * dBend / (L * L)));\n\n // Bristle-ragged edge: half-width gets chewed by 1D noise along the spine\n float edgeNoise = fbm(vec2(along1 * bristleFreq, streakSeed * 7.3)) - 0.5;\n float edgeNoise2 = fbm(vec2(along1 * bristleFreq * 2.3, streakSeed * 3.1 + 17.0)) - 0.5;\n float edgeMod = 1.0 - bristleAmp * (0.6 + 0.4 * edgeNoise) * (0.5 + edgeNoise2);\n\n // Width profile along the stroke\n float shape = strokeShape(along1, shapeType);\n float halfWNow = halfW * shape * edgeMod;\n\n // Inside-segment coverage\n float cov = 0.0;\n if (along1 >= 0.0 && along1 <= 1.0 && halfWNow > 1e-6) {\n float cn = abs(cross1) / halfWNow;\n // fwidth-derived AA: half-width from the field's screen-space gradient so the\n // edge holds ~1px regardless of zoom/DPR (mirrors metaball.frag.ts:252-255).\n float aaC = max(fwidth(cn), 1e-6);\n cov = 1.0 - smoothstep(1.0 - aaC, 1.0 + aaC, cn);\n }\n\n // End-cap blobs (rounded tips, not perpendicular cuts)\n float capA = 0.0, capB = 0.0;\n {\n vec2 capCenterA = a + norm * (bulge * 0.0); // at a\n vec2 capCenterB = a + tang * L + norm * bend * 0.0 + norm * 0.0; // at b; bend is 0 at endpoints\n capCenterB = b;\n float endShapeA = strokeShape(0.02, shapeType);\n float endShapeB = strokeShape(0.98, shapeType);\n float rA = halfW * endShapeA * (0.9 + 0.2 * fbm(vec2(streakSeed * 13.0)));\n float rB = halfW * endShapeB * (0.9 + 0.2 * fbm(vec2(streakSeed * 19.0 + 5.0)));\n float dA = length(p - capCenterA);\n float dB = length(p - capCenterB);\n // fwidth-derived AA on the clean Euclidean cap distances (mirrors metaball.frag.ts:252-255).\n float aaA = max(fwidth(dA), 1e-6);\n float aaB = max(fwidth(dB), 1e-6);\n capA = 1.0 - smoothstep(rA - aaA, rA + aaA, dA);\n capB = 1.0 - smoothstep(rB - aaB, rB + aaB, dB);\n // Only apply caps if we're BEYOND the segment; otherwise the segment wins.\n capA *= (along1 < 0.05) ? 1.0 : 0.0;\n capB *= (along1 > 0.95) ? 1.0 : 0.0;\n }\n\n float coverage = max(cov, max(capA, capB));\n if (coverage < 0.002) return noHit();\n\n // Edge distance (for impasto later): 0 at edge, 1 at spine\n float edgeDist = halfWNow > 1e-6 ? clamp(1.0 - abs(cross1) / halfWNow, 0.0, 1.0) : 0.0;\n\n return StrokeHit(\n coverage,\n colAtMid,\n clamp(along1, 0.0, 1.0),\n halfWNow > 1e-6 ? cross1 / halfWNow : 0.0,\n edgeDist\n );\n}\n\n// The painterly STROKE mediums (those that deposit via paintStrokeLayers/paintOver):\n// oil (3), van-Gogh (5), oil-pastel (6). For these the stroke OVER-composite runs in\n// OKLab so overlapping complementaries mix through a chromatic path, not the linear-\n// RGB grey mud; the smooth/atmospheric pole never reaches paintOver so it pays nothing.\nbool isPainterlyStroke() {\n return uMedium == 3 || uMedium == 5 || uMedium == 6;\n}\n\n// OKLab OVER-composite: lerp L,a,b of the over-color toward the under-color (the\n// Ottosson matrices spliced from procedural-color.glsl.ts — no new color math). Two\n// complementary-hued strokes overlapping transition through OKLab, where the midpoint\n// holds chroma, instead of the linear mix() that desaturates toward grey (slice 8 F2).\nvec3 paintOverOklab(vec3 under, vec3 over, float alpha) {\n vec3 labU = linOklab(under);\n vec3 labO = linOklab(over);\n return max(oklabToLinearSrgb(mix(labU, labO, alpha)), vec3(0.0));\n}\n\n// Composite stroke over col with internal streaking + ACCUMULATE paint HEIGHT.\n// streakAmp — 0..0.2 how much internal streaks darken/lighten\n// impastoAmp — 0..1 paint-thickness contribution (the height the relight reads)\n// hardness — 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\n//\n// AW.W4.2 — the fixed-RGB edge rim (the phantom upper-left light) is RETIRED. Each\n// stroke now deposits PAINT HEIGHT into the height accumulator (coverage x per-stroke\n// thickness, perturbed by the bristle/streak fbm for ridges/grooves); mediumOil\n// derives a normal from the accumulated height gradient (dFdx/dFdy) and relights it\n// once with the movable uLightDir source (relightImpasto), in linear before aces().\n//\n// AX.W13 — the internal streak now perturbs HUE + CHROMA in OKLCh (not value-only),\n// so a single stroke carries a small hue gradient (broken color at the ATOM level —\n// adjacent strokes shimmer like real impasto, not flat stamped swatches; slice 8 F5);\n// and the OVER-composite runs in OKLab on the painterly stroke mediums so overlapping\n// complementaries mix as pigment, not the linear-mix grey (slice 8 F2).\n// impastoFloor — the per-stroke height-crown floor: oil's 0.4+0.6*edgeN reads\n// floor=0.4 (the crown tapers to 40% at the stroke edge); the van-Gogh\n// profile passes floor=1.0 so each atomic dab catches a FULL-height\n// glint (scope item 1d). The crown magnitude is floor + (1-floor)*edgeN.\nvoid paintOver(inout vec3 col, inout float height, StrokeHit s,\n float streakFreq, float streakAmp,\n float impastoAmp, float hardness, float streakSeed,\n float impastoFloor) {\n if (s.coverage < 0.002) return;\n float strokeOpacity = clamp(uStrokeAmount, 0.0, 1.0);\n if (strokeOpacity <= 0.001) return;\n vec3 c = s.color;\n\n // Internal streaks — fbm along spine, modulated by cross position. Gives\n // the loaded-brush look: some bristles carry more pigment than others.\n float streakA = fbm(vec2(s.alongT * streakFreq, s.crossN * 2.7 + streakSeed));\n float streakB = fbm(vec2(s.alongT * streakFreq * 0.6 + streakSeed * 3.7, s.crossN * 4.1));\n float streak = 0.6 * (streakA - 0.5) + 0.4 * (streakB - 0.5);\n\n // ── WITHIN-stroke OKLCh broken color (AX.W13, slice 8 F5). The streak fBm perturbs\n // the stroke's HUE + CHROMA along alongT/crossN, seeded per-stroke, so a single\n // stroke reads a small hue gradient (the atom-level broken-color shimmer). Value is\n // carried by the same streak so the loaded-brush light/dark survives. Gated to the\n // painterly stroke mediums + uBrokenColor (the smooth pole keeps the cheap value-only\n // streak). A second decorrelated fBm drives the hue axis so hue ≠ a scaled value.\n float hueStreak = fbm(vec2(s.alongT * streakFreq * 0.45 + streakSeed * 5.3,\n s.crossN * 1.9 + streakSeed * 1.7)) - 0.5;\n if (isPainterlyStroke() && uBrokenColor > 0.001) {\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~10° within-stroke hue swing + ±~9% chroma, scaled by uBrokenColor. Smaller\n // than the per-stroke cell jitter (brokenColorJitter) — this is the intra-stroke\n // gradient layered on top of the per-cell pigment patch.\n lch.z += hueStreak * (20.0 * PI / 180.0) * uBrokenColor;\n lch.y = max(lch.y * (1.0 + (streak) * 0.18 * uBrokenColor), 0.0);\n c = max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n }\n // Value streak (the loaded-brush light/dark across the bristles) — kept for every\n // medium that reaches paintOver (the smooth pole never does).\n c *= 1.0 + streak * streakAmp * 2.0;\n\n // Subtle value variance across width (hollow-center catch-light)\n float crossShade = smoothstep(0.0, 0.4, s.edgeN) * (1.0 - smoothstep(0.65, 1.0, s.edgeN));\n c *= 1.0 + crossShade * 0.05;\n\n float softLimit = mix(0.35, 0.98, hardness);\n float alpha = smoothstep(0.0, 1.0 - softLimit, s.coverage) * strokeOpacity;\n // OKLab OVER-composite on the painterly stroke mediums (overlap mixes as pigment,\n // not grey); the smooth pole never reaches paintOver, so it keeps the cheap path.\n col = isPainterlyStroke() ? paintOverOklab(col, c, alpha) : mix(col, c, alpha);\n\n // ── Accumulate paint HEIGHT (AW.W4.2). Thickness peaks at the stroke spine and\n // falls toward the impastoFloor at the edge (edgeN: 0=edge, 1=spine), perturbed by\n // the bristle streak so ridges/grooves form. The relight reads the gradient of this\n // field. The van-Gogh profile's floor=1.0 gives every atomic dab a full-height crown.\n float ridge = 0.7 + 0.3 * (streak + 0.5);\n float crown = impastoFloor + (1.0 - impastoFloor) * s.edgeN;\n height += s.coverage * impastoAmp * crown * ridge * alpha;\n}\n\n// AW.W4.2 — relight the accumulated paint height. Derive a normal from the height\n// gradient (dFdx/dFdy — derivatives are in-pattern; fwidth is already used for AA),\n// then apply diffuse + Blinn specular from the movable uLightDir/uLightColor source.\n// LINEAR-space lighting BEFORE aces() (the tonemap/OETF stay locked); thin strokes\n// inherit a low shininess, thick impasto raises it (high-height ridges catch a sharp\n// glint). canvasBase is the tooth height so the canvas weave also catches the light.\nvec3 relightImpasto(vec3 col, float height, float canvasBase) {\n float h = height + canvasBase;\n // Height-gradient normal. heightScale trades relief depth for surface flatness.\n float heightScale = 6.0;\n vec3 N = normalize(vec3(-dFdx(h) * 40.0, -dFdy(h) * 40.0, 1.0 / heightScale));\n vec3 L = normalize(uLightDir);\n vec3 V = vec3(0.0, 0.0, 1.0); // view straight on\n vec3 H = normalize(L + V);\n float diff = max(dot(N, L), 0.0);\n // Thick impasto → high shininess (sharp glint); thin paint → broad soft sheen.\n float shininess = mix(8.0, 64.0, clamp(height * 2.0, 0.0, 1.0));\n float spec = pow(max(dot(N, H), 0.0), shininess);\n // Modulate the lit terms by how much paint sits here (no relief on bare ground).\n float relief = clamp(h * 1.5, 0.0, 1.0);\n vec3 lit = col * (1.0 + uImpasto * relief * (diff - 0.5) * 0.5 * uLightColor);\n lit += uImpasto * relief * spec * 0.6 * uLightColor;\n return lit;\n}\n\n// Best-of-9-neighbor placement: sample 3x3 surrounding cells and take the\n// stroke that covers this pixel most. Breaks the grid by per-cell jitter and\n// sparse density via noise thresholding.\n// energyGrade — the SBR energy-grade magnitude (0 = none, the oil/oil-pastel\n// baseline; 1 = the full van-Gogh Starry-Night cascade — long\n// confident strokes in the lights/coherent zones, short dabs in\n// the darks/flat zones). The van-Gogh profile carries 1.0 (the\n// energy grade is a PROFILE field, not a buried uMedium==5 branch).\nStrokeHit bestOil(vec2 p, float cellSize, float lenMul, float halfWMul,\n float jitterAmt, float density, int shapeType,\n float bristleAmp, vec2 flow, float t, float seed,\n float energyGrade) {\n vec2 cell = floor(p / cellSize);\n\n StrokeHit best = noHit();\n // 3x3 neighborhood\n for (int dy = -1; dy <= 1; dy++) {\n for (int dx = -1; dx <= 1; dx++) {\n vec2 cc = cell + vec2(float(dx), float(dy));\n vec2 hh = hash22(cc + seed) - 0.5;\n // Density gate — noise threshold; sparse placement.\n float gate = hash21(cc * 1.7 + seed * 0.3);\n if (gate > density) continue;\n\n vec2 center = (cc + 0.5 + hh * jitterAmt) * cellSize;\n\n // Per-stroke direction source — the AW.W4.1 strokeOrient switch:\n // uStrokeOrient==0 (flow) — the layer-provided hand-authored flowField.\n // uStrokeOrient==1 (tensor) — the structure-tensor minor eigenvector at the\n // cell center (the color field's edge-tangent),\n // so strokes hug the color zones (van Gogh).\n // coh carries the per-cell coherence A for the W4.3 energy grading (long\n // confident strokes where structure is coherent, stubby dabs in flat zones).\n vec2 f;\n float coh = 1.0;\n if (uStrokeOrient == 1) {\n vec3 tf = structureTensorField(center, t, safeDir(flow));\n f = safeDir(tf.xy);\n coh = tf.z;\n } else {\n f = safeDir(flow);\n }\n // Per-stroke deterministic local perturbation so alternate stroke layers stay\n // live. Tensor strokes get LESS angular jitter (the field already orients them).\n float jScale = (uStrokeOrient == 1) ? 0.4 : 1.0;\n float angJ = (hash21(cc + seed + 11.0) - 0.5) * 0.9 * jScale; // +/- 0.45/0.18 rad\n float localCurl = (fbm(center * (2.6 + seed * 0.11) + seed * 1.9) - 0.5) * 0.55 * uFlowCurl;\n vec2 dir = rotateDir(f, angJ + localCurl);\n\n // AX.W13 — the SBR energy grade is a PROFILE field (energyGrade), not a buried\n // uMedium==5 branch. The van-Gogh profile passes energyGrade=1.0 (the full\n // Starry-Night cascade); oil/oil-pastel pass 0.0 (uniform length). The grade\n // modulates stroke length by local luminance (bright → long confident strokes,\n // dark → short dabs, the Kolmogorov/Batchelor congruence) AND by coherence\n // (coherent zones → long, flat → stubby), lerped in by energyGrade so a profile\n // can dial the cascade from off (0) to full (1).\n float energy = 1.0;\n if (energyGrade > 0.001) {\n float luma = clamp(dot(sampleBase(center, t), W_LUMA), 0.0, 1.0);\n float graded = mix(0.5, 1.5, luma) * mix(0.6, 1.0, coh);\n energy = mix(1.0, graded, energyGrade);\n }\n\n float lenV = cellSize * lenMul * (0.65 + 0.55 * hash21(cc + seed + 23.0)) * energy;\n float halfW = cellSize * halfWMul * (0.70 + 0.55 * hash21(cc + seed + 41.0));\n float bulge = (hash21(cc + seed + 53.0) - 0.5) * lenV * 0.35;\n\n vec2 a = center - dir * (lenV * 0.5);\n vec2 b = center + dir * (lenV * 0.5);\n\n vec3 colMid = brokenColorJitter(\n sampleBase(center, t),\n hash21(cc + seed + 89.0),\n hash21(cc * 2.3 + seed + 97.0),\n 1.0\n );\n\n StrokeHit h = curvedStroke(p, a, b, halfW, bulge, shapeType,\n 7.0, bristleAmp,\n hash21(cc + seed + 67.0), colMid);\n if (h.coverage > best.coverage) best = h;\n }\n }\n return best;\n}\n", ve = "// ── Medium overlays ───────────────────────────────────────────────────────\n\n// A quick re-computation of base color for edge-mask sampling\nvec3 sampleBase(vec2 p, float t) {\n vec2 pw = domainWarp(p, t);\n float id; float vm;\n nucleiField(pw, t, id, vm);\n vec3 c = samplePalette(id);\n c *= 1.0 + uValueVariance * vm;\n return c;\n}\n\n// ── Structure-tensor / edge-tangent-flow (AW.W4.1) ─────────────────────────\n// The keystone of the painterly engine: derive stroke orientation from the COLOR\n// FIELD's OWN structure, not a hand-authored flow pattern. A 3x3 Sobel over\n// luma(sampleBase) yields the gradient (Gx,Gy); the 2x2 structure tensor\n// J = [[Gx², GxGy],[GxGy, Gy²]] eigen-decomposes closed-form into the MINOR\n// eigenvector (the edge-TANGENT — least color change, the stroke direction; the\n// MAJOR eigenvector is the gradient/edge-NORMAL and would make strokes cross the\n// bands) and the coherence A = (λ1-λ2)/(λ1+λ2). Low-coherence (flat) zones relax\n// toward the smooth fallbackDir by (1-A) so tensor noise never reads as jitter.\n// Single-pass small-tap WebGL2 form; the Gaussian-smoothed multi-tap + LIC smear\n// is the AW.W7 WebGPU multi-pass fold. Returns vec3(tangent.x, tangent.y, A).\nvec3 structureTensorField(vec2 p, float t, vec2 fallbackDir) {\n float e = 0.0035; // small-tap neighborhood (edge-mask scale)\n // 3x3 luma samples.\n float l00 = dot(sampleBase(p + vec2(-e, -e), t), W_LUMA);\n float l10 = dot(sampleBase(p + vec2( 0.0, -e), t), W_LUMA);\n float l20 = dot(sampleBase(p + vec2( e, -e), t), W_LUMA);\n float l01 = dot(sampleBase(p + vec2(-e, 0.0), t), W_LUMA);\n float l21 = dot(sampleBase(p + vec2( e, 0.0), t), W_LUMA);\n float l02 = dot(sampleBase(p + vec2(-e, e), t), W_LUMA);\n float l12 = dot(sampleBase(p + vec2( 0.0, e), t), W_LUMA);\n float l22 = dot(sampleBase(p + vec2( e, e), t), W_LUMA);\n // Sobel derivatives.\n float Gx = (l20 + 2.0 * l21 + l22) - (l00 + 2.0 * l01 + l02);\n float Gy = (l02 + 2.0 * l12 + l22) - (l00 + 2.0 * l10 + l20);\n // Structure tensor components.\n float Jxx = Gx * Gx;\n float Jyy = Gy * Gy;\n float Jxy = Gx * Gy;\n // Closed-form 2x2 eigenvalues: λ = 0.5(tr ± sqrt((Jxx-Jyy)² + 4Jxy²)).\n float tr = Jxx + Jyy;\n float disc = sqrt(max((Jxx - Jyy) * (Jxx - Jyy) + 4.0 * Jxy * Jxy, 0.0));\n float lambdaMaj = 0.5 * (tr + disc);\n float lambdaMin = 0.5 * (tr - disc);\n // The MAJOR eigenvector points along the gradient (the edge NORMAL); the MINOR\n // is its perpendicular — the edge TANGENT (the stroke direction). The principal\n // angle is θ = 0.5·atan2(2·Jxy, Jxx-Jyy); this form is robust at Jxy≈0 (where the\n // (λ-Jyy, Jxy) eigenvector formula collapses to a zero vector). The tangent is at\n // θ + 90°. Guard the fully-isotropic (no-gradient) degenerate case → fallback dir.\n vec2 tangent;\n if (disc < 1e-7) {\n tangent = fallbackDir;\n } else {\n float theta = 0.5 * atan(2.0 * Jxy, Jxx - Jyy); // major (gradient) angle\n tangent = vec2(-sin(theta), cos(theta)); // perpendicular = tangent\n }\n // Coherence A in [0,1] — high where one eigenvalue dominates (a strong edge),\n // ~0 in flat zones. Relax toward the smooth fallback by (1 - A).\n float A = (lambdaMaj - lambdaMin) / (lambdaMaj + lambdaMin + 1e-6);\n A = clamp(A, 0.0, 1.0);\n // Coherence-weighted blend: full tensor in coherent zones, smooth in flat ones.\n // Resolve the 180° sign ambiguity toward the fallback so the blend is continuous.\n if (dot(tangent, fallbackDir) < 0.0) tangent = -tangent;\n vec2 dir = normalize(mix(fallbackDir, tangent, A) + 1e-6);\n return vec3(dir, A);\n}\n\nvec3 mediumWatercolor(vec3 col, vec2 p, float t) {\n // Wet-edge cauliflowers via luma-gradient magnitude\n float eps = 0.004;\n vec3 cx1 = sampleBase(p + vec2(eps, 0.0), t);\n vec3 cx2 = sampleBase(p - vec2(eps, 0.0), t);\n vec3 cy1 = sampleBase(p + vec2(0.0, eps), t);\n vec3 cy2 = sampleBase(p - vec2(0.0, eps), t);\n float gx = dot(cx1 - cx2, W_LUMA);\n float gy = dot(cy1 - cy2, W_LUMA);\n float edge = sqrt(gx * gx + gy * gy) / (2.0 * eps);\n float mask = smoothstep(0.0, 2.5, edge);\n col *= mix(1.0, 0.78, mask * uWetEdge);\n\n // Granulation — pigment settles in paper tooth\n float paper = 0.5 * vnoise(p * 160.0) + 0.5 * vnoise(p * 360.0);\n float pigLoad = 1.0 - dot(col, W_LUMA);\n col *= 1.0 - uGranulation * pigLoad * (paper - 0.5);\n\n // Wash banding — faint horizontal wet gradient\n float band = fbm(vec2(p.x * 1.5, p.y * 0.4));\n col *= 1.0 + 0.04 * (band - 0.5);\n return col;\n}\n\nvec3 mediumPastel(vec3 col, vec2 p, float t) {\n vec2 flow = flowField(p, t);\n vec2 perp = vec2(-flow.y, flow.x);\n float along = dot(p, flow) * uStrokeScale;\n float across = dot(p, perp) * uStrokeScale;\n across += 0.03 * (vnoise(p * 260.0) - 0.5);\n\n float aniso = mix(1.0, 0.18, uStrokeAnisotropy);\n float stroke = fbm(vec2(along * aniso, across));\n col *= mix(1.0, 0.82 + 0.32 * stroke, uStrokeAmount);\n\n // Pastel tooth — tiny high-frequency grain\n float tooth = vnoise(p * 800.0);\n col *= 1.0 - 0.08 * uStrokeAmount * (tooth - 0.5);\n return col;\n}\n", ye = "// ── Medium id constants (mirror uniformBridge.ts MEDIUM_ID — the uMedium ladder) ──\n// The StrokeProfile selector (AX.W12 + AX.W13) dispatches on the medium id: oil,\n// van-Gogh, and oil-pastel each AUTHOR a first-class profile in profileFor.\n#define MEDIUM_OIL 3\n#define MEDIUM_CRAYON 4\n#define MEDIUM_VANGOGH 5\n#define MEDIUM_OILPASTEL 6\n\n// ── Crayon — DRY wax pigment on paper tooth (first-class medium, uMedium==4) ──\n// Crayon is not strokes. It's dry pigment crumbs dragged across paper tooth — the\n// DRY tooth-multiply model: anisotropic tooth noise multiplied into the base, a hard\n// SCUMBLE that lets the lower color show through the broken upper layer, and stable\n// OKLCh broken-color pigment patches. NO sheen, NO burnish film — that waxy gloss is\n// the OIL-PASTEL deposition's signature (mediumOilPastel), distinct from dry crayon\n// (AX.W13 slice 8 F1 — the split). Crayon shares the SUBSTRATE (the structure-tensor\n// orientation, the tooth noise, OKLCh brokenColorJitter), not the dispatch body.\nvec3 mediumCrayon(vec3 col, vec2 p, float t) {\n // Orient the deposition along the structure-tensor edge-tangent (the bridge forces\n // tensor for crayon, so read the tensor field directly).\n vec2 flow = structureTensorField(p, t, flowField(p, t)).xy;\n float ang = atan(flow.y, flow.x);\n float ca = cos(-ang), sa = sin(-ang);\n vec2 pr = vec2(p.x * ca - p.y * sa, p.x * sa + p.y * ca);\n\n float aniso = mix(0.45, 0.95, uStrokeAnisotropy);\n float scale = max(uStrokeScale * 1.6, 180.0);\n\n // ── Paper TOOTH height field — the relief the dry pigment rides. Anisotropic\n // (squished along flow, coarse cross-flow). Peaks = ridges, valleys = paper pits.\n float t1 = vnoise(vec2(pr.x * scale * aniso, pr.y * scale));\n float t2 = vnoise(vec2(pr.x * scale * aniso * 0.4, pr.y * scale * 0.4) + 11.0);\n float t3 = vnoise(vec2(pr.x * scale * aniso * 2.1, pr.y * scale * 2.1) + 23.0);\n float tooth = 0.55 * t1 + 0.30 * t2 + 0.15 * t3;\n\n // ── DRY TOOTH-MULTIPLY — the crayon multiplies the tooth relief into the base\n // color (dark in the pits, bright on the ridges). The pressure (uStrokeAmount) sets\n // how hard the multiply bites. This is the DRY model — no pigment build-up, no sheen.\n float pressure = clamp(uStrokeAmount, 0.0, 1.0);\n float bite = mix(0.18, 0.46, pressure); // multiply strength\n vec3 result = col * (1.0 - bite * (0.5 - tooth));\n\n // ── HARD SCUMBLE — a broken upper layer that lets the lower color show through the\n // gaps. A coarse mask gates a HARD (high-contrast) dragged pass so the dry crayon\n // reads as broken pigment crumbs, not a smooth wash. Drag follows the tensor flow.\n float scumbleMask = smoothstep(0.42, 0.78, vnoise(vec2(pr.x * scale * 0.55, pr.y * scale * 0.22) + 7.0));\n float scumbleCoverage = scumbleMask * pressure * 0.7; // hard broken upper layer\n vec3 paper = col * mix(0.80, 0.94, pressure); // the paper showing through\n result = mix(paper, result, scumbleCoverage);\n\n // ── Broken-color pigment: stable wax/pigment patches in OKLCh (W5), not flicker.\n vec2 pigmentCell = floor(pr * max(scale * 0.18, 32.0));\n float pigmentMask = smoothstep(0.28, 0.82, vnoise(pr * scale * 0.21 + 19.0));\n result = brokenColorJitter(\n result,\n hash21(pigmentCell + 17.0),\n hash21(pigmentCell * 2.1 + 31.0),\n 0.45 + 0.55 * pigmentMask\n );\n\n // Crayon is saturation-amplified (the wax chroma punch — but DRY, no sheen).\n result = saturate3(result, 1.08);\n\n return result;\n}\n\n// ── StrokeProfile — the oil-stroke parameter vector (AX.W12, slice 8 F6) ──────\n// The SBR \"stroke = parameter vector\" canon (facet 11): the per-mode if-ladder knobs\n// become struct FIELDS — logic-as-DATA, not an imperative branch. profileFor(medium,\n// mode) populates the profile for a (medium, mode) pair; paintStrokeLayers(profile) runs\n// the four-layer bestOil/paintOver cascade off it. A new medium (the W13 van-Gogh /\n// oil-pastel profiles) AUTHORS a profile entry — it never edits a monolith.\nstruct StrokeProfile {\n int shapeType; // 0 tapered, 1 load-drag, 2 dab, 3 even, 4 comma/crescent\n float bristleAmp; // edge raggedness 0..0.5\n float streakFreq; // internal-streak spatial frequency\n float streakAmp; // internal-streak amplitude\n float impastoAmp; // paint-thickness contribution (the relight reads the height)\n float hardness; // edge-compositing crispness 0..1\n float toothScale; // canvas-tooth (linen weave) spatial frequency\n float toothAmp; // canvas-tooth amplitude\n float pigmentSat; // pigment saturation boost (OKLCh chroma scale)\n float densityBig; // layer-1 (big gestural) placement density gate\n float densityMed; // layer-2 (medium body) placement density gate\n float densitySml; // layer-3 (small dabs) placement density gate\n float energyGrade; // AX.W13 — SBR energy-grade magnitude (0 off; 1 the van-Gogh\n // Starry-Night length cascade). The energy grade is a PROFILE\n // field, NOT a buried uMedium==5 bestOil branch (slice 8 F0).\n float impastoFloor;// AX.W13 — the per-stroke height-crown floor (0.4 oil falloff;\n // 1.0 van-Gogh FULL-height crown so each dab catches its glint).\n float densityFill; // AX.W13 — layer-4 fill-dab density. Oil/oil-pastel fill the\n // bald spots (0.95, full coverage); van-Gogh sets it LOW so the\n // sparse atomic dabs keep their visible inter-stroke ground gaps\n // (the atomicity read — a 0.95 fill would close every gap to a smear).\n float groundFloor; // AX.W13 — the bare-ground darken floor. The gaps between strokes\n // (low accumulated height) multiply toward this floor, so van-Gogh's\n // sparse atomic dabs read over a visibly darker underpainting (the\n // Starry-Night visible ground). Oil/oil-pastel keep 1.0 (no darken).\n};\n\n// The (medium, mode) -> StrokeProfile selector. The if-ladder's knobs are the profile's\n// fields; a new medium adds a CASE here (never an edit to a dispatch body). mode is the\n// uStrokeMode oil-stroke sub-mode (0 oil, 1 palette-knife, 3 brushwork). crayon\n// (uMedium==4) is a PEER tooth-multiply medium that never reaches the stroke cascade.\n// The painterly STROKE mediums are oil (MEDIUM_OIL), van-Gogh (MEDIUM_VANGOGH) and\n// oil-pastel (MEDIUM_OILPASTEL) — each AUTHORS its own profile entry below.\nStrokeProfile profileFor(int medium, int mode) {\n // The oil baseline (mode 0) — balanced modern-abstract/palette-knife hybrid.\n StrokeProfile prof = StrokeProfile(\n 0, // shapeType — tapered\n 0.25, // bristleAmp\n 9.0, // streakFreq\n 0.09, // streakAmp\n 0.9, // impastoAmp\n 0.80, // hardness\n 240.0, // toothScale\n 0.09, // toothAmp\n 1.03, // pigmentSat\n 0.65, // densityBig\n 0.78, // densityMed\n 0.90, // densitySml\n 0.0, // energyGrade — oil is uniform-length (no Starry-Night cascade)\n 0.4, // impastoFloor — oil's 0.4+0.6·edgeN crown falloff\n 0.95, // densityFill — oil fills the bald spots (full coverage, no bare canvas)\n 1.0 // groundFloor — oil keeps the full base (no ground darken; fills everywhere)\n );\n\n // ── Van-Gogh — first-class atomic comma/crescent dabs (slice 8 F0). NOT a length\n // modulation of oil: sparse high-contrast SEPARABLE marks with their own asymmetric\n // shape, the full Starry-Night energy cascade (energyGrade=1), and full-height\n // impasto crowns so each dab catches its own glint. The mode sub-modes don't apply\n // (van-Gogh has its own grammar), so this returns early.\n if (medium == MEDIUM_VANGOGH) {\n prof.shapeType = 4; // comma / crescent — the divisionist atomic dab\n prof.bristleAmp = 0.22; // a touch of raggedness on the loaded head\n prof.streakFreq = 16.0; // tight within-stroke streaks (the loaded comma)\n prof.streakAmp = 0.16;\n prof.impastoAmp = 1.5; // heavy paint per atomic dab\n prof.hardness = 0.92; // crisp separable edges (visible inter-stroke gaps)\n prof.toothScale = 220.0;\n prof.toothAmp = 0.06;\n prof.pigmentSat = 1.12; // the van-Gogh chroma punch\n // SPARSE placement — visible inter-stroke canvas gaps (the atomicity read). Lower\n // density gates than oil so each mark stays separable, not a coverage smear.\n prof.densityBig = 0.42;\n prof.densityMed = 0.52;\n prof.densitySml = 0.60;\n prof.energyGrade = 1.0; // the full Starry-Night length cascade (the profile field)\n prof.impastoFloor = 1.0; // FULL-height crowns — every atomic dab catches its glint\n prof.densityFill = 0.09; // SPARSE fill — the bald-spot layer stays mostly open so\n // the atomic dabs read as separable marks over a bare\n // ground, not a fill-closed coverage smear (the gap read).\n prof.groundFloor = 0.38; // darken the bare-ground gaps to the visible Starry-Night\n // underpainting so the bright impasto dabs read separable.\n return prof;\n }\n\n // ── Oil-pastel — broad smeared directional strokes deposited via the brush engine\n // (slice 8 F1). Creamy soft edges (low hardness), heavy build-up, and a chroma punch.\n // Distinct from the dry-crayon tooth-multiply (mediumCrayon): oil-pastel LAYS strokes.\n if (medium == MEDIUM_OILPASTEL) {\n prof.shapeType = 2; // dab / blob — broad short smeared marks\n prof.bristleAmp = 0.10; // smooth creamy edges (waxy pastel, not bristle)\n prof.streakFreq = 6.0; // soft low-frequency internal smear\n prof.streakAmp = 0.07;\n prof.impastoAmp = 0.7; // build-up, but flatter than oil impasto\n prof.hardness = 0.42; // CREAMY — soft compositing, strokes blend on overlap\n prof.toothScale = 280.0;\n prof.toothAmp = 0.12; // the pastel tooth reads stronger\n prof.pigmentSat = 1.16; // the waxy chroma punch\n prof.densityBig = 0.80; // dense — broad coverage, pigment-on-pigment build-up\n prof.densityMed = 0.90;\n prof.densitySml = 0.95;\n prof.energyGrade = 0.0; // uniform (no Starry-Night grade)\n prof.impastoFloor = 0.55; // a softer crown than van-Gogh's full-height glint\n return prof;\n }\n\n // ── Oil sub-modes (mode dispatch on the oil medium).\n if (mode == 1) { // palette knife — razor edges, heavy impasto\n prof.shapeType = 3; // flat, even\n prof.bristleAmp = 0.12;\n prof.streakFreq = 4.0; prof.streakAmp = 0.05;\n prof.impastoAmp = 1.6;\n prof.hardness = 0.95;\n prof.toothAmp = 0.04;\n prof.densityBig = 0.80; prof.densityMed = 0.88; prof.densitySml = 0.70;\n } else if (mode == 3) { // thick brushwork — heavy bristle brush\n prof.shapeType = 0; // tapered\n prof.bristleAmp = 0.32;\n prof.streakFreq = 14.0; prof.streakAmp = 0.14;\n prof.impastoAmp = 1.2;\n prof.hardness = 0.85;\n prof.toothAmp = 0.07;\n }\n return prof;\n}\n\n// The single parameterized four-layer stroke cascade (AX.W12). The four hand-unrolled\n// bestOil/paintOver invocations collapse into ONE body driven by the profile + the\n// uniform-derived per-layer scale/anisotropy multipliers (which are mode-INVARIANT — the\n// per-layer offsets, seeds, and len/wid muls are fixed across modes, so they stay here as\n// the cascade's structure; only the profile's knobs differentiate the medium). The\n// mode passes through for the two mode-special-cased shapes (knife layer-3 dabs +\n// layer-4 fill shape), which are placement details of the cascade, not profile knobs.\nvoid paintStrokeLayers(inout vec3 col, inout float height, StrokeProfile prof,\n int mode, vec2 p, float t) {\n // Scales & multipliers from uniforms (mode-invariant cascade structure).\n float baseScale = max(uStrokeScale * 0.006, 0.008);\n // Three primary layers: big gestural, medium body, small dabs.\n float sBig = baseScale * 2.4;\n float sMed = baseScale * 1.1;\n float sSml = baseScale * 0.45;\n\n float lenMulBig = mix(2.2, 3.8, uStrokeAnisotropy);\n float widMulBig = mix(0.55, 0.32, uStrokeAnisotropy);\n float lenMulMed = mix(2.0, 3.4, uStrokeAnisotropy);\n float widMulMed = mix(0.50, 0.30, uStrokeAnisotropy);\n float lenMulSml = mix(1.6, 2.6, uStrokeAnisotropy);\n float widMulSml = mix(0.45, 0.32, uStrokeAnisotropy);\n\n float jitterAmt = 0.75; // large jitter — no grid\n vec2 flow = flowField(p, t);\n\n // Layer 1 — big gestural strokes (sparse, shaping)\n StrokeHit hBig = bestOil(p, sBig, lenMulBig, widMulBig, jitterAmt * 0.55,\n prof.densityBig, prof.shapeType, prof.bristleAmp, flow, t, 1.3,\n prof.energyGrade);\n paintOver(col, height, hBig, prof.streakFreq * 0.7, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 1.3, prof.impastoFloor);\n\n // Layer 2 — medium body strokes\n StrokeHit hMed = bestOil(p + vec2(11.3, 3.7), sMed, lenMulMed, widMulMed,\n jitterAmt, prof.densityMed, prof.shapeType, prof.bristleAmp, flow, t, 2.7,\n prof.energyGrade);\n paintOver(col, height, hMed, prof.streakFreq, prof.streakAmp,\n uImpasto * prof.impastoAmp * uStrokeAmount, prof.hardness, 2.7, prof.impastoFloor);\n\n // Layer 3 — small dabs (more frequent, smaller)\n int smlShape = (mode == 1) ? 2 : prof.shapeType; // knife uses dabs for sparkle\n StrokeHit hSml = bestOil(p + vec2(-5.1, 8.4), sSml, lenMulSml, widMulSml,\n jitterAmt * 1.3, prof.densitySml, smlShape,\n prof.bristleAmp * 0.85, flow, t, 4.1, prof.energyGrade);\n paintOver(col, height, hSml, prof.streakFreq * 1.4, prof.streakAmp * 0.8,\n uImpasto * prof.impastoAmp * 0.65 * uStrokeAmount, prof.hardness, 4.1, prof.impastoFloor);\n\n // Layer 4 — fill dabs (very dense, very small) — covers bald spots. The density is\n // PROFILE-driven (prof.densityFill): oil/oil-pastel fill to full coverage; van-Gogh\n // keeps it sparse so the bare-ground gaps between its atomic dabs survive.\n float sFill = baseScale * 0.22;\n float lenMulFill = mix(1.4, 2.0, uStrokeAnisotropy);\n float widMulFill = mix(0.50, 0.38, uStrokeAnisotropy);\n int fillShape = (mode == 1) ? 3 : 2; // knife=even, others=dab (round fills)\n StrokeHit hFill = bestOil(p + vec2(3.9, -6.2), sFill, lenMulFill, widMulFill,\n jitterAmt * 1.5, prof.densityFill, fillShape,\n prof.bristleAmp * 0.6, flow, t, 8.9, prof.energyGrade);\n paintOver(col, height, hFill, prof.streakFreq * 1.8, prof.streakAmp * 0.6,\n uImpasto * prof.impastoAmp * 0.4 * uStrokeAmount, prof.hardness * 0.9, 8.9, prof.impastoFloor);\n\n // Optional crosshatch layer\n if (uStrokeLayers == 2) {\n vec2 flow2 = vec2(-flow.y, flow.x);\n StrokeHit hX = bestOil(p + vec2(7.3, -2.1), sMed, lenMulMed * 0.9, widMulMed,\n jitterAmt, prof.densityMed * 0.7, prof.shapeType, prof.bristleAmp, flow2, t, 6.5,\n prof.energyGrade);\n paintOver(col, height, hX, prof.streakFreq, prof.streakAmp * 0.85,\n uImpasto * prof.impastoAmp * 0.55 * uStrokeAmount, prof.hardness, 6.5, prof.impastoFloor);\n }\n}\n\n// The shared stroke-medium SUBSTRATE finish (AX.W12 + AX.W13): paint the four-layer\n// cascade off the profile, then canvas-tooth + relight + saturation. The three painterly\n// STROKE mediums (oil / van-Gogh / oil-pastel) each AUTHOR their own profile and call\n// this — the substrate is shared, the PROFILE (not a dispatch-body fork) differentiates\n// the medium (slice 8 F6). mode passes through for the cascade's mode-special-cased shapes.\nvec3 paintStrokeMedium(vec3 col, vec2 p, float t, StrokeProfile prof, int mode) {\n vec3 result = col;\n // AW.W4.2 — accumulated paint HEIGHT across the stroke layers. The relight reads\n // its gradient for the normal; the canvas tooth seeds the base relief.\n float height = 0.0;\n paintStrokeLayers(result, height, prof, mode, p, t);\n\n // Canvas tooth — linen weave\n float tooth1 = vnoise(p * prof.toothScale);\n float tooth2 = vnoise(p * prof.toothScale * vec2(0.6, 2.4) + 37.0);\n float tooth = (0.6 * tooth1 + 0.4 * tooth2) - 0.5;\n result *= 1.0 + tooth * prof.toothAmp * uCanvasGrain;\n\n // AW.W4.2 — relight the accumulated paint height with the movable uLightDir\n // source (diffuse + Blinn specular, in LINEAR before aces()). The canvas tooth\n // is the base relief term so the weave also catches the raking light.\n float canvasBase = tooth * prof.toothAmp * 0.5;\n result = relightImpasto(result, height, canvasBase);\n\n // AX.W13 — the visible ground. The bare-ground gaps (low accumulated stroke height)\n // multiply toward prof.groundFloor, so van-Gogh's sparse atomic dabs read as separable\n // marks over a darker underpainting (oil/oil-pastel keep groundFloor=1.0 → no-op). The\n // smoothstep keeps the impasto dabs (high height) at full value; only the open ground\n // between them darkens.\n result *= mix(prof.groundFloor, 1.0, smoothstep(0.0, 0.38, height));\n\n // Pigment saturation boost\n result = saturate3(result, prof.pigmentSat);\n\n return result;\n}\n\nvec3 mediumOil(vec3 col, vec2 p, float t) {\n // The oil medium is a thin body: fetch the oil profile (mode-dispatched), paint via\n // the shared stroke substrate. uStrokeMode: 0 oil (gestural), 1 knife, 3 brushwork.\n int mode = uStrokeMode;\n StrokeProfile prof = profileFor(MEDIUM_OIL, mode);\n return paintStrokeMedium(col, p, t, prof, mode);\n}\n\n// AX.W13 — the van-Gogh atomic-stroke medium is now a FIRST-CLASS body, NOT a\n// mediumOil passthrough (slice 8 F0). It composes its OWN vangogh StrokeProfile (the\n// comma/crescent shapeType, sparse high-contrast atomic placement with visible\n// inter-stroke gaps, the full Starry-Night energy-grade cascade as a PROFILE field,\n// and full-height impasto crowns) through the shared stroke substrate. The bridge forces\n// the ETF (tensor) orientation so the swirl-rows queue along the color field's tangent;\n// the within-stroke OKLCh broken color (paintOver) + the OKLab stroke OVER-composite\n// give pigment-true atom-level shimmer. No subject matter — the \"source image\" is the\n// generated nuclei field, so dabs trace its iso-bands (the Starry-Night swirl-rows).\nvec3 mediumVangogh(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_VANGOGH, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}\n\n// AX.W13 — the oil-pastel medium (uMedium==6) is a DISTINCT stroke-deposition body,\n// split out of the dry-crayon tooth-multiply (mediumCrayon) (slice 8 F1). It DEPOSITS\n// broad smeared directional strokes via the same brush engine with a creamy soft\n// hardness, heavy pigment build-up where strokes overlap, and a chroma punch — the\n// stroke-deposition model that gives oil-pastel its depth and less-uniform read.\n// Shares the SUBSTRATE (the stroke cascade + tooth + relight) with oil/van-Gogh, NOT\n// the dispatch body of dry crayon. The bridge forces tensor orientation.\nvec3 mediumOilPastel(vec3 col, vec2 p, float t) {\n StrokeProfile prof = profileFor(MEDIUM_OILPASTEL, 0);\n return paintStrokeMedium(col, p, t, prof, 0);\n}", j = "\n", be = `#version 300 es
11
11
  precision highp float;
12
12
 
13
13
  in vec2 vUv;
@@ -142,7 +142,7 @@ float vnoise(vec2 p) {
142
142
  // The rotated-octave FBM rotation constant — spliced from the shared chunk (AV.W2
143
143
  // — the one FBM_ROT). The loop below stays aurora-local (2.02 lacunarity +
144
144
  // uniform-driven octaves, per §3a — only the rotation constant converges).
145
- ${u}
145
+ ${m}
146
146
 
147
147
  float fbm(vec2 p) {
148
148
  float v = 0.0;
@@ -156,11 +156,22 @@ float fbm(vec2 p) {
156
156
  return v;
157
157
  }
158
158
 
159
+ // AX.W12 — the painterly-medium organic noise basis (Jarzynski PCG2D integer-bit hash
160
+ // + 2D simplex gradient noise), SPLICED from the shared procedural-color chunk — the
161
+ // SAME single source the WebGPU twin (aurora.wgsl.ts) splices, so the hash can NEVER
162
+ // drift between backends (proof:aurora-noise-hash-equivalence locks the twins to 1e-6).
163
+ // The painterly mediums (mediumOil's tooth/granulation) opt into pcgHash2/gnoise for
164
+ // organic paper/pigment grain; the smooth/atmospheric pole KEEPS its cheap value-noise
165
+ // fbm above (the cost-tiering is preserved per AX.W12 §3a — this is an ADDITIVE basis,
166
+ // not a blanket upgrade). The aurora-local hash21/vnoise/fbm loop stays (legitimately
167
+ // divergent per the chunk's §3a).
168
+ ${o}
169
+
159
170
  // The sRGB OETF (linearToSrgb) — spliced from the shared procedural-color chunk
160
171
  // (AV.W2 — the single OETF source; the AV.W1 local copy is deleted here). Aurora's
161
172
  // linear pipeline closes the seam with this transfer before fragColor (the
162
173
  // proof:aurora-space-gamma seam).
163
- ${o}
174
+ ${c}
164
175
 
165
176
  // W5 — the four Ottosson OKLab/OKLCh matrices + their space fns, spliced from the
166
177
  // SAME shared chunk the goo-blob uses (zero new payload, 1e-6-verified). PI is
@@ -168,57 +179,18 @@ ${o}
168
179
  const float PI = 3.141592653589793;
169
180
  ${r}
170
181
 
171
- // ── OKLCh palette interpolation (W5) ─────────────────────────────────────────
182
+ // ── OKLCh palette ramp (W5 interp · AX.W11 single-sourced twin) ───────────────
172
183
  // The muddy-midtone kill: distant-hue stops interpolated by a plain linear mix()
173
184
  // desaturate toward grey at the midpoint. The OKLab-rectangular lerp (L,a,b) holds
174
- // chroma across the ramp; the OKLCh hue-arc form (used only for deliberate rainbow
175
- // travel via uHuePath increasing/decreasing) sweeps the hue wheel without flipping.
176
-
177
- // value.js's interpolateHue, transcribed in the NORMALIZED-TURNS domain (h_rad/TAU
178
- // → turns, the exact .5/+1.0 branch per method, fract() wrap, *TAU back). A
179
- // radians-native port (PI thresholds / +TAU wrap) diverges 180° at the antipode.
180
- // methods: 0 shorter, 1 longer, 2 increasing, 3 decreasing.
181
- float interpolateHueTurns(float h0, float h1, float t, int method) {
182
- // h0/h1 are radians in [0, 2pi); convert to turns [0,1).
183
- float TAU = 2.0 * PI;
184
- float a = h0 / TAU;
185
- float b = h1 / TAU;
186
- float i = b - a;
187
- if (method == 0) { // shorter
188
- if (i > 0.5) a += 1.0; else if (i < -0.5) b += 1.0;
189
- } else if (method == 1) { // longer
190
- if (i > 0.0 && i < 0.5) a += 1.0; else if (i > -0.5 && i <= 0.0) b += 1.0;
191
- } else if (method == 2) { // increasing
192
- if (i < 0.0) b += 1.0;
193
- } else { // decreasing
194
- if (i > 0.0) a += 1.0;
195
- }
196
- float r = a + t * (b - a);
197
- r = fract(r); // (r % 1 + 1) % 1 — fract is non-negative in GLSL
198
- return r * TAU; // back to radians
199
- }
200
-
201
- // Linear-sRGB endpoint pair → OKLab-rectangular interpolation → linear sRGB. Holds
202
- // chroma across the ramp (no grey midpoint). Used for the default/shorter ramp path.
203
- vec3 mixPaletteOklab(vec3 linA, vec3 linB, float t) {
204
- vec3 labA = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * linA) * pow(abs(LINEAR_SRGB_TO_LMS * linA), vec3(1.0 / 3.0)));
205
- vec3 labB = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * linB) * pow(abs(LINEAR_SRGB_TO_LMS * linB), vec3(1.0 / 3.0)));
206
- vec3 lab = mix(labA, labB, t);
207
- return oklabToLinearSrgb(lab);
208
- }
209
-
210
- // Linear-sRGB endpoint pair → OKLCh interpolation along the uHuePath arc → linear
211
- // sRGB. L and C lerp; H walks the chosen arc. The deliberate-rainbow path.
212
- vec3 mixPaletteOklchArc(vec3 linA, vec3 linB, float t, int method) {
213
- vec3 labA = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * linA) * pow(abs(LINEAR_SRGB_TO_LMS * linA), vec3(1.0 / 3.0)));
214
- vec3 labB = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * linB) * pow(abs(LINEAR_SRGB_TO_LMS * linB), vec3(1.0 / 3.0)));
215
- vec3 lchA = oklabToOklch(labA);
216
- vec3 lchB = oklabToOklch(labB);
217
- float L = mix(lchA.x, lchB.x, t);
218
- float C = mix(lchA.y, lchB.y, t);
219
- float H = interpolateHueTurns(lchA.z, lchB.z, t, method);
220
- return oklabToLinearSrgb(oklchToOklab(vec3(L, C, H)));
221
- }
185
+ // chroma across the ramp; the OKLCh hue-arc form (deliberate rainbow travel via
186
+ // uHuePath increasing/decreasing) sweeps the hue wheel without flipping.
187
+ //
188
+ // AX.W11 — the ramp (interpolateHueTurns + mixPaletteOklab + mixPaletteOklchArc +
189
+ // the smoothstep-eased samplePaletteRamp dispatcher) is SPLICED from the shared
190
+ // procedural-color chunk, the SAME single source the WebGPU twin (aurora.wgsl.ts)
191
+ // splices — so the ramp can NEVER drift between backends (the AV.W1 two-copy class,
192
+ // applied to the ramp). composition.glsl.ts's samplePalette calls samplePaletteRamp.
193
+ ${f}
222
194
 
223
195
  // Interleaved Gradient Noise (Jimenez) — a 1-LSB triangular dither applied in
224
196
  // DISPLAY space AFTER the OETF, so 8-bit mid-tone banding on the soft gradient is
@@ -287,37 +259,37 @@ vec2 domainWarp(vec2 p, float t) {
287
259
  return warped;
288
260
  }
289
261
 
290
- ` + le + A + ue + A + "// ── Color utils (OKLCh — W5; the sRGB YIQ hueShift matrix is DELETED) ──────────\nconst vec3 W_LUMA = vec3(0.2126, 0.7152, 0.0722);\n\n// Linear sRGB → OKLab (the cbrt-LMS path; the goo-blob's srgbToOklab takes gamma,\n// here the color field is already linear so we skip the OETF inverse).\nvec3 linOklab(vec3 lin) {\n vec3 lms = LINEAR_SRGB_TO_LMS * lin;\n return LMS_TO_OKLAB * (sign(lms) * pow(abs(lms), vec3(1.0 / 3.0)));\n}\n\n// Broken color (W5) — hue + chroma jitter at fixed PERCEPTUAL lightness, in OKLCh.\n// Broken color is hue variation at constant VALUE, which only OKLCh makes true (the\n// prior YIQ-style sRGB rotation muddied value). This is the seam AW.W4's van-Gogh /\n// oil-pastel per-stroke pigment jitter consumes (the pigment-variation read).\nvec3 brokenColorJitter(vec3 c, float hueSeed, float valueSeed, float strength) {\n float amt = clamp(uBrokenColor * strength, 0.0, 1.0);\n if (amt <= 0.001) return c;\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~16° hue swing + ±~12% chroma swing; L held so value reads constant.\n lch.z += (hueSeed - 0.5) * (32.0 * PI / 180.0) * amt;\n lch.y = max(lch.y * (1.0 + (valueSeed - 0.5) * 0.28 * amt), 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n// Saturation (W5) — chroma scale at fixed L/H in OKLCh (the perceptual saturation,\n// replacing the luma-mix sRGB form). amt=1 is identity; amt<1 desaturates toward\n// the achromatic axis with NO lightness shift (the muddy-luma-mix grey is gone).\nvec3 saturate3(vec3 c, float amt) {\n vec3 lch = oklabToOklch(linOklab(c));\n lch.y = max(lch.y * amt, 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n" + de + A + pe + A + fe + A + me + A + "\n// ── Main ──────────────────────────────────────────────────────────────────\nvoid main() {\n // Normalized 0..1 coordinates for nuclei, domain warp, and medium sampling.\n vec2 uv = vUv;\n vec2 pN = uv;\n\n float t = uTime;\n\n // Warp in pN space\n vec2 p_warp = domainWarp(pN, t);\n\n // Composition\n float paletteId; float valueMod;\n nucleiField(p_warp, t, paletteId, valueMod);\n vec3 col = samplePalette(paletteId);\n col *= 1.0 + uValueVariance * valueMod;\n\n // Breath — slow global luminance wobble\n float breath = sin(t * 6.2831 / max(uBreathPeriod, 1.0));\n col *= 1.0 + uBreathDepth * breath * 0.5;\n\n // Medium — crayon (4) is a peer dispatched here, not a mediumOil sub-mode.\n // AW.W4.3/W4.4 — vangogh (5) is the energy-graded atomic-stroke medium;\n // oil-pastel (6) is the reworked deposition+scumble+waxy model. The legacy crayon\n // peer (4) and oil-pastel (6) share the reworked mediumCrayon body (no duplicate).\n if (uMedium == 1) col = mediumPastel(col, pN, t);\n else if (uMedium == 2) col = mediumWatercolor(col, pN, t);\n else if (uMedium == 3) col = mediumOil(col, pN, t);\n else if (uMedium == 4) col = mediumCrayon(col, pN, t);\n else if (uMedium == 5) col = mediumVangogh(col, pN, t);\n else if (uMedium == 6) col = mediumCrayon(col, pN, t);\n\n // Saturation trim\n col = saturate3(col, uSaturation);\n\n // Tonemap + film grain\n col = aces(col);\n float grain = hash21(gl_FragCoord.xy + t * 17.0);\n col += (grain - 0.5) * uPaperGrain;\n\n col = clamp(col * 0.985 + 0.008, 0.0, 1.0);\n\n // MANDATORY OETF — closes the seam (mirrors metaball.frag.ts:278). The whole\n // pipeline above (linear palette LUT, ACES tonemap, grain) runs in LINEAR; without\n // this transfer the aurora ships ~2.2× too dark (linear 0.5 → ~0.215, not ~0.735).\n col = linearToSrgb(col);\n\n // 1-LSB triangular IGN dither in DISPLAY space, AFTER the OETF — breaks 8-bit\n // mid-tone banding on the soft gradient. Texture-free (the canonical Jimenez IGN).\n col += (1.0 / 255.0) * (ign(gl_FragCoord.xy) - 0.5);\n\n // Premultiply operates on DISPLAY-space color (matches the blob: OETF, then premul).\n fragColor = vec4(col * uAlpha, uAlpha);\n}\n";
262
+ ` + me + j + he + j + "// ── Color utils (OKLCh — W5; the sRGB YIQ hueShift matrix is DELETED) ──────────\nconst vec3 W_LUMA = vec3(0.2126, 0.7152, 0.0722);\n\n// Linear sRGB → OKLab (the cbrt-LMS path; the goo-blob's srgbToOklab takes gamma,\n// here the color field is already linear so we skip the OETF inverse).\nvec3 linOklab(vec3 lin) {\n vec3 lms = LINEAR_SRGB_TO_LMS * lin;\n return LMS_TO_OKLAB * (sign(lms) * pow(abs(lms), vec3(1.0 / 3.0)));\n}\n\n// Broken color (W5) — hue + chroma jitter at fixed PERCEPTUAL lightness, in OKLCh.\n// Broken color is hue variation at constant VALUE, which only OKLCh makes true (the\n// prior YIQ-style sRGB rotation muddied value). This is the seam AW.W4's van-Gogh /\n// oil-pastel per-stroke pigment jitter consumes (the pigment-variation read).\nvec3 brokenColorJitter(vec3 c, float hueSeed, float valueSeed, float strength) {\n float amt = clamp(uBrokenColor * strength, 0.0, 1.0);\n if (amt <= 0.001) return c;\n vec3 lch = oklabToOklch(linOklab(c));\n // ±~16° hue swing + ±~12% chroma swing; L held so value reads constant.\n lch.z += (hueSeed - 0.5) * (32.0 * PI / 180.0) * amt;\n lch.y = max(lch.y * (1.0 + (valueSeed - 0.5) * 0.28 * amt), 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n// Saturation (W5) — chroma scale at fixed L/H in OKLCh (the perceptual saturation,\n// replacing the luma-mix sRGB form). amt=1 is identity; amt<1 desaturates toward\n// the achromatic axis with NO lightness shift (the muddy-luma-mix grey is gone).\nvec3 saturate3(vec3 c, float amt) {\n vec3 lch = oklabToOklch(linOklab(c));\n lch.y = max(lch.y * amt, 0.0);\n return max(oklabToLinearSrgb(oklchToOklab(lch)), vec3(0.0));\n}\n\n" + ge + j + ve + j + _e + j + ye + j + "\n// ── Main ──────────────────────────────────────────────────────────────────\nvoid main() {\n // Normalized 0..1 coordinates for nuclei, domain warp, and medium sampling.\n vec2 uv = vUv;\n vec2 pN = uv;\n\n float t = uTime;\n\n // Warp in pN space\n vec2 p_warp = domainWarp(pN, t);\n\n // Composition\n float paletteId; float valueMod;\n nucleiField(p_warp, t, paletteId, valueMod);\n vec3 col = samplePalette(paletteId);\n col *= 1.0 + uValueVariance * valueMod;\n\n // Breath — slow global luminance wobble\n float breath = sin(t * 6.2831 / max(uBreathPeriod, 1.0));\n col *= 1.0 + uBreathDepth * breath * 0.5;\n\n // Medium dispatch (AX.W13 — each is a FIRST-CLASS body, no shared dispatch):\n // 1 pastel · 2 watercolor · 3 oil · 4 crayon (DRY tooth-multiply) ·\n // 5 van-Gogh (atomic comma/crescent dabs) · 6 oil-pastel (stroke deposition).\n // van-Gogh is no longer a mediumOil passthrough; oil-pastel and crayon no longer\n // share a body — they share the SUBSTRATE (the tooth/placement helpers), not the\n // dispatch body (slice 8 F0/F1).\n if (uMedium == 1) col = mediumPastel(col, pN, t);\n else if (uMedium == 2) col = mediumWatercolor(col, pN, t);\n else if (uMedium == 3) col = mediumOil(col, pN, t);\n else if (uMedium == 4) col = mediumCrayon(col, pN, t);\n else if (uMedium == 5) col = mediumVangogh(col, pN, t);\n else if (uMedium == 6) col = mediumOilPastel(col, pN, t);\n\n // Saturation trim\n col = saturate3(col, uSaturation);\n\n // Tonemap + film grain\n col = aces(col);\n float grain = hash21(gl_FragCoord.xy + t * 17.0);\n col += (grain - 0.5) * uPaperGrain;\n\n col = clamp(col * 0.985 + 0.008, 0.0, 1.0);\n\n // MANDATORY OETF — closes the seam (mirrors metaball.frag.ts:278). The whole\n // pipeline above (linear palette LUT, ACES tonemap, grain) runs in LINEAR; without\n // this transfer the aurora ships ~2.2× too dark (linear 0.5 → ~0.215, not ~0.735).\n col = linearToSrgb(col);\n\n // 1-LSB triangular IGN dither in DISPLAY space, AFTER the OETF — breaks 8-bit\n // mid-tone banding on the soft gradient. Texture-free (the canonical Jimenez IGN).\n col += (1.0 / 255.0) * (ign(gl_FragCoord.xy) - 0.5);\n\n // Premultiply operates on DISPLAY-space color (matches the blob: OETF, then premul).\n fragColor = vec4(col * uAlpha, uAlpha);\n}\n";
291
263
  //#endregion
292
264
  //#region src/composables/glass/createGPUCanvas.ts
293
- function ge(e, t) {
294
- let { device: n, setup: r } = t, i = typeof navigator < "u" && navigator.gpu ? navigator.gpu.getPreferredCanvasFormat() : "bgra8unorm", o = null, s = null, c = n;
265
+ function xe(e, t) {
266
+ let { device: n, setup: r } = t, i = typeof navigator < "u" && navigator.gpu ? navigator.gpu.getPreferredCanvasFormat() : "bgra8unorm", a = null, o = null, c = n;
295
267
  function l() {
296
- let a = e.getContext("webgpu");
297
- if (!a) throw Error("[createGPUCanvas] WebGPU canvas context unavailable");
298
- return o = a, o.configure({
268
+ let s = e.getContext("webgpu");
269
+ if (!s) throw Error("[createGPUCanvas] WebGPU canvas context unavailable");
270
+ return a = s, a.configure({
299
271
  device: n,
300
272
  format: i,
301
273
  alphaMode: t.alphaMode ?? "premultiplied"
302
- }), s = r({
274
+ }), o = r({
303
275
  device: n,
304
- context: o,
276
+ context: a,
305
277
  format: i
306
278
  }), {
307
- frame: s.frame,
308
- shouldContinue: s.shouldContinue,
309
- time: s.time,
279
+ frame: o.frame,
280
+ shouldContinue: o.shouldContinue,
281
+ time: o.time,
310
282
  teardown: () => {
311
- s?.teardown?.(), o?.unconfigure(), o = null, s = null, c = null;
283
+ o?.teardown?.(), a?.unconfigure(), a = null, o = null, c = null;
312
284
  }
313
285
  };
314
286
  }
315
- let u = a({
287
+ let u = s({
316
288
  canvas: e,
317
289
  mode: t.mode,
318
290
  respectReducedMotion: t.respectReducedMotion,
319
291
  buildContext: l,
320
- resize: () => s?.resize()
292
+ resize: () => o?.resize()
321
293
  });
322
294
  return {
323
295
  arm: u.arm,
@@ -336,14 +308,14 @@ function ge(e, t) {
336
308
  }
337
309
  //#endregion
338
310
  //#region src/components/custom/aurora/composables/glSetup.ts
339
- var j = "[Aurora]", _e = /* @__PURE__ */ "uTime.uPalette.uStopCount.uNucleiCount.uNucleiPos.uNucleiRadius.uNucleiPaletteBias.uNucleiValueBias.uNucleiDriftRadius.uNucleiDriftPhase.uNucleiElong.uNucleiAngle.uSoftmaxBeta.uValueVariance.uWarpAmount.uWarpScale.uWarpDrift.uWarpMode.uNoiseOctaves.uMedium.uHuePath.uFlowPattern.uFlowFocal.uFlowAngle.uFlowCurl.uCursor.uCursorStrength.uCursorRadius.uCursorVelocity.uCursorBurst.uStrokeAmount.uStrokeScale.uStrokeAnisotropy.uStrokeLayers.uStrokeMode.uStrokeOrient.uLightDir.uLightColor.uWetEdge.uGranulation.uImpasto.uBrokenColor.uCanvasGrain.uNucleiDrift.uPaletteDrift.uBreathDepth.uBreathPeriod.uSaturation.uPaperGrain.uAlpha".split(".");
340
- function M(e, t, n) {
341
- let r = d(e, e.VERTEX_SHADER, t, j), i = d(e, e.FRAGMENT_SHADER, n, j), a = s(e, r, i, j);
311
+ var M = "[Aurora]", Se = /* @__PURE__ */ "uTime.uPalette.uStopCount.uNucleiCount.uNucleiPos.uNucleiRadius.uNucleiPaletteBias.uNucleiValueBias.uNucleiDriftRadius.uNucleiDriftPhase.uNucleiElong.uNucleiAngle.uSoftmaxBeta.uValueVariance.uWarpAmount.uWarpScale.uWarpDrift.uWarpMode.uNoiseOctaves.uMedium.uHuePath.uFlowPattern.uFlowFocal.uFlowAngle.uFlowCurl.uCursor.uCursorStrength.uCursorRadius.uCursorVelocity.uCursorBurst.uStrokeAmount.uStrokeScale.uStrokeAnisotropy.uStrokeLayers.uStrokeMode.uStrokeOrient.uLightDir.uLightColor.uWetEdge.uGranulation.uImpasto.uBrokenColor.uCanvasGrain.uNucleiDrift.uPaletteDrift.uBreathDepth.uBreathPeriod.uSaturation.uPaperGrain.uAlpha".split(".");
312
+ function Ce(e, t, n) {
313
+ let r = h(e, e.VERTEX_SHADER, t, M), i = h(e, e.FRAGMENT_SHADER, n, M), a = u(e, r, i, M);
342
314
  e.useProgram(a);
343
315
  let o = e.createVertexArray();
344
316
  e.bindVertexArray(o);
345
- let c = e.createBuffer();
346
- e.bindBuffer(e.ARRAY_BUFFER, c), e.bufferData(e.ARRAY_BUFFER, new Float32Array([
317
+ let s = e.createBuffer();
318
+ e.bindBuffer(e.ARRAY_BUFFER, s), e.bufferData(e.ARRAY_BUFFER, new Float32Array([
347
319
  -1,
348
320
  -1,
349
321
  3,
@@ -351,18 +323,18 @@ function M(e, t, n) {
351
323
  -1,
352
324
  3
353
325
  ]), e.STATIC_DRAW);
354
- let l = e.getAttribLocation(a, "aPos");
355
- e.enableVertexAttribArray(l), e.vertexAttribPointer(l, 2, e.FLOAT, !1, 0, 0);
356
- let u = {};
357
- for (let t of _e) u[t] = e.getUniformLocation(a, t);
326
+ let c = e.getAttribLocation(a, "aPos");
327
+ e.enableVertexAttribArray(c), e.vertexAttribPointer(c, 2, e.FLOAT, !1, 0, 0);
328
+ let l = {};
329
+ for (let t of Se) l[t] = e.getUniformLocation(a, t);
358
330
  return {
359
331
  program: a,
360
332
  vs: r,
361
333
  fs: i,
362
- uniforms: u,
334
+ uniforms: l,
363
335
  geometry: {
364
336
  vao: o,
365
- buf: c
337
+ buf: s
366
338
  }
367
339
  };
368
340
  }
@@ -371,7 +343,7 @@ function M(e, t, n) {
371
343
  function N(e, t = 8, n) {
372
344
  let r = n ?? new Float32Array(t * 3), i = Math.min(e.length, t);
373
345
  for (let t = 0; t < i; t++) {
374
- let [n, i, a] = _(e[t]);
346
+ let [n, i, a] = b(e[t]);
375
347
  r[t * 3 + 0] = n, r[t * 3 + 1] = i, r[t * 3 + 2] = a;
376
348
  }
377
349
  for (let e = i; e < t; e++) r[e * 3 + 0] = 0, r[e * 3 + 1] = 0, r[e * 3 + 2] = 0;
@@ -379,140 +351,58 @@ function N(e, t = 8, n) {
379
351
  }
380
352
  function P(e) {
381
353
  if (e.length === 0) return "transparent";
382
- if (e.length === 1) return y(e[0]);
354
+ if (e.length === 1) return S(e[0]);
383
355
  let t = e.length;
384
356
  return `linear-gradient(135deg, ${e.map((e, n) => {
385
357
  let r = Math.round(n / (t - 1) * 100);
386
- return `${y(e)} ${r}%`;
358
+ return `${S(e)} ${r}%`;
387
359
  }).join(", ")})`;
388
360
  }
389
- function F(e) {
390
- let t = e.replace("#", ""), n = parseInt(t.slice(0, 2), 16), r = parseInt(t.slice(2, 4), 16), i = parseInt(t.slice(4, 6), 16), [a, o, s] = se(n / 255, r / 255, i / 255), [c, l, u] = oe(a, o, s);
361
+ function we(e) {
362
+ let t = e.replace("#", ""), n = parseInt(t.slice(0, 2), 16), r = parseInt(t.slice(2, 4), 16), i = parseInt(t.slice(4, 6), 16), [a, o, s] = fe(n / 255, r / 255, i / 255), [c, l, u] = de(a, o, s);
391
363
  return {
392
364
  L: c,
393
365
  C: l,
394
366
  h: u
395
367
  };
396
368
  }
397
- var I = [.35, .95];
398
- function L(e, t = {}) {
399
- let n = typeof e == "string" ? b(e) : e, { stopCount: r = 4, harmony: i = "analogous", lightnessSpread: a = .32, chromaFalloff: o = .85, hueSpread: s = 28, lightnessEasing: c = "linear", chromaEasing: l = "bell", temperatureShift: u = 0 } = t, d = Math.max(2, Math.min(8, Math.round(r))), [f, p] = I, m = n.L - a / 2, h = n.L + a / 2;
369
+ var Te = [.35, .95];
370
+ function F(e, t = {}) {
371
+ let n = typeof e == "string" ? C(e) : e, { stopCount: r = 4, harmony: i = "analogous", lightnessSpread: a = .32, chromaFalloff: o = .85, hueSpread: s = 28, lightnessEasing: c = "linear", chromaEasing: l = "bell", temperatureShift: u = 0 } = t, d = Math.max(2, Math.min(8, Math.round(r))), [f, p] = Te, m = n.L - a / 2, h = n.L + a / 2;
400
372
  m < f && (h += f - m, m = f), h > p && (m -= h - p, h = p), m = Math.max(f, m), h = Math.min(p, h);
401
- let _ = [];
373
+ let g = [];
402
374
  for (let e = 0; e < d; e++) {
403
- let t = d === 1 ? 0 : e / (d - 1), r = m + (h - m) * R(t, c), a = n.C * o, f;
404
- if (l === "bell") f = a + (n.C - a) * ve(t);
375
+ let t = d === 1 ? 0 : e / (d - 1), r = m + (h - m) * I(t, c), a = n.C * o, f;
376
+ if (l === "bell") f = a + (n.C - a) * Ee(t);
405
377
  else {
406
- let e = l === "sine" ? R(t, "sine") : t;
378
+ let e = l === "sine" ? I(t, "sine") : t;
407
379
  f = n.C - (n.C - a) * e;
408
380
  }
409
- let p = v(n.h, i, s, t);
410
- u > 0 && (p = ye(p, t, u)), _.push(g({
381
+ let p = te(n.h, i, s, t);
382
+ u > 0 && (p = Ae(p, t, u)), g.push(ee({
411
383
  L: r,
412
384
  C: f,
413
385
  h: p
414
386
  }));
415
387
  }
416
- return _;
388
+ return g;
417
389
  }
418
- function R(e, t) {
390
+ function I(e, t) {
419
391
  return t === "sine" ? .5 - .5 * Math.cos(Math.PI * e) : e;
420
392
  }
421
- function ve(e) {
393
+ function Ee(e) {
422
394
  return Math.sin(Math.PI * e);
423
395
  }
424
- function ye(e, t, n) {
425
- return ((e) => (e % 360 + 360) % 360)(e + (t - .5) * 2 * n * 22);
426
- }
427
- function be(e) {
428
- let t = [.33, .67], n = [
429
- [t[0], t[0]],
430
- [t[1], t[1]],
431
- [t[1], t[0]],
432
- [t[0], t[1]],
433
- [.5, .5],
434
- [.5, t[0]]
435
- ], r = Math.max(1, Math.min(n.length, e));
436
- return Array.from({ length: r }, (e, t) => {
437
- let [i, a] = n[t];
438
- return {
439
- x: i,
440
- y: a,
441
- radius: .5,
442
- paletteBias: r === 1 ? 0 : t / (r - 1),
443
- valueBias: (t % 2 == 0 ? 1 : -1) * .05,
444
- driftRadius: .045,
445
- driftPhase: t * 2.4 % (Math.PI * 2)
446
- };
447
- });
448
- }
449
- var xe = {
450
- atmospheric: {
451
- harmony: "analogous",
452
- medium: "smooth",
453
- temperatureShift: .3,
454
- chromaEasing: "bell",
455
- saturation: .95,
456
- warpAmount: .55,
457
- valueVariance: .08,
458
- breathDepth: .06,
459
- zones: 2
460
- },
461
- painterly: {
462
- harmony: "split-complementary",
463
- medium: "oil",
464
- temperatureShift: .6,
465
- chromaEasing: "bell",
466
- saturation: 1.05,
467
- warpAmount: .45,
468
- valueVariance: .12,
469
- breathDepth: .04,
470
- zones: 3
471
- },
472
- vivid: {
473
- harmony: "triad",
474
- medium: "watercolor",
475
- temperatureShift: .2,
476
- chromaEasing: "linear",
477
- saturation: 1.2,
478
- warpAmount: .5,
479
- valueVariance: .14,
480
- breathDepth: .05,
481
- zones: 3
482
- },
483
- muted: {
484
- harmony: "monochrome",
485
- medium: "pastel",
486
- temperatureShift: .15,
487
- chromaEasing: "bell",
488
- saturation: .78,
489
- warpAmount: .4,
490
- valueVariance: .06,
491
- breathDepth: .03,
492
- zones: 2
493
- }
494
- };
495
- function Se(e, t = "atmospheric") {
496
- let n = xe[t], r = L(e, {
497
- harmony: n.harmony,
498
- temperatureShift: n.temperatureShift,
499
- chromaEasing: n.chromaEasing,
500
- stopCount: 4
501
- });
502
- return {
503
- ...h,
504
- palette: r,
505
- nuclei: be(n.zones),
506
- medium: n.medium,
507
- saturation: n.saturation,
508
- warpAmount: n.warpAmount,
509
- valueVariance: n.valueVariance,
510
- breathDepth: n.breathDepth
511
- };
396
+ var De = 70, Oe = 250, ke = 22;
397
+ function Ae(e, t, n) {
398
+ let r = (t - .5) * 2;
399
+ if (r === 0 || n === 0) return e;
400
+ let i = (ue(e, r >= 0 ? De : Oe, 1, "shorter") - e + 540) % 360 - 180;
401
+ return ((e + Math.sign(i) * Math.min(Math.abs(r) * n * ke, Math.abs(i))) % 360 + 360) % 360;
512
402
  }
513
403
  //#endregion
514
404
  //#region src/components/custom/aurora/composables/uniformBridge.ts
515
- var z = {
405
+ var je = {
516
406
  smooth: 0,
517
407
  pastel: 1,
518
408
  watercolor: 2,
@@ -520,58 +410,63 @@ var z = {
520
410
  crayon: 4,
521
411
  vangogh: 5,
522
412
  "oil-pastel": 6
523
- }, Ce = {
413
+ }, L = {
524
414
  shorter: 0,
525
415
  longer: 1,
526
416
  increasing: 2,
527
417
  decreasing: 3
528
- }, we = {
418
+ }, Me = {
529
419
  none: 0,
530
420
  radial: 1,
531
421
  swirl: 2,
532
422
  diagonal: 3,
533
423
  multi: 4,
534
424
  tensor: 5
535
- }, B = {
425
+ }, R = {
536
426
  flow: 0,
537
427
  tensor: 1
538
- }, V = {
428
+ }, z = {
539
429
  fbm: 0,
540
430
  cellular: 1,
541
431
  hybrid: 2
542
- }, H = {
432
+ }, Ne = {
543
433
  oil: 0,
544
434
  knife: 1,
545
435
  chunky: 3
436
+ }, B = {
437
+ L: .985,
438
+ C: .0125,
439
+ h: 77.5
546
440
  };
547
- function U(e) {
548
- return e.medium === "oil" && e.strokeMode === "crayon" ? z.crayon : z[e.medium];
441
+ function Pe(e) {
442
+ return e === void 0 ? x(B.L, B.C, B.h) : Array.isArray(e) ? e : x(e.L, e.C, e.h);
549
443
  }
550
- function Te(e) {
551
- return e.strokeMode === "crayon" ? H.oil : H[e.strokeMode];
444
+ function V(e) {
445
+ return je[e.medium];
552
446
  }
553
- function Ee(e) {
554
- return e.medium === "vangogh" || e.medium === "oil-pastel" ? B.tensor : B[e.strokeOrient ?? "flow"];
447
+ function Fe(e) {
448
+ return Ne[e.strokeMode];
449
+ }
450
+ function Ie(e) {
451
+ return e.medium === "vangogh" || e.medium === "oil-pastel" || e.medium === "crayon" ? R.tensor : R[e.strokeOrient ?? "flow"];
555
452
  }
556
- var W = 16, De = W + 32 + 48;
557
- function Oe(e, t, n) {
558
- n.fill(0), n[0] = t, n[1] = Math.min(e.palette.length, 8), n[2] = Math.min(e.nuclei.length, 6), n[3] = e.softmaxBeta, n[4] = e.valueVariance, n[5] = e.warpAmount, n[6] = e.warpScale, n[7] = e.warpDrift, n[8] = V[e.warpMode], n[9] = e.noiseOctaves, n[10] = U(e), n[11] = e.breathDepth, n[12] = e.breathPeriod, n[13] = e.saturation, n[14] = e.paperGrain, n[15] = e.alpha;
559
- let r = N(e.palette, 8), i = W;
560
- for (let e = 0; e < 8; e++) n[i + e * 4 + 0] = r[e * 3 + 0], n[i + e * 4 + 1] = r[e * 3 + 1], n[i + e * 4 + 2] = r[e * 3 + 2];
561
- i = W + 32;
562
- let a = Math.min(e.nuclei.length, 6);
563
- for (let t = 0; t < a; t++) {
564
- let r = e.nuclei[t];
565
- n[i + t * 4 + 0] = r.x, n[i + t * 4 + 1] = 1 - r.y, n[i + t * 4 + 2] = r.radius, n[i + t * 4 + 3] = r.paletteBias;
453
+ var Le = 80;
454
+ function Re(e, t, n, r) {
455
+ n.fill(0), r.fill(0), n[0] = t, n[1] = Math.min(e.palette.length, 8), n[2] = Math.min(e.nuclei.length, 6), n[3] = e.softmaxBeta, n[4] = e.valueVariance, n[5] = e.warpAmount, n[6] = e.warpScale, n[7] = e.warpDrift, n[8] = z[e.warpMode], n[9] = e.noiseOctaves, n[10] = V(e), n[11] = e.breathDepth, n[12] = e.breathPeriod, n[13] = e.saturation, n[14] = e.paperGrain, n[15] = e.alpha, n[16] = L[e.huePath ?? "shorter"];
456
+ let i = N(e.palette, 8);
457
+ for (let e = 0; e < 8; e++) r[e * 4 + 0] = i[e * 3 + 0], r[e * 4 + 1] = i[e * 3 + 1], r[e * 4 + 2] = i[e * 3 + 2];
458
+ let a = 32, o = Math.min(e.nuclei.length, 6);
459
+ for (let t = 0; t < o; t++) {
460
+ let n = e.nuclei[t];
461
+ r[a + t * 4 + 0] = n.x, r[a + t * 4 + 1] = 1 - n.y, r[a + t * 4 + 2] = n.radius, r[a + t * 4 + 3] = n.paletteBias;
566
462
  }
567
- i = W + 32 + 24;
568
- for (let t = 0; t < a; t++) {
569
- let r = e.nuclei[t];
570
- n[i + t * 4 + 0] = r.valueBias, n[i + t * 4 + 1] = r.elongation ?? 1, n[i + t * 4 + 2] = -(r.angle ?? 0) * Math.PI / 180, n[i + t * 4 + 3] = r.driftRadius;
463
+ a = 56;
464
+ for (let t = 0; t < o; t++) {
465
+ let n = e.nuclei[t];
466
+ r[a + t * 4 + 0] = n.valueBias, r[a + t * 4 + 1] = n.elongation ?? 1, r[a + t * 4 + 2] = -(n.angle ?? 0) * Math.PI / 180, r[a + t * 4 + 3] = n.driftRadius;
571
467
  }
572
- return n;
573
468
  }
574
- function G(e, t, n, r) {
469
+ function ze(e, t, n, r) {
575
470
  let i = (e) => 1 - e, a = {
576
471
  palette: new Float32Array(24),
577
472
  pos: new Float32Array(12),
@@ -592,17 +487,13 @@ function G(e, t, n, r) {
592
487
  a.pos[e * 2 + 0] = t.x, a.pos[e * 2 + 1] = i(t.y), a.rad[e] = t.radius, a.pb[e] = t.paletteBias, a.vb[e] = t.valueBias, a.dr[e] = t.driftRadius, a.dp[e] = t.driftPhase, a.elong[e] = t.elongation ?? 1, a.angle[e] = -(t.angle ?? 0) * Math.PI / 180;
593
488
  }
594
489
  for (let e = s; e < 6; e++) a.pos[e * 2 + 0] = 0, a.pos[e * 2 + 1] = 0, a.rad[e] = 0, a.pb[e] = 0, a.vb[e] = 0, a.dr[e] = 0, a.dp[e] = 0, a.elong[e] = 1, a.angle[e] = 0;
595
- e.uniform2fv(n.uNucleiPos, a.pos), e.uniform1fv(n.uNucleiRadius, a.rad), e.uniform1fv(n.uNucleiPaletteBias, a.pb), e.uniform1fv(n.uNucleiValueBias, a.vb), e.uniform1fv(n.uNucleiDriftRadius, a.dr), e.uniform1fv(n.uNucleiDriftPhase, a.dp), e.uniform1fv(n.uNucleiElong, a.elong), e.uniform1fv(n.uNucleiAngle, a.angle), e.uniform1f(n.uSoftmaxBeta, o.softmaxBeta), e.uniform1f(n.uValueVariance, o.valueVariance), e.uniform1f(n.uWarpAmount, o.warpAmount), e.uniform1f(n.uWarpScale, o.warpScale), e.uniform1f(n.uWarpDrift, o.warpDrift), e.uniform1i(n.uWarpMode, V[o.warpMode]), e.uniform1i(n.uNoiseOctaves, o.noiseOctaves), e.uniform1i(n.uMedium, U(o)), e.uniform1i(n.uHuePath, Ce[o.huePath ?? "shorter"]), e.uniform1i(n.uFlowPattern, we[o.flow.pattern]), e.uniform2f(n.uFlowFocal, o.flow.focalX, i(o.flow.focalY)), e.uniform1f(n.uFlowAngle, o.flow.angle), e.uniform1f(n.uFlowCurl, o.flow.curl), e.uniform2f(n.uCursor, r.x, i(r.y)), e.uniform1f(n.uCursorStrength, r.strength), e.uniform1f(n.uCursorRadius, r.radius), e.uniform2f(n.uCursorVelocity, r.velX, -r.velY), e.uniform1f(n.uCursorBurst, r.burst), e.uniform1f(n.uStrokeAmount, o.strokeAmount), e.uniform1f(n.uStrokeScale, o.strokeScale), e.uniform1f(n.uStrokeAnisotropy, o.strokeAnisotropy), e.uniform1i(n.uStrokeLayers, o.strokeLayers), e.uniform1i(n.uStrokeMode, Te(o)), e.uniform1i(n.uStrokeOrient, Ee(o));
490
+ e.uniform2fv(n.uNucleiPos, a.pos), e.uniform1fv(n.uNucleiRadius, a.rad), e.uniform1fv(n.uNucleiPaletteBias, a.pb), e.uniform1fv(n.uNucleiValueBias, a.vb), e.uniform1fv(n.uNucleiDriftRadius, a.dr), e.uniform1fv(n.uNucleiDriftPhase, a.dp), e.uniform1fv(n.uNucleiElong, a.elong), e.uniform1fv(n.uNucleiAngle, a.angle), e.uniform1f(n.uSoftmaxBeta, o.softmaxBeta), e.uniform1f(n.uValueVariance, o.valueVariance), e.uniform1f(n.uWarpAmount, o.warpAmount), e.uniform1f(n.uWarpScale, o.warpScale), e.uniform1f(n.uWarpDrift, o.warpDrift), e.uniform1i(n.uWarpMode, z[o.warpMode]), e.uniform1i(n.uNoiseOctaves, o.noiseOctaves), e.uniform1i(n.uMedium, V(o)), e.uniform1i(n.uHuePath, L[o.huePath ?? "shorter"]), e.uniform1i(n.uFlowPattern, Me[o.flow.pattern]), e.uniform2f(n.uFlowFocal, o.flow.focalX, i(o.flow.focalY)), e.uniform1f(n.uFlowAngle, o.flow.angle), e.uniform1f(n.uFlowCurl, o.flow.curl), e.uniform2f(n.uCursor, r.x, i(r.y)), e.uniform1f(n.uCursorStrength, r.strength), e.uniform1f(n.uCursorRadius, r.radius), e.uniform2f(n.uCursorVelocity, r.velX, -r.velY), e.uniform1f(n.uCursorBurst, r.burst), e.uniform1f(n.uStrokeAmount, o.strokeAmount), e.uniform1f(n.uStrokeScale, o.strokeScale), e.uniform1f(n.uStrokeAnisotropy, o.strokeAnisotropy), e.uniform1i(n.uStrokeLayers, o.strokeLayers), e.uniform1i(n.uStrokeMode, Fe(o)), e.uniform1i(n.uStrokeOrient, Ie(o));
596
491
  {
597
492
  let t = o.lightDir ?? [
598
493
  -.5,
599
494
  .6,
600
495
  .62
601
- ], r = o.lightColor ?? [
602
- 1,
603
- .95,
604
- .88
605
- ];
496
+ ], r = Pe(o.lightColor);
606
497
  e.uniform3f(n.uLightDir, t[0], t[1], t[2]), e.uniform3f(n.uLightColor, r[0], r[1], r[2]);
607
498
  }
608
499
  e.uniform1f(n.uWetEdge, o.wetEdge), e.uniform1f(n.uGranulation, o.granulation), e.uniform1f(n.uImpasto, o.impasto), e.uniform1f(n.uBrokenColor, o.brokenColor), e.uniform1f(n.uCanvasGrain, o.canvasGrain), e.uniform1f(n.uNucleiDrift, o.nucleiDrift), e.uniform1f(n.uPaletteDrift, o.paletteDrift), e.uniform1f(n.uBreathDepth, o.breathDepth), e.uniform1f(n.uBreathPeriod, o.breathPeriod), e.uniform1f(n.uSaturation, o.saturation), e.uniform1f(n.uPaperGrain, o.paperGrain), e.uniform1f(n.uAlpha, o.alpha);
@@ -610,8 +501,8 @@ function G(e, t, n, r) {
610
501
  }
611
502
  //#endregion
612
503
  //#region src/components/custom/aurora/composables/cursorModel.ts
613
- var K = .22, ke = .18, Ae = .992, je = .96, q = .3;
614
- function Me() {
504
+ var H = .22, Be = .18, Ve = .992, He = .96, U = .3;
505
+ function Ue() {
615
506
  return {
616
507
  x: .5,
617
508
  y: .5,
@@ -625,22 +516,22 @@ function Me() {
625
516
  burst: 0
626
517
  };
627
518
  }
628
- function J(e, t = 1) {
519
+ function W(e, t = 1) {
629
520
  let n = Math.max(0, Math.min(1, t));
630
- e.x += (e.targetX - e.x) * K, e.y += (e.targetY - e.y) * K, e.strength += (e.targetStrength - e.strength) * ke, e.targetStrength *= Ae, e.velX *= (1 - q) * n, e.velY *= (1 - q) * n, e.burst *= je * n;
521
+ e.x += (e.targetX - e.x) * H, e.y += (e.targetY - e.y) * H, e.strength += (e.targetStrength - e.strength) * Be, e.targetStrength *= Ve, e.velX *= (1 - U) * n, e.velY *= (1 - U) * n, e.burst *= He * n;
631
522
  }
632
- function Ne(e, t, n) {
523
+ function We(e, t, n) {
633
524
  let r = .12, i = Math.max(-r, Math.min(r, t)), a = Math.max(-r, Math.min(r, n));
634
- e.velX += (i - e.velX) * q, e.velY += (a - e.velY) * q;
525
+ e.velX += (i - e.velX) * U, e.velY += (a - e.velY) * U;
635
526
  let o = Math.hypot(i, a);
636
527
  e.burst = Math.min(1, e.burst + o * 4);
637
528
  }
638
- function Pe(e) {
529
+ function G(e) {
639
530
  return e.targetStrength > .001 || e.strength > .001 || Math.abs(e.x - e.targetX) > .001 || Math.abs(e.y - e.targetY) > .001;
640
531
  }
641
532
  //#endregion
642
533
  //#region src/components/custom/aurora/composables/frameLoop.ts
643
- function Fe(e) {
534
+ function Ge(e) {
644
535
  let { gl: t, prog: n, uniforms: r, cursor: i, getConfig: a, getReducedMotion: o } = e, s = (e) => 1 - e;
645
536
  function c() {
646
537
  return +!o();
@@ -655,49 +546,76 @@ function Fe(e) {
655
546
  function u() {
656
547
  if (o()) return !1;
657
548
  let e = a();
658
- return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0 || e.interactivity?.light && i.burst > .001 ? !0 : Pe(i);
549
+ return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0 || e.interactivity?.light && i.burst > .001 ? !0 : G(i);
659
550
  }
660
551
  return {
661
552
  frame: (e) => {
662
- J(i, c()), l(e);
553
+ W(i, c()), l(e);
663
554
  },
664
555
  needsAnimation: u
665
556
  };
666
557
  }
667
- var Ie = "\nstruct VSOut {\n @builtin(position) pos: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {\n // Full-screen triangle (matches the WebGL2 [-1,-1, 3,-1, -1,3] quad).\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var out: VSOut;\n let xy = p[vi];\n out.pos = vec4f(xy, 0.0, 1.0);\n out.uv = 0.5 * (xy + vec2f(1.0));\n return out;\n}", Le = `
558
+ var Ke = "\nstruct VSOut {\n @builtin(position) pos: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {\n // Full-screen triangle (matches the WebGL2 [-1,-1, 3,-1, -1,3] quad).\n var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var out: VSOut;\n let xy = p[vi];\n out.pos = vec4f(xy, 0.0, 1.0);\n out.uv = 0.5 * (xy + vec2f(1.0));\n return out;\n}", qe = `
668
559
  const PI: f32 = 3.141592653589793;
669
560
  const W_LUMA: vec3f = vec3f(0.2126, 0.7152, 0.0722);
670
561
  const MAX_NUCLEI: u32 = 6u;
671
562
  const MAX_STOPS: u32 = 8u;
672
563
 
673
- // The std140 uniform struct — the WebGPU write path packs an AuroraConfig here.
564
+ // The uniform struct — ONLY constant-indexed scalars (safe in the uniform address
565
+ // space). The five count/enum fields are f32 (AX.W07 1a): the WebGPU write path packs
566
+ // them as floats and the shader i32-casts at each use, so WGSL never reads a float
567
+ // bit-pattern as a raw i32. The dynamically-indexed arrays moved OUT to struct Field
568
+ // below (AX.W07 1b).
674
569
  struct Uniforms {
675
570
  time: f32,
676
- stopCount: i32,
677
- nucleiCount: i32,
571
+ stopCount: f32,
572
+ nucleiCount: f32,
678
573
  softmaxBeta: f32,
679
574
  valueVariance: f32,
680
575
  warpAmount: f32,
681
576
  warpScale: f32,
682
577
  warpDrift: f32,
683
- warpMode: i32,
684
- noiseOctaves: i32,
685
- medium: i32,
578
+ warpMode: f32,
579
+ noiseOctaves: f32,
580
+ medium: f32,
686
581
  breathDepth: f32,
687
582
  breathPeriod: f32,
688
583
  saturation: f32,
689
584
  paperGrain: f32,
690
585
  alpha: f32,
586
+ // AX.W11 — the huePath enum (f32-packed, i32()-cast in samplePalette) carries the
587
+ // shared ramp's OKLab-rectangular-vs-OKLCh-hue-arc dispatch into the WebGPU twin so
588
+ // a huePath:'increasing' rainbow config arcs identically to the WebGL2 oracle. The
589
+ // CPU pad keeps the buffer vec4-aligned (WGPU_UNIFORM_FLOATS rounds to 20).
590
+ huePath: f32,
591
+ };
592
+
593
+ // The dynamically-indexed arrays — bound var<storage, read> (AX.W07 1b). A runtime
594
+ // palette/nuclei index into a var<uniform> array<vec4f> returns [0,0,0,0] on Metal
595
+ // (MSL forbids dynamic indexing of constant); storage is always-legal. The vec4 stride
596
+ // is std140-identical to the uniform layout, so the move is byte-exact.
597
+ struct Field {
691
598
  palette: array<vec4f, 8>, // .xyz = linear-sRGB stop
692
599
  nucleiPos: array<vec4f, 6>, // .xy pos, .z radius, .w paletteBias
693
600
  nucleiMod: array<vec4f, 6>, // .x valueBias, .y elong, .z angle, .w driftRadius
694
601
  };
695
602
 
696
603
  @group(0) @binding(0) var<uniform> U: Uniforms;
604
+ @group(0) @binding(1) var<storage, read> F: Field;
697
605
 
698
- ${f}
606
+ ${g}
699
607
  ${n}
700
608
  ${i}
609
+ ${l}
610
+
611
+ // AX.W12 — the painterly-medium organic noise basis (PCG2D integer-bit hash + 2D
612
+ // simplex gradient noise), SPLICED from the shared procedural-color chunk — the EXACT
613
+ // twin of the GLSL leaf aurora.frag.ts splices, so the hash can NEVER drift between
614
+ // backends (proof:aurora-noise-hash-equivalence locks the twins to 1e-6). The WebGPU
615
+ // single-pass fs_main below stays the smooth pole (it runs no medium body — the
616
+ // painterly mediums are the W7c/W14 multi-pass), so the leaf is single-sourced HERE for
617
+ // the twin-equivalence contract; the W14 painterly passes consume it.
618
+ ${a}
701
619
 
702
620
  fn hash21(p0: vec2f) -> f32 {
703
621
  var p = fract(p0 * vec2f(123.34, 456.21));
@@ -718,8 +636,9 @@ fn fbm(p0: vec2f) -> f32 {
718
636
  var v = 0.0;
719
637
  var a = 0.5;
720
638
  var p = p0;
639
+ let octaves = i32(U.noiseOctaves); // AX.W07 1a — i32()-cast the f32-packed count.
721
640
  for (var i = 0; i < 5; i = i + 1) {
722
- if (i >= U.noiseOctaves) { break; }
641
+ if (i >= octaves) { break; }
723
642
  v = v + a * vnoise(p);
724
643
  p = FBM_ROT * p * 2.02;
725
644
  a = a * 0.5;
@@ -728,16 +647,19 @@ fn fbm(p0: vec2f) -> f32 {
728
647
  }
729
648
 
730
649
  fn samplePalette(id: f32) -> vec3f {
731
- // Nearest-pair OKLab interpolation across the stops (mirrors the GLSL default ramp).
732
- let n = U.stopCount;
733
- if (n <= 1) { return U.palette[0].xyz; }
650
+ // AX.W11 — splice the SHARED samplePaletteRamp (the smoothstep ease + the OKLab-
651
+ // rectangular-vs-OKLCh-hue-arc huePath dispatch), the EXACT twin the GLSL oracle
652
+ // splices. The body here only selects the bracketing stop pair + the raw inter-stop
653
+ // t (the W07 storage-Field dynamic index), then hands them to the shared ramp.
654
+ let n = i32(U.stopCount); // AX.W07 1a — i32()-cast the f32-packed count.
655
+ if (n <= 1) { return F.palette[0].xyz; }
734
656
  let t = clamp(id, 0.0, 1.0) * f32(n - 1);
735
657
  let i0 = i32(floor(t));
736
658
  let i1 = min(i0 + 1, n - 1);
737
659
  let f = t - f32(i0);
738
- let labA = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * U.palette[i0].xyz) * pow(abs(LINEAR_SRGB_TO_LMS * U.palette[i0].xyz), vec3f(1.0 / 3.0)));
739
- let labB = LMS_TO_OKLAB * (sign(LINEAR_SRGB_TO_LMS * U.palette[i1].xyz) * pow(abs(LINEAR_SRGB_TO_LMS * U.palette[i1].xyz), vec3f(1.0 / 3.0)));
740
- return oklabToLinearSrgb(mix(labA, labB, f));
660
+ // AX.W07 1b — read the palette from the storage Field (dynamic i0/i1 index);
661
+ // AX.W11 — i32()-cast the f32-packed huePath enum (parity with the count casts).
662
+ return samplePaletteRamp(F.palette[i0].xyz, F.palette[i1].xyz, f, i32(U.huePath));
741
663
  }
742
664
 
743
665
  fn domainWarp(p: vec2f, t: f32) -> vec2f {
@@ -759,15 +681,16 @@ fn nucleiField(p: vec2f, t: f32) -> vec2f {
759
681
  var accumId = 0.0;
760
682
  var accumVal = 0.0;
761
683
  var w = 0.0;
762
- let n = U.nucleiCount;
684
+ let n = i32(U.nucleiCount); // AX.W07 1a — i32()-cast the f32-packed count.
763
685
  for (var i = 0; i < 6; i = i + 1) {
764
686
  if (i >= n) { break; }
765
- let pos = U.nucleiPos[i].xy;
766
- let rad = max(U.nucleiPos[i].z, 0.01);
687
+ // AX.W07 1b — read the nuclei from the storage Field (dynamic i index).
688
+ let pos = F.nucleiPos[i].xy;
689
+ let rad = max(F.nucleiPos[i].z, 0.01);
767
690
  let d2 = dot(p - pos, p - pos);
768
691
  let wi = exp((-U.softmaxBeta * d2) / (rad * rad));
769
- accumId = accumId + wi * U.nucleiPos[i].w;
770
- accumVal = accumVal + wi * U.nucleiMod[i].x;
692
+ accumId = accumId + wi * F.nucleiPos[i].w;
693
+ accumVal = accumVal + wi * F.nucleiMod[i].x;
771
694
  w = w + wi;
772
695
  }
773
696
  if (w < 1e-4) { return vec2f(0.5, 0.0); }
@@ -796,20 +719,20 @@ fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
796
719
  col = aces(col);
797
720
  col = linearToSrgb(col); // the mandatory OETF (linear → display).
798
721
  return vec4f(col * U.alpha, U.alpha); // premultiply AFTER the OETF (parity).
799
- }`, Re = Ie + "\n" + Le;
722
+ }`, Je = Ke + "\n" + qe;
800
723
  //#endregion
801
724
  //#region src/components/custom/aurora/composables/gpuRuntime.ts
802
- function ze(e) {
725
+ function Ye(e) {
803
726
  let { canvas: t, getConfig: n, cursor: r, getReducedMotion: i, frozenOffset: a } = e;
804
727
  return function({ device: e, context: o, format: s }) {
805
- let l = e.createShaderModule({ code: Re }), u = e.createRenderPipeline({
728
+ let c = e.createShaderModule({ code: Je }), l = e.createRenderPipeline({
806
729
  layout: "auto",
807
730
  vertex: {
808
- module: l,
731
+ module: c,
809
732
  entryPoint: "vs_main"
810
733
  },
811
734
  fragment: {
812
- module: l,
735
+ module: c,
813
736
  entryPoint: "fs_main",
814
737
  targets: [{
815
738
  format: s,
@@ -826,22 +749,31 @@ function ze(e) {
826
749
  }]
827
750
  },
828
751
  primitive: { topology: "triangle-list" }
829
- }), d = new Float32Array(De), f = e.createBuffer({
830
- size: d.byteLength,
752
+ }), u = new Float32Array(20), f = e.createBuffer({
753
+ size: u.byteLength,
831
754
  usage: 72
832
- }), p = e.createBindGroup({
833
- layout: u.getBindGroupLayout(0),
755
+ }), p = new Float32Array(Le), m = e.createBuffer({
756
+ size: p.byteLength,
757
+ usage: 136
758
+ }), h = e.createBindGroup({
759
+ layout: l.getBindGroupLayout(0),
834
760
  entries: [{
835
761
  binding: 0,
836
762
  resource: { buffer: f }
763
+ }, {
764
+ binding: 1,
765
+ resource: { buffer: m }
837
766
  }]
838
767
  });
839
- function m() {
840
- let e = c(), n = t.clientWidth || t.parentElement?.clientWidth || 1, r = t.clientHeight || t.parentElement?.clientHeight || 1;
768
+ function g() {
769
+ return +!i();
770
+ }
771
+ function _() {
772
+ let e = d(), n = t.clientWidth || t.parentElement?.clientWidth || 1, r = t.clientHeight || t.parentElement?.clientHeight || 1;
841
773
  t.width = Math.max(1, Math.floor(n * e)), t.height = Math.max(1, Math.floor(r * e));
842
774
  }
843
- function h(t) {
844
- J(r), Oe(n(), t, d), e.queue.writeBuffer(f, 0, d);
775
+ function v(t) {
776
+ W(r, g()), Re(n(), t, u, p), e.queue.writeBuffer(f, 0, u), e.queue.writeBuffer(m, 0, p);
845
777
  let i = e.createCommandEncoder(), a = o.getCurrentTexture().createView(), s = i.beginRenderPass({ colorAttachments: [{
846
778
  view: a,
847
779
  clearValue: {
@@ -853,45 +785,45 @@ function ze(e) {
853
785
  loadOp: "clear",
854
786
  storeOp: "store"
855
787
  }] });
856
- s.setPipeline(u), s.setBindGroup(0, p), s.draw(3), s.end(), e.queue.submit([i.finish()]);
788
+ s.setPipeline(l), s.setBindGroup(0, h), s.draw(3), s.end(), e.queue.submit([i.finish()]);
857
789
  }
858
- function g() {
790
+ function y() {
859
791
  if (i()) return !1;
860
792
  let e = n();
861
- return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0;
793
+ return e.nucleiDrift !== 0 || e.paletteDrift !== 0 || e.breathDepth !== 0 || e.warpDrift !== 0 || e.interactivity?.light && r.burst > .001 ? !0 : G(r);
862
794
  }
863
795
  return {
864
- frame: h,
865
- shouldContinue: g,
866
- resize: m,
796
+ frame: v,
797
+ shouldContinue: y,
798
+ resize: _,
867
799
  time: (e) => i() ? a : e,
868
800
  teardown: () => {
869
- f.destroy();
801
+ f.destroy(), m.destroy();
870
802
  }
871
803
  };
872
804
  };
873
805
  }
874
806
  //#endregion
875
807
  //#region src/components/custom/aurora/composables/runtime.ts
876
- function Be(e) {
808
+ function Xe(e) {
877
809
  return e.mode === "capture" ? !0 : e.initStrategy === "eager";
878
810
  }
879
- function Ve(e) {
811
+ function Ze(e) {
880
812
  return typeof e.preserveDrawingBuffer == "boolean" ? e.preserveDrawingBuffer : e.mode === "capture";
881
813
  }
882
- function Y(e, t, n = {}) {
883
- let r = Ve(n), i = t, a = 3.7, o = Me(), s = null, u = n.gpuDevice ? ge(e, {
814
+ function K(e, t, n = {}) {
815
+ let r = Ze(n), i = t, a = 3.7, o = Ue(), s = null, c = n.gpuDevice ? xe(e, {
884
816
  device: n.gpuDevice,
885
817
  mode: n.mode === "capture" ? "capture" : "live",
886
818
  alphaMode: "premultiplied",
887
- setup: ze({
819
+ setup: Ye({
888
820
  canvas: e,
889
821
  getConfig: () => i,
890
822
  cursor: o,
891
- getReducedMotion: () => u.reducedMotion,
823
+ getReducedMotion: () => c.reducedMotion,
892
824
  frozenOffset: a
893
825
  })
894
- }) : l(e, {
826
+ }) : p(e, {
895
827
  mode: n.mode === "capture" ? "capture" : "live",
896
828
  contextAttrs: {
897
829
  antialias: !1,
@@ -900,18 +832,18 @@ function Y(e, t, n = {}) {
900
832
  preserveDrawingBuffer: r
901
833
  },
902
834
  setup: (t) => {
903
- let { program: n, vs: r, fs: l, uniforms: d, geometry: f } = M(t, ce, he);
835
+ let { program: n, vs: r, fs: l, uniforms: u, geometry: f } = Ce(t, pe, be);
904
836
  function p() {
905
- let r = c(), i = e.clientWidth || e.parentElement?.clientWidth || 1, a = e.clientHeight || e.parentElement?.clientHeight || 1, o = Math.max(1, Math.floor(i * r)), s = Math.max(1, Math.floor(a * r));
837
+ let r = d(), i = e.clientWidth || e.parentElement?.clientWidth || 1, a = e.clientHeight || e.parentElement?.clientHeight || 1, o = Math.max(1, Math.floor(i * r)), s = Math.max(1, Math.floor(a * r));
906
838
  e.width = o, e.height = s, t.viewport(0, 0, o, s), t.useProgram(n);
907
839
  }
908
- let m = G(t, n, d, o), h = Fe({
840
+ let m = ze(t, n, u, o), h = Ge({
909
841
  gl: t,
910
842
  prog: n,
911
- uniforms: d,
843
+ uniforms: u,
912
844
  cursor: o,
913
845
  getConfig: () => i,
914
- getReducedMotion: () => u.reducedMotion
846
+ getReducedMotion: () => c.reducedMotion
915
847
  });
916
848
  return t.clearColor(0, 0, 0, 0), t.disable(t.DEPTH_TEST), t.enable(t.BLEND), t.blendFunc(t.ONE, t.ONE_MINUS_SRC_ALPHA), s = (e) => {
917
849
  i = e, m(e);
@@ -921,92 +853,72 @@ function Y(e, t, n = {}) {
921
853
  frame: h.frame,
922
854
  shouldContinue: h.needsAnimation,
923
855
  resize: p,
924
- time: (e) => u.reducedMotion ? a : e,
856
+ time: (e) => c.reducedMotion ? a : e,
925
857
  teardown: () => {
926
858
  t.deleteProgram(n), t.deleteShader(r), t.deleteShader(l), t.deleteBuffer(f.buf), t.deleteVertexArray(f.vao);
927
859
  }
928
860
  };
929
861
  }
930
862
  });
931
- function d(e, t, n = .8) {
932
- o.targetX = e, o.targetY = t, o.targetStrength = n, u.wake();
863
+ function l(e, t, n = .8) {
864
+ o.targetX = e, o.targetY = t, o.targetStrength = n, c.wake();
933
865
  }
934
- function f(e, t) {
935
- u.reducedMotion || (Ne(o, e, t), u.wake());
866
+ function u(e, t) {
867
+ c.reducedMotion || (We(o, e, t), c.wake());
936
868
  }
937
- function p() {
938
- o.targetStrength = 0, u.wake();
869
+ function f() {
870
+ o.targetStrength = 0, c.wake();
939
871
  }
940
872
  function m(e) {
941
- o.radius = e, u.wake();
873
+ o.radius = e, c.wake();
942
874
  }
943
875
  function h(e) {
944
- u.wake();
876
+ c.wake();
945
877
  }
946
- return n.mode !== "capture" && Be(n) && u.arm(), {
947
- arm: () => u.arm(),
878
+ return n.mode !== "capture" && Xe(n) && c.arm(), {
879
+ arm: () => c.arm(),
948
880
  update: (e) => {
949
- i = e, s?.(e), u.wake();
881
+ i = e, s?.(e), c.wake();
950
882
  },
951
- setCursor: d,
952
- clearCursor: p,
883
+ setCursor: l,
884
+ clearCursor: f,
953
885
  setCursorRadius: m,
954
886
  setReducedMotion: h,
955
- injectCursorVelocity: f,
887
+ injectCursorVelocity: u,
956
888
  get reducedMotion() {
957
- return u.reducedMotion;
889
+ return c.reducedMotion;
958
890
  },
959
- pause: (e = "manual") => u.suspend(e),
960
- resume: (e = "manual") => u.resume(e),
961
- renderAt: (e) => u.renderAt(e),
962
- dispose: () => u.dispose()
891
+ pause: (e = "manual") => c.suspend(e),
892
+ resume: (e = "manual") => c.resume(e),
893
+ renderAt: (e) => c.renderAt(e),
894
+ dispose: () => c.dispose()
963
895
  };
964
896
  }
965
897
  //#endregion
966
898
  //#region src/components/custom/aurora/composables/configSource.ts
967
- function X(e) {
899
+ function q(e) {
968
900
  return typeof e == "function" ? e : e && typeof e == "object" && "value" in e ? () => e.value : () => e;
969
901
  }
970
902
  //#endregion
971
903
  //#region src/components/custom/aurora/constants/renderMode.ts
972
- function Z(e) {
904
+ function J(e) {
973
905
  if (e !== "auto") return e;
974
906
  if (typeof navigator > "u" || typeof window > "u") return "webgl";
975
907
  let t = typeof navigator.hardwareConcurrency == "number" && navigator.hardwareConcurrency <= 4, n = typeof window.matchMedia == "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches, r = navigator.connection?.saveData === !0;
976
908
  return t || n || r ? "css" : "webgl";
977
909
  }
978
- function He(e) {
979
- let t = e;
980
- return typeof t.isFallbackAdapter == "boolean" ? t.isFallbackAdapter : e.info?.isFallbackAdapter === !0;
981
- }
982
- async function Ue(e) {
983
- if (Z(e) === "css") return {
910
+ async function Qe(e) {
911
+ return J(e) === "css" ? {
984
912
  substrate: "css",
985
913
  device: null
986
- };
987
- if (typeof navigator > "u" || !navigator.gpu) return {
914
+ } : {
988
915
  substrate: "webgl",
989
916
  device: null
990
917
  };
991
- try {
992
- let e = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
993
- return !e || He(e) ? {
994
- substrate: "webgl",
995
- device: null
996
- } : {
997
- substrate: "webgpu",
998
- device: await e.requestDevice()
999
- };
1000
- } catch {
1001
- return {
1002
- substrate: "webgl",
1003
- device: null
1004
- };
1005
- }
1006
918
  }
1007
919
  //#endregion
1008
920
  //#region src/components/custom/aurora/composables/useAurora.ts
1009
- function We(e) {
921
+ function $e(e) {
1010
922
  if (typeof window > "u") return () => {};
1011
923
  if (typeof window.requestIdleCallback == "function") {
1012
924
  let t = window.requestIdleCallback(() => e(), { timeout: 2e3 });
@@ -1023,8 +935,8 @@ function We(e) {
1023
935
  r = !0, cancelAnimationFrame(t), n !== void 0 && clearTimeout(n);
1024
936
  };
1025
937
  }
1026
- function Q(e, t, n = {}, r = {}) {
1027
- let i = X(t), a = r.renderMode === "css", o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = !1, m = O(!1), h = n.mode === "capture" || n.initStrategy === "eager";
938
+ function Y(e, t, n = {}, r = {}) {
939
+ let i = q(t), a = r.renderMode === "css", o = null, s = null, c = null, l = null, u = null, d = null, f = null, p = !1, m = k(!1), h = n.mode === "capture" || n.initStrategy === "eager";
1028
940
  function g() {
1029
941
  f && o && o.setReducedMotion(f.matches);
1030
942
  }
@@ -1045,7 +957,7 @@ function Q(e, t, n = {}, r = {}) {
1045
957
  _(e);
1046
958
  return;
1047
959
  }
1048
- m.value = !0, f = window.matchMedia("(prefers-reduced-motion: reduce)"), f.addEventListener("change", g), s = k(i, (e) => o?.update(e), { deep: !0 }), i().interactivity?.scroll && e.value && (l = k(ee({
960
+ m.value = !0, f = window.matchMedia("(prefers-reduced-motion: reduce)"), f.addEventListener("change", g), s = A(i, (e) => o?.update(e), { deep: !0 }), i().interactivity?.scroll && e.value && (l = A(re({
1049
961
  target: e,
1050
962
  trackExit: !0
1051
963
  }), (e) => {
@@ -1053,11 +965,11 @@ function Q(e, t, n = {}, r = {}) {
1053
965
  }));
1054
966
  }
1055
967
  }
1056
- return D(() => {
968
+ return O(() => {
1057
969
  let t = e.value;
1058
970
  if (!t || a) return;
1059
971
  try {
1060
- o = Y(t, i(), {
972
+ o = K(t, i(), {
1061
973
  initStrategy: "deferred",
1062
974
  ...n
1063
975
  });
@@ -1069,15 +981,15 @@ function Q(e, t, n = {}, r = {}) {
1069
981
  v();
1070
982
  return;
1071
983
  }
1072
- d = x(e, {
984
+ d = ne(e, {
1073
985
  pause: () => o?.pause("off-screen"),
1074
986
  resume: () => o?.resume("off-screen")
1075
987
  }, { pauseWhenHidden: !1 });
1076
988
  let r = d.isIntersecting;
1077
- c = k(r, (t) => {
1078
- !t || p || u || (u = We(() => {
1079
- u = null, r.value && Ue("webgl").then(({ substrate: t, device: a }) => {
1080
- p || (t === "webgpu" && a && o && e.value && (o.dispose(), o = Y(e.value, i(), {
989
+ c = A(r, (t) => {
990
+ !t || p || u || (u = $e(() => {
991
+ u = null, r.value && Qe("webgl").then(({ substrate: t, device: a }) => {
992
+ p || (t === "webgpu" && a && o && e.value && (o.dispose(), o = K(e.value, i(), {
1081
993
  initStrategy: "deferred",
1082
994
  ...n,
1083
995
  gpuDevice: a
@@ -1087,7 +999,7 @@ function Q(e, t, n = {}, r = {}) {
1087
999
  });
1088
1000
  }));
1089
1001
  }, { immediate: !0 });
1090
- }), E(() => {
1002
+ }), D(() => {
1091
1003
  u?.(), c?.(), s?.(), l?.(), d?.dispose(), f?.removeEventListener("change", g), o?.dispose(), o = null;
1092
1004
  }), {
1093
1005
  setCursor: (e, t, n) => o?.setCursor(e, t, n),
@@ -1097,25 +1009,25 @@ function Q(e, t, n = {}, r = {}) {
1097
1009
  renderAt: (e) => o?.renderAt(e),
1098
1010
  pause: () => o?.pause(),
1099
1011
  resume: () => o?.resume(),
1100
- isArmed: ie(m)
1012
+ isArmed: ce(m)
1101
1013
  };
1102
1014
  }
1103
1015
  //#endregion
1104
1016
  //#region src/components/custom/aurora/Aurora.vue
1105
- var Ge = /* @__PURE__ */ e(/* @__PURE__ */ te({
1017
+ var et = /* @__PURE__ */ e(/* @__PURE__ */ ae({
1106
1018
  __name: "Aurora",
1107
1019
  props: {
1108
- config: { default: () => h },
1020
+ config: { default: () => y },
1109
1021
  runtimeOptions: {},
1110
1022
  onInitError: {},
1111
1023
  renderMode: { default: "auto" },
1112
1024
  opacityCeiling: { default: 1 }
1113
1025
  },
1114
1026
  setup(e, { expose: t }) {
1115
- let n = e, r = Z(n.renderMode), i = S(() => Math.max(0, Math.min(1, n.opacityCeiling))), a = O(null), o = Q(a, () => n.config, S(() => ({
1027
+ let n = e, r = J(n.renderMode), i = w(() => Math.max(0, Math.min(1, n.opacityCeiling))), a = k(null), o = Y(a, () => n.config, w(() => ({
1116
1028
  ...n.runtimeOptions ?? {},
1117
1029
  ...n.onInitError ? { onInitError: n.onInitError } : {}
1118
- })).value, { renderMode: r }), s = S(() => P(n.config.palette));
1030
+ })).value, { renderMode: r }), s = w(() => P(n.config.palette));
1119
1031
  return t({
1120
1032
  config: n.config,
1121
1033
  canvasRef: a,
@@ -1127,25 +1039,25 @@ var Ge = /* @__PURE__ */ e(/* @__PURE__ */ te({
1127
1039
  pause: o.pause,
1128
1040
  resume: o.resume,
1129
1041
  isArmed: o.isArmed
1130
- }), (e, t) => (re(), C("div", {
1042
+ }), (e, t) => (se(), ie("div", {
1131
1043
  class: "aurora-root block h-full w-full overflow-hidden",
1132
- style: T({ "--aurora-opacity-ceiling": i.value })
1133
- }, [w("div", {
1044
+ style: E({ "--aurora-opacity-ceiling": i.value })
1045
+ }, [T("div", {
1134
1046
  class: "aurora-placeholder h-full w-full",
1135
1047
  "aria-hidden": "true",
1136
- style: T({ backgroundImage: s.value })
1137
- }, null, 4), w("canvas", {
1048
+ style: E({ backgroundImage: s.value })
1049
+ }, null, 4), T("canvas", {
1138
1050
  ref_key: "canvasRef",
1139
1051
  ref: a,
1140
1052
  "aria-hidden": "true",
1141
- class: ne(["aurora-canvas block h-full w-full", { "aurora-canvas--armed": ae(o).isArmed.value }])
1053
+ class: oe(["aurora-canvas block h-full w-full", { "aurora-canvas--armed": le(o).isArmed.value }])
1142
1054
  }, null, 2)], 4));
1143
1055
  }
1144
1056
  }), [["__scopeId", "data-v-59e84382"]]);
1145
1057
  //#endregion
1146
1058
  //#region src/components/custom/aurora/composables/useCursorInteraction.ts
1147
- function Ke(e, t, n) {
1148
- let r = X(t), i = n.hitFraction ?? .35, a = null, o = null, s = null, c = null;
1059
+ function tt(e, t, n) {
1060
+ let r = q(t), i = n.hitFraction ?? .35, a = null, o = null, s = null, c = null;
1149
1061
  function l(e, t) {
1150
1062
  let n = t.getBoundingClientRect();
1151
1063
  return {
@@ -1232,36 +1144,38 @@ function Ke(e, t, n) {
1232
1144
  }, a = n.getBoundingClientRect(), o = d((i.clientX - a.left) / Math.max(a.width, 1), (i.clientY - a.top) / Math.max(a.height, 1));
1233
1145
  o !== null && r().nuclei.length > 1 && (r().nuclei.splice(o, 1), t.preventDefault());
1234
1146
  }
1235
- return D(() => {
1147
+ return O(() => {
1236
1148
  let t = e.value;
1237
1149
  t && (t.addEventListener("pointermove", h), t.addEventListener("pointerleave", g), t.addEventListener("pointerdown", _), t.addEventListener("pointerup", v), t.addEventListener("pointercancel", v), t.addEventListener("contextmenu", y));
1238
- }), E(() => {
1150
+ }), D(() => {
1239
1151
  let t = e.value;
1240
1152
  t && (t.removeEventListener("pointermove", h), t.removeEventListener("pointerleave", g), t.removeEventListener("pointerdown", _), t.removeEventListener("pointerup", v), t.removeEventListener("pointercancel", v), t.removeEventListener("contextmenu", y));
1241
1153
  }), { isDragging: () => a };
1242
1154
  }
1243
1155
  //#endregion
1244
1156
  //#region src/components/custom/aurora/composables/atoms.ts
1245
- var $ = {}, qe = {
1246
- calm: {
1247
- saturation: .85,
1248
- warpAmount: .4,
1249
- valueVariance: .06,
1250
- breathDepth: .03
1157
+ var X = {};
1158
+ function Z(e, t, n) {
1159
+ return e + (t - e) * n;
1160
+ }
1161
+ var Q = {
1162
+ saturation: {
1163
+ calm: .85,
1164
+ vivid: 1.2
1251
1165
  },
1252
- balanced: {
1253
- saturation: 1,
1254
- warpAmount: .5,
1255
- valueVariance: .08,
1256
- breathDepth: .05
1166
+ valueVariance: {
1167
+ calm: .04,
1168
+ vivid: .14
1169
+ },
1170
+ breathDepth: {
1171
+ calm: .03,
1172
+ vivid: .08
1257
1173
  },
1258
- vivid: {
1259
- saturation: 1.2,
1260
- warpAmount: .58,
1261
- valueVariance: .14,
1262
- breathDepth: .08
1174
+ temperatureShift: {
1175
+ calm: .15,
1176
+ vivid: .5
1263
1177
  }
1264
- }, Je = {
1178
+ }, nt = {
1265
1179
  still: {
1266
1180
  nucleiDrift: 0,
1267
1181
  paletteDrift: 0,
@@ -1280,21 +1194,34 @@ var $ = {}, qe = {
1280
1194
  warpDrift: .008,
1281
1195
  breathDepth: .05
1282
1196
  }
1283
- };
1284
- function Ye(e) {
1285
- let t = [.33, .67], n = [
1286
- [t[0], t[0]],
1287
- [t[1], t[1]],
1288
- [t[1], t[0]],
1289
- [t[0], t[1]],
1197
+ }, rt = .61803398875;
1198
+ function $(e, n = "composed") {
1199
+ let r = Math.max(1, Math.min(6, Math.round(e))), i = [
1200
+ [.33, .33],
1201
+ [.67, .67],
1202
+ [.67, .33],
1203
+ [.33, .67],
1290
1204
  [.5, .5],
1291
- [.5, t[0]]
1292
- ], r = Math.max(1, Math.min(6, n.length, Math.round(e)));
1205
+ [.5, .33]
1206
+ ], a = (e) => {
1207
+ switch (n) {
1208
+ case "scattered": {
1209
+ let n = e * rt * Math.PI * 2, i = .12 + .3 * Math.sqrt((e + .5) / r);
1210
+ return [t(.5 + i * Math.cos(n), .1, .9), t(.5 + i * Math.sin(n), .1, .9)];
1211
+ }
1212
+ case "centred": {
1213
+ if (r === 1) return [.5, .5];
1214
+ let t = e / r * Math.PI * 2, n = .12;
1215
+ return [.5 + n * Math.cos(t), .5 + n * Math.sin(t)];
1216
+ }
1217
+ default: return i[e % i.length];
1218
+ }
1219
+ };
1293
1220
  return Array.from({ length: r }, (e, t) => {
1294
- let [i, a] = n[t];
1221
+ let [n, i] = a(t);
1295
1222
  return {
1296
- x: i,
1297
- y: a,
1223
+ x: n,
1224
+ y: i,
1298
1225
  radius: .5,
1299
1226
  paletteBias: r === 1 ? 0 : t / (r - 1),
1300
1227
  valueBias: (t % 2 == 0 ? 1 : -1) * .05,
@@ -1303,7 +1230,14 @@ function Ye(e) {
1303
1230
  };
1304
1231
  });
1305
1232
  }
1306
- function Xe(e, n, r) {
1233
+ function it(e) {
1234
+ return e < .4 ? "fbm" : e < .75 ? "hybrid" : "cellular";
1235
+ }
1236
+ function at(e, n) {
1237
+ let r = t(n, 0, 1);
1238
+ e.warpAmount = t(Z(.2, .6, r), 0, .6), e.warpScale = t(Z(1, 2.6, r), .5, 3), e.warpMode = it(r), e.noiseOctaves = r < .5 ? 3 : r < .85 ? 4 : 5;
1239
+ }
1240
+ function ot(e, n, r) {
1307
1241
  let i = t(r, 0, 1);
1308
1242
  switch (n) {
1309
1243
  case "watercolor":
@@ -1317,30 +1251,27 @@ function Xe(e, n, r) {
1317
1251
  e.strokeAmount = i, e.impasto = i, e.canvasGrain = i * .06;
1318
1252
  break;
1319
1253
  case "oil-pastel":
1254
+ case "crayon":
1320
1255
  e.strokeAmount = i, e.canvasGrain = i * .05;
1321
1256
  break;
1322
- default: break;
1323
1257
  }
1324
1258
  }
1325
- function Ze(e = $) {
1259
+ function st(e = X) {
1326
1260
  let n = {
1327
- ...h,
1328
- palette: h.palette.map((e) => ({ ...e })),
1329
- nuclei: h.nuclei.map((e) => ({ ...e })),
1330
- flow: { ...h.flow }
1331
- };
1332
- if (e.seed !== void 0 && (n.palette = L(e.seed, {
1261
+ ...y,
1262
+ palette: y.palette.map((e) => ({ ...e })),
1263
+ nuclei: y.nuclei.map((e) => ({ ...e })),
1264
+ flow: { ...y.flow }
1265
+ }, r = e.colorEnergy === void 0 ? void 0 : t(e.colorEnergy, 0, 1);
1266
+ if (e.seed !== void 0 && (n.palette = F(e.seed, {
1333
1267
  harmony: e.harmony ?? "analogous",
1334
- stopCount: 4
1335
- })), e.mood !== void 0) {
1336
- let r = qe[e.mood];
1337
- n.saturation = t(r.saturation, .6, 1.3), n.warpAmount = t(r.warpAmount, 0, .6), n.valueVariance = t(r.valueVariance, 0, .3), n.breathDepth = t(r.breathDepth, 0, .15);
1338
- }
1339
- if (e.medium !== void 0 && (n.medium = e.medium, (e.medium === "vangogh" || e.medium === "oil-pastel") && (n.strokeOrient = "tensor")), e.textureAmount !== void 0 && Xe(n, n.medium, e.textureAmount), e.motion !== void 0) {
1340
- let r = Je[e.motion];
1268
+ stopCount: 4,
1269
+ temperatureShift: r === void 0 ? 0 : Z(Q.temperatureShift.calm, Q.temperatureShift.vivid, r)
1270
+ })), r !== void 0 && (n.saturation = t(Z(Q.saturation.calm, Q.saturation.vivid, r), .6, 1.3), n.valueVariance = t(Z(Q.valueVariance.calm, Q.valueVariance.vivid, r), 0, .3), n.breathDepth = t(Z(Q.breathDepth.calm, Q.breathDepth.vivid, r), 0, .15)), e.zones !== void 0 && (n.nuclei = $(e.zones.count, e.zones.arrangement ?? "composed")), e.noise !== void 0 && at(n, e.noise), e.medium !== void 0 && (n.medium = e.medium.kind, (e.medium.kind === "vangogh" || e.medium.kind === "oil-pastel") && (n.strokeOrient = "tensor"), e.medium.kind !== "smooth" && e.medium.amount !== void 0 && ot(n, e.medium.kind, e.medium.amount)), e.motion !== void 0) {
1271
+ let r = nt[e.motion];
1341
1272
  n.nucleiDrift = t(r.nucleiDrift, 0, .05), n.paletteDrift = t(r.paletteDrift, 0, .04), n.warpDrift = t(r.warpDrift, 0, .015), n.breathDepth = t(e.motion === "still" ? 0 : Math.max(n.breathDepth, r.breathDepth), 0, .15);
1342
1273
  }
1343
- return e.zones !== void 0 && (n.nuclei = Ye(e.zones)), e.interactivity !== void 0 && (n.interactivity = { ...e.interactivity }), n;
1274
+ return e.interactivity !== void 0 && (n.interactivity = { ...e.interactivity }), n;
1344
1275
  }
1345
1276
  //#endregion
1346
- export { Ge as Aurora, $ as DEFAULT_ATOMS, h as DEFAULT_AURORA_CONFIG, p as MAX_NUCLEI, m as MAX_STOPS, Y as createAurora, b as cssToOklch, L as deriveAurora, Se as deriveScene, N as flattenPalette, F as hexToOklchStop, y as oklchStopToHex, _ as oklchToLinear, P as paletteToCssGradient, Ze as resolveAtoms, Z as resolveRenderMode, Q as useAurora, Ke as useCursorInteraction };
1277
+ export { et as Aurora, X as DEFAULT_ATOMS, y as DEFAULT_AURORA_CONFIG, _ as MAX_NUCLEI, v as MAX_STOPS, K as createAurora, C as cssToOklch, F as deriveAurora, N as flattenPalette, we as hexToOklchStop, $ as nucleiPrior, S as oklchStopToHex, b as oklchToLinear, P as paletteToCssGradient, st as resolveAtoms, J as resolveRenderMode, Y as useAurora, tt as useCursorInteraction };