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.
- package/README.md +35 -0
- package/package.json +5 -2
- package/src/cli.js +39 -3
- package/src/commands/shop.js +59 -0
- package/src/commands/warehouse.js +73 -0
- package/src/utils/paths.js +13 -13
- package/templates/blocks/DD-Accordion/Component.tsx +229 -0
- package/templates/blocks/DD-Accordion/README.md +3 -0
- package/templates/blocks/DD-Accordion/config.ts +73 -0
- package/templates/blocks/DD-Accordion/manifest.json +75 -0
- package/templates/blocks/DD-AnimText/Component.tsx +788 -0
- package/templates/blocks/DD-AnimText/README.md +3 -0
- package/templates/blocks/DD-AnimText/config.ts +295 -0
- package/templates/blocks/DD-AnimText/manifest.json +85 -0
- package/templates/blocks/DD-AsideNav/Component.tsx +321 -0
- package/templates/blocks/DD-AsideNav/config.ts +204 -0
- package/templates/blocks/DD-AsideNav/manifest.json +80 -0
- package/templates/blocks/DD-Banner/Component.tsx +51 -0
- package/templates/blocks/DD-Banner/config.ts +57 -0
- package/templates/blocks/DD-Banner/manifest.json +72 -0
- package/templates/blocks/DD-BgLightning/Component.tsx +44 -0
- package/templates/blocks/DD-BgLightning/README.md +3 -0
- package/templates/blocks/DD-BgLightning/config.ts +97 -0
- package/templates/blocks/DD-BgLightning/manifest.json +79 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -535
- package/templates/blocks/DD-BookCall/config.ts +238 -238
- package/templates/blocks/DD-ButtonPop/Component.tsx +117 -0
- package/templates/blocks/DD-ButtonPop/config.ts +105 -0
- package/templates/blocks/DD-ButtonPop/manifest.json +88 -0
- package/templates/blocks/DD-ButtonPop/svg-icons/Dust2.tsx +167 -0
- package/templates/blocks/DD-Card/Component.tsx +251 -0
- package/templates/blocks/DD-Card/README.md +4 -0
- package/templates/blocks/DD-Card/config.ts +154 -0
- package/templates/blocks/DD-Card/manifest.json +82 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -140
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -106
- package/templates/blocks/DD-Carousel-A/Component.tsx +249 -249
- package/templates/blocks/DD-Carousel-B/Component.tsx +403 -403
- package/templates/blocks/DD-Chip/Component.tsx +250 -0
- package/templates/blocks/DD-Chip/EXAMPLES.md +313 -0
- package/templates/blocks/DD-Chip/README.md +202 -0
- package/templates/blocks/DD-Chip/config.ts +369 -0
- package/templates/blocks/DD-Chip/manifest.json +74 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -256
- package/templates/blocks/DD-Contact/Component.tsx +435 -434
- package/templates/blocks/DD-Contact/config.ts +22 -22
- package/templates/blocks/DD-Counter/Component.tsx +190 -0
- package/templates/blocks/DD-Counter/README.md +4 -0
- package/templates/blocks/DD-Counter/config.ts +291 -0
- package/templates/blocks/DD-Counter/manifest.json +74 -0
- package/templates/blocks/DD-CustomCSS/Component.tsx +8 -0
- package/templates/blocks/DD-CustomCSS/README.md +4 -0
- package/templates/blocks/DD-CustomCSS/addToLayout.md +5 -0
- package/templates/blocks/DD-CustomCSS/config.ts +22 -0
- package/templates/blocks/DD-CustomCSS/manifest.json +72 -0
- package/templates/blocks/DD-CustomCSS/renderCustomCSS.md +7 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
- package/templates/blocks/DD-FeatCat/config.ts +201 -201
- package/templates/blocks/DD-FeatStrip/Component.tsx +585 -585
- package/templates/blocks/DD-FeatStrip/config.ts +134 -134
- package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +628 -0
- package/templates/blocks/DD-FluidBtn/Component.tsx +43 -0
- package/templates/blocks/DD-FluidBtn/README.md +6 -0
- package/templates/blocks/DD-FluidBtn/config.ts +102 -0
- package/templates/blocks/DD-FluidBtn/manifest.json +86 -0
- package/templates/blocks/DD-GooeyInfo/Component.tsx +149 -0
- package/templates/blocks/DD-GooeyInfo/config.ts +135 -0
- package/templates/blocks/DD-GooeyInfo/manifest.json +72 -0
- package/templates/blocks/DD-Hero/Component.tsx +297 -297
- package/templates/blocks/DD-Hero/config.ts +267 -267
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -936
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -231
- package/templates/blocks/DD-Icon/Component.tsx +287 -0
- package/templates/blocks/DD-Icon/README.md +4 -0
- package/templates/blocks/DD-Icon/config.ts +258 -0
- package/templates/blocks/DD-Icon/hooks/revalidateBlockIds.ts +66 -0
- package/templates/blocks/DD-Icon/manifest.json +82 -0
- package/templates/blocks/DD-IconButton/Component.tsx +455 -0
- package/templates/blocks/DD-IconButton/README.md +326 -0
- package/templates/blocks/DD-IconButton/config.ts +239 -0
- package/templates/blocks/DD-IconButton/manifest.json +85 -0
- package/templates/blocks/DD-IdTag/Component.tsx +21 -0
- package/templates/blocks/DD-IdTag/README.md +4 -0
- package/templates/blocks/DD-IdTag/config.ts +41 -0
- package/templates/blocks/DD-IdTag/manifest.json +72 -0
- package/templates/blocks/DD-Iframe/Component.tsx +45 -0
- package/templates/blocks/DD-Iframe/README.md +4 -0
- package/templates/blocks/DD-Iframe/config.ts +47 -0
- package/templates/blocks/DD-Iframe/manifest.json +72 -0
- package/templates/blocks/DD-IframeMedia/Component.tsx +133 -0
- package/templates/blocks/DD-IframeMedia/README.md +4 -0
- package/templates/blocks/DD-IframeMedia/config.ts +79 -0
- package/templates/blocks/DD-IframeMedia/manifest.json +72 -0
- package/templates/blocks/DD-LineAnim/Component.tsx +36 -0
- package/templates/blocks/DD-LineAnim/README.md +4 -0
- package/templates/blocks/DD-LineAnim/config.ts +90 -0
- package/templates/blocks/DD-LineAnim/manifest.json +80 -0
- package/templates/blocks/DD-LinkGrid/Component.tsx +119 -0
- package/templates/blocks/DD-LinkGrid/README.md +4 -0
- package/templates/blocks/DD-LinkGrid/config.ts +51 -0
- package/templates/blocks/DD-LinkGrid/manifest.json +74 -0
- package/templates/blocks/DD-LogoMarq/Component.tsx +132 -0
- package/templates/blocks/DD-LogoMarq/README.md +4 -0
- package/templates/blocks/DD-LogoMarq/config.ts +264 -0
- package/templates/blocks/DD-LogoMarq/manifest.json +82 -0
- package/templates/blocks/DD-Lottie/Component.tsx +459 -0
- package/templates/blocks/DD-Lottie/README.md +4 -0
- package/templates/blocks/DD-Lottie/config.ts +379 -0
- package/templates/blocks/DD-Lottie/index.tsx +28 -0
- package/templates/blocks/DD-Lottie/manifest.json +83 -0
- package/templates/blocks/DD-MagicBento/Component.tsx +777 -0
- package/templates/blocks/DD-MagicBento/README.md +4 -0
- package/templates/blocks/DD-MagicBento/config.ts +264 -0
- package/templates/blocks/DD-MagicBento/manifest.json +74 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +545 -545
- package/templates/blocks/DD-MasonaryMedia/config.ts +2 -2
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -2
- package/templates/blocks/DD-Masonry/Component.tsx +645 -0
- package/templates/blocks/DD-Masonry/README.md +4 -0
- package/templates/blocks/DD-Masonry/config.ts +475 -0
- package/templates/blocks/DD-Masonry/manifest.json +82 -0
- package/templates/blocks/DD-Pricing/Component.tsx +2 -5
- package/templates/blocks/DD-Process/Component.tsx +2 -4
- package/templates/blocks/DD-Process/config.ts +150 -150
- package/templates/blocks/DD-Progress/Component.tsx +156 -0
- package/templates/blocks/DD-Progress/README.md +4 -0
- package/templates/blocks/DD-Progress/config.ts +175 -0
- package/templates/blocks/DD-Progress/manifest.json +74 -0
- package/templates/blocks/DD-ProjHeader/Component.tsx +91 -0
- package/templates/blocks/DD-ProjHeader/README.md +4 -0
- package/templates/blocks/DD-ProjHeader/config.ts +96 -0
- package/templates/blocks/DD-ProjHeader/manifest.json +72 -0
- package/templates/blocks/DD-ProjSlider/Component.tsx +123 -0
- package/templates/blocks/DD-ProjSlider/README.md +4 -0
- package/templates/blocks/DD-ProjSlider/config.ts +64 -0
- package/templates/blocks/DD-ProjSlider/manifest.json +72 -0
- package/templates/blocks/DD-ScrollText/Component.tsx +114 -0
- package/templates/blocks/DD-ScrollText/README.md +4 -0
- package/templates/blocks/DD-ScrollText/config.ts +58 -0
- package/templates/blocks/DD-ScrollText/manifest.json +72 -0
- package/templates/blocks/DD-Section/Component.tsx +266 -266
- package/templates/blocks/DD-Separator/Component.tsx +66 -0
- package/templates/blocks/DD-Separator/README.md +4 -0
- package/templates/blocks/DD-Separator/config.ts +58 -0
- package/templates/blocks/DD-Separator/manifest.json +72 -0
- package/templates/blocks/DD-Services/Component.tsx +176 -176
- package/templates/blocks/DD-Services/config.ts +149 -149
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -307
- package/templates/blocks/DD-Slider-A/Component.tsx +1 -1
- package/templates/blocks/DD-StackCards/Component.tsx +1 -1
- package/templates/blocks/DD-StackCards/config.ts +399 -399
- package/templates/blocks/DD-Team/Component.tsx +247 -247
- package/templates/blocks/DD-Team/config.ts +173 -173
- package/templates/blocks/DD-TeamCards/Component.tsx +251 -0
- package/templates/blocks/DD-TeamCards/README.md +4 -0
- package/templates/blocks/DD-TeamCards/config.ts +302 -0
- package/templates/blocks/DD-TeamCards/manifest.json +74 -0
- package/templates/blocks/DD-TechStack/Component.tsx +174 -174
- package/templates/blocks/DD-TechStack/config.ts +153 -153
- package/templates/blocks/DD-TestMarq/Component.tsx +132 -0
- package/templates/blocks/DD-TestMarq/README.md +4 -0
- package/templates/blocks/DD-TestMarq/RowLabel.tsx +9 -0
- package/templates/blocks/DD-TestMarq/config.ts +88 -0
- package/templates/blocks/DD-TestMarq/manifest.json +72 -0
- package/templates/blocks/DD-Testim/Component.tsx +247 -0
- package/templates/blocks/DD-Testim/README.md +4 -0
- package/templates/blocks/DD-Testim/config.ts +101 -0
- package/templates/blocks/DD-Testim/manifest.json +75 -0
- package/templates/blocks/DD-Testim2/Component.tsx +256 -0
- package/templates/blocks/DD-Testim2/README.md +4 -0
- package/templates/blocks/DD-Testim2/config.ts +100 -0
- package/templates/blocks/DD-Testim2/manifest.json +74 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
- package/templates/blocks/DD-Testimonials/config.ts +120 -120
- package/templates/blocks/DD-TextMarq/Component.tsx +157 -0
- package/templates/blocks/DD-TextMarq/README.md +4 -0
- package/templates/blocks/DD-TextMarq/config.ts +237 -0
- package/templates/blocks/DD-TextMarq/manifest.json +82 -0
- package/templates/blocks/DD-Thanks/Component.tsx +501 -0
- package/templates/blocks/DD-Thanks/README.md +4 -0
- package/templates/blocks/DD-Thanks/config.ts +224 -0
- package/templates/blocks/DD-Thanks/manifest.json +72 -0
- package/templates/blocks/DD-VertMarq/Component.tsx +154 -0
- package/templates/blocks/DD-VertMarq/README.md +4 -0
- package/templates/blocks/DD-VertMarq/config.ts +236 -0
- package/templates/blocks/DD-VertMarq/dist/index.css +6 -0
- package/templates/blocks/DD-VertMarq/dist/index.min.css +1 -0
- package/templates/blocks/DD-VertMarq/manifest.json +74 -0
- package/templates/blocks/DD-Work/Component.tsx +318 -315
- package/templates/blocks/DD-Work/config.ts +165 -165
- package/templates/blocks/DD-Work-B/Component.tsx +294 -294
- package/templates/blocks/DDHG-CTASection2/Component.tsx +185 -0
- package/templates/blocks/DDHG-CTASection2/config.ts +140 -0
- package/templates/blocks/DDHG-CTASection2/manifest.json +45 -0
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -0
- package/templates/blocks/DDHG-HighlightArc/config.ts +85 -0
- package/templates/blocks/DDHG-HighlightArc/manifest.json +46 -0
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -0
- package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -0
- package/templates/blocks/DDHG-LogoMarquee/manifest.json +39 -0
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -0
- package/templates/blocks/DDHG-NavCodeGrid/config.ts +64 -0
- package/templates/blocks/DDHG-NavCodeGrid/manifest.json +45 -0
- package/templates/blocks/DDHG-ProcessSec/Component.tsx +574 -0
- package/templates/blocks/DDHG-ProcessSec/config.ts +170 -0
- package/templates/blocks/DDHG-ProcessSec/manifest.json +56 -0
- package/templates/blocks/DDHG-ServiceDetails/Component.tsx +164 -0
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +390 -0
- package/templates/blocks/DDHG-ServiceDetails/config.ts +77 -0
- package/templates/blocks/DDHG-ServiceDetails/manifest.json +50 -0
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +234 -0
- package/templates/blocks/DDHG-ServiceHero/config.ts +49 -0
- package/templates/blocks/DDHG-ServiceHero/manifest.json +26 -0
- package/templates/blocks/DDHG-ServiceOverview/Component.tsx +277 -0
- package/templates/blocks/DDHG-ServiceOverview/config.ts +64 -0
- package/templates/blocks/DDHG-ServiceOverview/manifest.json +44 -0
- package/templates/blocks/DDHG-StatsFAQ/Component.tsx +389 -0
- package/templates/blocks/DDHG-StatsFAQ/config.ts +204 -0
- package/templates/blocks/DDHG-StatsFAQ/manifest.json +45 -0
- package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -0
- package/templates/blocks/DDHG-TeamCards/config.ts +48 -0
- package/templates/blocks/DDHG-TeamCards/manifest.json +29 -0
- package/templates/shared/src/components/BgLightning/index.tsx +242 -0
- package/templates/shared/src/components/ui/ShareButton/index.tsx +80 -0
- package/templates/shared/src/components/ui/anims/FadeOnScroll.tsx +48 -0
- package/templates/shared/src/components/ui/anims/FadeUpOnInView.tsx +63 -0
- package/templates/shared/src/components/ui/anims/GlareHover.tsx +111 -0
- package/templates/shared/src/components/ui/anims/LineAnims.tsx +81 -0
- package/templates/shared/src/components/ui/anims/TitleAnims01.tsx +54 -0
- package/templates/shared/src/components/ui/anims/animation-container.tsx +29 -0
- package/templates/shared/src/components/ui/anims/blur-in.tsx +26 -0
- package/templates/shared/src/components/ui/anims/counter.tsx +53 -0
- package/templates/shared/src/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/src/components/ui/anims/gradual-spacing.tsx +39 -0
- package/templates/shared/src/components/ui/anims/pull-up.tsx +93 -0
- package/templates/shared/src/components/ui/anims/reveal-text.tsx +76 -0
- package/templates/shared/src/components/ui/anims/rotate-words.tsx +43 -0
- package/templates/shared/src/components/ui/anims/shiny-text.tsx +58 -0
- package/templates/shared/src/components/ui/anims/shuffle-text.tsx +85 -0
- package/templates/shared/src/components/ui/anims/split-text.tsx +211 -0
- package/templates/shared/src/components/ui/anims/staggered-fade.tsx +133 -0
- package/templates/shared/src/components/ui/anims/text-scroll.tsx +111 -0
- package/templates/shared/src/components/ui/anims/typing-effect.tsx +112 -0
- package/templates/shared/src/components/ui/button-dusted-fluid/index.tsx +1521 -0
- package/templates/shared/src/components/ui/separator.tsx +85 -0
- package/templates/shared/src/hooks/useBodyScrollLock.ts +97 -0
- package/templates/shared/src/hooks/useClickOutside.ts +28 -0
- package/templates/shared/src/utilities/renderCustomCSS.ts +5 -0
- 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
|
+
}
|