@mkbabb/glass-ui 3.6.0 → 3.7.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 (206) hide show
  1. package/dist/CardAction-D8nSE62a.js +163 -0
  2. package/dist/CollapsibleContent-D7UNsXQr.js +63 -0
  3. package/dist/{CommandShortcut-BiVyqipe.js → CommandShortcut-CY0aQTz5.js} +4 -4
  4. package/dist/{ContextMenuContent-De01_83g.js → ContextMenuContent-DGKZX1Du.js} +1 -1
  5. package/dist/{ContextMenuSubContent-DrWkXKQP.js → ContextMenuSubContent-BeDz7G6f.js} +4 -4
  6. package/dist/{DataTable-BGVXx0Ww.js → DataTable-gtPOLZvl.js} +14 -7
  7. package/dist/{DialogContent-IQ8_BRrC.js → DialogContent-DG1YMR-P.js} +24 -24
  8. package/dist/{DialogFooter-Er0wA3K6.js → DialogFooter-BCIaDP15.js} +2 -2
  9. package/dist/{DiscoGlyph-BaZ8OawK.js → DiscoGlyph-ZoBYpoxI.js} +1 -1
  10. package/dist/{GlyphFace-B_7vOmYn.js → GlyphFace-Ce_oe3tM.js} +1 -1
  11. package/dist/{HoverCardContent-ICfIMZX1.js → HoverCardContent-Bn0UBMY9.js} +1 -1
  12. package/dist/{HoverPopover-DTSYkJtw.js → HoverPopover-DXpOEw1P.js} +1 -1
  13. package/dist/{IconTooltip-DZlCkU-0.js → IconTooltip-Cu86JR4e.js} +1 -1
  14. package/dist/{Input-CRJ2owjN.js → Input-DWQQIOC4.js} +4 -1
  15. package/dist/{Label-CZk-3nTc.js → Label-DBc7bKzZ.js} +1 -1
  16. package/dist/{MetricBadge-BlrdbOGN.js → MetricBadge-D-bRaQUD.js} +1 -1
  17. package/dist/{Notification-DP_ApJLo.js → Notification-B5WHP3_1.js} +5 -4
  18. package/dist/{NumberFieldContent-Bhhmk95p.js → NumberFieldContent-CLNVR1iY.js} +5 -5
  19. package/dist/{PopoverContent-B8WtJECb.js → PopoverContent-F1bWnYoh.js} +2 -2
  20. package/dist/{Progress-DjM86vfb.js → Progress-c4vo4ALi.js} +6 -6
  21. package/dist/{ScrollingText-P9o_DuMn.js → ScrollingText-BEp377U5.js} +2 -2
  22. package/dist/{SelectScrollDownButton-BGn3rjs9.js → SelectScrollDownButton-DAo5NmY_.js} +8 -7
  23. package/dist/{Separator-C2XtAXRp.js → Separator-DZyqsCum.js} +1 -1
  24. package/dist/{Switch-x8n6husW.js → Switch-2rsAtDfA.js} +8 -2
  25. package/dist/TabsList-COqIdW7i.js +101 -0
  26. package/dist/{Toaster-DdhMKfus.js → Toaster-BMaeziJJ.js} +54 -60
  27. package/dist/{ToggleGroupItem-gyXj998A.js → ToggleGroupItem-C-AIkBUw.js} +13 -5
  28. package/dist/{TooltipProvider-D-JrSqDu.js → TooltipProvider-CD2IXfRv.js} +1 -1
  29. package/dist/{UnderlineTabs-CuIK16JI.js → UnderlineTabs-D_c1iYo-.js} +1 -1
  30. package/dist/animated-digit.js +2 -2
  31. package/dist/api/index.d.ts +1 -1
  32. package/dist/api.js +1 -1
  33. package/dist/aurora.js +690 -227
  34. package/dist/{badge-Cl6JZ1B7.js → badge-DHQrc_GQ.js} +13 -9
  35. package/dist/badge.js +1 -1
  36. package/dist/{button-BcMzVrqs.js → button-CAywcrhO.js} +6 -3
  37. package/dist/button.js +1 -1
  38. package/dist/card.js +2 -2
  39. package/dist/carousel.js +5 -5
  40. package/dist/{check-CdkxGxXJ.js → check-BBKKKSTf.js} +1 -1
  41. package/dist/{chevron-down-pBY8sYfV.js → chevron-down-BSkTP2aL.js} +1 -1
  42. package/dist/{chevron-right-BjeKC22V.js → chevron-right-sDM0Yvrf.js} +1 -1
  43. package/dist/{chevron-up-DBeNHUm1.js → chevron-up-CCOD_FTz.js} +1 -1
  44. package/dist/collapsible.js +1 -1
  45. package/dist/color-DjJCAQ6f.js +76 -0
  46. package/dist/color.js +2 -2
  47. package/dist/command.js +1 -1
  48. package/dist/compile-iM4IUB66.js +158 -0
  49. package/dist/components/custom/aurora/Aurora.vue.d.ts +1 -0
  50. package/dist/components/custom/aurora/composables/atoms.d.ts +60 -0
  51. package/dist/components/custom/aurora/composables/color.d.ts +7 -13
  52. package/dist/components/custom/aurora/composables/cursorModel.d.ts +35 -2
  53. package/dist/components/custom/aurora/composables/glSetup.d.ts +1 -1
  54. package/dist/components/custom/aurora/composables/gpuRuntime.d.ts +16 -0
  55. package/dist/components/custom/aurora/composables/runtime.d.ts +18 -0
  56. package/dist/components/custom/aurora/composables/uniformBridge.d.ts +23 -0
  57. package/dist/components/custom/aurora/composables/useAurora.d.ts +6 -0
  58. package/dist/components/custom/aurora/composables/useCursorInteraction.d.ts +8 -0
  59. package/dist/components/custom/aurora/constants/presets.d.ts +55 -2
  60. package/dist/components/custom/aurora/constants/renderMode.d.ts +10 -0
  61. package/dist/components/custom/aurora/constants/shaders/aurora.wgsl.d.ts +7 -0
  62. package/dist/components/custom/aurora/constants/shaders/brush.glsl.d.ts +1 -1
  63. package/dist/components/custom/aurora/constants/shaders/flow.glsl.d.ts +1 -1
  64. package/dist/components/custom/aurora/constants/shaders/mediums.glsl.d.ts +2 -2
  65. package/dist/components/custom/aurora/constants/shaders/painterly.wgsl.d.ts +7 -0
  66. package/dist/components/custom/aurora/constants/shaders/wake.wgsl.d.ts +4 -0
  67. package/dist/components/custom/aurora/index.d.ts +2 -1
  68. package/dist/components/custom/fourier-field/FourierField.vue.d.ts +38 -0
  69. package/dist/components/custom/fourier-field/index.d.ts +2 -0
  70. package/dist/components/custom/fourier-field/math.d.ts +57 -0
  71. package/dist/components/custom/goo-blob/GooBlob.vue.d.ts +2 -0
  72. package/dist/components/custom/goo-blob/composables/useBlobMood.d.ts +14 -2
  73. package/dist/components/custom/goo-blob/composables/useBlobPointer.d.ts +45 -4
  74. package/dist/components/custom/goo-blob/composables/useMetaballRenderer.d.ts +1 -1
  75. package/dist/components/custom/goo-blob/index.d.ts +1 -1
  76. package/dist/components/custom/goo-blob/shaders/sdf-body.glsl.d.ts +1 -1
  77. package/dist/components/custom/goo-blob/shaders/watercolor-edges.glsl.d.ts +2 -2
  78. package/dist/components/custom/goo-blob/types.d.ts +40 -0
  79. package/dist/components/custom/metric-cell/MetricCell.vue.d.ts +1 -1
  80. package/dist/components/custom/metric-stack/MetricStack.vue.d.ts +1 -1
  81. package/dist/components/ui/_shared/ModalOverlay.vue.d.ts +1 -1
  82. package/dist/components/ui/alert/index.d.ts +1 -1
  83. package/dist/components/ui/card/Card.vue.d.ts +10 -3
  84. package/dist/components/ui/card/CardAction.vue.d.ts +24 -0
  85. package/dist/components/ui/card/index.d.ts +1 -0
  86. package/dist/components/ui/collapsible/CollapsibleTrigger.vue.d.ts +5 -1
  87. package/dist/components/ui/tabs/TabsList.vue.d.ts +13 -3
  88. package/dist/components/ui/toast/Toast.vue.d.ts +5 -5
  89. package/dist/components/ui/toast/use-toast.d.ts +1 -1
  90. package/dist/composables/color/index.d.ts +50 -0
  91. package/dist/composables/glass/createGPUCanvas.d.ts +40 -0
  92. package/dist/composables/glass/webgl/createCanvasLifecycle.d.ts +45 -0
  93. package/dist/composables/glass/webgl/shaders/procedural-color.glsl.d.ts +3 -0
  94. package/dist/composables/glass/webgl/useWebGLCanvas.d.ts +4 -4
  95. package/dist/configurator.js +1 -1
  96. package/dist/confirm-dialog.js +3 -3
  97. package/dist/constellation.js +3 -3
  98. package/dist/context-menu.js +2 -2
  99. package/dist/controls.js +2 -2
  100. package/dist/dark.js +1 -1
  101. package/dist/data-table.js +1 -1
  102. package/dist/dialog.js +2 -2
  103. package/dist/disco-glyph.js +1 -1
  104. package/dist/dock.js +9 -9
  105. package/dist/{dockContext-spUj_-E5.js → dockContext-DLgrVWsU.js} +1 -1
  106. package/dist/dom.js +5 -5
  107. package/dist/{dropdown-menu-EFjl5iKo.js → dropdown-menu-BWpwutt0.js} +5 -5
  108. package/dist/dropdown-menu.js +1 -1
  109. package/dist/expandable-container.js +2 -2
  110. package/dist/forms.js +10 -7
  111. package/dist/fourier-field.d.ts +1 -0
  112. package/dist/fourier-field.js +166 -0
  113. package/dist/glass-carousel.js +1 -1
  114. package/dist/glass-panel.js +2 -2
  115. package/dist/glass-ui.css +1 -1
  116. package/dist/glass-ui.js +207 -192
  117. package/dist/glyph-face.js +2 -2
  118. package/dist/goo-blob.js +336 -234
  119. package/dist/header-ribbon.js +1 -1
  120. package/dist/hover-card.js +1 -1
  121. package/dist/hover-popover.js +1 -1
  122. package/dist/icon-tooltip.js +1 -1
  123. package/dist/instrument-chassis.js +1 -1
  124. package/dist/instrument-rail.js +1 -1
  125. package/dist/keyboard.js +1 -1
  126. package/dist/{keys-BFoma9vm.js → keys-Dyf_uTs5.js} +1 -1
  127. package/dist/label.js +1 -1
  128. package/dist/labeled-field.js +6 -6
  129. package/dist/metric-badge.js +1 -1
  130. package/dist/metric-stack.js +1 -1
  131. package/dist/{minimize-2-BP27-qBY.js → minimize-2-lcXOuc6V.js} +1 -1
  132. package/dist/motion-core.js +76 -106
  133. package/dist/motion.js +3 -3
  134. package/dist/notification.js +1 -1
  135. package/dist/number-field.js +1 -1
  136. package/dist/paper-backdrop.js +1 -1
  137. package/dist/popover.js +1 -1
  138. package/dist/progress.js +1 -1
  139. package/dist/pulse.js +1 -1
  140. package/dist/reactive.js +2 -2
  141. package/dist/responsive-tabs.js +2 -2
  142. package/dist/scrolling-text.js +1 -1
  143. package/dist/{search-DBG8qaRs.js → search-nbkvr2dC.js} +1 -1
  144. package/dist/search.js +8 -8
  145. package/dist/select.js +3 -3
  146. package/dist/separator.js +1 -1
  147. package/dist/{sheet-CQYYrkr9.js → sheet-Cv1uO-Gq.js} +4 -4
  148. package/dist/sheet.js +1 -1
  149. package/dist/{slider-CgNzRSh6.js → slider-BA9wIs5e.js} +5 -4
  150. package/dist/slider.js +1 -1
  151. package/dist/sortable-list.js +3 -3
  152. package/dist/styles/components.css +1 -1
  153. package/dist/styles/theme.css +49 -0
  154. package/dist/styles/typography.css +3 -3
  155. package/dist/styles/utilities.css +17 -13
  156. package/dist/supportsCssTimeline-C5oe0lnD.js +12 -0
  157. package/dist/switch.js +1 -1
  158. package/dist/tabs.js +3 -3
  159. package/dist/timeline.js +2 -2
  160. package/dist/toast.js +1 -1
  161. package/dist/toggle-group.js +1 -1
  162. package/dist/tooltip.js +1 -1
  163. package/dist/typewriter.js +1 -1
  164. package/dist/{useAnimatedNumber-DfVgP4te.js → useAnimatedNumber-C5I3wWd8.js} +1 -1
  165. package/dist/{useConfiguratorState-DUtC1jxr.js → useConfiguratorState-B24TcgIn.js} +5 -5
  166. package/dist/useScrollProgress-CAH8Ofwt.js +35 -0
  167. package/dist/{useTouchGate-CS5Csc2h.js → useTouchGate-YQRwyUGU.js} +1 -1
  168. package/dist/{vReveal-u2wSG2El.js → vReveal-BE2TsPoR.js} +6 -17
  169. package/dist/watercolor-dot.js +2 -2
  170. package/dist/{x-C4pz9nbD.js → x-Dfsegz9Y.js} +1 -1
  171. package/package.json +29 -1
  172. package/src/styles/theme.css +49 -0
  173. package/src/styles/typography.css +3 -3
  174. package/src/styles/utilities.css +17 -13
  175. package/dist/CardFooter-C390imy7.js +0 -129
  176. package/dist/CollapsibleContent-cauTbZEM.js +0 -52
  177. package/dist/TabsIndicator-DA0x9gPr.js +0 -97
  178. package/dist/color-DS-iLRVM.js +0 -33
  179. package/dist/compile-C62K_FpC.js +0 -106
  180. /package/dist/{InstrumentChassis-DaHIZCqy.js → InstrumentChassis-BLJBl1BW.js} +0 -0
  181. /package/dist/{InstrumentRail-B0qqLFN0.js → InstrumentRail-BElFwKyt.js} +0 -0
  182. /package/dist/{ModalOverlay-B_CBtqcE.js → ModalOverlay-D1VrWyoi.js} +0 -0
  183. /package/dist/{PaperBackdrop-D_YZW47j.js → PaperBackdrop-DCGoGatv.js} +0 -0
  184. /package/dist/{SelectGroup-CMdoCjRE.js → SelectGroup-BcaFjvP4.js} +0 -0
  185. /package/dist/{SelectSeparator-CaJnPF3_.js → SelectSeparator-COFMJGng.js} +0 -0
  186. /package/dist/{_plugin-vue_export-helper-C1je1s0u.js → _plugin-vue_export-helper-gLzyemvl.js} +0 -0
  187. /package/dist/{constants-C8NMonuo.js → constants-CS75AWHG.js} +0 -0
  188. /package/dist/{createContext-DBMGRlx4.js → createContext-CvNyBk4o.js} +0 -0
  189. /package/dist/{createLucideIcon-DuDoe_ra.js → createLucideIcon-B6uNAU9G.js} +0 -0
  190. /package/dist/{menuItemVariants-C2QlXqT3.js → menuItemVariants-B4eFR5Ix.js} +0 -0
  191. /package/dist/{presets-Dyv9Zl7W.js → presets-BKVyQfbt.js} +0 -0
  192. /package/dist/{prng-CjVgzjQT.js → prng-wHuHlNHE.js} +0 -0
  193. /package/dist/{useCanvas2D-BeNJqqIS.js → useCanvas2D-C31yfUGY.js} +0 -0
  194. /package/dist/{useGlassRenderer-Dn3WpfG-.js → useGlassRenderer-HebgSEgN.js} +0 -0
  195. /package/dist/{useGlobalDark-D7R9OypB.js → useGlobalDark-yhL4Wp-s.js} +0 -0
  196. /package/dist/{useIntersectionPause-DsQYGc8V.js → useIntersectionPause-Cc-H9ees.js} +0 -0
  197. /package/dist/{useInterval-CHVYFpXV.js → useInterval-D3USn0yc.js} +0 -0
  198. /package/dist/{useKeyboardShortcuts-i8tHd5WO.js → useKeyboardShortcuts-Be3w_Rwk.js} +0 -0
  199. /package/dist/{useResizeObserver-DX-STszm.js → useResizeObserver-CSyxGxIX.js} +0 -0
  200. /package/dist/{useSortable-muI29agz.js → useSortable-C8ReQ50P.js} +0 -0
  201. /package/dist/{useSpringMount-CnizvZGm.js → useSpringMount-LRZqJTaF.js} +0 -0
  202. /package/dist/{useTextHighlight-Dmtofpk2.js → useTextHighlight-aMPdEpkq.js} +0 -0
  203. /package/dist/{useTimer-DGgoxTXL.js → useTimer-DEJonOZa.js} +0 -0
  204. /package/dist/{useUserInvalidAria-DmvZ_6Dx.js → useUserInvalidAria-CQKjEfwP.js} +0 -0
  205. /package/dist/{useViewTransition-DI0Rs2rV.js → useViewTransition-DaN5Zrox.js} +0 -0
  206. /package/dist/{useViewportReady-Dcvf0c4p.js → useViewportReady-K96m-Mdv.js} +0 -0
@@ -24,6 +24,8 @@ export declare const MEDIUM_ID: {
24
24
  readonly watercolor: 2;
25
25
  readonly oil: 3;
26
26
  readonly crayon: 4;
27
+ readonly vangogh: 5;
28
+ readonly "oil-pastel": 6;
27
29
  };
28
30
  export declare const HUE_PATH_ID: {
29
31
  readonly shorter: 0;
@@ -37,6 +39,11 @@ export declare const FLOW_ID: {
37
39
  readonly swirl: 2;
38
40
  readonly diagonal: 3;
39
41
  readonly multi: 4;
42
+ readonly tensor: 5;
43
+ };
44
+ export declare const STROKE_ORIENT_ID: {
45
+ readonly flow: 0;
46
+ readonly tensor: 1;
40
47
  };
41
48
  export declare const WARP_ID: {
42
49
  readonly fbm: 0;
@@ -61,6 +68,22 @@ export declare function resolveMediumId(cfg: AuroraConfig): number;
61
68
  * oil default; the other modes map directly.
62
69
  */
63
70
  export declare function resolveStrokeModeId(cfg: AuroraConfig): number;
71
+ /**
72
+ * AW.W4.1/W4.3/W4.4 — the `uStrokeOrient` int for a config. The painterly mediums
73
+ * (`vangogh`, `oil-pastel`) FORCE the structure-tensor orientation regardless of
74
+ * the config's `strokeOrient` field — their brushwork must hug the color zones (the
75
+ * congruent-to-real-Van-Gogh contract). Every other medium honors the config's
76
+ * `strokeOrient` (default `"flow"` — the pre-W4 path renders identically).
77
+ */
78
+ export declare function resolveStrokeOrientId(cfg: AuroraConfig): number;
79
+ export declare const WGPU_UNIFORM_FLOATS: number;
80
+ /**
81
+ * Pack an `AuroraConfig` + cursor into the std140 WGSL Uniforms layout (the WebGPU
82
+ * write path). Fills `out` in place (the caller owns one reused buffer so a slider
83
+ * drag does not churn the GC). The WebGL2 fallback path is unaffected (it keeps its
84
+ * `gl.uniform*` calls). The float layout MUST mirror aurora.wgsl.ts `struct Uniforms`.
85
+ */
86
+ export declare function packGPUUniforms(cfg: AuroraConfig, timeSec: number, out: Float32Array): Float32Array;
64
87
  /**
65
88
  * Build the per-config uniform uploader bound to a program's uniform location
66
89
  * cache + cursor state. Returns `uploadConfig` — call it to push a fresh
@@ -33,6 +33,12 @@ export interface UseAuroraReturn {
33
33
  setCursor: (x: number, y: number, strength?: number) => void;
34
34
  clearCursor: () => void;
35
35
  setCursorRadius: (r: number) => void;
36
+ /**
37
+ * AW.W8.1 — feed a pointer delta into the velocity-reactive flow (a fast flick →
38
+ * a transient swirl-burst). PRM-gated at the runtime (the cursor write-path
39
+ * early-out). The aurora demo wires this from its stage pointermove listener.
40
+ */
41
+ injectCursorVelocity: (dx: number, dy: number) => void;
36
42
  renderAt: (t: number) => void;
37
43
  pause: () => void;
38
44
  resume: () => void;
@@ -10,6 +10,14 @@ export interface CursorInteractionOptions {
10
10
  * 0.35 matches the overlay's default ring thickness.
11
11
  */
12
12
  hitFraction?: number;
13
+ /**
14
+ * AW.W8.1 — feed the per-move pointer delta into the velocity-reactive flow (a
15
+ * fast flick → a transient swirl-burst). The runtime's `injectCursorVelocity`
16
+ * EARLY-OUTS on reduced-motion (the cursor write-path PRM check), so a parked
17
+ * loop with a live pointermove does NOT move the field under reduce. Optional —
18
+ * absent keeps the position-only steady swirl.
19
+ */
20
+ injectVelocity?: (dx: number, dy: number) => void;
13
21
  }
14
22
  /**
15
23
  * Wires pointer events on the stage element to (a) continuous cursor swirl,
@@ -39,11 +39,35 @@ export interface AuroraNucleus {
39
39
  */
40
40
  angle?: number;
41
41
  }
42
- export type AuroraMedium = "smooth" | "pastel" | "watercolor" | "oil";
42
+ export type AuroraMedium = "smooth" | "pastel" | "watercolor" | "oil" | "vangogh" | "oil-pastel";
43
43
  /** Applied only when `medium === "oil"`. Routes inside the shader. */
44
44
  export type StrokeMode = "oil" | "knife" | "crayon" | "chunky";
45
- export type FlowPattern = "none" | "radial" | "swirl" | "diagonal" | "multi";
45
+ export type FlowPattern = "none" | "radial" | "swirl" | "diagonal" | "multi" | "tensor";
46
+ /**
47
+ * AW.W4.1 — how a stroke's orientation is sourced. `flow` keeps the hand-authored
48
+ * `flowField` pattern + per-cell jitter (the named legacy choice); `tensor`
49
+ * substitutes the structure-tensor minor-eigenvector (the color field's own
50
+ * edge-tangent flow). No default that hides one as a duplicate of the other.
51
+ */
52
+ export type StrokeOrient = "flow" | "tensor";
46
53
  export type WarpMode = "fbm" | "cellular" | "hybrid";
54
+ /**
55
+ * AW.W6/W8 — the interactivity flag SHAPE (declared at W6, default OFF; behavior
56
+ * wired at W8). Each axis opts the aurora into a pointer/scroll response:
57
+ * - `light` — the cursor drives the impasto `uLightDir` (cursor-as-light + idle orbit).
58
+ * - `flow` — pointer/scroll VELOCITY injects a transient swirl-burst (velocity-reactive).
59
+ * - `scroll` — palette/breath progress couples to scroll (via `useScrollProgress`).
60
+ * - `wake` — the WebGPU stateful pointer wake (a ping-pong velocity texture; AW.W8.2).
61
+ * Every axis is suppressed under `prefers-reduced-motion` + the DockBackgroundToggle
62
+ * pause (the master tempo scalar is the single suppression seam). The wispy-sky
63
+ * default carries no `interactivity` (every axis OFF — the default stays static).
64
+ */
65
+ export interface AuroraInteractivity {
66
+ light?: boolean;
67
+ flow?: boolean;
68
+ scroll?: boolean;
69
+ wake?: boolean;
70
+ }
47
71
  export interface AuroraFlow {
48
72
  pattern: FlowPattern;
49
73
  /** 0..1 CSS-top-origin. Runtime flips Y. */
@@ -73,6 +97,14 @@ export interface AuroraConfig {
73
97
  */
74
98
  huePath?: AuroraHuePath;
75
99
  flow: AuroraFlow;
100
+ /**
101
+ * AW.W4.1 — how the painterly stroke direction is sourced. Optional; omitted =
102
+ * `"flow"` (the hand-authored `flowField` pattern — the pre-W4 path, so an
103
+ * unset config renders identically). `"tensor"` substitutes the structure-tensor
104
+ * minor eigenvector (the color field's own edge-tangent flow); the `vangogh` /
105
+ * `oil-pastel` mediums force it.
106
+ */
107
+ strokeOrient?: StrokeOrient;
76
108
  strokeAmount: number;
77
109
  strokeScale: number;
78
110
  strokeAnisotropy: number;
@@ -83,6 +115,20 @@ export interface AuroraConfig {
83
115
  impasto: number;
84
116
  brokenColor: number;
85
117
  canvasGrain: number;
118
+ /**
119
+ * AW.W4.2 — the impasto relight direction (the movable directional source the
120
+ * accumulated paint-height field catches). Optional; omitted = upper-left
121
+ * (matching the prior fixed-rim default, so the still default reads identically).
122
+ * Unit-ish [x, y, z] in screen space (z toward the viewer). AW.W8 drives this
123
+ * from the cursor (cursor-as-light); the shader re-normalizes it.
124
+ */
125
+ lightDir?: [number, number, number];
126
+ /**
127
+ * AW.W4.2 — the relight tint (linear-light RGB). Optional; omitted = a warm
128
+ * white (the canonical impasto catch-light). The diffuse + Blinn-specular terms
129
+ * modulate by this.
130
+ */
131
+ lightColor?: [number, number, number];
86
132
  nucleiDrift: number;
87
133
  paletteDrift: number;
88
134
  breathDepth: number;
@@ -90,6 +136,13 @@ export interface AuroraConfig {
90
136
  saturation: number;
91
137
  paperGrain: number;
92
138
  alpha: number;
139
+ /**
140
+ * AW.W6/W8 — the pointer/scroll interactivity opt-in (default OFF — the wispy-sky
141
+ * default stays static). W6 declares the SHAPE; W8 wires the cursor-as-light /
142
+ * velocity-reactive flow / scroll-coupling / WebGPU-wake behavior. Omitted = every
143
+ * axis off. Aliased on `/api` as part of the `AuroraConfig` surface.
144
+ */
145
+ interactivity?: AuroraInteractivity;
93
146
  }
94
147
  /**
95
148
  * Pointer-cursor API exposed by `createAurora`. Values in 0..1 (CSS-top-origin).
@@ -7,6 +7,12 @@
7
7
  * low-power / reduced-motion / save-data devices.
8
8
  */
9
9
  export type AuroraRenderMode = "webgl" | "css" | "auto";
10
+ /**
11
+ * AW.W7b — the resolved render SUBSTRATE after the async WebGPU probe. `"webgpu"` is
12
+ * the capability-gated enhancement; `"webgl"` the universal fallback; `"css"` the
13
+ * placeholder floor.
14
+ */
15
+ export type AuroraSubstrate = "webgpu" | "webgl" | "css";
10
16
  /**
11
17
  * Resolve `"auto"` to a concrete substrate per device tier. `"webgl"` and
12
18
  * `"css"` pass through unchanged.
@@ -28,3 +34,7 @@ export type AuroraRenderMode = "webgl" | "css" | "auto";
28
34
  * the consumer remounts to re-evaluate, matching the deferred-arm contract).
29
35
  */
30
36
  export declare function resolveRenderMode(mode: AuroraRenderMode): "webgl" | "css";
37
+ export declare function resolveRenderModeAsync(mode: AuroraRenderMode): Promise<{
38
+ substrate: AuroraSubstrate;
39
+ device: GPUDevice | null;
40
+ }>;
@@ -0,0 +1,7 @@
1
+ declare const WGSL_MAX_NUCLEI = 6;
2
+ declare const WGSL_MAX_STOPS = 8;
3
+ export declare const AURORA_VERTEX_WGSL = "\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}";
4
+ export declare const AURORA_FRAGMENT_WGSL = "\nconst PI: f32 = 3.141592653589793;\nconst W_LUMA: vec3f = vec3f(0.2126, 0.7152, 0.0722);\nconst MAX_NUCLEI: u32 = 6u;\nconst MAX_STOPS: u32 = 8u;\n\n// The std140 uniform struct \u2014 the WebGPU write path packs an AuroraConfig here.\nstruct Uniforms {\n time: f32,\n stopCount: i32,\n nucleiCount: i32,\n softmaxBeta: f32,\n valueVariance: f32,\n warpAmount: f32,\n warpScale: f32,\n warpDrift: f32,\n warpMode: i32,\n noiseOctaves: i32,\n medium: i32,\n breathDepth: f32,\n breathPeriod: f32,\n saturation: f32,\n paperGrain: f32,\n alpha: f32,\n palette: array<vec4f, 8>, // .xyz = linear-sRGB stop\n nucleiPos: array<vec4f, 6>, // .xy pos, .z radius, .w paletteBias\n nucleiMod: array<vec4f, 6>, // .x valueBias, .y elong, .z angle, .w driftRadius\n};\n\n@group(0) @binding(0) var<uniform> U: Uniforms;\n\n\nfn srgbToLinearCh(c: f32) -> f32 {\n return select(pow((c + 0.055) / 1.055, 2.4), c / 12.92, c <= 0.04045);\n}\nfn srgbToLinear(c: vec3f) -> vec3f {\n return vec3f(srgbToLinearCh(c.r), srgbToLinearCh(c.g), srgbToLinearCh(c.b));\n}\nfn linearToSrgbCh(c: f32) -> f32 {\n return select(1.055 * pow(c, 1.0 / 2.4) - 0.055, c * 12.92, c <= 0.0031308);\n}\nfn linearToSrgb(c: vec3f) -> vec3f {\n return vec3f(linearToSrgbCh(c.r), linearToSrgbCh(c.g), linearToSrgbCh(c.b));\n}\nconst FBM_ROT: mat2x2f = mat2x2f(0.8, 0.6, -0.6, 0.8);\n\nconst LINEAR_SRGB_TO_LMS: mat3x3f = mat3x3f(\n 0.4122214708, 0.2119034982, 0.0883024619,\n 0.5363325363, 0.6806995451, 0.2817188376,\n 0.0514459929, 0.1073969566, 0.6299787005\n);\nconst LMS_TO_OKLAB: mat3x3f = mat3x3f(\n 0.2104542553, 1.9779984951, 0.0259040371,\n 0.7936177850, -2.4285922050, 0.7827717662,\n -0.0040720468, 0.4505937099, -0.8086757660\n);\nconst OKLAB_TO_LMS: mat3x3f = mat3x3f(\n 1.0, 1.0, 1.0,\n 0.3963377774, -0.1055613458, -0.0894841775,\n 0.2158037573, -0.0638541728, -1.2914855480\n);\nconst LMS_TO_LINEAR_SRGB: mat3x3f = mat3x3f(\n 4.0767416621, -1.2684380046, -0.0041960863,\n -3.3077115913, 2.6097574011, -0.7034186147,\n 0.2309699292, -0.3413193965, 1.7076147010\n);\n\nfn srgbToOklab(c: vec3f) -> vec3f {\n let lin = srgbToLinear(c);\n let lms = LINEAR_SRGB_TO_LMS * lin;\n let lmsCbrt = sign(lms) * pow(abs(lms), vec3f(1.0 / 3.0));\n return LMS_TO_OKLAB * lmsCbrt;\n}\nfn oklabToLinearSrgb(lab: vec3f) -> vec3f {\n let lms_ = OKLAB_TO_LMS * lab;\n let lms = lms_ * lms_ * lms_;\n return LMS_TO_LINEAR_SRGB * lms;\n}\nfn oklabToOklch(lab: vec3f) -> vec3f {\n let C = length(lab.yz);\n var H = atan2(lab.z, lab.y);\n if (H < 0.0) { H = H + 2.0 * PI; }\n return vec3f(lab.x, C, H);\n}\nfn oklchToOklab(lch: vec3f) -> vec3f {\n return vec3f(lch.x, lch.y * cos(lch.z), lch.y * sin(lch.z));\n}\n\nfn hash21(p0: vec2f) -> f32 {\n var p = fract(p0 * vec2f(123.34, 456.21));\n p = p + dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\nfn vnoise(p: vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = hash21(i);\n let b = hash21(i + vec2f(1.0, 0.0));\n let c = hash21(i + vec2f(0.0, 1.0));\n let d = hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfn fbm(p0: vec2f) -> f32 {\n var v = 0.0;\n var a = 0.5;\n var p = p0;\n for (var i = 0; i < 5; i = i + 1) {\n if (i >= U.noiseOctaves) { break; }\n v = v + a * vnoise(p);\n p = FBM_ROT * p * 2.02;\n a = a * 0.5;\n }\n return v;\n}\n\nfn samplePalette(id: f32) -> vec3f {\n // Nearest-pair OKLab interpolation across the stops (mirrors the GLSL default ramp).\n let n = U.stopCount;\n if (n <= 1) { return U.palette[0].xyz; }\n let t = clamp(id, 0.0, 1.0) * f32(n - 1);\n let i0 = i32(floor(t));\n let i1 = min(i0 + 1, n - 1);\n let f = t - f32(i0);\n 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)));\n 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)));\n return oklabToLinearSrgb(mix(labA, labB, f));\n}\n\nfn domainWarp(p: vec2f, t: f32) -> vec2f {\n let drift = t * U.warpDrift * 5.0;\n let q = vec2f(\n fbm(p * U.warpScale + vec2f(drift, 0.0)),\n fbm(p * U.warpScale + vec2f(5.2, 1.3 + drift))\n );\n let r = vec2f(\n fbm(p * U.warpScale + 4.0 * q + vec2f(1.7, 9.2)),\n fbm(p * U.warpScale + 4.0 * q + vec2f(8.3, 2.8))\n );\n return p + U.warpAmount * r;\n}\n\nfn nucleiField(p: vec2f, t: f32) -> vec2f {\n // Returns (paletteId, valueMod) \u2014 softmax over the nuclei (isotropic; the WebGPU\n // twin keeps the parity-floor isotropic Gaussian).\n var accumId = 0.0;\n var accumVal = 0.0;\n var w = 0.0;\n let n = U.nucleiCount;\n for (var i = 0; i < 6; i = i + 1) {\n if (i >= n) { break; }\n let pos = U.nucleiPos[i].xy;\n let rad = max(U.nucleiPos[i].z, 0.01);\n let d2 = dot(p - pos, p - pos);\n let wi = exp((-U.softmaxBeta * d2) / (rad * rad));\n accumId = accumId + wi * U.nucleiPos[i].w;\n accumVal = accumVal + wi * U.nucleiMod[i].x;\n w = w + wi;\n }\n if (w < 1e-4) { return vec2f(0.5, 0.0); }\n return vec2f(accumId / w, accumVal / w);\n}\n\n// ACES (Narkowicz) \u2014 matches the GLSL tonemap.\nfn aces(x: vec3f) -> vec3f {\n let a = 2.51; let b = 0.03; let c = 2.43; let d = 0.59; let e = 0.14;\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3f(0.0), vec3f(1.0));\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n let t = U.time;\n let pw = domainWarp(uv, t);\n let nf = nucleiField(pw, t);\n var col = samplePalette(nf.x);\n col = col * (1.0 + U.valueVariance * nf.y);\n\n // Breath.\n let breath = sin(t * 6.2831 / max(U.breathPeriod, 1.0));\n col = col * (1.0 + U.breathDepth * breath * 0.5);\n\n // (The WebGPU smooth pole; the painterly mediums run the W7c multi-pass passes.)\n col = aces(col);\n col = linearToSrgb(col); // the mandatory OETF (linear \u2192 display).\n return vec4f(col * U.alpha, U.alpha); // premultiply AFTER the OETF (parity).\n}";
5
+ /** The assembled WGSL module string (vertex + fragment) for createGPUCanvas. */
6
+ export declare const AURORA_WGSL: string;
7
+ export { WGSL_MAX_NUCLEI, WGSL_MAX_STOPS };
@@ -1 +1 @@
1
- export declare const AURORA_BRUSH_GLSL = "// \u2500\u2500 Curved swept brushstroke primitive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\u00B7K\u00B7along0\u00B7(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 \u2014 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 \u2014 thin-fat-thin (classic brush)\n// 1 load-drag \u2014 fat start, tapers to a point\n// 2 dab \u2014 ellipse-like, round center\n// 3 even \u2014 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 \u2014 endpoints\n// halfW \u2014 base half-width (world units)\n// bulge \u2014 signed perpendicular midpoint offset, in world units\n// shapeType \u2014 0..3\n// bristleFreq \u2014 spatial frequency of edge raggedness\n// bristleAmp \u2014 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed \u2014 uniqueness seed for internal streaks\n// colAtMid \u2014 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 + impasto edge highlight.\n// streakAmp \u2014 0..0.2 how much internal streaks darken/lighten\n// impastoAmp \u2014 0..1 edge catch-light strength\n// hardness \u2014 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\nvoid paintOver(inout vec3 col, StrokeHit s, 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 \u2014 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 // Impasto edge highlight \u2014 bright rim on one side of the stroke\n float rim = smoothstep(0.85, 1.0, 1.0 - s.edgeN) * step(0.0, s.crossN);\n c += impastoAmp * rim * vec3(0.18, 0.15, 0.11);\n // Shadow on the other side (darker, cooler)\n float shadow = smoothstep(0.85, 1.0, 1.0 - s.edgeN) * step(0.0, -s.crossN);\n c -= impastoAmp * shadow * 0.25 * vec3(0.10, 0.09, 0.07);\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\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 \u2014 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: consume the layer-provided flow, then add only\n // deterministic local perturbation so alternate stroke layers stay live.\n vec2 f = safeDir(flow);\n float angJ = (hash21(cc + seed + 11.0) - 0.5) * 0.9; // +/- 0.45 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 float lenV = cellSize * lenMul * (0.65 + 0.55 * hash21(cc + seed + 23.0));\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";
1
+ export declare const AURORA_BRUSH_GLSL = "// \u2500\u2500 Curved swept brushstroke primitive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\u00B7K\u00B7along0\u00B7(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 \u2014 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 \u2014 thin-fat-thin (classic brush)\n// 1 load-drag \u2014 fat start, tapers to a point\n// 2 dab \u2014 ellipse-like, round center\n// 3 even \u2014 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 \u2014 endpoints\n// halfW \u2014 base half-width (world units)\n// bulge \u2014 signed perpendicular midpoint offset, in world units\n// shapeType \u2014 0..3\n// bristleFreq \u2014 spatial frequency of edge raggedness\n// bristleAmp \u2014 0..0.5 fraction of halfW chewed away at edge extrema\n// streakSeed \u2014 uniqueness seed for internal streaks\n// colAtMid \u2014 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 \u2014 0..0.2 how much internal streaks darken/lighten\n// impastoAmp \u2014 0..1 paint-thickness contribution (the height the relight reads)\n// hardness \u2014 0..1 how crisp the compositing transition is (1 = razor, 0 = creamy)\n//\n// AW.W4.2 \u2014 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 \u2014 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 // \u2500\u2500 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 \u2014 relight the accumulated paint height. Derive a normal from the height\n// gradient (dFdx/dFdy \u2014 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 \u2192 high shininess (sharp glint); thin paint \u2192 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 \u2014 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 \u2014 the AW.W4.1 strokeOrient switch:\n // uStrokeOrient==0 (flow) \u2014 the layer-provided hand-authored flowField.\n // uStrokeOrient==1 (tensor) \u2014 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 \u2014 energy-graded length/density for the van-Gogh cascade: in vangogh\n // mode (uMedium==5) modulate the stroke length by local luminance (bright \u2192\n // long confident strokes, dark \u2192 short dabs, the Starry-Night Kolmogorov/\n // Batchelor congruence) AND by coherence (coherent zones \u2192 long, flat \u2192 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";
@@ -1 +1 @@
1
- export declare const AURORA_FLOW_GLSL = "// \u2500\u2500 Flow field \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 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 \u2014 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 }\n // curl \u2014 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 \u2014 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 return dir;\n}\n";
1
+ export declare const AURORA_FLOW_GLSL = "// \u2500\u2500 Flow field \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Structure-tensor prototype (body defined post-sampleBase in the mediums block).\n// Returns vec3(dir.x, dir.y, coherence) \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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";
@@ -1,2 +1,2 @@
1
- export declare const AURORA_MEDIUMS_PRE_BRUSH_GLSL = "// \u2500\u2500 Medium overlays \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\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 \u2014 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 \u2014 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 \u2014 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";
2
- export declare const AURORA_MEDIUMS_POST_BRUSH_GLSL = "// \u2500\u2500 Crayon / oil-pastel \u2014 paper tooth \u00D7 wax pigment (PEER medium) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 never a branch inside mediumOil().\nvec3 mediumCrayon(vec3 col, vec2 p, float t) {\n vec2 flow = 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 // Anisotropic tooth \u2014 squished along flow, coarse cross flow.\n float aniso = mix(0.45, 0.95, uStrokeAnisotropy);\n float scale = max(uStrokeScale * 1.6, 180.0);\n\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 // Center at 0, amplify\n tooth = (tooth - 0.5) * 1.4;\n\n // Multiplicative darkening where tooth is low (pigment skipped paper valleys)\n float lay = 1.0 + tooth * 0.32 * uStrokeAmount;\n vec3 result = col * lay;\n\n // Occasional pressed-in crumbs \u2014 rare darker crumbs\n float crumbs = smoothstep(0.78, 0.95, vnoise(pr * scale * 3.0));\n result *= 1.0 - crumbs * 0.18 * uStrokeAmount;\n\n // Waxy highlight film \u2014 slight lightening on tooth peaks\n float waxy = smoothstep(0.55, 0.85, t1);\n result += waxy * 0.04 * vec3(1.0);\n\n // Paper tooth overlay (subtler than oil's canvas)\n float paperTooth = vnoise(p * 340.0) - 0.5;\n result *= 1.0 + paperTooth * 0.14 * uCanvasGrain;\n\n // Broken-color pigment: stable wax/pigment patches, not temporal 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\n result = saturate3(result, 1.12);\n\n return result;\n}\n\nvec3 mediumOil(vec3 col, vec2 p, float t) {\n // Mode knobs (uStrokeMode) \u2014 oil-stroke modes ONLY (crayon is a peer medium,\n // uMedium==4, dispatched at main()):\n // 0 oil \u2014 balanced modern-abstract/palette-knife hybrid\n // 1 knife \u2014 palette-knife impasto: razor edges, heavy bristle/shadow\n // 3 brushwork \u2014 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 \u2014 no grid\n vec2 flow = flowField(p, t);\n\n vec3 result = col;\n\n // Layer 1 \u2014 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, hBig, streakFreq * 0.7, streakAmp,\n uImpasto * impastoAmp * uStrokeAmount, hardness, 1.3);\n\n // Layer 2 \u2014 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, hMed, streakFreq, streakAmp,\n uImpasto * impastoAmp * uStrokeAmount, hardness, 2.7);\n\n // Layer 3 \u2014 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, hSml, streakFreq * 1.4, streakAmp * 0.8,\n uImpasto * impastoAmp * 0.65 * uStrokeAmount, hardness, 4.1);\n\n // Layer 4 \u2014 fill dabs (very dense, very small) \u2014 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, 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, hX, streakFreq, streakAmp * 0.85,\n uImpasto * impastoAmp * 0.55 * uStrokeAmount, hardness, 6.5);\n }\n\n // Canvas tooth \u2014 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 // Pigment saturation boost\n result = saturate3(result, pigmentSat);\n\n return result;\n}";
1
+ export declare const AURORA_MEDIUMS_PRE_BRUSH_GLSL = "// \u2500\u2500 Medium overlays \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// \u2500\u2500 Structure-tensor / edge-tangent-flow (AW.W4.1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\u00B2, GxGy],[GxGy, Gy\u00B2]] eigen-decomposes closed-form into the MINOR\n// eigenvector (the edge-TANGENT \u2014 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 = (\u03BB1-\u03BB2)/(\u03BB1+\u03BB2). 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: \u03BB = 0.5(tr \u00B1 sqrt((Jxx-Jyy)\u00B2 + 4Jxy\u00B2)).\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 \u2014 the edge TANGENT (the stroke direction). The principal\n // angle is \u03B8 = 0.5\u00B7atan2(2\u00B7Jxy, Jxx-Jyy); this form is robust at Jxy\u22480 (where the\n // (\u03BB-Jyy, Jxy) eigenvector formula collapses to a zero vector). The tangent is at\n // \u03B8 + 90\u00B0. Guard the fully-isotropic (no-gradient) degenerate case \u2192 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] \u2014 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\u00B0 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 \u2014 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 \u2014 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 \u2014 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";
2
+ export declare const AURORA_MEDIUMS_POST_BRUSH_GLSL = "// \u2500\u2500 Crayon / oil-pastel \u2014 paper tooth \u00D7 wax pigment (PEER medium) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 never a branch inside mediumOil().\n// AW.W4.4 \u2014 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 // \u2500\u2500 Paper TOOTH height field \u2014 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 // \u2500\u2500 DEPOSITION \u2014 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 // \u2500\u2500 SCUMBLE \u2014 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 \u2014 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 // \u2500\u2500 WAXY BURNISH FILM \u2014 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 // \u2500\u2500 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) \u2014 oil-stroke modes ONLY (crayon is a peer medium,\n // uMedium==4, dispatched at main()):\n // 0 oil \u2014 balanced modern-abstract/palette-knife hybrid\n // 1 knife \u2014 palette-knife impasto: razor edges, heavy bristle/shadow\n // 3 brushwork \u2014 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 \u2014 no grid\n vec2 flow = flowField(p, t);\n\n vec3 result = col;\n // AW.W4.2 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 fill dabs (very dense, very small) \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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// \u2014 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}";
@@ -0,0 +1,7 @@
1
+ /** The tensor-target format the CONSUMER declares (NOT baked in the substrate). */
2
+ export declare const PAINTERLY_TENSOR_FORMAT: "rgba16float";
3
+ /** The anisotropic-Kuwahara sector count the CONSUMER declares. */
4
+ export declare const PAINTERLY_KUWAHARA_SECTORS: 8;
5
+ export declare const PAINTERLY_TENSOR_WGSL = "\n@group(0) @binding(0) var baseTex: texture_2d<f32>;\n@group(0) @binding(1) var baseSampler: sampler;\n\nconst W_LUMA: vec3f = vec3f(0.2126, 0.7152, 0.0722);\n\n@fragment\nfn tensor_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n let texel = 1.0 / vec2f(textureDimensions(baseTex));\n // 3x3 Sobel over luma.\n var l: array<f32, 9>;\n var k = 0;\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let s = textureSample(baseTex, baseSampler, uv + vec2f(f32(dx), f32(dy)) * texel).rgb;\n l[k] = dot(s, W_LUMA);\n k = k + 1;\n }\n }\n let gx = (l[2] + 2.0 * l[5] + l[8]) - (l[0] + 2.0 * l[3] + l[6]);\n let gy = (l[6] + 2.0 * l[7] + l[8]) - (l[0] + 2.0 * l[1] + l[2]);\n // (Jxx, Jyy, Jxy) \u2014 RGBA16F target (magnitudes exceed [0,1], 8-bit LDR clips).\n return vec4f(gx * gx, gy * gy, gx * gy, 1.0);\n}";
6
+ export declare const PAINTERLY_SMOOTH_WGSL = "\n@group(0) @binding(0) var tensorTex: texture_2d<f32>;\n@group(0) @binding(1) var tensorSampler: sampler;\n@group(0) @binding(2) var<uniform> dir: vec2f;\n\n@fragment\nfn smooth_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n let texel = 1.0 / vec2f(textureDimensions(tensorTex));\n // 9-tap Gaussian (sigma ~2) along the dir axis.\n let w = array<f32, 5>(0.227027, 0.194594, 0.121622, 0.054054, 0.016216);\n var sum = textureSample(tensorTex, tensorSampler, uv).xyz * w[0];\n for (var i = 1; i < 5; i = i + 1) {\n let off = dir * texel * f32(i);\n sum = sum + textureSample(tensorTex, tensorSampler, uv + off).xyz * w[i];\n sum = sum + textureSample(tensorTex, tensorSampler, uv - off).xyz * w[i];\n }\n return vec4f(sum, 1.0);\n}";
7
+ export declare const PAINTERLY_KUWAHARA_WGSL: string;
@@ -0,0 +1,4 @@
1
+ /** The wake velocity-texture format (the CONSUMER declares it; the substrate bakes none). */
2
+ export declare const WAKE_TEXTURE_FORMAT: "rg16float";
3
+ export declare const WAKE_ADVECT_WGSL = "\nstruct WakeUniforms {\n dt: f32,\n tempo: f32, // the master tempo scalar (0 under PRM/pause \u2014 freezes injection)\n dissipation: f32,\n pad: f32,\n splatPos: vec2f, // the delta-tracked pointer position (0..1)\n splatVel: vec2f, // the pointer velocity (the splat direction/strength)\n splatRadius: f32,\n rippleAmp: f32, // a click \u2192 a radial-ripple impulse\n pad2: vec2f,\n};\n\n@group(0) @binding(0) var velTex: texture_2d<f32>;\n@group(0) @binding(1) var velSampler: sampler;\n@group(0) @binding(2) var<uniform> U: WakeUniforms;\n\n@fragment\nfn advect_main(@location(0) uv: vec2f) -> @location(0) vec4f {\n let texel = 1.0 / vec2f(textureDimensions(velTex));\n // Self-advection: trace back along the local velocity by dt (semi-Lagrangian).\n let v0 = textureSample(velTex, velSampler, uv).xy;\n let back = uv - v0 * U.dt;\n var v = textureSample(velTex, velSampler, back).xy;\n // Dissipate (the eddies decay so the wake lingers but fades).\n v = v * U.dissipation;\n\n // Delta-tracked Gaussian SPLAT \u2014 the pointer writes velocity into the field. The\n // injection is gated by the master tempo (0 under PRM \u2192 no new motion under reduce).\n let d = uv - U.splatPos;\n let g = exp(-dot(d, d) / max(U.splatRadius * U.splatRadius, 1e-6));\n v = v + U.splatVel * g * U.tempo;\n\n // Click \u2192 a radial-ripple impulse (pushes velocity radially outward from the splat).\n if (U.rippleAmp > 0.001) {\n let dir = d / max(length(d), 1e-4);\n v = v + dir * U.rippleAmp * g * U.tempo;\n }\n\n return vec4f(v, 0.0, 1.0);\n}";
4
+ export declare const WAKE_APPLY_WGSL = "\n@group(0) @binding(0) var wakeTex: texture_2d<f32>;\n@group(0) @binding(1) var wakeSampler: sampler;\n\nfn wakeBias(uv: vec2f) -> vec2f {\n return textureSample(wakeTex, wakeSampler, uv).xy;\n}";
@@ -5,7 +5,8 @@ export type { UseAuroraReturn } from "./composables/useAurora";
5
5
  export { useCursorInteraction } from "./composables/useCursorInteraction";
6
6
  export { createAurora } from "./composables/runtime";
7
7
  export type { AuroraRuntimeMode, AuroraRuntimeOptions } from "./composables/runtime";
8
- export { DEFAULT_AURORA_CONFIG, MAX_NUCLEI, MAX_STOPS, type AuroraConfig, type AuroraCursorApi, type AuroraFlow, type AuroraInstance, type AuroraMedium, type AuroraNucleus, type FlowPattern, type OklchStop, type StrokeMode, type WarpMode, } from "./constants/presets";
8
+ export { DEFAULT_AURORA_CONFIG, MAX_NUCLEI, MAX_STOPS, type AuroraConfig, type AuroraCursorApi, type AuroraFlow, type AuroraInstance, type AuroraInteractivity, type AuroraMedium, type AuroraNucleus, type FlowPattern, type OklchStop, type StrokeMode, type StrokeOrient, type WarpMode, } from "./constants/presets";
9
+ export { resolveAtoms, DEFAULT_ATOMS, type AuroraAtoms, type AuroraMoodAtom, type AuroraMotionAtom, } from "./composables/atoms";
9
10
  export { cssToOklch, deriveAurora, deriveScene, flattenPalette, hexToOklchStop, oklchStopToHex, oklchToLinear, paletteToCssGradient, } from "./composables/color";
10
11
  export type { AuroraHarmony, AuroraMood, DeriveAuroraOptions, DeriveEasing, } from "./composables/color";
11
12
  export type { AuroraHuePath } from "./constants/presets";
@@ -0,0 +1,38 @@
1
+ import { type ColorResolver } from "../../../composables/color";
2
+ /**
3
+ * FourierField — a Fourier epicycle field on a Canvas2D surface, a sibling
4
+ * primitive to Aurora and GooBlob. A seeded elliptic spectrum reconstructs a
5
+ * slow, never-ending closed curve via the inverse DFT; a fading comet trail
6
+ * chases the curve's head, and (in the `hero` preset) the nested epicycle
7
+ * circles and arms draw underneath in a harmonious second hue. It composes the
8
+ * `useCanvas2D` substrate, so it inherits the offscreen / tab-hidden /
9
+ * reduced-motion freeze for free — no hand-rolled RAF-park.
10
+ *
11
+ * Color resolves through the same INJECTED `colorResolver` seam as GooBlob: the
12
+ * field paints the gamma-sRGB triple it returns. The prop is REQUIRED — pass
13
+ * `defaultBlobColorResolver` from `@mkbabb/glass-ui/color` so a `var()` /
14
+ * `light-dark()` token, an `oklch()` / `hsl()` / hex string all resolve. A
15
+ * `var()` token is first resolved to a concrete `rgb()` against the host's
16
+ * inherited color-scheme, then re-resolved on a color change AND on a dark-mode
17
+ * toggle so the field retints with the theme.
18
+ *
19
+ * `variant` is the configuration BUNDLE, not a recolour of one curve: `hero`
20
+ * draws a few big phasors with the epicycles ON and a warm trail feel; `final`
21
+ * draws a denser elliptic spectrum with the epicycles OFF. One engine, two
22
+ * presets. The hue itself always comes from the consumer's `color`.
23
+ */
24
+ type __VLS_Props = {
25
+ /** Configuration bundle. `hero` = epicycles on, fewer harmonics; `final` = epicycles off, denser. Default `hero`. */
26
+ variant?: "hero" | "final";
27
+ /** Base CSS color (any form the resolver understands; a `var()`/`light-dark()` token resolves against the host). */
28
+ color: string;
29
+ /** REQUIRED color seam — resolves a concrete color to a gamma-sRGB [r,g,b] triple in [0,1]. */
30
+ colorResolver: ColorResolver;
31
+ /** Extra seed mixed into the spectrum PRNG for a unique-but-reproducible curve. Default "". */
32
+ seed?: string;
33
+ /** When true, paint ONE static deterministic best-frame and never animate (the capture/export lever). Default false. */
34
+ freeze?: boolean;
35
+ };
36
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
+ declare const _default: typeof __VLS_export;
38
+ export default _default;
@@ -0,0 +1,2 @@
1
+ export { default as FourierField } from "./FourierField.vue";
2
+ export { comp, evalFourier, positionsAt, makeEllipticSpectrum, type BasisComponent, type EllipticSpectrumOptions, } from "./math";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * One Fourier phasor. `index` is the integer frequency (a counter-rotating pair
3
+ * is `+1` and `-1`); `coefficient` is the complex coefficient `[re, im]`;
4
+ * `amplitude`/`phase` are its polar form (carried for callers that prefer the
5
+ * polar read — the reconstruction uses the rectangular `coefficient`).
6
+ */
7
+ export interface BasisComponent {
8
+ index: number;
9
+ coefficient: [number, number];
10
+ amplitude: number;
11
+ phase: number;
12
+ }
13
+ /**
14
+ * Build a {@link BasisComponent} from a frequency index and its rectangular
15
+ * coefficient `(re, im)`, deriving the polar `amplitude`/`phase`.
16
+ */
17
+ export declare function comp(index: number, re: number, im: number): BasisComponent;
18
+ /**
19
+ * Sum `c_k * exp(2*pi*i*k*t)` — the curve point at parameter `t`. `maxTerms`
20
+ * truncates the series (the partial-sum read). Returns the complex value as
21
+ * `[re, im]` (the curve's `[x, y]` before view-fit).
22
+ */
23
+ export declare function evalFourier(components: BasisComponent[], t: number, maxTerms?: number): [number, number];
24
+ /**
25
+ * The epicycle chain at parameter `t`: the running tip of each phasor stacked on
26
+ * the last, starting at the origin. `positions[0]` is `[0, 0]`; the final entry
27
+ * is the curve point ({@link evalFourier} at the same `t`). `maxCircles`
28
+ * truncates the chain (draw fewer arms than the spectrum carries).
29
+ */
30
+ export declare function positionsAt(components: BasisComponent[], t: number, maxCircles?: number): [number, number][];
31
+ /** Tuning for {@link makeEllipticSpectrum}. */
32
+ export interface EllipticSpectrumOptions {
33
+ /**
34
+ * How many smaller higher-order harmonics to layer on top of the dominant
35
+ * counter-rotating pair (the dominant pair is always present). Default 4.
36
+ * The `final` preset passes a larger count for a denser, busier curve.
37
+ */
38
+ harmonics?: number;
39
+ /**
40
+ * Peak magnitude of the higher-order harmonics relative to the dominant
41
+ * phasor. Smaller keeps the curve a clean, smooth ellipse with a little
42
+ * character; larger crinkles it. Default ~0.22.
43
+ */
44
+ harmonicScale?: number;
45
+ }
46
+ /**
47
+ * Emit a procedural elliptic spectrum: a DOMINANT counter-rotating pair (index
48
+ * `+1` and `-1`) of UNEQUAL magnitude — the inequality is what draws a tilted
49
+ * ellipse rather than a circle — plus a few smaller higher-order harmonics with
50
+ * random small magnitudes and phases for character. Lower orders carry more
51
+ * weight (a `1 / order` falloff), so the curve stays large, smooth and
52
+ * few-big-phasor; the `harmonics` knob lifts it to the denser `final` look.
53
+ *
54
+ * Deterministic in `rng`: the same seeded `() => number` yields the same
55
+ * spectrum, so a frozen frame reproduces exactly.
56
+ */
57
+ export declare function makeEllipticSpectrum(rng: () => number, opts?: EllipticSpectrumOptions): BasisComponent[];
@@ -35,9 +35,11 @@ type __VLS_Props = {
35
35
  };
36
36
  declare function nudge(): void;
37
37
  declare function setMood(m: BlobMood): void;
38
+ declare function pulse(): void;
38
39
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
39
40
  nudge: typeof nudge;
40
41
  setMood: typeof setMood;
42
+ pulse: typeof pulse;
41
43
  currentMood: Readonly<import("vue").Ref<BlobMood, BlobMood>>;
42
44
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
43
45
  click: () => any;
@@ -1,14 +1,26 @@
1
1
  import { type Ref } from "vue";
2
2
  import type { BlobMood, MoodParams } from "../types";
3
+ /** The interaction signals `update` reads to drive the auto-mood arc. */
4
+ export interface MoodInteraction {
5
+ /** The pointer is over the blob (drives `curious`). */
6
+ pointerActive: boolean;
7
+ /** A click landed this frame (drives a one-shot `excited`). */
8
+ clicked: boolean;
9
+ /** ms since the last pointer activity (drives `sleepy` past IDLE_SLEEP_MS). */
10
+ idleMs: number;
11
+ }
3
12
  /**
4
13
  * Animates the blob's mood parameters — a cross-fade between named mood targets
5
- * driven per frame by `tick(dt)`. `setMood` retargets; `params` is the eased
6
- * current value the renderer reads.
14
+ * driven per frame by `tick(dt)`. `setMood` retargets; `update(interaction)` wires
15
+ * `setMood` internally from the AW.W10 pointer/idle state (curious on approach,
16
+ * excited on click, sleepy after inactivity); `params` is the eased current value
17
+ * the renderer reads.
7
18
  */
8
19
  export declare function useBlobMood(): {
9
20
  currentMood: Readonly<Ref<BlobMood>>;
10
21
  params: Readonly<Ref<MoodParams>>;
11
22
  setMood: (mood: BlobMood) => void;
23
+ update: (interaction: MoodInteraction) => void;
12
24
  tick: (dt: number) => void;
13
25
  };
14
26
  export type BlobMoodSystem = ReturnType<typeof useBlobMood>;
@@ -1,8 +1,31 @@
1
1
  import { type Ref } from "vue";
2
+ /** Compile-time trail length — mirrors `TRAIL_N` in metaball.frag.ts. */
3
+ export declare const TRAIL_N = 15;
4
+ /** A single trail sample — a decaying-radius metaball source the shader smin-merges. */
5
+ export interface TrailSample {
6
+ x: number;
7
+ y: number;
8
+ radius: number;
9
+ }
2
10
  /**
3
- * Tracks the pointer over a host element, normalised to [-1, 1] relative to the
4
- * element centre, and smooths it per frame via `tick()`. On pointer-leave the
5
- * smoothed position decays back toward centre.
11
+ * The blob pointer/interaction machine (W10). Tracks the pointer over a host
12
+ * element (normalised to [-1, 1] about the element centre) and resolves it with a
13
+ * FRAME-RATE-INDEPENDENT critically-damped spring fed the renderer's per-frame
14
+ * `dtMs` (NOT a fixed-α lerp — the prior `SMOOTH_FACTOR = 0.12` was framerate
15
+ * dependent). The spring is `@mkbabb/keyframes.js` `SpringProgress` driven through
16
+ * its `tick(dt)` seam by the SUBSTRATE's single rAF — it does NOT call
17
+ * `SpringProgress.play()` (that would start a parallel rAF, the
18
+ * single-substrate-loop violation `proof:blob-interaction-prm` forbids).
19
+ *
20
+ * Exposes the smoothed `pointer` position AND its `velocity` (the spring's own
21
+ * per-frame velocity, units/s in normalized space), plus a decaying-radius `trail`
22
+ * buffer (the elastic pseudopod reaching toward the cursor and snapping back).
23
+ *
24
+ * Under `prefers-reduced-motion` the substrate freezes the rAF, so `tick` is not
25
+ * called and every axis holds its rest value — there is NO parallel matchMedia
26
+ * here (the substrate owns PRM). The deterministic rest pose: the spring at the
27
+ * pointer target (or centre when inactive), zero velocity, the trail collapsed
28
+ * onto the body.
6
29
  */
7
30
  export declare function useBlobPointer(el: Ref<HTMLElement | null>): {
8
31
  pointer: Readonly<Ref<{
@@ -12,7 +35,25 @@ export declare function useBlobPointer(el: Ref<HTMLElement | null>): {
12
35
  readonly x: number;
13
36
  readonly y: number;
14
37
  }>>;
38
+ velocity: Readonly<Ref<{
39
+ readonly x: number;
40
+ readonly y: number;
41
+ }, {
42
+ readonly x: number;
43
+ readonly y: number;
44
+ }>>;
15
45
  active: Readonly<Ref<boolean, boolean>>;
16
- tick: () => void;
46
+ /** The live click-impulse pulse value (folds into uPulseAmp). */
47
+ pulse: Readonly<Ref<number, number>>;
48
+ /** ms since the last pointer activity (W11.c mood idle drift). */
49
+ idleMs: () => number;
50
+ tick: (dtMs?: number) => void;
51
+ trailSources: (baseRadius: number) => {
52
+ sources: TrailSample[];
53
+ count: number;
54
+ };
55
+ click: (amp: number) => void;
56
+ consumeClick: () => boolean;
57
+ rest: () => void;
17
58
  };
18
59
  export type BlobPointer = ReturnType<typeof useBlobPointer>;
@@ -1,6 +1,6 @@
1
1
  import { type Ref } from "vue";
2
2
  import type { ColorResolver } from "../../../../composables/color";
3
- import type { BlobConfig } from "../types";
3
+ import { type BlobConfig } from "../types";
4
4
  import type { BlobMoodSystem } from "./useBlobMood";
5
5
  import type { BlobPointer } from "./useBlobPointer";
6
6
  import type { BlobSatelliteSystem } from "./useBlobSatellites";
@@ -1,5 +1,5 @@
1
1
  export { default as GooBlob } from "./GooBlob.vue";
2
- export type { BlobMood, MoodParams, MetaballSource, BlobConfig, SatellitePhase, SatelliteInternal, } from "./types";
2
+ export type { BlobMood, BlobMerge, MoodParams, MetaballSource, BlobConfig, SatellitePhase, SatelliteInternal, } from "./types";
3
3
  export { BLOB_CONFIG_DEFAULTS, BLOB_CONFIG_KEY } from "./types";
4
4
  export { useBlobMood, type BlobMoodSystem } from "./composables/useBlobMood";
5
5
  export { useBlobPointer, type BlobPointer } from "./composables/useBlobPointer";
@@ -1 +1 @@
1
- export declare const METABALL_SDF_GLSL = "// --- SDF ---\n\nfloat sdCircle(vec2 p, vec2 center, float radius) {\n return length(p - center) - radius;\n}\n\n// Edit #2 \u2014 Inigo Quilez quadratic polynomial smin (same uSmoothK uniform). The\n// quadratic h*h*k*0.25 form has a cleaner C1 seam than the prior cubic smin.\nfloat smin(float a, float b, float k) {\n float h = max(k - abs(a - b), 0.0) / k;\n return min(a, b) - h * h * k * 0.25;\n}";
1
+ export declare const METABALL_SDF_GLSL = "// --- SDF ---\n\nfloat sdCircle(vec2 p, vec2 center, float radius) {\n return length(p - center) - radius;\n}\n\n// IQ 2024 quadratic-polynomial smin, NORMALIZED so k is a blend band in distance\n// units. The k *= 4.0 pre-scale makes the h*h*k*0.25 poly's effective blend width\n// equal to k (the un-normalized form's width was 0.25*k).\nfloat sminQuadratic(float a, float b, float k) {\n k *= 4.0;\n float h = max(k - abs(a - b), 0.0) / k;\n return min(a, b) - h * h * k * 0.25;\n}\n\n// IQ circular smin \u2014 a quarter-circle fillet at the seam (rounder than the\n// quadratic). The k *= 1.0/(1.0-sqrt(0.5)) factor normalizes k to the same\n// blend-band units so the merge axis swaps shape without re-tuning uSmoothK.\nfloat sminCircular(float a, float b, float k) {\n k *= 1.0 / (1.0 - sqrt(0.5));\n float h = max(k - abs(a - b), 0.0) / k;\n return min(a, b) - k * 0.5 * (1.0 + h - sqrt(1.0 - h * (h - 2.0)));\n}\n\n// Config-gated merge dispatch \u2014 uMerge 0 = quadratic, 1 = circular.\nfloat smin(float a, float b, float k) {\n if (uMerge > 0.5) return sminCircular(a, b, k);\n return sminQuadratic(a, b, k);\n}";