blocks-dusted 0.1.10 → 0.1.12

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 (249) hide show
  1. package/README.md +35 -0
  2. package/package.json +5 -2
  3. package/src/cli.js +39 -3
  4. package/src/commands/shop.js +59 -0
  5. package/src/commands/warehouse.js +73 -0
  6. package/src/utils/paths.js +13 -13
  7. package/templates/blocks/DD-Accordion/Component.tsx +229 -0
  8. package/templates/blocks/DD-Accordion/README.md +3 -0
  9. package/templates/blocks/DD-Accordion/config.ts +73 -0
  10. package/templates/blocks/DD-Accordion/manifest.json +75 -0
  11. package/templates/blocks/DD-AnimText/Component.tsx +788 -0
  12. package/templates/blocks/DD-AnimText/README.md +3 -0
  13. package/templates/blocks/DD-AnimText/config.ts +295 -0
  14. package/templates/blocks/DD-AnimText/manifest.json +85 -0
  15. package/templates/blocks/DD-AsideNav/Component.tsx +321 -0
  16. package/templates/blocks/DD-AsideNav/config.ts +204 -0
  17. package/templates/blocks/DD-AsideNav/manifest.json +80 -0
  18. package/templates/blocks/DD-Banner/Component.tsx +51 -0
  19. package/templates/blocks/DD-Banner/config.ts +57 -0
  20. package/templates/blocks/DD-Banner/manifest.json +72 -0
  21. package/templates/blocks/DD-BgLightning/Component.tsx +44 -0
  22. package/templates/blocks/DD-BgLightning/README.md +3 -0
  23. package/templates/blocks/DD-BgLightning/config.ts +97 -0
  24. package/templates/blocks/DD-BgLightning/manifest.json +79 -0
  25. package/templates/blocks/DD-BookCall/Component.tsx +535 -535
  26. package/templates/blocks/DD-BookCall/config.ts +238 -238
  27. package/templates/blocks/DD-ButtonPop/Component.tsx +117 -0
  28. package/templates/blocks/DD-ButtonPop/config.ts +105 -0
  29. package/templates/blocks/DD-ButtonPop/manifest.json +88 -0
  30. package/templates/blocks/DD-ButtonPop/svg-icons/Dust2.tsx +167 -0
  31. package/templates/blocks/DD-Card/Component.tsx +251 -0
  32. package/templates/blocks/DD-Card/README.md +4 -0
  33. package/templates/blocks/DD-Card/config.ts +154 -0
  34. package/templates/blocks/DD-Card/manifest.json +82 -0
  35. package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -140
  36. package/templates/blocks/DD-CardTemplate01/config.ts +106 -106
  37. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -249
  38. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -403
  39. package/templates/blocks/DD-Chip/Component.tsx +250 -0
  40. package/templates/blocks/DD-Chip/EXAMPLES.md +313 -0
  41. package/templates/blocks/DD-Chip/README.md +202 -0
  42. package/templates/blocks/DD-Chip/config.ts +369 -0
  43. package/templates/blocks/DD-Chip/manifest.json +74 -0
  44. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -256
  45. package/templates/blocks/DD-Contact/Component.tsx +435 -434
  46. package/templates/blocks/DD-Contact/config.ts +22 -22
  47. package/templates/blocks/DD-Counter/Component.tsx +190 -0
  48. package/templates/blocks/DD-Counter/README.md +4 -0
  49. package/templates/blocks/DD-Counter/config.ts +291 -0
  50. package/templates/blocks/DD-Counter/manifest.json +74 -0
  51. package/templates/blocks/DD-CustomCSS/Component.tsx +8 -0
  52. package/templates/blocks/DD-CustomCSS/README.md +4 -0
  53. package/templates/blocks/DD-CustomCSS/addToLayout.md +5 -0
  54. package/templates/blocks/DD-CustomCSS/config.ts +22 -0
  55. package/templates/blocks/DD-CustomCSS/manifest.json +72 -0
  56. package/templates/blocks/DD-CustomCSS/renderCustomCSS.md +7 -0
  57. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  58. package/templates/blocks/DD-FeatCat/config.ts +201 -201
  59. package/templates/blocks/DD-FeatStrip/Component.tsx +585 -585
  60. package/templates/blocks/DD-FeatStrip/config.ts +134 -134
  61. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +628 -0
  62. package/templates/blocks/DD-FluidBtn/Component.tsx +43 -0
  63. package/templates/blocks/DD-FluidBtn/README.md +6 -0
  64. package/templates/blocks/DD-FluidBtn/config.ts +102 -0
  65. package/templates/blocks/DD-FluidBtn/manifest.json +86 -0
  66. package/templates/blocks/DD-GooeyInfo/Component.tsx +149 -0
  67. package/templates/blocks/DD-GooeyInfo/config.ts +135 -0
  68. package/templates/blocks/DD-GooeyInfo/manifest.json +72 -0
  69. package/templates/blocks/DD-Hero/Component.tsx +297 -297
  70. package/templates/blocks/DD-Hero/config.ts +267 -267
  71. package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -936
  72. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -231
  73. package/templates/blocks/DD-Icon/Component.tsx +287 -0
  74. package/templates/blocks/DD-Icon/README.md +4 -0
  75. package/templates/blocks/DD-Icon/config.ts +258 -0
  76. package/templates/blocks/DD-Icon/hooks/revalidateBlockIds.ts +66 -0
  77. package/templates/blocks/DD-Icon/manifest.json +82 -0
  78. package/templates/blocks/DD-IconButton/Component.tsx +455 -0
  79. package/templates/blocks/DD-IconButton/README.md +326 -0
  80. package/templates/blocks/DD-IconButton/config.ts +239 -0
  81. package/templates/blocks/DD-IconButton/manifest.json +85 -0
  82. package/templates/blocks/DD-IdTag/Component.tsx +21 -0
  83. package/templates/blocks/DD-IdTag/README.md +4 -0
  84. package/templates/blocks/DD-IdTag/config.ts +41 -0
  85. package/templates/blocks/DD-IdTag/manifest.json +72 -0
  86. package/templates/blocks/DD-Iframe/Component.tsx +45 -0
  87. package/templates/blocks/DD-Iframe/README.md +4 -0
  88. package/templates/blocks/DD-Iframe/config.ts +47 -0
  89. package/templates/blocks/DD-Iframe/manifest.json +72 -0
  90. package/templates/blocks/DD-IframeMedia/Component.tsx +133 -0
  91. package/templates/blocks/DD-IframeMedia/README.md +4 -0
  92. package/templates/blocks/DD-IframeMedia/config.ts +79 -0
  93. package/templates/blocks/DD-IframeMedia/manifest.json +72 -0
  94. package/templates/blocks/DD-LineAnim/Component.tsx +36 -0
  95. package/templates/blocks/DD-LineAnim/README.md +4 -0
  96. package/templates/blocks/DD-LineAnim/config.ts +90 -0
  97. package/templates/blocks/DD-LineAnim/manifest.json +80 -0
  98. package/templates/blocks/DD-LinkGrid/Component.tsx +119 -0
  99. package/templates/blocks/DD-LinkGrid/README.md +4 -0
  100. package/templates/blocks/DD-LinkGrid/config.ts +51 -0
  101. package/templates/blocks/DD-LinkGrid/manifest.json +74 -0
  102. package/templates/blocks/DD-LogoMarq/Component.tsx +132 -0
  103. package/templates/blocks/DD-LogoMarq/README.md +4 -0
  104. package/templates/blocks/DD-LogoMarq/config.ts +264 -0
  105. package/templates/blocks/DD-LogoMarq/manifest.json +82 -0
  106. package/templates/blocks/DD-Lottie/Component.tsx +459 -0
  107. package/templates/blocks/DD-Lottie/README.md +4 -0
  108. package/templates/blocks/DD-Lottie/config.ts +379 -0
  109. package/templates/blocks/DD-Lottie/index.tsx +28 -0
  110. package/templates/blocks/DD-Lottie/manifest.json +83 -0
  111. package/templates/blocks/DD-MagicBento/Component.tsx +777 -0
  112. package/templates/blocks/DD-MagicBento/README.md +4 -0
  113. package/templates/blocks/DD-MagicBento/config.ts +264 -0
  114. package/templates/blocks/DD-MagicBento/manifest.json +74 -0
  115. package/templates/blocks/DD-MasonaryMedia/Component.tsx +545 -545
  116. package/templates/blocks/DD-MasonaryMedia/config.ts +2 -2
  117. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -2
  118. package/templates/blocks/DD-Masonry/Component.tsx +645 -0
  119. package/templates/blocks/DD-Masonry/README.md +4 -0
  120. package/templates/blocks/DD-Masonry/config.ts +475 -0
  121. package/templates/blocks/DD-Masonry/manifest.json +82 -0
  122. package/templates/blocks/DD-Pricing/Component.tsx +2 -5
  123. package/templates/blocks/DD-Process/Component.tsx +2 -4
  124. package/templates/blocks/DD-Process/config.ts +150 -150
  125. package/templates/blocks/DD-Progress/Component.tsx +156 -0
  126. package/templates/blocks/DD-Progress/README.md +4 -0
  127. package/templates/blocks/DD-Progress/config.ts +175 -0
  128. package/templates/blocks/DD-Progress/manifest.json +74 -0
  129. package/templates/blocks/DD-ProjHeader/Component.tsx +91 -0
  130. package/templates/blocks/DD-ProjHeader/README.md +4 -0
  131. package/templates/blocks/DD-ProjHeader/config.ts +96 -0
  132. package/templates/blocks/DD-ProjHeader/manifest.json +72 -0
  133. package/templates/blocks/DD-ProjSlider/Component.tsx +123 -0
  134. package/templates/blocks/DD-ProjSlider/README.md +4 -0
  135. package/templates/blocks/DD-ProjSlider/config.ts +64 -0
  136. package/templates/blocks/DD-ProjSlider/manifest.json +72 -0
  137. package/templates/blocks/DD-ScrollText/Component.tsx +114 -0
  138. package/templates/blocks/DD-ScrollText/README.md +4 -0
  139. package/templates/blocks/DD-ScrollText/config.ts +58 -0
  140. package/templates/blocks/DD-ScrollText/manifest.json +72 -0
  141. package/templates/blocks/DD-Section/Component.tsx +266 -266
  142. package/templates/blocks/DD-Separator/Component.tsx +66 -0
  143. package/templates/blocks/DD-Separator/README.md +4 -0
  144. package/templates/blocks/DD-Separator/config.ts +58 -0
  145. package/templates/blocks/DD-Separator/manifest.json +72 -0
  146. package/templates/blocks/DD-Services/Component.tsx +176 -176
  147. package/templates/blocks/DD-Services/config.ts +149 -149
  148. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -307
  149. package/templates/blocks/DD-Slider-A/Component.tsx +1 -1
  150. package/templates/blocks/DD-StackCards/Component.tsx +1 -1
  151. package/templates/blocks/DD-StackCards/config.ts +399 -399
  152. package/templates/blocks/DD-Team/Component.tsx +247 -247
  153. package/templates/blocks/DD-Team/config.ts +173 -173
  154. package/templates/blocks/DD-TeamCards/Component.tsx +251 -0
  155. package/templates/blocks/DD-TeamCards/README.md +4 -0
  156. package/templates/blocks/DD-TeamCards/config.ts +302 -0
  157. package/templates/blocks/DD-TeamCards/manifest.json +74 -0
  158. package/templates/blocks/DD-TechStack/Component.tsx +174 -174
  159. package/templates/blocks/DD-TechStack/config.ts +153 -153
  160. package/templates/blocks/DD-TestMarq/Component.tsx +132 -0
  161. package/templates/blocks/DD-TestMarq/README.md +4 -0
  162. package/templates/blocks/DD-TestMarq/RowLabel.tsx +9 -0
  163. package/templates/blocks/DD-TestMarq/config.ts +88 -0
  164. package/templates/blocks/DD-TestMarq/manifest.json +72 -0
  165. package/templates/blocks/DD-Testim/Component.tsx +247 -0
  166. package/templates/blocks/DD-Testim/README.md +4 -0
  167. package/templates/blocks/DD-Testim/config.ts +101 -0
  168. package/templates/blocks/DD-Testim/manifest.json +75 -0
  169. package/templates/blocks/DD-Testim2/Component.tsx +256 -0
  170. package/templates/blocks/DD-Testim2/README.md +4 -0
  171. package/templates/blocks/DD-Testim2/config.ts +100 -0
  172. package/templates/blocks/DD-Testim2/manifest.json +74 -0
  173. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  174. package/templates/blocks/DD-Testimonials/config.ts +120 -120
  175. package/templates/blocks/DD-TextMarq/Component.tsx +157 -0
  176. package/templates/blocks/DD-TextMarq/README.md +4 -0
  177. package/templates/blocks/DD-TextMarq/config.ts +237 -0
  178. package/templates/blocks/DD-TextMarq/manifest.json +82 -0
  179. package/templates/blocks/DD-Thanks/Component.tsx +501 -0
  180. package/templates/blocks/DD-Thanks/README.md +4 -0
  181. package/templates/blocks/DD-Thanks/config.ts +224 -0
  182. package/templates/blocks/DD-Thanks/manifest.json +72 -0
  183. package/templates/blocks/DD-VertMarq/Component.tsx +154 -0
  184. package/templates/blocks/DD-VertMarq/README.md +4 -0
  185. package/templates/blocks/DD-VertMarq/config.ts +236 -0
  186. package/templates/blocks/DD-VertMarq/dist/index.css +6 -0
  187. package/templates/blocks/DD-VertMarq/dist/index.min.css +1 -0
  188. package/templates/blocks/DD-VertMarq/manifest.json +74 -0
  189. package/templates/blocks/DD-Work/Component.tsx +318 -315
  190. package/templates/blocks/DD-Work/config.ts +165 -165
  191. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  192. package/templates/blocks/DDHG-CTASection2/Component.tsx +185 -0
  193. package/templates/blocks/DDHG-CTASection2/config.ts +140 -0
  194. package/templates/blocks/DDHG-CTASection2/manifest.json +45 -0
  195. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -0
  196. package/templates/blocks/DDHG-HighlightArc/config.ts +85 -0
  197. package/templates/blocks/DDHG-HighlightArc/manifest.json +46 -0
  198. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -0
  199. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -0
  200. package/templates/blocks/DDHG-LogoMarquee/manifest.json +39 -0
  201. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -0
  202. package/templates/blocks/DDHG-NavCodeGrid/config.ts +64 -0
  203. package/templates/blocks/DDHG-NavCodeGrid/manifest.json +45 -0
  204. package/templates/blocks/DDHG-ProcessSec/Component.tsx +574 -0
  205. package/templates/blocks/DDHG-ProcessSec/config.ts +170 -0
  206. package/templates/blocks/DDHG-ProcessSec/manifest.json +56 -0
  207. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +164 -0
  208. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +390 -0
  209. package/templates/blocks/DDHG-ServiceDetails/config.ts +77 -0
  210. package/templates/blocks/DDHG-ServiceDetails/manifest.json +50 -0
  211. package/templates/blocks/DDHG-ServiceHero/Component.tsx +234 -0
  212. package/templates/blocks/DDHG-ServiceHero/config.ts +49 -0
  213. package/templates/blocks/DDHG-ServiceHero/manifest.json +26 -0
  214. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +277 -0
  215. package/templates/blocks/DDHG-ServiceOverview/config.ts +64 -0
  216. package/templates/blocks/DDHG-ServiceOverview/manifest.json +44 -0
  217. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +389 -0
  218. package/templates/blocks/DDHG-StatsFAQ/config.ts +204 -0
  219. package/templates/blocks/DDHG-StatsFAQ/manifest.json +45 -0
  220. package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -0
  221. package/templates/blocks/DDHG-TeamCards/config.ts +48 -0
  222. package/templates/blocks/DDHG-TeamCards/manifest.json +29 -0
  223. package/templates/shared/src/components/BgLightning/index.tsx +242 -0
  224. package/templates/shared/src/components/ui/ShareButton/index.tsx +80 -0
  225. package/templates/shared/src/components/ui/anims/FadeOnScroll.tsx +48 -0
  226. package/templates/shared/src/components/ui/anims/FadeUpOnInView.tsx +63 -0
  227. package/templates/shared/src/components/ui/anims/GlareHover.tsx +111 -0
  228. package/templates/shared/src/components/ui/anims/LineAnims.tsx +81 -0
  229. package/templates/shared/src/components/ui/anims/TitleAnims01.tsx +54 -0
  230. package/templates/shared/src/components/ui/anims/animation-container.tsx +29 -0
  231. package/templates/shared/src/components/ui/anims/blur-in.tsx +26 -0
  232. package/templates/shared/src/components/ui/anims/counter.tsx +53 -0
  233. package/templates/shared/src/components/ui/anims/fade.tsx +83 -0
  234. package/templates/shared/src/components/ui/anims/gradual-spacing.tsx +39 -0
  235. package/templates/shared/src/components/ui/anims/pull-up.tsx +93 -0
  236. package/templates/shared/src/components/ui/anims/reveal-text.tsx +76 -0
  237. package/templates/shared/src/components/ui/anims/rotate-words.tsx +43 -0
  238. package/templates/shared/src/components/ui/anims/shiny-text.tsx +58 -0
  239. package/templates/shared/src/components/ui/anims/shuffle-text.tsx +85 -0
  240. package/templates/shared/src/components/ui/anims/split-text.tsx +211 -0
  241. package/templates/shared/src/components/ui/anims/staggered-fade.tsx +133 -0
  242. package/templates/shared/src/components/ui/anims/text-scroll.tsx +111 -0
  243. package/templates/shared/src/components/ui/anims/typing-effect.tsx +112 -0
  244. package/templates/shared/src/components/ui/button-dusted-fluid/index.tsx +1521 -0
  245. package/templates/shared/src/components/ui/separator.tsx +85 -0
  246. package/templates/shared/src/hooks/useBodyScrollLock.ts +97 -0
  247. package/templates/shared/src/hooks/useClickOutside.ts +28 -0
  248. package/templates/shared/src/utilities/renderCustomCSS.ts +5 -0
  249. package/templates/shared/src/utilities/smoothScroll.ts +65 -0
@@ -0,0 +1,1521 @@
1
+ /* eslint-disable prefer-const */
2
+ 'use client'
3
+
4
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
5
+ import { ButtonsEyeCatching } from '@/blocks/DD-FluidBtn/ButtonsEyeCatching'
6
+ import { cn } from '@/utilities/ui'
7
+
8
+ interface ColorRGB {
9
+ r: number
10
+ g: number
11
+ b: number
12
+ }
13
+
14
+ function clamp01(n: number) {
15
+ return Math.max(0, Math.min(1, n))
16
+ }
17
+
18
+ function lerp(a: number, b: number, t: number) {
19
+ return a + (b - a) * t
20
+ }
21
+
22
+ function mulColor(c: ColorRGB, m: number): ColorRGB {
23
+ return { r: c.r * m, g: c.g * m, b: c.b * m }
24
+ }
25
+
26
+ function HSVtoRGB(h: number, s: number, v: number): ColorRGB {
27
+ let r = 0,
28
+ g = 0,
29
+ b = 0
30
+ const i = Math.floor(h * 6)
31
+ const f = h * 6 - i
32
+ const p = v * (1 - s)
33
+ const q = v * (1 - f * s)
34
+ const t = v * (1 - (1 - f) * s)
35
+
36
+ switch (i % 6) {
37
+ case 0:
38
+ r = v
39
+ g = t
40
+ b = p
41
+ break
42
+ case 1:
43
+ r = q
44
+ g = v
45
+ b = p
46
+ break
47
+ case 2:
48
+ r = p
49
+ g = v
50
+ b = t
51
+ break
52
+ case 3:
53
+ r = p
54
+ g = q
55
+ b = v
56
+ break
57
+ case 4:
58
+ r = t
59
+ g = p
60
+ b = v
61
+ break
62
+ case 5:
63
+ r = v
64
+ g = p
65
+ b = q
66
+ break
67
+ }
68
+ return { r, g, b }
69
+ }
70
+
71
+ function randColor(): ColorRGB {
72
+ const c = HSVtoRGB(Math.random(), 1, 1)
73
+ // keep it “dusty” not neon
74
+ c.r *= 0.15
75
+ c.g *= 0.15
76
+ c.b *= 0.15
77
+ return c
78
+ }
79
+
80
+ export interface FluidDustRegionHandle {
81
+ /**
82
+ * Emit left-right trail behind the button, confined to this region.
83
+ * This does not read mouse position.
84
+ */
85
+ emitButtonTrail: (opts?: {
86
+ durationMs?: number
87
+ sweeps?: number
88
+ marginPx?: number
89
+ yBias?: number // 0..1 (0 top, 0.5 center, 1 bottom)
90
+ intensity?: number
91
+ }) => void
92
+
93
+ /** Immediately clears the effect in this region. */
94
+ clear: () => void
95
+
96
+ /** Pause rendering (keeps buffers). */
97
+ setEnabled: (v: boolean) => void
98
+
99
+ /** Start fading out the canvas after a delay */
100
+ startFadeOut: (delayMs: number, fadeMs: number) => void
101
+ }
102
+
103
+ interface FluidDustRegionProps {
104
+ SIM_RESOLUTION?: number
105
+ DYE_RESOLUTION?: number
106
+ DENSITY_DISSIPATION?: number
107
+ VELOCITY_DISSIPATION?: number
108
+ PRESSURE?: number
109
+ PRESSURE_ITERATIONS?: number
110
+ CURL?: number
111
+ SPLAT_RADIUS?: number
112
+ SPLAT_FORCE?: number
113
+ SHADING?: boolean
114
+ COLOR_UPDATE_SPEED?: number
115
+ BACK_COLOR?: ColorRGB
116
+ TRANSPARENT?: boolean
117
+
118
+ /** The region is sized by its parent. */
119
+ className?: string
120
+ }
121
+
122
+ export const FluidDustRegion = forwardRef<FluidDustRegionHandle, FluidDustRegionProps>(
123
+ function FluidDustRegion(
124
+ {
125
+ SIM_RESOLUTION = 128,
126
+ DYE_RESOLUTION = 900,
127
+ DENSITY_DISSIPATION = 3.35,
128
+ VELOCITY_DISSIPATION = 2.05,
129
+ PRESSURE = 0.1,
130
+ PRESSURE_ITERATIONS = 18,
131
+ CURL = 2.5,
132
+ SPLAT_RADIUS = 0.16,
133
+ SPLAT_FORCE = 5000, // adjust to suit
134
+ SHADING = true,
135
+ COLOR_UPDATE_SPEED = 10,
136
+ BACK_COLOR = { r: 0, g: 0, b: 0 },
137
+ TRANSPARENT = true,
138
+ className
139
+ },
140
+ ref
141
+ ) {
142
+ const canvasRef = useRef<HTMLCanvasElement>(null)
143
+ const fadeRafRef = useRef<number | null>(null)
144
+
145
+ // runtime flags
146
+ const enabledRef = useRef(true)
147
+ const rafRef = useRef<number | null>(null)
148
+
149
+ // burst state (no cursor)
150
+ const burstRef = useRef<{
151
+ active: boolean
152
+ t0: number
153
+ durationMs: number
154
+ sweeps: number
155
+ marginPx: number
156
+ yBias: number
157
+ intensity: number
158
+ baseColor: ColorRGB
159
+ }>({
160
+ active: false,
161
+ t0: 0,
162
+ durationMs: 420,
163
+ sweeps: 2,
164
+ marginPx: 18,
165
+ yBias: 0.55,
166
+ intensity: 1,
167
+ baseColor: randColor()
168
+ })
169
+
170
+ // API storage (assigned after GL init)
171
+ const apiRef = useRef<{
172
+ emitButtonTrail: (opts?: {
173
+ durationMs?: number
174
+ sweeps?: number
175
+ marginPx?: number
176
+ yBias?: number
177
+ intensity?: number
178
+ }) => void
179
+ clear: () => void
180
+ setEnabled: (v: boolean) => void
181
+ startFadeOut: (delayMs: number, fadeMs: number) => void
182
+ } | null>(null)
183
+
184
+ useImperativeHandle(
185
+ ref,
186
+ () => ({
187
+ emitButtonTrail: (opts) => apiRef.current?.emitButtonTrail(opts),
188
+ clear: () => apiRef.current?.clear(),
189
+ setEnabled: (v) => apiRef.current?.setEnabled(v),
190
+ startFadeOut: (delayMs, fadeMs) => apiRef.current?.startFadeOut(delayMs, fadeMs)
191
+ }),
192
+ []
193
+ )
194
+
195
+ useEffect(() => {
196
+ const canvas = canvasRef.current!!
197
+ if (!canvas) return
198
+
199
+ function getWebGLContext(canvasEl: HTMLCanvasElement) {
200
+ const params: WebGLContextAttributes = {
201
+ alpha: true,
202
+ depth: false,
203
+ stencil: false,
204
+ antialias: false,
205
+ preserveDrawingBuffer: false
206
+ }
207
+
208
+ let gl = canvasEl.getContext('webgl2', params) as WebGL2RenderingContext | null
209
+
210
+ if (!gl) {
211
+ gl = (canvasEl.getContext('webgl', params) ||
212
+ canvasEl.getContext('experimental-webgl', params)) as WebGL2RenderingContext | null
213
+ }
214
+
215
+ if (!gl) throw new Error('Unable to initialize WebGL.')
216
+
217
+ const isWebGL2 = 'drawBuffers' in gl
218
+
219
+ let supportLinearFiltering = false
220
+ let halfFloat: any = null
221
+
222
+ if (isWebGL2) {
223
+ ;(gl as WebGL2RenderingContext).getExtension('EXT_color_buffer_float')
224
+ supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
225
+ 'OES_texture_float_linear'
226
+ )
227
+ } else {
228
+ halfFloat = (gl as WebGLRenderingContext).getExtension('OES_texture_half_float')
229
+ supportLinearFiltering = !!(gl as WebGLRenderingContext).getExtension(
230
+ 'OES_texture_half_float_linear'
231
+ )
232
+ }
233
+
234
+ gl.clearColor(0, 0, 0, 1)
235
+
236
+ const halfFloatTexType = isWebGL2
237
+ ? (gl as WebGL2RenderingContext).HALF_FLOAT
238
+ : (halfFloat && halfFloat.HALF_FLOAT_OES) || 0
239
+
240
+ function supportRenderTextureFormat(
241
+ glAny: WebGLRenderingContext | WebGL2RenderingContext,
242
+ internalFormat: number,
243
+ format: number,
244
+ type: number
245
+ ) {
246
+ const texture = glAny.createTexture()
247
+ if (!texture) return false
248
+
249
+ glAny.bindTexture(glAny.TEXTURE_2D, texture)
250
+ glAny.texParameteri(glAny.TEXTURE_2D, glAny.TEXTURE_MIN_FILTER, glAny.NEAREST)
251
+ glAny.texParameteri(glAny.TEXTURE_2D, glAny.TEXTURE_MAG_FILTER, glAny.NEAREST)
252
+ glAny.texParameteri(glAny.TEXTURE_2D, glAny.TEXTURE_WRAP_S, glAny.CLAMP_TO_EDGE)
253
+ glAny.texParameteri(glAny.TEXTURE_2D, glAny.TEXTURE_WRAP_T, glAny.CLAMP_TO_EDGE)
254
+ glAny.texImage2D(glAny.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null)
255
+
256
+ const fbo = glAny.createFramebuffer()
257
+ if (!fbo) return false
258
+
259
+ glAny.bindFramebuffer(glAny.FRAMEBUFFER, fbo)
260
+ glAny.framebufferTexture2D(
261
+ glAny.FRAMEBUFFER,
262
+ glAny.COLOR_ATTACHMENT0,
263
+ glAny.TEXTURE_2D,
264
+ texture,
265
+ 0
266
+ )
267
+ const status = glAny.checkFramebufferStatus(glAny.FRAMEBUFFER)
268
+ return status === glAny.FRAMEBUFFER_COMPLETE
269
+ }
270
+
271
+ function getSupportedFormat(
272
+ glAny: WebGLRenderingContext | WebGL2RenderingContext,
273
+ internalFormat: number,
274
+ format: number,
275
+ type: number
276
+ ): { internalFormat: number; format: number } | null {
277
+ if (!supportRenderTextureFormat(glAny, internalFormat, format, type)) {
278
+ if ('drawBuffers' in glAny) {
279
+ const gl2 = glAny as WebGL2RenderingContext
280
+ switch (internalFormat) {
281
+ case gl2.R16F:
282
+ return getSupportedFormat(gl2, gl2.RG16F, gl2.RG, type)
283
+ case gl2.RG16F:
284
+ return getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, type)
285
+ default:
286
+ return null
287
+ }
288
+ }
289
+ return null
290
+ }
291
+ return { internalFormat, format }
292
+ }
293
+
294
+ let formatRGBA: any
295
+ let formatRG: any
296
+ let formatR: any
297
+
298
+ if (isWebGL2) {
299
+ const gl2 = gl as WebGL2RenderingContext
300
+ formatRGBA = getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, halfFloatTexType)
301
+ formatRG = getSupportedFormat(gl2, gl2.RG16F, gl2.RG, halfFloatTexType)
302
+ formatR = getSupportedFormat(gl2, gl2.R16F, gl2.RED, halfFloatTexType)
303
+ } else {
304
+ formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
305
+ formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
306
+ formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
307
+ }
308
+
309
+ return {
310
+ gl,
311
+ ext: {
312
+ formatRGBA,
313
+ formatRG,
314
+ formatR,
315
+ halfFloatTexType,
316
+ supportLinearFiltering
317
+ }
318
+ }
319
+ }
320
+
321
+ const { gl, ext } = getWebGLContext(canvas)
322
+ if (!gl || !ext) return
323
+
324
+ const config = {
325
+ SIM_RESOLUTION,
326
+ DYE_RESOLUTION,
327
+ DENSITY_DISSIPATION,
328
+ VELOCITY_DISSIPATION,
329
+ PRESSURE,
330
+ PRESSURE_ITERATIONS,
331
+ CURL,
332
+ SPLAT_RADIUS,
333
+ SPLAT_FORCE,
334
+ SHADING,
335
+ COLOR_UPDATE_SPEED,
336
+ PAUSED: false,
337
+ BACK_COLOR,
338
+ TRANSPARENT
339
+ }
340
+
341
+ if (!ext.supportLinearFiltering) {
342
+ config.DYE_RESOLUTION = 256
343
+ config.SHADING = false
344
+ }
345
+
346
+ function hashCode(s: string) {
347
+ if (!s.length) return 0
348
+ let hash = 0
349
+ for (let i = 0; i < s.length; i++) {
350
+ hash = (hash << 5) - hash + s.charCodeAt(i)
351
+ hash |= 0
352
+ }
353
+ return hash
354
+ }
355
+
356
+ function addKeywords(source: string, keywords: string[] | null) {
357
+ if (!keywords) return source
358
+ let keywordsString = ''
359
+ for (const keyword of keywords)
360
+ keywordsString += `#define ${keyword}
361
+ `
362
+ return keywordsString + source
363
+ }
364
+
365
+ function compileShader(
366
+ type: number,
367
+ source: string,
368
+ keywords: string[] | null = null
369
+ ): WebGLShader | null {
370
+ const shaderSource = addKeywords(source, keywords)
371
+ const shader = gl.createShader(type)
372
+ if (!shader) return null
373
+ gl.shaderSource(shader, shaderSource)
374
+ gl.compileShader(shader)
375
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
376
+ console.trace(gl.getShaderInfoLog(shader))
377
+ return shader
378
+ }
379
+
380
+ function createProgram(
381
+ vertexShader: WebGLShader | null,
382
+ fragmentShader: WebGLShader | null
383
+ ): WebGLProgram | null {
384
+ if (!vertexShader || !fragmentShader) return null
385
+ const program = gl.createProgram()
386
+ if (!program) return null
387
+ gl.attachShader(program, vertexShader)
388
+ gl.attachShader(program, fragmentShader)
389
+ gl.linkProgram(program)
390
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS))
391
+ console.trace(gl.getProgramInfoLog(program))
392
+ return program
393
+ }
394
+
395
+ function getUniforms(program: WebGLProgram) {
396
+ let uniforms: Record<string, WebGLUniformLocation | null> = {}
397
+ const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)
398
+ for (let i = 0; i < uniformCount; i++) {
399
+ const uniformInfo = gl.getActiveUniform(program, i)
400
+ if (uniformInfo)
401
+ uniforms[uniformInfo.name] = gl.getUniformLocation(program, uniformInfo.name)
402
+ }
403
+ return uniforms
404
+ }
405
+
406
+ class Program {
407
+ program: WebGLProgram | null
408
+ uniforms: Record<string, WebGLUniformLocation | null>
409
+ constructor(vertexShader: WebGLShader | null, fragmentShader: WebGLShader | null) {
410
+ this.program = createProgram(vertexShader, fragmentShader)
411
+ this.uniforms = this.program ? getUniforms(this.program) : {}
412
+ }
413
+ bind() {
414
+ if (this.program) gl.useProgram(this.program)
415
+ }
416
+ }
417
+
418
+ class Material {
419
+ vertexShader: WebGLShader | null
420
+ fragmentShaderSource: string
421
+ programs: Record<number, WebGLProgram | null>
422
+ activeProgram: WebGLProgram | null
423
+ uniforms: Record<string, WebGLUniformLocation | null>
424
+
425
+ constructor(vertexShader: WebGLShader | null, fragmentShaderSource: string) {
426
+ this.vertexShader = vertexShader
427
+ this.fragmentShaderSource = fragmentShaderSource
428
+ this.programs = {}
429
+ this.activeProgram = null
430
+ this.uniforms = {}
431
+ }
432
+
433
+ setKeywords(keywords: string[]) {
434
+ let hash = 0
435
+ for (const kw of keywords) hash += hashCode(kw)
436
+ let program = this.programs[hash]
437
+ if (program == null) {
438
+ const fragmentShader = compileShader(
439
+ gl.FRAGMENT_SHADER,
440
+ this.fragmentShaderSource,
441
+ keywords
442
+ )
443
+ program = createProgram(this.vertexShader, fragmentShader)
444
+ this.programs[hash] = program
445
+ }
446
+ if (program === this.activeProgram) return
447
+ if (program) this.uniforms = getUniforms(program)
448
+ this.activeProgram = program
449
+ }
450
+
451
+ bind() {
452
+ if (this.activeProgram) gl.useProgram(this.activeProgram)
453
+ }
454
+ }
455
+
456
+ const baseVertexShader = compileShader(
457
+ gl.VERTEX_SHADER,
458
+ `
459
+ precision highp float;
460
+ attribute vec2 aPosition;
461
+ varying vec2 vUv;
462
+ varying vec2 vL;
463
+ varying vec2 vR;
464
+ varying vec2 vT;
465
+ varying vec2 vB;
466
+ uniform vec2 texelSize;
467
+
468
+ void main () {
469
+ vUv = aPosition * 0.5 + 0.5;
470
+ vL = vUv - vec2(texelSize.x, 0.0);
471
+ vR = vUv + vec2(texelSize.x, 0.0);
472
+ vT = vUv + vec2(0.0, texelSize.y);
473
+ vB = vUv - vec2(0.0, texelSize.y);
474
+ gl_Position = vec4(aPosition, 0.0, 1.0);
475
+ }
476
+ `
477
+ )
478
+
479
+ const copyShader = compileShader(
480
+ gl.FRAGMENT_SHADER,
481
+ `
482
+ precision mediump float;
483
+ precision mediump sampler2D;
484
+ varying highp vec2 vUv;
485
+ uniform sampler2D uTexture;
486
+
487
+ void main () {
488
+ gl_FragColor = texture2D(uTexture, vUv);
489
+ }
490
+ `
491
+ )
492
+
493
+ const clearShader = compileShader(
494
+ gl.FRAGMENT_SHADER,
495
+ `
496
+ precision mediump float;
497
+ precision mediump sampler2D;
498
+ varying highp vec2 vUv;
499
+ uniform sampler2D uTexture;
500
+ uniform float value;
501
+
502
+ void main () {
503
+ gl_FragColor = value * texture2D(uTexture, vUv);
504
+ }
505
+ `
506
+ )
507
+
508
+ const displayShaderSource = `
509
+ precision highp float;
510
+ precision highp sampler2D;
511
+ varying vec2 vUv;
512
+ varying vec2 vL;
513
+ varying vec2 vR;
514
+ varying vec2 vT;
515
+ varying vec2 vB;
516
+ uniform sampler2D uTexture;
517
+ uniform vec2 texelSize;
518
+
519
+ void main () {
520
+ vec3 c = texture2D(uTexture, vUv).rgb;
521
+ #ifdef SHADING
522
+ vec3 lc = texture2D(uTexture, vL).rgb;
523
+ vec3 rc = texture2D(uTexture, vR).rgb;
524
+ vec3 tc = texture2D(uTexture, vT).rgb;
525
+ vec3 bc = texture2D(uTexture, vB).rgb;
526
+
527
+ float dx = length(rc) - length(lc);
528
+ float dy = length(tc) - length(bc);
529
+
530
+ vec3 n = normalize(vec3(dx, dy, length(texelSize)));
531
+ vec3 l = vec3(0.0, 0.0, 1.0);
532
+
533
+ float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
534
+ c *= diffuse;
535
+ #endif
536
+
537
+ float a = max(c.r, max(c.g, c.b));
538
+ gl_FragColor = vec4(c, a);
539
+ }
540
+ `
541
+
542
+ const splatShader = compileShader(
543
+ gl.FRAGMENT_SHADER,
544
+ `
545
+ precision highp float;
546
+ precision highp sampler2D;
547
+ varying vec2 vUv;
548
+ uniform sampler2D uTarget;
549
+ uniform float aspectRatio;
550
+ uniform vec3 color;
551
+ uniform vec2 point;
552
+ uniform float radius;
553
+
554
+ void main () {
555
+ vec2 p = vUv - point.xy;
556
+ p.x *= aspectRatio;
557
+ vec3 splat = exp(-dot(p, p) / radius) * color;
558
+ vec3 base = texture2D(uTarget, vUv).xyz;
559
+ gl_FragColor = vec4(base + splat, 1.0);
560
+ }
561
+ `
562
+ )
563
+
564
+ const advectionShader = compileShader(
565
+ gl.FRAGMENT_SHADER,
566
+ `
567
+ precision highp float;
568
+ precision highp sampler2D;
569
+ varying vec2 vUv;
570
+ uniform sampler2D uVelocity;
571
+ uniform sampler2D uSource;
572
+ uniform vec2 texelSize;
573
+ uniform vec2 dyeTexelSize;
574
+ uniform float dt;
575
+ uniform float dissipation;
576
+
577
+ vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
578
+ vec2 st = uv / tsize - 0.5;
579
+ vec2 iuv = floor(st);
580
+ vec2 fuv = fract(st);
581
+
582
+ vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
583
+ vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
584
+ vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
585
+ vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
586
+
587
+ return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
588
+ }
589
+
590
+ void main () {
591
+ #ifdef MANUAL_FILTERING
592
+ vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
593
+ vec4 result = bilerp(uSource, coord, dyeTexelSize);
594
+ #else
595
+ vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
596
+ vec4 result = texture2D(uSource, coord);
597
+ #endif
598
+ float decay = 1.0 + dissipation * dt;
599
+ gl_FragColor = result / decay;
600
+ }
601
+ `,
602
+ ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']
603
+ )
604
+
605
+ const divergenceShader = compileShader(
606
+ gl.FRAGMENT_SHADER,
607
+ `
608
+ precision mediump float;
609
+ precision mediump sampler2D;
610
+ varying highp vec2 vUv;
611
+ varying highp vec2 vL;
612
+ varying highp vec2 vR;
613
+ varying highp vec2 vT;
614
+ varying highp vec2 vB;
615
+ uniform sampler2D uVelocity;
616
+
617
+ void main () {
618
+ float L = texture2D(uVelocity, vL).x;
619
+ float R = texture2D(uVelocity, vR).x;
620
+ float T = texture2D(uVelocity, vT).y;
621
+ float B = texture2D(uVelocity, vB).y;
622
+
623
+ vec2 C = texture2D(uVelocity, vUv).xy;
624
+ if (vL.x < 0.0) { L = -C.x; }
625
+ if (vR.x > 1.0) { R = -C.x; }
626
+ if (vT.y > 1.0) { T = -C.y; }
627
+ if (vB.y < 0.0) { B = -C.y; }
628
+
629
+ float div = 0.5 * (R - L + T - B);
630
+ gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
631
+ }
632
+ `
633
+ )
634
+
635
+ const curlShader = compileShader(
636
+ gl.FRAGMENT_SHADER,
637
+ `
638
+ precision mediump float;
639
+ precision mediump sampler2D;
640
+ varying highp vec2 vUv;
641
+ varying highp vec2 vL;
642
+ varying highp vec2 vR;
643
+ varying highp vec2 vT;
644
+ varying highp vec2 vB;
645
+ uniform sampler2D uVelocity;
646
+
647
+ void main () {
648
+ float L = texture2D(uVelocity, vL).y;
649
+ float R = texture2D(uVelocity, vR).y;
650
+ float T = texture2D(uVelocity, vT).x;
651
+ float B = texture2D(uVelocity, vB).x;
652
+ float vorticity = R - L - T + B;
653
+ gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
654
+ }
655
+ `
656
+ )
657
+
658
+ const vorticityShader = compileShader(
659
+ gl.FRAGMENT_SHADER,
660
+ `
661
+ precision highp float;
662
+ precision highp sampler2D;
663
+ varying vec2 vUv;
664
+ varying vec2 vL;
665
+ varying vec2 vR;
666
+ varying vec2 vT;
667
+ varying vec2 vB;
668
+ uniform sampler2D uVelocity;
669
+ uniform sampler2D uCurl;
670
+ uniform float curl;
671
+ uniform float dt;
672
+
673
+ void main () {
674
+ float L = texture2D(uCurl, vL).x;
675
+ float R = texture2D(uCurl, vR).x;
676
+ float T = texture2D(uCurl, vT).x;
677
+ float B = texture2D(uCurl, vB).x;
678
+ float C = texture2D(uCurl, vUv).x;
679
+
680
+ vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
681
+ force /= length(force) + 0.0001;
682
+ force *= curl * C;
683
+ force.y *= -1.0;
684
+
685
+ vec2 velocity = texture2D(uVelocity, vUv).xy;
686
+ velocity += force * dt;
687
+ velocity = min(max(velocity, -1000.0), 1000.0);
688
+ gl_FragColor = vec4(velocity, 0.0, 1.0);
689
+ }
690
+ `
691
+ )
692
+
693
+ const pressureShader = compileShader(
694
+ gl.FRAGMENT_SHADER,
695
+ `
696
+ precision mediump float;
697
+ precision mediump sampler2D;
698
+ varying highp vec2 vUv;
699
+ varying highp vec2 vL;
700
+ varying highp vec2 vR;
701
+ varying highp vec2 vT;
702
+ varying highp vec2 vB;
703
+ uniform sampler2D uPressure;
704
+ uniform sampler2D uDivergence;
705
+
706
+ void main () {
707
+ float L = texture2D(uPressure, vL).x;
708
+ float R = texture2D(uPressure, vR).x;
709
+ float T = texture2D(uPressure, vT).x;
710
+ float B = texture2D(uPressure, vB).x;
711
+ float divergence = texture2D(uDivergence, vUv).x;
712
+ float pressure = (L + R + B + T - divergence) * 0.25;
713
+ gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
714
+ }
715
+ `
716
+ )
717
+
718
+ const gradientSubtractShader = compileShader(
719
+ gl.FRAGMENT_SHADER,
720
+ `
721
+ precision mediump float;
722
+ precision mediump sampler2D;
723
+ varying highp vec2 vUv;
724
+ varying highp vec2 vL;
725
+ varying highp vec2 vR;
726
+ varying highp vec2 vT;
727
+ varying highp vec2 vB;
728
+ uniform sampler2D uPressure;
729
+ uniform sampler2D uVelocity;
730
+
731
+ void main () {
732
+ float L = texture2D(uPressure, vL).x;
733
+ float R = texture2D(uPressure, vR).x;
734
+ float T = texture2D(uPressure, vT).x;
735
+ float B = texture2D(uPressure, vB).x;
736
+ vec2 velocity = texture2D(uVelocity, vUv).xy;
737
+ velocity.xy -= vec2(R - L, T - B);
738
+ gl_FragColor = vec4(velocity, 0.0, 1.0);
739
+ }
740
+ `
741
+ )
742
+
743
+ const blit = (() => {
744
+ const buffer = gl.createBuffer()!
745
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
746
+ gl.bufferData(
747
+ gl.ARRAY_BUFFER,
748
+ new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
749
+ gl.STATIC_DRAW
750
+ )
751
+ const elemBuffer = gl.createBuffer()!
752
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elemBuffer)
753
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW)
754
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
755
+ gl.enableVertexAttribArray(0)
756
+
757
+ return (target: FBO | null, doClear = false) => {
758
+ if (!gl) return
759
+ if (!target) {
760
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
761
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
762
+ } else {
763
+ gl.viewport(0, 0, target.width, target.height)
764
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo)
765
+ }
766
+ if (doClear) {
767
+ gl.clearColor(0, 0, 0, 1)
768
+ gl.clear(gl.COLOR_BUFFER_BIT)
769
+ }
770
+ gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
771
+ }
772
+ })()
773
+
774
+ interface FBO {
775
+ texture: WebGLTexture
776
+ fbo: WebGLFramebuffer
777
+ width: number
778
+ height: number
779
+ texelSizeX: number
780
+ texelSizeY: number
781
+ attach: (id: number) => number
782
+ }
783
+
784
+ interface DoubleFBO {
785
+ width: number
786
+ height: number
787
+ texelSizeX: number
788
+ texelSizeY: number
789
+ read: FBO
790
+ write: FBO
791
+ swap: () => void
792
+ }
793
+
794
+ const copyProgram = new Program(baseVertexShader, copyShader)
795
+ const clearProgram = new Program(baseVertexShader, clearShader)
796
+ const splatProgram = new Program(baseVertexShader, splatShader)
797
+ const advectionProgram = new Program(baseVertexShader, advectionShader)
798
+ const divergenceProgram = new Program(baseVertexShader, divergenceShader)
799
+ const curlProgram = new Program(baseVertexShader, curlShader)
800
+ const vorticityProgram = new Program(baseVertexShader, vorticityShader)
801
+ const pressureProgram = new Program(baseVertexShader, pressureShader)
802
+ const gradientSubtractProgram = new Program(baseVertexShader, gradientSubtractShader)
803
+ const displayMaterial = new Material(baseVertexShader, displayShaderSource)
804
+
805
+ function createFBO(
806
+ w: number,
807
+ h: number,
808
+ internalFormat: number,
809
+ format: number,
810
+ type: number,
811
+ param: number
812
+ ): FBO {
813
+ gl.activeTexture(gl.TEXTURE0)
814
+ const texture = gl.createTexture()!
815
+ gl.bindTexture(gl.TEXTURE_2D, texture)
816
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param)
817
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param)
818
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
819
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
820
+ gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null)
821
+ const fbo = gl.createFramebuffer()!
822
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
823
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
824
+ gl.viewport(0, 0, w, h)
825
+ gl.clear(gl.COLOR_BUFFER_BIT)
826
+
827
+ const texelSizeX = 1 / w
828
+ const texelSizeY = 1 / h
829
+
830
+ return {
831
+ texture,
832
+ fbo,
833
+ width: w,
834
+ height: h,
835
+ texelSizeX,
836
+ texelSizeY,
837
+ attach(id: number) {
838
+ gl.activeTexture(gl.TEXTURE0 + id)
839
+ gl.bindTexture(gl.TEXTURE_2D, texture)
840
+ return id
841
+ }
842
+ }
843
+ }
844
+
845
+ function createDoubleFBO(
846
+ w: number,
847
+ h: number,
848
+ internalFormat: number,
849
+ format: number,
850
+ type: number,
851
+ param: number
852
+ ): DoubleFBO {
853
+ const fbo1 = createFBO(w, h, internalFormat, format, type, param)
854
+ const fbo2 = createFBO(w, h, internalFormat, format, type, param)
855
+ return {
856
+ width: w,
857
+ height: h,
858
+ texelSizeX: fbo1.texelSizeX,
859
+ texelSizeY: fbo1.texelSizeY,
860
+ read: fbo1,
861
+ write: fbo2,
862
+ swap() {
863
+ const tmp = this.read
864
+ this.read = this.write
865
+ this.write = tmp
866
+ }
867
+ }
868
+ }
869
+
870
+ function resizeFBO(
871
+ target: FBO,
872
+ w: number,
873
+ h: number,
874
+ internalFormat: number,
875
+ format: number,
876
+ type: number,
877
+ param: number
878
+ ) {
879
+ const newFBO = createFBO(w, h, internalFormat, format, type, param)
880
+ copyProgram.bind()
881
+ if (copyProgram.uniforms.uTexture)
882
+ gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0))
883
+ blit(newFBO, false)
884
+ return newFBO
885
+ }
886
+
887
+ function resizeDoubleFBO(
888
+ target: DoubleFBO,
889
+ w: number,
890
+ h: number,
891
+ internalFormat: number,
892
+ format: number,
893
+ type: number,
894
+ param: number
895
+ ) {
896
+ if (target.width === w && target.height === h) return target
897
+ target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param)
898
+ target.write = createFBO(w, h, internalFormat, format, type, param)
899
+ target.width = w
900
+ target.height = h
901
+ target.texelSizeX = 1 / w
902
+ target.texelSizeY = 1 / h
903
+ return target
904
+ }
905
+
906
+ function getResolution(resolution: number) {
907
+ const w = gl.drawingBufferWidth
908
+ const h = gl.drawingBufferHeight
909
+ const aspectRatio = w / h
910
+ let aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio
911
+ const min = Math.round(resolution)
912
+ const max = Math.round(resolution * aspect)
913
+ if (w > h) return { width: max, height: min }
914
+ return { width: min, height: max }
915
+ }
916
+
917
+ function scaleByPixelRatio(input: number) {
918
+ const pixelRatio = window.devicePixelRatio || 1
919
+ return Math.floor(input * pixelRatio)
920
+ }
921
+
922
+ function resizeCanvas() {
923
+ const width = scaleByPixelRatio(canvas.clientWidth)
924
+ const height = scaleByPixelRatio(canvas.clientHeight)
925
+ if (canvas.width !== width || canvas.height !== height) {
926
+ canvas.width = width
927
+ canvas.height = height
928
+ return true
929
+ }
930
+ return false
931
+ }
932
+
933
+ let dye!: DoubleFBO
934
+ let velocity!: DoubleFBO
935
+ let divergence!: FBO
936
+ let curl!: FBO
937
+ let pressure!: DoubleFBO
938
+
939
+ function initFramebuffers() {
940
+ const simRes = getResolution(config.SIM_RESOLUTION)
941
+ const dyeRes = getResolution(config.DYE_RESOLUTION)
942
+
943
+ const texType = ext.halfFloatTexType
944
+ const rgba = ext.formatRGBA
945
+ const rg = ext.formatRG
946
+ const r = ext.formatR
947
+ const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST
948
+
949
+ gl.disable(gl.BLEND)
950
+
951
+ if (!dye) {
952
+ dye = createDoubleFBO(
953
+ dyeRes.width,
954
+ dyeRes.height,
955
+ rgba.internalFormat,
956
+ rgba.format,
957
+ texType,
958
+ filtering
959
+ )
960
+ } else {
961
+ dye = resizeDoubleFBO(
962
+ dye,
963
+ dyeRes.width,
964
+ dyeRes.height,
965
+ rgba.internalFormat,
966
+ rgba.format,
967
+ texType,
968
+ filtering
969
+ )
970
+ }
971
+
972
+ if (!velocity) {
973
+ velocity = createDoubleFBO(
974
+ simRes.width,
975
+ simRes.height,
976
+ rg.internalFormat,
977
+ rg.format,
978
+ texType,
979
+ filtering
980
+ )
981
+ } else {
982
+ velocity = resizeDoubleFBO(
983
+ velocity,
984
+ simRes.width,
985
+ simRes.height,
986
+ rg.internalFormat,
987
+ rg.format,
988
+ texType,
989
+ filtering
990
+ )
991
+ }
992
+
993
+ divergence = createFBO(
994
+ simRes.width,
995
+ simRes.height,
996
+ r.internalFormat,
997
+ r.format,
998
+ texType,
999
+ gl.NEAREST
1000
+ )
1001
+ curl = createFBO(
1002
+ simRes.width,
1003
+ simRes.height,
1004
+ r.internalFormat,
1005
+ r.format,
1006
+ texType,
1007
+ gl.NEAREST
1008
+ )
1009
+ pressure = createDoubleFBO(
1010
+ simRes.width,
1011
+ simRes.height,
1012
+ r.internalFormat,
1013
+ r.format,
1014
+ texType,
1015
+ gl.NEAREST
1016
+ )
1017
+ }
1018
+
1019
+ function updateKeywords() {
1020
+ const displayKeywords: string[] = []
1021
+ if (config.SHADING) displayKeywords.push('SHADING')
1022
+ displayMaterial.setKeywords(displayKeywords)
1023
+ }
1024
+
1025
+ function correctRadius(radius: number) {
1026
+ const aspectRatio = canvas.width / canvas.height
1027
+ if (aspectRatio > 1) radius *= aspectRatio
1028
+ return radius
1029
+ }
1030
+
1031
+ function splat(u: number, v: number, dx: number, dy: number, color: ColorRGB) {
1032
+ splatProgram.bind()
1033
+ if (splatProgram.uniforms.uTarget)
1034
+ gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0))
1035
+ if (splatProgram.uniforms.aspectRatio)
1036
+ gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height)
1037
+ if (splatProgram.uniforms.point) gl.uniform2f(splatProgram.uniforms.point, u, v)
1038
+ if (splatProgram.uniforms.color) gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0)
1039
+ if (splatProgram.uniforms.radius)
1040
+ gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100))
1041
+ blit(velocity.write)
1042
+ velocity.swap()
1043
+
1044
+ if (splatProgram.uniforms.uTarget)
1045
+ gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0))
1046
+ if (splatProgram.uniforms.color)
1047
+ gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b)
1048
+ blit(dye.write)
1049
+ dye.swap()
1050
+ }
1051
+
1052
+ function clearAll() {
1053
+ // multiply existing buffers by 0 (hard clear)
1054
+ gl.disable(gl.BLEND)
1055
+
1056
+ clearProgram.bind()
1057
+ if (clearProgram.uniforms.value) gl.uniform1f(clearProgram.uniforms.value, 0.0)
1058
+
1059
+ if (clearProgram.uniforms.uTexture) {
1060
+ gl.uniform1i(clearProgram.uniforms.uTexture, dye.read.attach(0))
1061
+ blit(dye.write)
1062
+ dye.swap()
1063
+
1064
+ gl.uniform1i(clearProgram.uniforms.uTexture, velocity.read.attach(0))
1065
+ blit(velocity.write)
1066
+ velocity.swap()
1067
+
1068
+ gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0))
1069
+ blit(pressure.write)
1070
+ pressure.swap()
1071
+ }
1072
+
1073
+ // render once to ensure it visually clears
1074
+ render(null)
1075
+ }
1076
+
1077
+ function step(dt: number) {
1078
+ gl.disable(gl.BLEND)
1079
+
1080
+ curlProgram.bind()
1081
+ if (curlProgram.uniforms.texelSize)
1082
+ gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
1083
+ if (curlProgram.uniforms.uVelocity)
1084
+ gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0))
1085
+ blit(curl)
1086
+
1087
+ vorticityProgram.bind()
1088
+ if (vorticityProgram.uniforms.texelSize)
1089
+ gl.uniform2f(
1090
+ vorticityProgram.uniforms.texelSize,
1091
+ velocity.texelSizeX,
1092
+ velocity.texelSizeY
1093
+ )
1094
+ if (vorticityProgram.uniforms.uVelocity)
1095
+ gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0))
1096
+ if (vorticityProgram.uniforms.uCurl)
1097
+ gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1))
1098
+ if (vorticityProgram.uniforms.curl)
1099
+ gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL)
1100
+ if (vorticityProgram.uniforms.dt) gl.uniform1f(vorticityProgram.uniforms.dt, dt)
1101
+ blit(velocity.write)
1102
+ velocity.swap()
1103
+
1104
+ divergenceProgram.bind()
1105
+ if (divergenceProgram.uniforms.texelSize)
1106
+ gl.uniform2f(
1107
+ divergenceProgram.uniforms.texelSize,
1108
+ velocity.texelSizeX,
1109
+ velocity.texelSizeY
1110
+ )
1111
+ if (divergenceProgram.uniforms.uVelocity)
1112
+ gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0))
1113
+ blit(divergence)
1114
+
1115
+ clearProgram.bind()
1116
+ if (clearProgram.uniforms.uTexture)
1117
+ gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0))
1118
+ if (clearProgram.uniforms.value) gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE)
1119
+ blit(pressure.write)
1120
+ pressure.swap()
1121
+
1122
+ pressureProgram.bind()
1123
+ if (pressureProgram.uniforms.texelSize)
1124
+ gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
1125
+ if (pressureProgram.uniforms.uDivergence)
1126
+ gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0))
1127
+ for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
1128
+ if (pressureProgram.uniforms.uPressure)
1129
+ gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1))
1130
+ blit(pressure.write)
1131
+ pressure.swap()
1132
+ }
1133
+
1134
+ gradientSubtractProgram.bind()
1135
+ if (gradientSubtractProgram.uniforms.texelSize)
1136
+ gl.uniform2f(
1137
+ gradientSubtractProgram.uniforms.texelSize,
1138
+ velocity.texelSizeX,
1139
+ velocity.texelSizeY
1140
+ )
1141
+ if (gradientSubtractProgram.uniforms.uPressure)
1142
+ gl.uniform1i(gradientSubtractProgram.uniforms.uPressure, pressure.read.attach(0))
1143
+ if (gradientSubtractProgram.uniforms.uVelocity)
1144
+ gl.uniform1i(gradientSubtractProgram.uniforms.uVelocity, velocity.read.attach(1))
1145
+ blit(velocity.write)
1146
+ velocity.swap()
1147
+
1148
+ advectionProgram.bind()
1149
+ if (advectionProgram.uniforms.texelSize)
1150
+ gl.uniform2f(
1151
+ advectionProgram.uniforms.texelSize,
1152
+ velocity.texelSizeX,
1153
+ velocity.texelSizeY
1154
+ )
1155
+ if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize)
1156
+ gl.uniform2f(
1157
+ advectionProgram.uniforms.dyeTexelSize,
1158
+ velocity.texelSizeX,
1159
+ velocity.texelSizeY
1160
+ )
1161
+ const velocityId = velocity.read.attach(0)
1162
+ if (advectionProgram.uniforms.uVelocity)
1163
+ gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId)
1164
+ if (advectionProgram.uniforms.uSource)
1165
+ gl.uniform1i(advectionProgram.uniforms.uSource, velocityId)
1166
+ if (advectionProgram.uniforms.dt) gl.uniform1f(advectionProgram.uniforms.dt, dt)
1167
+ if (advectionProgram.uniforms.dissipation)
1168
+ gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION)
1169
+ blit(velocity.write)
1170
+ velocity.swap()
1171
+
1172
+ if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize)
1173
+ gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY)
1174
+ if (advectionProgram.uniforms.uVelocity)
1175
+ gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0))
1176
+ if (advectionProgram.uniforms.uSource)
1177
+ gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1))
1178
+ if (advectionProgram.uniforms.dissipation)
1179
+ gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION)
1180
+ blit(dye.write)
1181
+ dye.swap()
1182
+ }
1183
+
1184
+ function drawDisplay(target: FBO | null) {
1185
+ const width = target ? target.width : gl.drawingBufferWidth
1186
+ const height = target ? target.height : gl.drawingBufferHeight
1187
+ displayMaterial.bind()
1188
+ if (config.SHADING && displayMaterial.uniforms.texelSize)
1189
+ gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / width, 1 / height)
1190
+ if (displayMaterial.uniforms.uTexture)
1191
+ gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0))
1192
+ blit(target, false)
1193
+ }
1194
+
1195
+ function render(target: FBO | null) {
1196
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
1197
+ gl.enable(gl.BLEND)
1198
+ drawDisplay(target)
1199
+ }
1200
+
1201
+ // init
1202
+ updateKeywords()
1203
+ initFramebuffers()
1204
+ clearAll()
1205
+
1206
+ // run loop only while a burst is active
1207
+ let lastUpdateTime = performance.now()
1208
+
1209
+ function tick() {
1210
+ rafRef.current = null
1211
+ if (!enabledRef.current) return
1212
+
1213
+ const b = burstRef.current
1214
+ if (!b.active) return
1215
+
1216
+ const now = performance.now()
1217
+ const dt = Math.min(0.016666, (now - lastUpdateTime) / 1000)
1218
+ lastUpdateTime = now
1219
+
1220
+ if (resizeCanvas()) initFramebuffers()
1221
+
1222
+ // burst injection: left-right path inside THIS canvas
1223
+ const t = now - b.t0
1224
+ const p = clamp01(t / b.durationMs)
1225
+
1226
+ const px = scaleByPixelRatio(b.marginPx)
1227
+ const startX = px
1228
+ const endX = Math.max(px + 1, canvas.width - px)
1229
+
1230
+ // yBias (0 top -> 1 bottom). Note: our sim uses v with origin bottom-left.
1231
+ const yPx = lerp(canvas.height * 0.15, canvas.height * 0.85, 1 - b.yBias)
1232
+
1233
+ const cycles = Math.max(1, b.sweeps)
1234
+ const cyclePos = p * cycles
1235
+ const cycleIndex = Math.floor(cyclePos)
1236
+ const inCycleT = cyclePos - cycleIndex
1237
+ const forward = cycleIndex % 2 === 0
1238
+
1239
+ // core X along sweep
1240
+ const xPx = forward ? lerp(startX, endX, inCycleT) : lerp(endX, startX, inCycleT)
1241
+
1242
+ // small wobble to feel “dusty”
1243
+ const wobbleY = Math.sin(p * Math.PI * 6) * scaleByPixelRatio(1.5)
1244
+ const wobbleX = Math.cos(p * Math.PI * 4) * scaleByPixelRatio(1.0)
1245
+
1246
+ const x = xPx + wobbleX
1247
+ const y = yPx + wobbleY
1248
+
1249
+ const u = clamp01(x / canvas.width)
1250
+ const v = clamp01(1 - y / canvas.height)
1251
+
1252
+ // dx/dy to mimic cursor move; keep it noticeable for small region
1253
+ const dx = (forward ? 1 : -1) * (26 + Math.random() * 18) * b.intensity
1254
+ const dy = (Math.random() - 0.5) * 10 * b.intensity
1255
+
1256
+ // primary splat at path point
1257
+ splat(
1258
+ u,
1259
+ v,
1260
+ dx * config.SPLAT_FORCE * 0.0005,
1261
+ dy * config.SPLAT_FORCE * 0.0005,
1262
+ mulColor(b.baseColor, 1.6)
1263
+ )
1264
+
1265
+ // thicken trail: micro-splats around it
1266
+ for (let i = 0; i < 2; i++) {
1267
+ const ju = clamp01(u + (Math.random() - 0.5) * 0.012)
1268
+ const jv = clamp01(v + (Math.random() - 0.5) * 0.012)
1269
+ const jdx = dx * (0.7 + Math.random() * 0.6) * config.SPLAT_FORCE * 0.0005
1270
+ const jdy = dy * (0.7 + Math.random() * 0.6) * config.SPLAT_FORCE * 0.0005
1271
+ splat(ju, jv, jdx, jdy, mulColor(b.baseColor, 1.2 + Math.random() * 0.8))
1272
+ }
1273
+
1274
+ // step sim + render
1275
+ step(dt)
1276
+ render(null)
1277
+
1278
+ // stop after duration; keep a short fade-out simulation so it dissipates nicely
1279
+ if (p >= 1) {
1280
+ // let it dissipate for a moment then stop and leave canvas idle
1281
+ // (no extra splats)
1282
+ const tailMs = 500
1283
+ if (t >= b.durationMs + tailMs) {
1284
+ b.active = false
1285
+ return
1286
+ }
1287
+ }
1288
+
1289
+ rafRef.current = requestAnimationFrame(tick)
1290
+ }
1291
+
1292
+ function ensureTicking() {
1293
+ if (rafRef.current != null) return
1294
+ lastUpdateTime = performance.now()
1295
+ rafRef.current = requestAnimationFrame(tick)
1296
+ }
1297
+
1298
+ // Exposed API
1299
+ apiRef.current = {
1300
+ emitButtonTrail: (opts) => {
1301
+ const b = burstRef.current
1302
+ b.active = true
1303
+ b.t0 = performance.now()
1304
+ b.durationMs = Math.max(120, opts?.durationMs ?? 420)
1305
+ b.sweeps = Math.max(1, opts?.sweeps ?? 2)
1306
+ b.marginPx = Math.max(6, opts?.marginPx ?? 18)
1307
+ b.yBias = clamp01(opts?.yBias ?? 0.55)
1308
+ b.intensity = Math.max(0.2, opts?.intensity ?? 1)
1309
+ b.baseColor = randColor()
1310
+ ensureTicking()
1311
+
1312
+ // Cancel any existing fade
1313
+ if (fadeRafRef.current != null) {
1314
+ cancelAnimationFrame(fadeRafRef.current)
1315
+ fadeRafRef.current = null
1316
+ }
1317
+ // Reset canvas opacity
1318
+ if (canvas) canvas.style.opacity = '1'
1319
+ },
1320
+ clear: () => {
1321
+ burstRef.current.active = false
1322
+ if (rafRef.current != null) {
1323
+ cancelAnimationFrame(rafRef.current)
1324
+ rafRef.current = null
1325
+ }
1326
+ if (fadeRafRef.current != null) {
1327
+ cancelAnimationFrame(fadeRafRef.current)
1328
+ fadeRafRef.current = null
1329
+ }
1330
+ clearAll()
1331
+ if (canvas) canvas.style.opacity = '1'
1332
+ },
1333
+ setEnabled: (v: boolean) => {
1334
+ enabledRef.current = v
1335
+ if (!v) {
1336
+ burstRef.current.active = false
1337
+ if (rafRef.current != null) {
1338
+ cancelAnimationFrame(rafRef.current)
1339
+ rafRef.current = null
1340
+ }
1341
+ if (fadeRafRef.current != null) {
1342
+ cancelAnimationFrame(fadeRafRef.current)
1343
+ fadeRafRef.current = null
1344
+ }
1345
+ }
1346
+ },
1347
+ startFadeOut: (delayMs: number, fadeMs: number) => {
1348
+ if (fadeRafRef.current != null) {
1349
+ cancelAnimationFrame(fadeRafRef.current)
1350
+ fadeRafRef.current = null
1351
+ }
1352
+
1353
+ const startTime = performance.now() + delayMs
1354
+ const fade = () => {
1355
+ const now = performance.now()
1356
+ if (now < startTime) {
1357
+ fadeRafRef.current = requestAnimationFrame(fade)
1358
+ return
1359
+ }
1360
+
1361
+ const elapsed = now - startTime
1362
+ const progress = Math.min(1, elapsed / fadeMs)
1363
+ const opacity = 1 - progress
1364
+
1365
+ if (canvas) canvas.style.opacity = String(opacity)
1366
+
1367
+ if (progress < 1) {
1368
+ fadeRafRef.current = requestAnimationFrame(fade)
1369
+ } else {
1370
+ fadeRafRef.current = null
1371
+ clearAll()
1372
+ if (canvas) canvas.style.opacity = '1'
1373
+ }
1374
+ }
1375
+ fadeRafRef.current = requestAnimationFrame(fade)
1376
+ }
1377
+ }
1378
+
1379
+ return () => {
1380
+ apiRef.current = null
1381
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current)
1382
+ rafRef.current = null
1383
+ }
1384
+ }, [
1385
+ SIM_RESOLUTION,
1386
+ DYE_RESOLUTION,
1387
+ DENSITY_DISSIPATION,
1388
+ VELOCITY_DISSIPATION,
1389
+ PRESSURE,
1390
+ PRESSURE_ITERATIONS,
1391
+ CURL,
1392
+ SPLAT_RADIUS,
1393
+ SPLAT_FORCE,
1394
+ SHADING,
1395
+ COLOR_UPDATE_SPEED,
1396
+ BACK_COLOR,
1397
+ TRANSPARENT
1398
+ ])
1399
+
1400
+ return (
1401
+ <div className={className ?? 'absolute inset-0 pointer-events-none'}>
1402
+ <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
1403
+ </div>
1404
+ )
1405
+ }
1406
+ )
1407
+
1408
+ export type ButtonDustedFluidLinkType = {
1409
+ type?: 'reference' | 'custom' | null
1410
+ url?: string | null
1411
+ reference?: {
1412
+ relationTo: 'pages' | 'posts'
1413
+ value: any
1414
+ } | null
1415
+ newTab?: boolean | null
1416
+ icon?: string | null
1417
+ }
1418
+
1419
+ export type ButtonDustedFluidProps = {
1420
+ variant?: 'v1' | 'v2' | 'v3' | 'v4' | 'v5'
1421
+ label?: string
1422
+ className?: string
1423
+ disabled?: boolean
1424
+ form?: string
1425
+ type?: 'submit' | 'button' | 'reset'
1426
+ size?: 'sm' | 'default' | 'lg' | 'icon'
1427
+ displayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly'
1428
+ iconPosition?: 'before' | 'after'
1429
+ iconName?: string
1430
+ link?: ButtonDustedFluidLinkType | null
1431
+ onClick?: () => void
1432
+ }
1433
+
1434
+ export default function ButtonDustedFluid({
1435
+ variant = 'v1',
1436
+ label = 'BUTTON',
1437
+ className,
1438
+ disabled,
1439
+ form,
1440
+ type,
1441
+ size,
1442
+ displayMode,
1443
+ iconPosition,
1444
+ iconName,
1445
+ link,
1446
+ onClick
1447
+ }: ButtonDustedFluidProps) {
1448
+ const effectiveIconName = iconName || link?.icon || undefined
1449
+ const regionRef = useRef<FluidDustRegionHandle | null>(null)
1450
+ const wrapRef = useRef<HTMLDivElement | null>(null)
1451
+
1452
+ const burstFromCenter = () => {
1453
+ if (disabled) return
1454
+
1455
+ const wrap = wrapRef.current
1456
+ if (!wrap) return
1457
+
1458
+ // Make the left-right sweep happen in a *tight band* around the center,
1459
+ // so it reads like a burst emanating from behind the button.
1460
+ const rect = wrap.getBoundingClientRect()
1461
+ const half = Math.min(rect.width, rect.height) / 1
1462
+ const marginPx = Math.max(6, half - 16) // keeps start/end near center
1463
+
1464
+ regionRef.current?.emitButtonTrail({
1465
+ durationMs: 600,
1466
+ sweeps: 2.5,
1467
+ marginPx,
1468
+ yBias: 0.45,
1469
+ intensity: 0.5
1470
+ })
1471
+
1472
+ // Fade out after burst completes: wait 500ms, then fade over 700ms
1473
+ regionRef.current?.startFadeOut(500, 700)
1474
+ }
1475
+
1476
+ const sizeClasses = {
1477
+ sm: 'h-8 w-fit',
1478
+ default: 'h-10 w-40',
1479
+ lg: 'h-12 w-48',
1480
+ icon: 'h-10 w-10'
1481
+ }
1482
+
1483
+ const resolvedSize = (size ?? 'default') as keyof typeof sizeClasses
1484
+
1485
+ return (
1486
+ <div
1487
+ ref={wrapRef}
1488
+ className={cn(
1489
+ 'dustedfluid-button relative inline-flex items-center justify-center p-4',
1490
+ className
1491
+ )}
1492
+ >
1493
+ {/* Canvas extends beyond button without affecting layout footprint */}
1494
+ <div className="absolute p-32 w-full h-full pointer-events-none overflow-visible">
1495
+ <FluidDustRegion ref={regionRef} />
1496
+ </div>
1497
+
1498
+ <ButtonsEyeCatching
1499
+ displayMode={displayMode}
1500
+ iconPosition={iconPosition}
1501
+ iconName={effectiveIconName}
1502
+ variant={variant}
1503
+ onClick={() => {
1504
+ burstFromCenter()
1505
+ onClick?.()
1506
+ }}
1507
+ onMouseEnter={burstFromCenter}
1508
+ onSubmit={burstFromCenter}
1509
+ className="relative z-10"
1510
+ label={label}
1511
+ size={resolvedSize}
1512
+ disabled={disabled}
1513
+ form={form}
1514
+ htmlType={type}
1515
+ url={link?.url ?? undefined}
1516
+ newTab={link?.newTab ?? false}
1517
+ reference={link?.reference ?? undefined}
1518
+ />
1519
+ </div>
1520
+ )
1521
+ }