fancy-ui-svelte 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1132 -1111
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +3 -5
- package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +29 -0
- package/dist/fancy-ui/fluid-cursor/README.md +57 -6
- package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +40 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +29 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +847 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +5 -3
- package/dist/fancy-ui/frosted-glass/README.md +4 -2
- package/dist/fancy-ui/index.d.ts +1 -0
- package/dist/fancy-ui/index.js +1 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
- package/dist/fancy-ui/liquid-glass/README.md +7 -1
- package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
- package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
- package/dist/fancy-ui/liquid-text/README.md +139 -0
- package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
- package/dist/fancy-ui/liquid-text/index.js +1 -0
- package/dist/fancy-ui/registry.js +124 -0
- package/package.json +4 -3
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from "../../utils.js";
|
|
3
3
|
import { onMount } from "svelte";
|
|
4
|
+
import {
|
|
5
|
+
type ColorRGB,
|
|
6
|
+
type Pointer,
|
|
7
|
+
pointerPrototype,
|
|
8
|
+
hexToRgb,
|
|
9
|
+
HSVtoRGB,
|
|
10
|
+
scaleRadiusForContainer,
|
|
11
|
+
wrap,
|
|
12
|
+
} from "./fluid-shared.js";
|
|
13
|
+
import { startWebGpuFluid } from "./webgpu-engine.js";
|
|
4
14
|
|
|
5
15
|
// Module-level singleton registry
|
|
6
16
|
let activeInstance: (() => void) | null = null;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
g: number;
|
|
11
|
-
b: number;
|
|
12
|
-
}
|
|
17
|
+
// Monotonic mount counter so async (WebGPU) startups that finish out of
|
|
18
|
+
// order cannot let an older instance destroy a newer one.
|
|
19
|
+
let mountCounter = 0;
|
|
13
20
|
|
|
14
21
|
interface Props {
|
|
15
22
|
simResolution?: number;
|
|
@@ -37,6 +44,8 @@
|
|
|
37
44
|
splatOnMount?: boolean;
|
|
38
45
|
allowMultiple?: boolean;
|
|
39
46
|
contained?: boolean;
|
|
47
|
+
hdr?: boolean;
|
|
48
|
+
hdrBoost?: number;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
let {
|
|
@@ -65,63 +74,95 @@
|
|
|
65
74
|
splatOnMount = false,
|
|
66
75
|
allowMultiple = false,
|
|
67
76
|
contained = true,
|
|
77
|
+
hdr = false,
|
|
78
|
+
hdrBoost = 1.5,
|
|
68
79
|
}: Props = $props();
|
|
69
80
|
|
|
70
|
-
function hexToRgb(hex: string): ColorRGB {
|
|
71
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
72
|
-
if (!result) {
|
|
73
|
-
console.warn(
|
|
74
|
-
`[FluidCursor] Invalid hex color: "${hex}". Expected format: "#rrggbb". Falling back to white.`
|
|
75
|
-
);
|
|
76
|
-
return { r: 1, g: 1, b: 1 };
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
r: parseInt(result[1], 16) / 255,
|
|
80
|
-
g: parseInt(result[2], 16) / 255,
|
|
81
|
-
b: parseInt(result[3], 16) / 255,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
81
|
const clampedColorIntensity = Math.max(0, Math.min(1, colorIntensity));
|
|
82
|
+
const clampedHdrBoost = Math.max(1, Math.min(4, hdrBoost));
|
|
86
83
|
|
|
87
84
|
const resolvedBackColor: ColorRGB =
|
|
88
85
|
typeof backColor === "string" ? hexToRgb(backColor) : backColor;
|
|
89
86
|
|
|
90
87
|
let canvasRef: HTMLCanvasElement;
|
|
91
88
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// Cached pre-scaled colors to avoid re-parsing hex on every splat
|
|
90
|
+
let colorIndex = 0;
|
|
91
|
+
let cachedFluidColorHex: string | undefined;
|
|
92
|
+
let cachedFluidColor: ColorRGB | null = null;
|
|
93
|
+
let cachedFluidColorsRef: string[] | undefined;
|
|
94
|
+
let cachedFluidColorsScaled: ColorRGB[] = [];
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
function getScaledColor(hex: string): ColorRGB {
|
|
97
|
+
const { r, g, b } = hexToRgb(hex);
|
|
98
|
+
return {
|
|
99
|
+
r: r * clampedColorIntensity,
|
|
100
|
+
g: g * clampedColorIntensity,
|
|
101
|
+
b: b * clampedColorIntensity,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getCachedFluidColor(hex: string): ColorRGB {
|
|
106
|
+
if (cachedFluidColor === null || cachedFluidColorHex !== hex) {
|
|
107
|
+
cachedFluidColorHex = hex;
|
|
108
|
+
cachedFluidColor = getScaledColor(hex);
|
|
109
|
+
}
|
|
110
|
+
return cachedFluidColor;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getCachedFluidColors(colors: string[]): ColorRGB[] {
|
|
114
|
+
if (cachedFluidColorsRef !== colors) {
|
|
115
|
+
cachedFluidColorsRef = colors;
|
|
116
|
+
cachedFluidColorsScaled = colors.map(getScaledColor);
|
|
107
117
|
}
|
|
118
|
+
return cachedFluidColorsScaled;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateColor(): ColorRGB {
|
|
122
|
+
if (fluidColor) {
|
|
123
|
+
return getCachedFluidColor(fluidColor);
|
|
124
|
+
}
|
|
125
|
+
if (fluidColors && fluidColors.length > 0) {
|
|
126
|
+
const colors = getCachedFluidColors(fluidColors);
|
|
127
|
+
const idx = colorIndex % colors.length;
|
|
128
|
+
colorIndex++;
|
|
129
|
+
return colors[idx];
|
|
130
|
+
}
|
|
131
|
+
const c = HSVtoRGB(Math.random(), 1.0, 1.0);
|
|
132
|
+
c.r *= clampedColorIntensity;
|
|
133
|
+
c.g *= clampedColorIntensity;
|
|
134
|
+
c.b *= clampedColorIntensity;
|
|
135
|
+
return c;
|
|
136
|
+
}
|
|
108
137
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
// Singleton wiring shared by the WebGL and WebGPU paths.
|
|
139
|
+
function registerInstance(cleanup: () => void): () => void {
|
|
140
|
+
if (!allowMultiple) {
|
|
141
|
+
if (activeInstance) {
|
|
142
|
+
if (import.meta.env.DEV) {
|
|
143
|
+
console.warn(
|
|
144
|
+
"[FluidCursor] Destroying previous instance. Only one instance is allowed by default. Use `allowMultiple={true}` to opt out of singleton behavior."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
activeInstance();
|
|
148
|
+
}
|
|
149
|
+
activeInstance = cleanup;
|
|
150
|
+
return () => {
|
|
151
|
+
cleanup();
|
|
152
|
+
if (activeInstance === cleanup) {
|
|
153
|
+
activeInstance = null;
|
|
154
|
+
}
|
|
121
155
|
};
|
|
122
156
|
}
|
|
157
|
+
return cleanup;
|
|
158
|
+
}
|
|
123
159
|
|
|
124
|
-
|
|
160
|
+
onMount(() => {
|
|
161
|
+
const canvas = canvasRef;
|
|
162
|
+
if (!canvas) return;
|
|
163
|
+
|
|
164
|
+
const mountToken = ++mountCounter;
|
|
165
|
+
const pointers = [pointerPrototype()];
|
|
125
166
|
|
|
126
167
|
const config = {
|
|
127
168
|
SIM_RESOLUTION: simResolution,
|
|
@@ -141,270 +182,346 @@
|
|
|
141
182
|
TRANSPARENT: transparent,
|
|
142
183
|
};
|
|
143
184
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
// HDR path: WebGPU engine (rgba16float backbuffer, display-p3,
|
|
186
|
+
// extended tone mapping). Falls back to the WebGL path below when
|
|
187
|
+
// WebGPU is unavailable or initialization fails.
|
|
188
|
+
if (hdr && typeof navigator !== "undefined" && "gpu" in navigator) {
|
|
189
|
+
let disposed = false;
|
|
190
|
+
let activeCleanup: (() => void) | null = null;
|
|
191
|
+
startWebGpuFluid(canvas, {
|
|
192
|
+
simResolution: config.SIM_RESOLUTION,
|
|
193
|
+
dyeResolution: config.DYE_RESOLUTION,
|
|
194
|
+
densityDissipation: config.DENSITY_DISSIPATION,
|
|
195
|
+
velocityDissipation: config.VELOCITY_DISSIPATION,
|
|
196
|
+
pressure: config.PRESSURE,
|
|
197
|
+
pressureIterations: config.PRESSURE_ITERATIONS,
|
|
198
|
+
curl: config.CURL,
|
|
199
|
+
splatRadius: config.SPLAT_RADIUS,
|
|
200
|
+
splatForce: config.SPLAT_FORCE,
|
|
201
|
+
shading: config.SHADING,
|
|
202
|
+
colorUpdateSpeed: config.COLOR_UPDATE_SPEED,
|
|
203
|
+
exposure: clampedHdrBoost,
|
|
204
|
+
contained,
|
|
205
|
+
interactive,
|
|
206
|
+
autoSplat,
|
|
207
|
+
autoSplatInterval,
|
|
208
|
+
pauseWhenHidden,
|
|
209
|
+
splatOnMount,
|
|
210
|
+
generateColor,
|
|
211
|
+
})
|
|
212
|
+
.then((cleanup) => {
|
|
213
|
+
if (disposed) {
|
|
214
|
+
cleanup?.();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// A newer singleton instance mounted while we were starting up:
|
|
218
|
+
// registering now would destroy it (newest-mount-wins would
|
|
219
|
+
// invert). Discard this stale completion instead.
|
|
220
|
+
if (!allowMultiple && mountToken !== mountCounter) {
|
|
221
|
+
cleanup?.();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!cleanup) {
|
|
225
|
+
activeCleanup = startWebGl() ?? null;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
activeCleanup = registerInstance(cleanup);
|
|
229
|
+
})
|
|
230
|
+
.catch((error) => {
|
|
231
|
+
// Unexpected failure while wiring the WebGPU path: fall back
|
|
232
|
+
// to WebGL instead of leaving a permanently blank canvas.
|
|
233
|
+
if (import.meta.env.DEV) {
|
|
234
|
+
console.warn("[FluidCursor] WebGPU setup failed, falling back to WebGL:", error);
|
|
235
|
+
}
|
|
236
|
+
if (!disposed) activeCleanup = startWebGl() ?? null;
|
|
237
|
+
});
|
|
238
|
+
return () => {
|
|
239
|
+
disposed = true;
|
|
240
|
+
activeCleanup?.();
|
|
241
|
+
};
|
|
156
242
|
}
|
|
157
243
|
|
|
158
|
-
|
|
159
|
-
const params = {
|
|
160
|
-
alpha: true,
|
|
161
|
-
depth: false,
|
|
162
|
-
stencil: false,
|
|
163
|
-
antialias: false,
|
|
164
|
-
preserveDrawingBuffer: false,
|
|
165
|
-
};
|
|
244
|
+
return startWebGl();
|
|
166
245
|
|
|
167
|
-
|
|
246
|
+
// WebGL engine path (default, and fallback when WebGPU is missing).
|
|
247
|
+
function startWebGl(): (() => void) | undefined {
|
|
248
|
+
// Get WebGL context
|
|
249
|
+
const context = getWebGLContext(canvas);
|
|
250
|
+
if (!context.gl || !context.ext) return;
|
|
168
251
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
252
|
+
// These are guaranteed non-null after the check above
|
|
253
|
+
const gl = context.gl;
|
|
254
|
+
const ext = context.ext;
|
|
173
255
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
256
|
+
// If no linear filtering, reduce resolution
|
|
257
|
+
if (!ext.supportLinearFiltering) {
|
|
258
|
+
config.DYE_RESOLUTION = 256;
|
|
259
|
+
config.SHADING = false;
|
|
177
260
|
}
|
|
178
261
|
|
|
179
|
-
|
|
262
|
+
function getWebGLContext(canvas: HTMLCanvasElement) {
|
|
263
|
+
const params = {
|
|
264
|
+
alpha: true,
|
|
265
|
+
depth: false,
|
|
266
|
+
stencil: false,
|
|
267
|
+
antialias: false,
|
|
268
|
+
preserveDrawingBuffer: false,
|
|
269
|
+
};
|
|
180
270
|
|
|
181
|
-
|
|
182
|
-
let halfFloat = null;
|
|
271
|
+
let gl = canvas.getContext("webgl2", params) as WebGL2RenderingContext | null;
|
|
183
272
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
);
|
|
189
|
-
} else {
|
|
190
|
-
halfFloat = gl.getExtension("OES_texture_half_float");
|
|
191
|
-
supportLinearFiltering = !!gl.getExtension("OES_texture_half_float_linear");
|
|
192
|
-
}
|
|
273
|
+
if (!gl) {
|
|
274
|
+
gl = (canvas.getContext("webgl", params) ||
|
|
275
|
+
canvas.getContext("experimental-webgl", params)) as WebGL2RenderingContext | null;
|
|
276
|
+
}
|
|
193
277
|
|
|
194
|
-
|
|
278
|
+
if (!gl) {
|
|
279
|
+
console.error("Unable to initialize WebGL.");
|
|
280
|
+
return { gl: null, ext: null };
|
|
281
|
+
}
|
|
195
282
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
283
|
+
// HDR fallback: wide-gamut backbuffer when the WebGPU engine is
|
|
284
|
+
// unavailable. Colors are reinterpreted in P3 (more saturated).
|
|
285
|
+
if (hdr && "drawingBufferColorSpace" in gl) {
|
|
286
|
+
try {
|
|
287
|
+
gl.drawingBufferColorSpace = "display-p3";
|
|
288
|
+
if (import.meta.env.DEV) {
|
|
289
|
+
console.info("[FluidCursor] HDR level: webgl-p3 (wide gamut fallback)");
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Unsupported color space: buffer stays sRGB.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
199
295
|
|
|
200
|
-
|
|
201
|
-
let formatRG: { internalFormat: number; format: number } | null;
|
|
202
|
-
let formatR: { internalFormat: number; format: number } | null;
|
|
296
|
+
const isWebGL2 = "drawBuffers" in gl;
|
|
203
297
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
gl,
|
|
207
|
-
(gl as WebGL2RenderingContext).RGBA16F,
|
|
208
|
-
gl.RGBA,
|
|
209
|
-
halfFloatTexType
|
|
210
|
-
);
|
|
211
|
-
formatRG = getSupportedFormat(
|
|
212
|
-
gl,
|
|
213
|
-
(gl as WebGL2RenderingContext).RG16F,
|
|
214
|
-
(gl as WebGL2RenderingContext).RG,
|
|
215
|
-
halfFloatTexType
|
|
216
|
-
);
|
|
217
|
-
formatR = getSupportedFormat(
|
|
218
|
-
gl,
|
|
219
|
-
(gl as WebGL2RenderingContext).R16F,
|
|
220
|
-
(gl as WebGL2RenderingContext).RED,
|
|
221
|
-
halfFloatTexType
|
|
222
|
-
);
|
|
223
|
-
} else {
|
|
224
|
-
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
225
|
-
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
226
|
-
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
gl,
|
|
231
|
-
ext: {
|
|
232
|
-
formatRGBA,
|
|
233
|
-
formatRG,
|
|
234
|
-
formatR,
|
|
235
|
-
halfFloatTexType,
|
|
236
|
-
supportLinearFiltering,
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
298
|
+
let supportLinearFiltering = false;
|
|
299
|
+
let halfFloat = null;
|
|
240
300
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const gl2 = gl as WebGL2RenderingContext;
|
|
250
|
-
switch (internalFormat) {
|
|
251
|
-
case gl2.R16F:
|
|
252
|
-
return getSupportedFormat(gl2, gl2.RG16F, gl2.RG, type);
|
|
253
|
-
case gl2.RG16F:
|
|
254
|
-
return getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, type);
|
|
255
|
-
default:
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
301
|
+
if (isWebGL2) {
|
|
302
|
+
(gl as WebGL2RenderingContext).getExtension("EXT_color_buffer_float");
|
|
303
|
+
supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
|
|
304
|
+
"OES_texture_float_linear"
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
halfFloat = gl.getExtension("OES_texture_half_float");
|
|
308
|
+
supportLinearFiltering = !!gl.getExtension("OES_texture_half_float_linear");
|
|
258
309
|
}
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
return { internalFormat, format };
|
|
262
|
-
}
|
|
263
310
|
|
|
264
|
-
|
|
265
|
-
gl: WebGLRenderingContext | WebGL2RenderingContext,
|
|
266
|
-
internalFormat: number,
|
|
267
|
-
format: number,
|
|
268
|
-
type: number
|
|
269
|
-
) {
|
|
270
|
-
const texture = gl.createTexture();
|
|
271
|
-
if (!texture) return false;
|
|
272
|
-
|
|
273
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
274
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
275
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
276
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
277
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
278
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
|
|
279
|
-
|
|
280
|
-
const fbo = gl.createFramebuffer();
|
|
281
|
-
if (!fbo) return false;
|
|
282
|
-
|
|
283
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
284
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
285
|
-
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
286
|
-
return status === gl.FRAMEBUFFER_COMPLETE;
|
|
287
|
-
}
|
|
311
|
+
gl.clearColor(0, 0, 0, 1);
|
|
288
312
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
for (let i = 0; i < s.length; i++) {
|
|
293
|
-
hash = (hash << 5) - hash + s.charCodeAt(i);
|
|
294
|
-
hash |= 0;
|
|
295
|
-
}
|
|
296
|
-
return hash;
|
|
297
|
-
}
|
|
313
|
+
const halfFloatTexType = isWebGL2
|
|
314
|
+
? (gl as WebGL2RenderingContext).HALF_FLOAT
|
|
315
|
+
: (halfFloat && halfFloat.HALF_FLOAT_OES) || 0;
|
|
298
316
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
for (const keyword of keywords) {
|
|
303
|
-
keywordsString += `#define ${keyword}\n`;
|
|
304
|
-
}
|
|
305
|
-
return keywordsString + source;
|
|
306
|
-
}
|
|
317
|
+
let formatRGBA: { internalFormat: number; format: number } | null;
|
|
318
|
+
let formatRG: { internalFormat: number; format: number } | null;
|
|
319
|
+
let formatR: { internalFormat: number; format: number } | null;
|
|
307
320
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
if (isWebGL2) {
|
|
322
|
+
formatRGBA = getSupportedFormat(
|
|
323
|
+
gl,
|
|
324
|
+
(gl as WebGL2RenderingContext).RGBA16F,
|
|
325
|
+
gl.RGBA,
|
|
326
|
+
halfFloatTexType
|
|
327
|
+
);
|
|
328
|
+
formatRG = getSupportedFormat(
|
|
329
|
+
gl,
|
|
330
|
+
(gl as WebGL2RenderingContext).RG16F,
|
|
331
|
+
(gl as WebGL2RenderingContext).RG,
|
|
332
|
+
halfFloatTexType
|
|
333
|
+
);
|
|
334
|
+
formatR = getSupportedFormat(
|
|
335
|
+
gl,
|
|
336
|
+
(gl as WebGL2RenderingContext).R16F,
|
|
337
|
+
(gl as WebGL2RenderingContext).RED,
|
|
338
|
+
halfFloatTexType
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
342
|
+
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
343
|
+
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
|
|
344
|
+
}
|
|
320
345
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
346
|
+
return {
|
|
347
|
+
gl,
|
|
348
|
+
ext: {
|
|
349
|
+
formatRGBA,
|
|
350
|
+
formatRG,
|
|
351
|
+
formatR,
|
|
352
|
+
halfFloatTexType,
|
|
353
|
+
supportLinearFiltering,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
333
357
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
358
|
+
function getSupportedFormat(
|
|
359
|
+
gl: WebGLRenderingContext | WebGL2RenderingContext,
|
|
360
|
+
internalFormat: number,
|
|
361
|
+
format: number,
|
|
362
|
+
type: number
|
|
363
|
+
): { internalFormat: number; format: number } | null {
|
|
364
|
+
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
|
|
365
|
+
if ("drawBuffers" in gl) {
|
|
366
|
+
const gl2 = gl as WebGL2RenderingContext;
|
|
367
|
+
switch (internalFormat) {
|
|
368
|
+
case gl2.R16F:
|
|
369
|
+
return getSupportedFormat(gl2, gl2.RG16F, gl2.RG, type);
|
|
370
|
+
case gl2.RG16F:
|
|
371
|
+
return getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, type);
|
|
372
|
+
default:
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
341
377
|
}
|
|
378
|
+
return { internalFormat, format };
|
|
342
379
|
}
|
|
343
|
-
return uniforms;
|
|
344
|
-
}
|
|
345
380
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
381
|
+
function supportRenderTextureFormat(
|
|
382
|
+
gl: WebGLRenderingContext | WebGL2RenderingContext,
|
|
383
|
+
internalFormat: number,
|
|
384
|
+
format: number,
|
|
385
|
+
type: number
|
|
386
|
+
) {
|
|
387
|
+
const texture = gl.createTexture();
|
|
388
|
+
if (!texture) return false;
|
|
349
389
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
390
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
391
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
392
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
393
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
394
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
395
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
|
|
396
|
+
|
|
397
|
+
const fbo = gl.createFramebuffer();
|
|
398
|
+
if (!fbo) return false;
|
|
354
399
|
|
|
355
|
-
|
|
356
|
-
|
|
400
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
401
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
402
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
403
|
+
return status === gl.FRAMEBUFFER_COMPLETE;
|
|
357
404
|
}
|
|
358
|
-
}
|
|
359
405
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
406
|
+
function hashCode(s: string) {
|
|
407
|
+
if (!s.length) return 0;
|
|
408
|
+
let hash = 0;
|
|
409
|
+
for (let i = 0; i < s.length; i++) {
|
|
410
|
+
hash = (hash << 5) - hash + s.charCodeAt(i);
|
|
411
|
+
hash |= 0;
|
|
412
|
+
}
|
|
413
|
+
return hash;
|
|
414
|
+
}
|
|
366
415
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
416
|
+
function addKeywords(source: string, keywords: string[] | null) {
|
|
417
|
+
if (!keywords) return source;
|
|
418
|
+
let keywordsString = "";
|
|
419
|
+
for (const keyword of keywords) {
|
|
420
|
+
keywordsString += `#define ${keyword}\n`;
|
|
421
|
+
}
|
|
422
|
+
return keywordsString + source;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function compileShader(
|
|
426
|
+
type: number,
|
|
427
|
+
source: string,
|
|
428
|
+
keywords: string[] | null = null
|
|
429
|
+
): WebGLShader | null {
|
|
430
|
+
const shaderSource = addKeywords(source, keywords);
|
|
431
|
+
const shader = gl.createShader(type);
|
|
432
|
+
if (!shader) return null;
|
|
433
|
+
gl.shaderSource(shader, shaderSource);
|
|
434
|
+
gl.compileShader(shader);
|
|
435
|
+
return shader;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function createProgram(
|
|
439
|
+
vertexShader: WebGLShader | null,
|
|
440
|
+
fragmentShader: WebGLShader | null
|
|
441
|
+
): WebGLProgram | null {
|
|
442
|
+
if (!vertexShader || !fragmentShader) return null;
|
|
443
|
+
const program = gl.createProgram();
|
|
444
|
+
if (!program) return null;
|
|
445
|
+
gl.attachShader(program, vertexShader);
|
|
446
|
+
gl.attachShader(program, fragmentShader);
|
|
447
|
+
gl.linkProgram(program);
|
|
448
|
+
return program;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getUniforms(program: WebGLProgram) {
|
|
452
|
+
const uniforms: Record<string, WebGLUniformLocation | null> = {};
|
|
453
|
+
const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
|
|
454
|
+
for (let i = 0; i < uniformCount; i++) {
|
|
455
|
+
const uniformInfo = gl.getActiveUniform(program, i);
|
|
456
|
+
if (uniformInfo) {
|
|
457
|
+
uniforms[uniformInfo.name] = gl.getUniformLocation(program, uniformInfo.name);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return uniforms;
|
|
373
461
|
}
|
|
374
462
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const fragmentShader = compileShader(
|
|
383
|
-
gl.FRAGMENT_SHADER,
|
|
384
|
-
this.fragmentShaderSource,
|
|
385
|
-
keywords
|
|
386
|
-
);
|
|
387
|
-
program = createProgram(this.vertexShader, fragmentShader);
|
|
388
|
-
this.programs[hash] = program;
|
|
463
|
+
class Program {
|
|
464
|
+
program: WebGLProgram | null;
|
|
465
|
+
uniforms: Record<string, WebGLUniformLocation | null>;
|
|
466
|
+
|
|
467
|
+
constructor(vertexShader: WebGLShader | null, fragmentShader: WebGLShader | null) {
|
|
468
|
+
this.program = createProgram(vertexShader, fragmentShader);
|
|
469
|
+
this.uniforms = this.program ? getUniforms(this.program) : {};
|
|
389
470
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.
|
|
471
|
+
|
|
472
|
+
bind() {
|
|
473
|
+
if (this.program) gl.useProgram(this.program);
|
|
393
474
|
}
|
|
394
|
-
this.activeProgram = program;
|
|
395
475
|
}
|
|
396
476
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
477
|
+
class Material {
|
|
478
|
+
vertexShader: WebGLShader | null;
|
|
479
|
+
fragmentShaderSource: string;
|
|
480
|
+
programs: Record<number, WebGLProgram | null>;
|
|
481
|
+
activeProgram: WebGLProgram | null;
|
|
482
|
+
uniforms: Record<string, WebGLUniformLocation | null>;
|
|
483
|
+
|
|
484
|
+
constructor(vertexShader: WebGLShader | null, fragmentShaderSource: string) {
|
|
485
|
+
this.vertexShader = vertexShader;
|
|
486
|
+
this.fragmentShaderSource = fragmentShaderSource;
|
|
487
|
+
this.programs = {};
|
|
488
|
+
this.activeProgram = null;
|
|
489
|
+
this.uniforms = {};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
setKeywords(keywords: string[]) {
|
|
493
|
+
let hash = 0;
|
|
494
|
+
for (const kw of keywords) {
|
|
495
|
+
hash += hashCode(kw);
|
|
496
|
+
}
|
|
497
|
+
let program = this.programs[hash];
|
|
498
|
+
if (program == null) {
|
|
499
|
+
const fragmentShader = compileShader(
|
|
500
|
+
gl.FRAGMENT_SHADER,
|
|
501
|
+
this.fragmentShaderSource,
|
|
502
|
+
keywords
|
|
503
|
+
);
|
|
504
|
+
program = createProgram(this.vertexShader, fragmentShader);
|
|
505
|
+
this.programs[hash] = program;
|
|
506
|
+
}
|
|
507
|
+
if (program === this.activeProgram) return;
|
|
508
|
+
if (program) {
|
|
509
|
+
this.uniforms = getUniforms(program);
|
|
510
|
+
}
|
|
511
|
+
this.activeProgram = program;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
bind() {
|
|
515
|
+
if (this.activeProgram) {
|
|
516
|
+
gl.useProgram(this.activeProgram);
|
|
517
|
+
}
|
|
400
518
|
}
|
|
401
519
|
}
|
|
402
|
-
}
|
|
403
520
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
521
|
+
// Shaders
|
|
522
|
+
const baseVertexShader = compileShader(
|
|
523
|
+
gl.VERTEX_SHADER,
|
|
524
|
+
`
|
|
408
525
|
precision highp float;
|
|
409
526
|
attribute vec2 aPosition;
|
|
410
527
|
varying vec2 vUv;
|
|
@@ -423,11 +540,11 @@
|
|
|
423
540
|
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
424
541
|
}
|
|
425
542
|
`
|
|
426
|
-
|
|
543
|
+
);
|
|
427
544
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
545
|
+
const copyShader = compileShader(
|
|
546
|
+
gl.FRAGMENT_SHADER,
|
|
547
|
+
`
|
|
431
548
|
precision mediump float;
|
|
432
549
|
precision mediump sampler2D;
|
|
433
550
|
varying highp vec2 vUv;
|
|
@@ -437,11 +554,11 @@
|
|
|
437
554
|
gl_FragColor = texture2D(uTexture, vUv);
|
|
438
555
|
}
|
|
439
556
|
`
|
|
440
|
-
|
|
557
|
+
);
|
|
441
558
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
559
|
+
const clearShader = compileShader(
|
|
560
|
+
gl.FRAGMENT_SHADER,
|
|
561
|
+
`
|
|
445
562
|
precision mediump float;
|
|
446
563
|
precision mediump sampler2D;
|
|
447
564
|
varying highp vec2 vUv;
|
|
@@ -452,9 +569,9 @@
|
|
|
452
569
|
gl_FragColor = value * texture2D(uTexture, vUv);
|
|
453
570
|
}
|
|
454
571
|
`
|
|
455
|
-
|
|
572
|
+
);
|
|
456
573
|
|
|
457
|
-
|
|
574
|
+
const displayShaderSource = `
|
|
458
575
|
precision highp float;
|
|
459
576
|
precision highp sampler2D;
|
|
460
577
|
varying vec2 vUv;
|
|
@@ -495,9 +612,9 @@
|
|
|
495
612
|
}
|
|
496
613
|
`;
|
|
497
614
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
615
|
+
const splatShader = compileShader(
|
|
616
|
+
gl.FRAGMENT_SHADER,
|
|
617
|
+
`
|
|
501
618
|
precision highp float;
|
|
502
619
|
precision highp sampler2D;
|
|
503
620
|
varying vec2 vUv;
|
|
@@ -515,11 +632,11 @@
|
|
|
515
632
|
gl_FragColor = vec4(base + splat, 1.0);
|
|
516
633
|
}
|
|
517
634
|
`
|
|
518
|
-
|
|
635
|
+
);
|
|
519
636
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
637
|
+
const advectionShader = compileShader(
|
|
638
|
+
gl.FRAGMENT_SHADER,
|
|
639
|
+
`
|
|
523
640
|
precision highp float;
|
|
524
641
|
precision highp sampler2D;
|
|
525
642
|
varying vec2 vUv;
|
|
@@ -555,12 +672,12 @@
|
|
|
555
672
|
gl_FragColor = result / decay;
|
|
556
673
|
}
|
|
557
674
|
`,
|
|
558
|
-
|
|
559
|
-
|
|
675
|
+
ext.supportLinearFiltering ? null : ["MANUAL_FILTERING"]
|
|
676
|
+
);
|
|
560
677
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
678
|
+
const divergenceShader = compileShader(
|
|
679
|
+
gl.FRAGMENT_SHADER,
|
|
680
|
+
`
|
|
564
681
|
precision mediump float;
|
|
565
682
|
precision mediump sampler2D;
|
|
566
683
|
varying highp vec2 vUv;
|
|
@@ -586,11 +703,11 @@
|
|
|
586
703
|
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
|
|
587
704
|
}
|
|
588
705
|
`
|
|
589
|
-
|
|
706
|
+
);
|
|
590
707
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
708
|
+
const curlShader = compileShader(
|
|
709
|
+
gl.FRAGMENT_SHADER,
|
|
710
|
+
`
|
|
594
711
|
precision mediump float;
|
|
595
712
|
precision mediump sampler2D;
|
|
596
713
|
varying highp vec2 vUv;
|
|
@@ -609,11 +726,11 @@
|
|
|
609
726
|
gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
|
|
610
727
|
}
|
|
611
728
|
`
|
|
612
|
-
|
|
729
|
+
);
|
|
613
730
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
731
|
+
const vorticityShader = compileShader(
|
|
732
|
+
gl.FRAGMENT_SHADER,
|
|
733
|
+
`
|
|
617
734
|
precision highp float;
|
|
618
735
|
precision highp sampler2D;
|
|
619
736
|
varying vec2 vUv;
|
|
@@ -644,11 +761,11 @@
|
|
|
644
761
|
gl_FragColor = vec4(velocity, 0.0, 1.0);
|
|
645
762
|
}
|
|
646
763
|
`
|
|
647
|
-
|
|
764
|
+
);
|
|
648
765
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
766
|
+
const pressureShader = compileShader(
|
|
767
|
+
gl.FRAGMENT_SHADER,
|
|
768
|
+
`
|
|
652
769
|
precision mediump float;
|
|
653
770
|
precision mediump sampler2D;
|
|
654
771
|
varying highp vec2 vUv;
|
|
@@ -670,11 +787,11 @@
|
|
|
670
787
|
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
|
|
671
788
|
}
|
|
672
789
|
`
|
|
673
|
-
|
|
790
|
+
);
|
|
674
791
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
792
|
+
const gradientSubtractShader = compileShader(
|
|
793
|
+
gl.FRAGMENT_SHADER,
|
|
794
|
+
`
|
|
678
795
|
precision mediump float;
|
|
679
796
|
precision mediump sampler2D;
|
|
680
797
|
varying highp vec2 vUv;
|
|
@@ -695,874 +812,778 @@
|
|
|
695
812
|
gl_FragColor = vec4(velocity, 0.0, 1.0);
|
|
696
813
|
}
|
|
697
814
|
`
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// Fullscreen Triangles
|
|
701
|
-
interface FBO {
|
|
702
|
-
texture: WebGLTexture;
|
|
703
|
-
fbo: WebGLFramebuffer;
|
|
704
|
-
width: number;
|
|
705
|
-
height: number;
|
|
706
|
-
texelSizeX: number;
|
|
707
|
-
texelSizeY: number;
|
|
708
|
-
attach: (id: number) => number;
|
|
709
|
-
}
|
|
815
|
+
);
|
|
710
816
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
817
|
+
// Fullscreen Triangles
|
|
818
|
+
interface FBO {
|
|
819
|
+
texture: WebGLTexture;
|
|
820
|
+
fbo: WebGLFramebuffer;
|
|
821
|
+
width: number;
|
|
822
|
+
height: number;
|
|
823
|
+
texelSizeX: number;
|
|
824
|
+
texelSizeY: number;
|
|
825
|
+
attach: (id: number) => number;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
interface DoubleFBO {
|
|
829
|
+
width: number;
|
|
830
|
+
height: number;
|
|
831
|
+
texelSizeX: number;
|
|
832
|
+
texelSizeY: number;
|
|
833
|
+
read: FBO;
|
|
834
|
+
write: FBO;
|
|
835
|
+
swap: () => void;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const blit = (() => {
|
|
839
|
+
const buffer = gl.createBuffer()!;
|
|
840
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
841
|
+
gl.bufferData(
|
|
842
|
+
gl.ARRAY_BUFFER,
|
|
843
|
+
new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
|
|
844
|
+
gl.STATIC_DRAW
|
|
845
|
+
);
|
|
846
|
+
const elemBuffer = gl.createBuffer()!;
|
|
847
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elemBuffer);
|
|
848
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
|
|
849
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
850
|
+
gl.enableVertexAttribArray(0);
|
|
851
|
+
|
|
852
|
+
return (target: FBO | null, doClear = false) => {
|
|
853
|
+
if (!gl) return;
|
|
854
|
+
if (!target) {
|
|
855
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
856
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
857
|
+
} else {
|
|
858
|
+
gl.viewport(0, 0, target.width, target.height);
|
|
859
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
860
|
+
}
|
|
861
|
+
if (doClear) {
|
|
862
|
+
gl.clearColor(0, 0, 0, 1);
|
|
863
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
864
|
+
}
|
|
865
|
+
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
|
866
|
+
};
|
|
867
|
+
})();
|
|
868
|
+
|
|
869
|
+
// FBO variables
|
|
870
|
+
let dye: DoubleFBO;
|
|
871
|
+
let velocity: DoubleFBO;
|
|
872
|
+
let divergenceFBO: FBO;
|
|
873
|
+
let curlFBO: FBO;
|
|
874
|
+
let pressureFBO: DoubleFBO;
|
|
875
|
+
|
|
876
|
+
// WebGL Programs
|
|
877
|
+
const copyProgram = new Program(baseVertexShader, copyShader);
|
|
878
|
+
const clearProgram = new Program(baseVertexShader, clearShader);
|
|
879
|
+
const splatProgram = new Program(baseVertexShader, splatShader);
|
|
880
|
+
const advectionProgram = new Program(baseVertexShader, advectionShader);
|
|
881
|
+
const divergenceProgram = new Program(baseVertexShader, divergenceShader);
|
|
882
|
+
const curlProgram = new Program(baseVertexShader, curlShader);
|
|
883
|
+
const vorticityProgram = new Program(baseVertexShader, vorticityShader);
|
|
884
|
+
const pressureProgram = new Program(baseVertexShader, pressureShader);
|
|
885
|
+
const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);
|
|
886
|
+
const displayMaterial = new Material(baseVertexShader, displayShaderSource);
|
|
887
|
+
|
|
888
|
+
// FBO creation
|
|
889
|
+
function createFBO(
|
|
890
|
+
w: number,
|
|
891
|
+
h: number,
|
|
892
|
+
internalFormat: number,
|
|
893
|
+
format: number,
|
|
894
|
+
type: number,
|
|
895
|
+
param: number
|
|
896
|
+
): FBO {
|
|
897
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
898
|
+
const texture = gl.createTexture()!;
|
|
899
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
900
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
|
|
901
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
|
|
902
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
903
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
904
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
|
|
905
|
+
const fbo = gl.createFramebuffer()!;
|
|
906
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
907
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
908
|
+
gl.viewport(0, 0, w, h);
|
|
909
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
910
|
+
|
|
911
|
+
const texelSizeX = 1 / w;
|
|
912
|
+
const texelSizeY = 1 / h;
|
|
720
913
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return (target: FBO | null, doClear = false) => {
|
|
736
|
-
if (!gl) return;
|
|
737
|
-
if (!target) {
|
|
738
|
-
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
739
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
740
|
-
} else {
|
|
741
|
-
gl.viewport(0, 0, target.width, target.height);
|
|
742
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
743
|
-
}
|
|
744
|
-
if (doClear) {
|
|
745
|
-
gl.clearColor(0, 0, 0, 1);
|
|
746
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
747
|
-
}
|
|
748
|
-
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
|
749
|
-
};
|
|
750
|
-
})();
|
|
751
|
-
|
|
752
|
-
// FBO variables
|
|
753
|
-
let dye: DoubleFBO;
|
|
754
|
-
let velocity: DoubleFBO;
|
|
755
|
-
let divergenceFBO: FBO;
|
|
756
|
-
let curlFBO: FBO;
|
|
757
|
-
let pressureFBO: DoubleFBO;
|
|
758
|
-
|
|
759
|
-
// WebGL Programs
|
|
760
|
-
const copyProgram = new Program(baseVertexShader, copyShader);
|
|
761
|
-
const clearProgram = new Program(baseVertexShader, clearShader);
|
|
762
|
-
const splatProgram = new Program(baseVertexShader, splatShader);
|
|
763
|
-
const advectionProgram = new Program(baseVertexShader, advectionShader);
|
|
764
|
-
const divergenceProgram = new Program(baseVertexShader, divergenceShader);
|
|
765
|
-
const curlProgram = new Program(baseVertexShader, curlShader);
|
|
766
|
-
const vorticityProgram = new Program(baseVertexShader, vorticityShader);
|
|
767
|
-
const pressureProgram = new Program(baseVertexShader, pressureShader);
|
|
768
|
-
const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);
|
|
769
|
-
const displayMaterial = new Material(baseVertexShader, displayShaderSource);
|
|
770
|
-
|
|
771
|
-
// FBO creation
|
|
772
|
-
function createFBO(
|
|
773
|
-
w: number,
|
|
774
|
-
h: number,
|
|
775
|
-
internalFormat: number,
|
|
776
|
-
format: number,
|
|
777
|
-
type: number,
|
|
778
|
-
param: number
|
|
779
|
-
): FBO {
|
|
780
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
781
|
-
const texture = gl.createTexture()!;
|
|
782
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
783
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
|
|
784
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
|
|
785
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
786
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
787
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
|
|
788
|
-
const fbo = gl.createFramebuffer()!;
|
|
789
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
790
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
791
|
-
gl.viewport(0, 0, w, h);
|
|
792
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
793
|
-
|
|
794
|
-
const texelSizeX = 1 / w;
|
|
795
|
-
const texelSizeY = 1 / h;
|
|
796
|
-
|
|
797
|
-
return {
|
|
798
|
-
texture,
|
|
799
|
-
fbo,
|
|
800
|
-
width: w,
|
|
801
|
-
height: h,
|
|
802
|
-
texelSizeX,
|
|
803
|
-
texelSizeY,
|
|
804
|
-
attach(id: number) {
|
|
805
|
-
gl.activeTexture(gl.TEXTURE0 + id);
|
|
806
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
807
|
-
return id;
|
|
808
|
-
},
|
|
809
|
-
};
|
|
810
|
-
}
|
|
914
|
+
return {
|
|
915
|
+
texture,
|
|
916
|
+
fbo,
|
|
917
|
+
width: w,
|
|
918
|
+
height: h,
|
|
919
|
+
texelSizeX,
|
|
920
|
+
texelSizeY,
|
|
921
|
+
attach(id: number) {
|
|
922
|
+
gl.activeTexture(gl.TEXTURE0 + id);
|
|
923
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
924
|
+
return id;
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
}
|
|
811
928
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
929
|
+
function createDoubleFBO(
|
|
930
|
+
w: number,
|
|
931
|
+
h: number,
|
|
932
|
+
internalFormat: number,
|
|
933
|
+
format: number,
|
|
934
|
+
type: number,
|
|
935
|
+
param: number
|
|
936
|
+
): DoubleFBO {
|
|
937
|
+
const fbo1 = createFBO(w, h, internalFormat, format, type, param);
|
|
938
|
+
const fbo2 = createFBO(w, h, internalFormat, format, type, param);
|
|
939
|
+
return {
|
|
940
|
+
width: w,
|
|
941
|
+
height: h,
|
|
942
|
+
texelSizeX: fbo1.texelSizeX,
|
|
943
|
+
texelSizeY: fbo1.texelSizeY,
|
|
944
|
+
read: fbo1,
|
|
945
|
+
write: fbo2,
|
|
946
|
+
swap() {
|
|
947
|
+
const tmp = this.read;
|
|
948
|
+
this.read = this.write;
|
|
949
|
+
this.write = tmp;
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
}
|
|
836
953
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
954
|
+
function resizeFBO(
|
|
955
|
+
target: FBO,
|
|
956
|
+
w: number,
|
|
957
|
+
h: number,
|
|
958
|
+
internalFormat: number,
|
|
959
|
+
format: number,
|
|
960
|
+
type: number,
|
|
961
|
+
param: number
|
|
962
|
+
) {
|
|
963
|
+
const newFBO = createFBO(w, h, internalFormat, format, type, param);
|
|
964
|
+
copyProgram.bind();
|
|
965
|
+
if (copyProgram.uniforms.uTexture)
|
|
966
|
+
gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));
|
|
967
|
+
blit(newFBO, false);
|
|
968
|
+
return newFBO;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function resizeDoubleFBO(
|
|
972
|
+
target: DoubleFBO,
|
|
973
|
+
w: number,
|
|
974
|
+
h: number,
|
|
975
|
+
internalFormat: number,
|
|
976
|
+
format: number,
|
|
977
|
+
type: number,
|
|
978
|
+
param: number
|
|
979
|
+
) {
|
|
980
|
+
if (target.width === w && target.height === h) return target;
|
|
981
|
+
target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param);
|
|
982
|
+
target.write = createFBO(w, h, internalFormat, format, type, param);
|
|
983
|
+
target.width = w;
|
|
984
|
+
target.height = h;
|
|
985
|
+
target.texelSizeX = 1 / w;
|
|
986
|
+
target.texelSizeY = 1 / h;
|
|
987
|
+
return target;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function initFramebuffers() {
|
|
991
|
+
const simRes = getResolution(config.SIM_RESOLUTION!);
|
|
992
|
+
const dyeRes = getResolution(config.DYE_RESOLUTION!);
|
|
993
|
+
|
|
994
|
+
const texType = ext.halfFloatTexType;
|
|
995
|
+
const rgba = ext.formatRGBA!;
|
|
996
|
+
const rg = ext.formatRG!;
|
|
997
|
+
const r = ext.formatR!;
|
|
998
|
+
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
|
|
999
|
+
gl.disable(gl.BLEND);
|
|
1000
|
+
|
|
1001
|
+
if (!dye) {
|
|
1002
|
+
dye = createDoubleFBO(
|
|
1003
|
+
dyeRes.width,
|
|
1004
|
+
dyeRes.height,
|
|
1005
|
+
rgba.internalFormat,
|
|
1006
|
+
rgba.format,
|
|
1007
|
+
texType,
|
|
1008
|
+
filtering
|
|
1009
|
+
);
|
|
1010
|
+
} else {
|
|
1011
|
+
dye = resizeDoubleFBO(
|
|
1012
|
+
dye,
|
|
1013
|
+
dyeRes.width,
|
|
1014
|
+
dyeRes.height,
|
|
1015
|
+
rgba.internalFormat,
|
|
1016
|
+
rgba.format,
|
|
1017
|
+
texType,
|
|
1018
|
+
filtering
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
853
1021
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1022
|
+
if (!velocity) {
|
|
1023
|
+
velocity = createDoubleFBO(
|
|
1024
|
+
simRes.width,
|
|
1025
|
+
simRes.height,
|
|
1026
|
+
rg.internalFormat,
|
|
1027
|
+
rg.format,
|
|
1028
|
+
texType,
|
|
1029
|
+
filtering
|
|
1030
|
+
);
|
|
1031
|
+
} else {
|
|
1032
|
+
velocity = resizeDoubleFBO(
|
|
1033
|
+
velocity,
|
|
1034
|
+
simRes.width,
|
|
1035
|
+
simRes.height,
|
|
1036
|
+
rg.internalFormat,
|
|
1037
|
+
rg.format,
|
|
1038
|
+
texType,
|
|
1039
|
+
filtering
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
872
1042
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const rgba = ext.formatRGBA!;
|
|
879
|
-
const rg = ext.formatRG!;
|
|
880
|
-
const r = ext.formatR!;
|
|
881
|
-
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
|
|
882
|
-
gl.disable(gl.BLEND);
|
|
883
|
-
|
|
884
|
-
if (!dye) {
|
|
885
|
-
dye = createDoubleFBO(
|
|
886
|
-
dyeRes.width,
|
|
887
|
-
dyeRes.height,
|
|
888
|
-
rgba.internalFormat,
|
|
889
|
-
rgba.format,
|
|
890
|
-
texType,
|
|
891
|
-
filtering
|
|
892
|
-
);
|
|
893
|
-
} else {
|
|
894
|
-
dye = resizeDoubleFBO(
|
|
895
|
-
dye,
|
|
896
|
-
dyeRes.width,
|
|
897
|
-
dyeRes.height,
|
|
898
|
-
rgba.internalFormat,
|
|
899
|
-
rgba.format,
|
|
1043
|
+
divergenceFBO = createFBO(
|
|
1044
|
+
simRes.width,
|
|
1045
|
+
simRes.height,
|
|
1046
|
+
r.internalFormat,
|
|
1047
|
+
r.format,
|
|
900
1048
|
texType,
|
|
901
|
-
|
|
1049
|
+
gl.NEAREST
|
|
902
1050
|
);
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (!velocity) {
|
|
906
|
-
velocity = createDoubleFBO(
|
|
1051
|
+
curlFBO = createFBO(
|
|
907
1052
|
simRes.width,
|
|
908
1053
|
simRes.height,
|
|
909
|
-
|
|
910
|
-
|
|
1054
|
+
r.internalFormat,
|
|
1055
|
+
r.format,
|
|
911
1056
|
texType,
|
|
912
|
-
|
|
1057
|
+
gl.NEAREST
|
|
913
1058
|
);
|
|
914
|
-
|
|
915
|
-
velocity = resizeDoubleFBO(
|
|
916
|
-
velocity,
|
|
1059
|
+
pressureFBO = createDoubleFBO(
|
|
917
1060
|
simRes.width,
|
|
918
1061
|
simRes.height,
|
|
919
|
-
|
|
920
|
-
|
|
1062
|
+
r.internalFormat,
|
|
1063
|
+
r.format,
|
|
921
1064
|
texType,
|
|
922
|
-
|
|
1065
|
+
gl.NEAREST
|
|
923
1066
|
);
|
|
924
1067
|
}
|
|
925
1068
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
r.format,
|
|
931
|
-
texType,
|
|
932
|
-
gl.NEAREST
|
|
933
|
-
);
|
|
934
|
-
curlFBO = createFBO(
|
|
935
|
-
simRes.width,
|
|
936
|
-
simRes.height,
|
|
937
|
-
r.internalFormat,
|
|
938
|
-
r.format,
|
|
939
|
-
texType,
|
|
940
|
-
gl.NEAREST
|
|
941
|
-
);
|
|
942
|
-
pressureFBO = createDoubleFBO(
|
|
943
|
-
simRes.width,
|
|
944
|
-
simRes.height,
|
|
945
|
-
r.internalFormat,
|
|
946
|
-
r.format,
|
|
947
|
-
texType,
|
|
948
|
-
gl.NEAREST
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function updateKeywords() {
|
|
953
|
-
const displayKeywords: string[] = [];
|
|
954
|
-
if (config.SHADING) displayKeywords.push("SHADING");
|
|
955
|
-
displayMaterial.setKeywords(displayKeywords);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function getResolution(resolution: number) {
|
|
959
|
-
const w = gl.drawingBufferWidth;
|
|
960
|
-
const h = gl.drawingBufferHeight;
|
|
961
|
-
const aspectRatio = w / h;
|
|
962
|
-
const aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
|
|
963
|
-
const min = Math.round(resolution);
|
|
964
|
-
const max = Math.round(resolution * aspect);
|
|
965
|
-
if (w > h) {
|
|
966
|
-
return { width: max, height: min };
|
|
967
|
-
}
|
|
968
|
-
return { width: min, height: max };
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function scaleByPixelRatio(input: number) {
|
|
972
|
-
const pixelRatio = window.devicePixelRatio || 1;
|
|
973
|
-
return Math.floor(input * pixelRatio);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
let canvasRectCache: DOMRect | null = null;
|
|
977
|
-
function updateCanvasRectCache() {
|
|
978
|
-
if (contained && canvas) canvasRectCache = canvas.getBoundingClientRect();
|
|
979
|
-
}
|
|
980
|
-
if (contained) {
|
|
981
|
-
window.addEventListener("resize", updateCanvasRectCache, { passive: true });
|
|
982
|
-
window.addEventListener("scroll", updateCanvasRectCache, { passive: true });
|
|
983
|
-
updateCanvasRectCache();
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function getCanvasPos(clientX: number, clientY: number): { x: number; y: number } {
|
|
987
|
-
if (contained) {
|
|
988
|
-
if (!canvasRectCache) updateCanvasRectCache();
|
|
989
|
-
const rect = canvasRectCache!;
|
|
990
|
-
return {
|
|
991
|
-
x: scaleByPixelRatio(clientX - rect.left),
|
|
992
|
-
y: scaleByPixelRatio(clientY - rect.top),
|
|
993
|
-
};
|
|
1069
|
+
function updateKeywords() {
|
|
1070
|
+
const displayKeywords: string[] = [];
|
|
1071
|
+
if (config.SHADING) displayKeywords.push("SHADING");
|
|
1072
|
+
displayMaterial.setKeywords(displayKeywords);
|
|
994
1073
|
}
|
|
995
|
-
return { x: scaleByPixelRatio(clientX), y: scaleByPixelRatio(clientY) };
|
|
996
|
-
}
|
|
997
1074
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
let cachedFluidColorsRef: string[] | undefined;
|
|
1011
|
-
let cachedFluidColorsScaled: ColorRGB[] = [];
|
|
1012
|
-
|
|
1013
|
-
function getScaledColor(hex: string): ColorRGB {
|
|
1014
|
-
const { r, g, b } = hexToRgb(hex);
|
|
1015
|
-
return {
|
|
1016
|
-
r: r * clampedColorIntensity,
|
|
1017
|
-
g: g * clampedColorIntensity,
|
|
1018
|
-
b: b * clampedColorIntensity,
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1075
|
+
function getResolution(resolution: number) {
|
|
1076
|
+
const w = gl.drawingBufferWidth;
|
|
1077
|
+
const h = gl.drawingBufferHeight;
|
|
1078
|
+
const aspectRatio = w / h;
|
|
1079
|
+
const aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
|
|
1080
|
+
const min = Math.round(resolution);
|
|
1081
|
+
const max = Math.round(resolution * aspect);
|
|
1082
|
+
if (w > h) {
|
|
1083
|
+
return { width: max, height: min };
|
|
1084
|
+
}
|
|
1085
|
+
return { width: min, height: max };
|
|
1086
|
+
}
|
|
1021
1087
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
cachedFluidColor = getScaledColor(hex);
|
|
1088
|
+
function scaleByPixelRatio(input: number) {
|
|
1089
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
1090
|
+
return Math.floor(input * pixelRatio);
|
|
1026
1091
|
}
|
|
1027
|
-
return cachedFluidColor;
|
|
1028
|
-
}
|
|
1029
1092
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1093
|
+
let canvasRectCache: DOMRect | null = null;
|
|
1094
|
+
function updateCanvasRectCache() {
|
|
1095
|
+
if (contained && canvas) canvasRectCache = canvas.getBoundingClientRect();
|
|
1096
|
+
}
|
|
1097
|
+
if (contained) {
|
|
1098
|
+
window.addEventListener("resize", updateCanvasRectCache, { passive: true });
|
|
1099
|
+
window.addEventListener("scroll", updateCanvasRectCache, { passive: true });
|
|
1100
|
+
updateCanvasRectCache();
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function getCanvasPos(clientX: number, clientY: number): { x: number; y: number } {
|
|
1104
|
+
if (contained) {
|
|
1105
|
+
if (!canvasRectCache) updateCanvasRectCache();
|
|
1106
|
+
const rect = canvasRectCache!;
|
|
1107
|
+
return {
|
|
1108
|
+
x: scaleByPixelRatio(clientX - rect.left),
|
|
1109
|
+
y: scaleByPixelRatio(clientY - rect.top),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return { x: scaleByPixelRatio(clientX), y: scaleByPixelRatio(clientY) };
|
|
1034
1113
|
}
|
|
1035
|
-
return cachedFluidColorsScaled;
|
|
1036
|
-
}
|
|
1037
1114
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (!isVisible) return;
|
|
1042
|
-
const dt = calcDeltaTime();
|
|
1043
|
-
if (resizeCanvas()) initFramebuffers();
|
|
1044
|
-
updateColors(dt);
|
|
1045
|
-
applyInputs();
|
|
1046
|
-
step(dt);
|
|
1047
|
-
render(null);
|
|
1048
|
-
animationFrameId = requestAnimationFrame(updateFrame);
|
|
1049
|
-
}
|
|
1115
|
+
// Simulation Setup
|
|
1116
|
+
updateKeywords();
|
|
1117
|
+
initFramebuffers();
|
|
1050
1118
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
let
|
|
1054
|
-
dt = Math.min(dt, 0.016666);
|
|
1055
|
-
lastUpdateTime = now;
|
|
1056
|
-
return dt;
|
|
1057
|
-
}
|
|
1119
|
+
let lastUpdateTime = Date.now();
|
|
1120
|
+
let colorUpdateTimer = 0.0;
|
|
1121
|
+
let animationFrameId: number;
|
|
1058
1122
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1123
|
+
let isVisible = true;
|
|
1124
|
+
|
|
1125
|
+
function updateFrame() {
|
|
1126
|
+
if (!isVisible) return;
|
|
1127
|
+
const dt = calcDeltaTime();
|
|
1128
|
+
if (resizeCanvas()) initFramebuffers();
|
|
1129
|
+
updateColors(dt);
|
|
1130
|
+
applyInputs();
|
|
1131
|
+
step(dt);
|
|
1132
|
+
render(null);
|
|
1133
|
+
animationFrameId = requestAnimationFrame(updateFrame);
|
|
1066
1134
|
}
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
1135
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
});
|
|
1136
|
+
function calcDeltaTime() {
|
|
1137
|
+
const now = Date.now();
|
|
1138
|
+
let dt = (now - lastUpdateTime) / 1000;
|
|
1139
|
+
dt = Math.min(dt, 0.016666);
|
|
1140
|
+
lastUpdateTime = now;
|
|
1141
|
+
return dt;
|
|
1077
1142
|
}
|
|
1078
|
-
}
|
|
1079
1143
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1144
|
+
function resizeCanvas() {
|
|
1145
|
+
const width = scaleByPixelRatio(canvas.clientWidth);
|
|
1146
|
+
const height = scaleByPixelRatio(canvas.clientHeight);
|
|
1147
|
+
if (canvas.width !== width || canvas.height !== height) {
|
|
1148
|
+
canvas.width = width;
|
|
1149
|
+
canvas.height = height;
|
|
1150
|
+
return true;
|
|
1085
1151
|
}
|
|
1152
|
+
return false;
|
|
1086
1153
|
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function step(dt: number) {
|
|
1090
|
-
gl.disable(gl.BLEND);
|
|
1091
1154
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1155
|
+
function updateColors(dt: number) {
|
|
1156
|
+
colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;
|
|
1157
|
+
if (colorUpdateTimer >= 1) {
|
|
1158
|
+
colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
|
|
1159
|
+
pointers.forEach((p) => {
|
|
1160
|
+
p.color = generateColor();
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1099
1163
|
}
|
|
1100
|
-
blit(curlFBO);
|
|
1101
1164
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
if (vorticityProgram.uniforms.uCurl) {
|
|
1111
|
-
gl.uniform1i(vorticityProgram.uniforms.uCurl, curlFBO.attach(1));
|
|
1112
|
-
}
|
|
1113
|
-
if (vorticityProgram.uniforms.curl) {
|
|
1114
|
-
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
|
|
1115
|
-
}
|
|
1116
|
-
if (vorticityProgram.uniforms.dt) {
|
|
1117
|
-
gl.uniform1f(vorticityProgram.uniforms.dt, dt);
|
|
1165
|
+
function applyInputs() {
|
|
1166
|
+
for (const p of pointers) {
|
|
1167
|
+
if (p.moved) {
|
|
1168
|
+
p.moved = false;
|
|
1169
|
+
splatPointer(p);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1118
1172
|
}
|
|
1119
|
-
blit(velocity.write);
|
|
1120
|
-
velocity.swap();
|
|
1121
1173
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
if (divergenceProgram.uniforms.texelSize) {
|
|
1125
|
-
gl.uniform2f(
|
|
1126
|
-
divergenceProgram.uniforms.texelSize,
|
|
1127
|
-
velocity.texelSizeX,
|
|
1128
|
-
velocity.texelSizeY
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
if (divergenceProgram.uniforms.uVelocity) {
|
|
1132
|
-
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
|
|
1133
|
-
}
|
|
1134
|
-
blit(divergenceFBO);
|
|
1174
|
+
function step(dt: number) {
|
|
1175
|
+
gl.disable(gl.BLEND);
|
|
1135
1176
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1177
|
+
// Curl
|
|
1178
|
+
curlProgram.bind();
|
|
1179
|
+
if (curlProgram.uniforms.texelSize) {
|
|
1180
|
+
gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
|
|
1181
|
+
}
|
|
1182
|
+
if (curlProgram.uniforms.uVelocity) {
|
|
1183
|
+
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
|
|
1184
|
+
}
|
|
1185
|
+
blit(curlFBO);
|
|
1186
|
+
|
|
1187
|
+
// Vorticity
|
|
1188
|
+
vorticityProgram.bind();
|
|
1189
|
+
if (vorticityProgram.uniforms.texelSize) {
|
|
1190
|
+
gl.uniform2f(
|
|
1191
|
+
vorticityProgram.uniforms.texelSize,
|
|
1192
|
+
velocity.texelSizeX,
|
|
1193
|
+
velocity.texelSizeY
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
if (vorticityProgram.uniforms.uVelocity) {
|
|
1197
|
+
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
|
|
1198
|
+
}
|
|
1199
|
+
if (vorticityProgram.uniforms.uCurl) {
|
|
1200
|
+
gl.uniform1i(vorticityProgram.uniforms.uCurl, curlFBO.attach(1));
|
|
1201
|
+
}
|
|
1202
|
+
if (vorticityProgram.uniforms.curl) {
|
|
1203
|
+
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
|
|
1204
|
+
}
|
|
1205
|
+
if (vorticityProgram.uniforms.dt) {
|
|
1206
|
+
gl.uniform1f(vorticityProgram.uniforms.dt, dt);
|
|
1207
|
+
}
|
|
1208
|
+
blit(velocity.write);
|
|
1209
|
+
velocity.swap();
|
|
1210
|
+
|
|
1211
|
+
// Divergence
|
|
1212
|
+
divergenceProgram.bind();
|
|
1213
|
+
if (divergenceProgram.uniforms.texelSize) {
|
|
1214
|
+
gl.uniform2f(
|
|
1215
|
+
divergenceProgram.uniforms.texelSize,
|
|
1216
|
+
velocity.texelSizeX,
|
|
1217
|
+
velocity.texelSizeY
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (divergenceProgram.uniforms.uVelocity) {
|
|
1221
|
+
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
|
|
1222
|
+
}
|
|
1223
|
+
blit(divergenceFBO);
|
|
1146
1224
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
|
|
1156
|
-
if (pressureProgram.uniforms.uPressure) {
|
|
1157
|
-
gl.uniform1i(pressureProgram.uniforms.uPressure, pressureFBO.read.attach(1));
|
|
1225
|
+
// Clear pressure
|
|
1226
|
+
clearProgram.bind();
|
|
1227
|
+
if (clearProgram.uniforms.uTexture) {
|
|
1228
|
+
gl.uniform1i(clearProgram.uniforms.uTexture, pressureFBO.read.attach(0));
|
|
1229
|
+
}
|
|
1230
|
+
if (clearProgram.uniforms.value) {
|
|
1231
|
+
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
|
|
1158
1232
|
}
|
|
1159
1233
|
blit(pressureFBO.write);
|
|
1160
1234
|
pressureFBO.swap();
|
|
1161
|
-
}
|
|
1162
1235
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1236
|
+
// Pressure
|
|
1237
|
+
pressureProgram.bind();
|
|
1238
|
+
if (pressureProgram.uniforms.texelSize) {
|
|
1239
|
+
gl.uniform2f(
|
|
1240
|
+
pressureProgram.uniforms.texelSize,
|
|
1241
|
+
velocity.texelSizeX,
|
|
1242
|
+
velocity.texelSizeY
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (pressureProgram.uniforms.uDivergence) {
|
|
1246
|
+
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergenceFBO.attach(0));
|
|
1247
|
+
}
|
|
1248
|
+
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
|
|
1249
|
+
if (pressureProgram.uniforms.uPressure) {
|
|
1250
|
+
gl.uniform1i(pressureProgram.uniforms.uPressure, pressureFBO.read.attach(1));
|
|
1251
|
+
}
|
|
1252
|
+
blit(pressureFBO.write);
|
|
1253
|
+
pressureFBO.swap();
|
|
1254
|
+
}
|
|
1180
1255
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1256
|
+
// Gradient Subtract
|
|
1257
|
+
gradienSubtractProgram.bind();
|
|
1258
|
+
if (gradienSubtractProgram.uniforms.texelSize) {
|
|
1259
|
+
gl.uniform2f(
|
|
1260
|
+
gradienSubtractProgram.uniforms.texelSize,
|
|
1261
|
+
velocity.texelSizeX,
|
|
1262
|
+
velocity.texelSizeY
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
if (gradienSubtractProgram.uniforms.uPressure) {
|
|
1266
|
+
gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressureFBO.read.attach(0));
|
|
1267
|
+
}
|
|
1268
|
+
if (gradienSubtractProgram.uniforms.uVelocity) {
|
|
1269
|
+
gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
|
|
1270
|
+
}
|
|
1271
|
+
blit(velocity.write);
|
|
1272
|
+
velocity.swap();
|
|
1273
|
+
|
|
1274
|
+
// Advection - velocity
|
|
1275
|
+
advectionProgram.bind();
|
|
1276
|
+
if (advectionProgram.uniforms.texelSize) {
|
|
1277
|
+
gl.uniform2f(
|
|
1278
|
+
advectionProgram.uniforms.texelSize,
|
|
1279
|
+
velocity.texelSizeX,
|
|
1280
|
+
velocity.texelSizeY
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
|
|
1284
|
+
gl.uniform2f(
|
|
1285
|
+
advectionProgram.uniforms.dyeTexelSize,
|
|
1286
|
+
velocity.texelSizeX,
|
|
1287
|
+
velocity.texelSizeY
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
const velocityId = velocity.read.attach(0);
|
|
1291
|
+
if (advectionProgram.uniforms.uVelocity) {
|
|
1292
|
+
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
|
|
1293
|
+
}
|
|
1294
|
+
if (advectionProgram.uniforms.uSource) {
|
|
1295
|
+
gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
|
|
1296
|
+
}
|
|
1297
|
+
if (advectionProgram.uniforms.dt) {
|
|
1298
|
+
gl.uniform1f(advectionProgram.uniforms.dt, dt);
|
|
1299
|
+
}
|
|
1300
|
+
if (advectionProgram.uniforms.dissipation) {
|
|
1301
|
+
gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
|
|
1302
|
+
}
|
|
1303
|
+
blit(velocity.write);
|
|
1304
|
+
velocity.swap();
|
|
1208
1305
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1306
|
+
// Advection - dye
|
|
1307
|
+
if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
|
|
1308
|
+
gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);
|
|
1309
|
+
}
|
|
1310
|
+
if (advectionProgram.uniforms.uVelocity) {
|
|
1311
|
+
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
|
|
1312
|
+
}
|
|
1313
|
+
if (advectionProgram.uniforms.uSource) {
|
|
1314
|
+
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
|
|
1315
|
+
}
|
|
1316
|
+
if (advectionProgram.uniforms.dissipation) {
|
|
1317
|
+
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
|
|
1318
|
+
}
|
|
1319
|
+
blit(dye.write);
|
|
1320
|
+
dye.swap();
|
|
1221
1321
|
}
|
|
1222
|
-
blit(dye.write);
|
|
1223
|
-
dye.swap();
|
|
1224
|
-
}
|
|
1225
1322
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function drawDisplay(target: FBO | null) {
|
|
1233
|
-
const width = target ? target.width : gl.drawingBufferWidth;
|
|
1234
|
-
const height = target ? target.height : gl.drawingBufferHeight;
|
|
1235
|
-
displayMaterial.bind();
|
|
1236
|
-
if (config.SHADING && displayMaterial.uniforms.texelSize) {
|
|
1237
|
-
gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / width, 1 / height);
|
|
1238
|
-
}
|
|
1239
|
-
if (displayMaterial.uniforms.uTexture) {
|
|
1240
|
-
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
|
|
1323
|
+
function render(target: FBO | null) {
|
|
1324
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1325
|
+
gl.enable(gl.BLEND);
|
|
1326
|
+
drawDisplay(target);
|
|
1241
1327
|
}
|
|
1242
|
-
blit(target, false);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Interaction
|
|
1246
|
-
function splatPointer(pointer: Pointer) {
|
|
1247
|
-
const dx = pointer.deltaX * config.SPLAT_FORCE;
|
|
1248
|
-
const dy = pointer.deltaY * config.SPLAT_FORCE;
|
|
1249
|
-
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function clickSplat(pointer: Pointer) {
|
|
1253
|
-
const color = generateColor();
|
|
1254
|
-
color.r *= 10;
|
|
1255
|
-
color.g *= 10;
|
|
1256
|
-
color.b *= 10;
|
|
1257
|
-
const dx = 10 * (Math.random() - 0.5);
|
|
1258
|
-
const dy = 30 * (Math.random() - 0.5);
|
|
1259
|
-
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
|
|
1260
|
-
}
|
|
1261
1328
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
if (splatProgram.uniforms.color) {
|
|
1274
|
-
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0);
|
|
1275
|
-
}
|
|
1276
|
-
if (splatProgram.uniforms.radius) {
|
|
1277
|
-
gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100)!);
|
|
1329
|
+
function drawDisplay(target: FBO | null) {
|
|
1330
|
+
const width = target ? target.width : gl.drawingBufferWidth;
|
|
1331
|
+
const height = target ? target.height : gl.drawingBufferHeight;
|
|
1332
|
+
displayMaterial.bind();
|
|
1333
|
+
if (config.SHADING && displayMaterial.uniforms.texelSize) {
|
|
1334
|
+
gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / width, 1 / height);
|
|
1335
|
+
}
|
|
1336
|
+
if (displayMaterial.uniforms.uTexture) {
|
|
1337
|
+
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
|
|
1338
|
+
}
|
|
1339
|
+
blit(target, false);
|
|
1278
1340
|
}
|
|
1279
|
-
blit(velocity.write);
|
|
1280
|
-
velocity.swap();
|
|
1281
1341
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1342
|
+
// Interaction
|
|
1343
|
+
function splatPointer(pointer: Pointer) {
|
|
1344
|
+
const dx = pointer.deltaX * config.SPLAT_FORCE;
|
|
1345
|
+
const dy = pointer.deltaY * config.SPLAT_FORCE;
|
|
1346
|
+
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
|
|
1284
1347
|
}
|
|
1285
|
-
if (splatProgram.uniforms.color) {
|
|
1286
|
-
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
|
|
1287
|
-
}
|
|
1288
|
-
blit(dye.write);
|
|
1289
|
-
dye.swap();
|
|
1290
|
-
}
|
|
1291
1348
|
|
|
1292
|
-
|
|
1293
|
-
// Simulate a cursor sweep along a random arc — each step on its own frame
|
|
1294
|
-
// so the velocity field has time to evolve between injections.
|
|
1295
|
-
const angle = Math.random() * Math.PI * 2;
|
|
1296
|
-
const cx = 0.25 + Math.random() * 0.5;
|
|
1297
|
-
const cy = 0.25 + Math.random() * 0.5;
|
|
1298
|
-
const arcSpan = Math.PI * (0.8 + Math.random() * 0.8);
|
|
1299
|
-
let i = 0;
|
|
1300
|
-
|
|
1301
|
-
function step() {
|
|
1302
|
-
if (i >= steps) return;
|
|
1303
|
-
const t = i / Math.max(1, steps - 1);
|
|
1304
|
-
const a = angle + t * arcSpan;
|
|
1305
|
-
const r = 0.15 + 0.05 * Math.sin(t * Math.PI);
|
|
1306
|
-
const x = cx + Math.cos(a) * r;
|
|
1307
|
-
const y = cy + Math.sin(a) * r;
|
|
1349
|
+
function clickSplat(pointer: Pointer) {
|
|
1308
1350
|
const color = generateColor();
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
function updatePointerMoveData(pointer: Pointer, posX: number, posY: number, color: ColorRGB) {
|
|
1342
|
-
pointer.prevTexcoordX = pointer.texcoordX;
|
|
1343
|
-
pointer.prevTexcoordY = pointer.texcoordY;
|
|
1344
|
-
pointer.texcoordX = posX / canvas.width;
|
|
1345
|
-
pointer.texcoordY = 1 - posY / canvas.height;
|
|
1346
|
-
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX)!;
|
|
1347
|
-
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY)!;
|
|
1348
|
-
pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
|
|
1349
|
-
pointer.color = color;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
function correctDeltaX(delta: number) {
|
|
1353
|
-
const aspectRatio = canvas.width / canvas.height;
|
|
1354
|
-
if (aspectRatio < 1) delta *= aspectRatio;
|
|
1355
|
-
return delta;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function correctDeltaY(delta: number) {
|
|
1359
|
-
const aspectRatio = canvas.width / canvas.height;
|
|
1360
|
-
if (aspectRatio > 1) delta /= aspectRatio;
|
|
1361
|
-
return delta;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function generateColor(): ColorRGB {
|
|
1365
|
-
if (fluidColor) {
|
|
1366
|
-
return getCachedFluidColor(fluidColor);
|
|
1367
|
-
}
|
|
1368
|
-
if (fluidColors && fluidColors.length > 0) {
|
|
1369
|
-
const colors = getCachedFluidColors(fluidColors);
|
|
1370
|
-
const idx = colorIndex % colors.length;
|
|
1371
|
-
colorIndex++;
|
|
1372
|
-
return colors[idx];
|
|
1373
|
-
}
|
|
1374
|
-
const c = HSVtoRGB(Math.random(), 1.0, 1.0);
|
|
1375
|
-
c.r *= clampedColorIntensity;
|
|
1376
|
-
c.g *= clampedColorIntensity;
|
|
1377
|
-
c.b *= clampedColorIntensity;
|
|
1378
|
-
return c;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
function HSVtoRGB(h: number, s: number, v: number): ColorRGB {
|
|
1382
|
-
let r = 0;
|
|
1383
|
-
let g = 0;
|
|
1384
|
-
let b = 0;
|
|
1385
|
-
const i = Math.floor(h * 6);
|
|
1386
|
-
const f = h * 6 - i;
|
|
1387
|
-
const p = v * (1 - s);
|
|
1388
|
-
const q = v * (1 - f * s);
|
|
1389
|
-
const t = v * (1 - (1 - f) * s);
|
|
1390
|
-
|
|
1391
|
-
switch (i % 6) {
|
|
1392
|
-
case 0:
|
|
1393
|
-
r = v;
|
|
1394
|
-
g = t;
|
|
1395
|
-
b = p;
|
|
1396
|
-
break;
|
|
1397
|
-
case 1:
|
|
1398
|
-
r = q;
|
|
1399
|
-
g = v;
|
|
1400
|
-
b = p;
|
|
1401
|
-
break;
|
|
1402
|
-
case 2:
|
|
1403
|
-
r = p;
|
|
1404
|
-
g = v;
|
|
1405
|
-
b = t;
|
|
1406
|
-
break;
|
|
1407
|
-
case 3:
|
|
1408
|
-
r = p;
|
|
1409
|
-
g = q;
|
|
1410
|
-
b = v;
|
|
1411
|
-
break;
|
|
1412
|
-
case 4:
|
|
1413
|
-
r = t;
|
|
1414
|
-
g = p;
|
|
1415
|
-
b = v;
|
|
1416
|
-
break;
|
|
1417
|
-
case 5:
|
|
1418
|
-
r = v;
|
|
1419
|
-
g = p;
|
|
1420
|
-
b = q;
|
|
1421
|
-
break;
|
|
1422
|
-
}
|
|
1423
|
-
return { r, g, b };
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function wrap(value: number, min: number, max: number) {
|
|
1427
|
-
const range = max - min;
|
|
1428
|
-
if (range === 0) return min;
|
|
1429
|
-
return ((value - min) % range) + min;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
// Event Listeners
|
|
1433
|
-
function handleMouseDown(e: MouseEvent) {
|
|
1434
|
-
const pointer = pointers[0];
|
|
1435
|
-
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1436
|
-
updatePointerDownData(pointer, -1, posX, posY);
|
|
1437
|
-
clickSplat(pointer);
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
function handleFirstMouseMove(e: MouseEvent) {
|
|
1441
|
-
const pointer = pointers[0];
|
|
1442
|
-
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1443
|
-
const color = generateColor();
|
|
1444
|
-
updatePointerMoveData(pointer, posX, posY, color);
|
|
1445
|
-
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function handleMouseMove(e: MouseEvent) {
|
|
1449
|
-
const pointer = pointers[0];
|
|
1450
|
-
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1451
|
-
updatePointerMoveData(pointer, posX, posY, pointer.color);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
function handleFirstTouchStart(e: TouchEvent) {
|
|
1455
|
-
const touches = e.targetTouches;
|
|
1456
|
-
const pointer = pointers[0];
|
|
1457
|
-
for (let i = 0; i < touches.length; i++) {
|
|
1458
|
-
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1459
|
-
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
|
|
1460
|
-
}
|
|
1461
|
-
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
1462
|
-
}
|
|
1351
|
+
color.r *= 10;
|
|
1352
|
+
color.g *= 10;
|
|
1353
|
+
color.b *= 10;
|
|
1354
|
+
const dx = 10 * (Math.random() - 0.5);
|
|
1355
|
+
const dy = 30 * (Math.random() - 0.5);
|
|
1356
|
+
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function splat(x: number, y: number, dx: number, dy: number, color: ColorRGB) {
|
|
1360
|
+
splatProgram.bind();
|
|
1361
|
+
if (splatProgram.uniforms.uTarget) {
|
|
1362
|
+
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
|
|
1363
|
+
}
|
|
1364
|
+
if (splatProgram.uniforms.aspectRatio) {
|
|
1365
|
+
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
|
|
1366
|
+
}
|
|
1367
|
+
if (splatProgram.uniforms.point) {
|
|
1368
|
+
gl.uniform2f(splatProgram.uniforms.point, x, y);
|
|
1369
|
+
}
|
|
1370
|
+
if (splatProgram.uniforms.color) {
|
|
1371
|
+
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0);
|
|
1372
|
+
}
|
|
1373
|
+
if (splatProgram.uniforms.radius) {
|
|
1374
|
+
let radius = correctRadius(config.SPLAT_RADIUS / 100)!;
|
|
1375
|
+
if (contained) {
|
|
1376
|
+
radius = scaleRadiusForContainer(radius, canvas.clientHeight, window.innerHeight);
|
|
1377
|
+
}
|
|
1378
|
+
gl.uniform1f(splatProgram.uniforms.radius, radius);
|
|
1379
|
+
}
|
|
1380
|
+
blit(velocity.write);
|
|
1381
|
+
velocity.swap();
|
|
1463
1382
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1383
|
+
if (splatProgram.uniforms.uTarget) {
|
|
1384
|
+
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
|
|
1385
|
+
}
|
|
1386
|
+
if (splatProgram.uniforms.color) {
|
|
1387
|
+
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
|
|
1388
|
+
}
|
|
1389
|
+
blit(dye.write);
|
|
1390
|
+
dye.swap();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function multipleSplats(steps: number) {
|
|
1394
|
+
// Simulate a cursor sweep along a random arc — each step on its own frame
|
|
1395
|
+
// so the velocity field has time to evolve between injections.
|
|
1396
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1397
|
+
const cx = 0.25 + Math.random() * 0.5;
|
|
1398
|
+
const cy = 0.25 + Math.random() * 0.5;
|
|
1399
|
+
const arcSpan = Math.PI * (0.8 + Math.random() * 0.8);
|
|
1400
|
+
let i = 0;
|
|
1401
|
+
|
|
1402
|
+
function step() {
|
|
1403
|
+
if (i >= steps) return;
|
|
1404
|
+
const t = i / Math.max(1, steps - 1);
|
|
1405
|
+
const a = angle + t * arcSpan;
|
|
1406
|
+
const r = 0.15 + 0.05 * Math.sin(t * Math.PI);
|
|
1407
|
+
const x = cx + Math.cos(a) * r;
|
|
1408
|
+
const y = cy + Math.sin(a) * r;
|
|
1409
|
+
const color = generateColor();
|
|
1410
|
+
splat(
|
|
1411
|
+
x,
|
|
1412
|
+
y,
|
|
1413
|
+
-Math.sin(a) * config.SPLAT_FORCE * 0.5,
|
|
1414
|
+
Math.cos(a) * config.SPLAT_FORCE * 0.5,
|
|
1415
|
+
color
|
|
1416
|
+
);
|
|
1417
|
+
i++;
|
|
1418
|
+
if (i < steps) requestAnimationFrame(step);
|
|
1419
|
+
}
|
|
1420
|
+
requestAnimationFrame(step);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function correctRadius(radius: number) {
|
|
1424
|
+
const aspectRatio = canvas.width / canvas.height;
|
|
1425
|
+
if (aspectRatio > 1) radius *= aspectRatio;
|
|
1426
|
+
return radius;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function updatePointerDownData(pointer: Pointer, id: number, posX: number, posY: number) {
|
|
1430
|
+
pointer.id = id;
|
|
1431
|
+
pointer.down = true;
|
|
1432
|
+
pointer.moved = false;
|
|
1433
|
+
pointer.texcoordX = posX / canvas.width;
|
|
1434
|
+
pointer.texcoordY = 1 - posY / canvas.height;
|
|
1435
|
+
pointer.prevTexcoordX = pointer.texcoordX;
|
|
1436
|
+
pointer.prevTexcoordY = pointer.texcoordY;
|
|
1437
|
+
pointer.deltaX = 0;
|
|
1438
|
+
pointer.deltaY = 0;
|
|
1439
|
+
pointer.color = generateColor();
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function updatePointerMoveData(
|
|
1443
|
+
pointer: Pointer,
|
|
1444
|
+
posX: number,
|
|
1445
|
+
posY: number,
|
|
1446
|
+
color: ColorRGB
|
|
1447
|
+
) {
|
|
1448
|
+
pointer.prevTexcoordX = pointer.texcoordX;
|
|
1449
|
+
pointer.prevTexcoordY = pointer.texcoordY;
|
|
1450
|
+
pointer.texcoordX = posX / canvas.width;
|
|
1451
|
+
pointer.texcoordY = 1 - posY / canvas.height;
|
|
1452
|
+
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX)!;
|
|
1453
|
+
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY)!;
|
|
1454
|
+
pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
|
|
1455
|
+
pointer.color = color;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function correctDeltaX(delta: number) {
|
|
1459
|
+
const aspectRatio = canvas.width / canvas.height;
|
|
1460
|
+
if (aspectRatio < 1) delta *= aspectRatio;
|
|
1461
|
+
return delta;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function correctDeltaY(delta: number) {
|
|
1465
|
+
const aspectRatio = canvas.width / canvas.height;
|
|
1466
|
+
if (aspectRatio > 1) delta /= aspectRatio;
|
|
1467
|
+
return delta;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Event Listeners
|
|
1471
|
+
function handleMouseDown(e: MouseEvent) {
|
|
1472
|
+
const pointer = pointers[0];
|
|
1473
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1474
|
+
updatePointerDownData(pointer, -1, posX, posY);
|
|
1475
|
+
clickSplat(pointer);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function handleFirstMouseMove(e: MouseEvent) {
|
|
1479
|
+
const pointer = pointers[0];
|
|
1480
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1481
|
+
const color = generateColor();
|
|
1482
|
+
updatePointerMoveData(pointer, posX, posY, color);
|
|
1483
|
+
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1470
1484
|
}
|
|
1471
|
-
}
|
|
1472
1485
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
for (let i = 0; i < touches.length; i++) {
|
|
1477
|
-
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1486
|
+
function handleMouseMove(e: MouseEvent) {
|
|
1487
|
+
const pointer = pointers[0];
|
|
1488
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1478
1489
|
updatePointerMoveData(pointer, posX, posY, pointer.color);
|
|
1479
1490
|
}
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// Add event listeners
|
|
1483
|
-
if (interactive) {
|
|
1484
|
-
window.addEventListener("mousedown", handleMouseDown);
|
|
1485
|
-
document.body.addEventListener("mousemove", handleFirstMouseMove);
|
|
1486
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
1487
|
-
document.body.addEventListener("touchstart", handleFirstTouchStart);
|
|
1488
|
-
window.addEventListener("touchstart", handleTouchStart, false);
|
|
1489
|
-
window.addEventListener("touchmove", handleTouchMove, false);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// Pause animation when scrolled out of view
|
|
1493
|
-
let observer: IntersectionObserver | null = null;
|
|
1494
|
-
if (pauseWhenHidden) {
|
|
1495
|
-
observer = new IntersectionObserver(
|
|
1496
|
-
([entry]) => {
|
|
1497
|
-
const wasVisible = isVisible;
|
|
1498
|
-
isVisible = entry.isIntersecting;
|
|
1499
|
-
if (isVisible && !wasVisible) {
|
|
1500
|
-
lastUpdateTime = Date.now();
|
|
1501
|
-
animationFrameId = requestAnimationFrame(updateFrame);
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
{ threshold: 0 }
|
|
1505
|
-
);
|
|
1506
|
-
observer.observe(canvas);
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// Start animation
|
|
1510
|
-
updateFrame();
|
|
1511
|
-
if (splatOnMount) multipleSplats(Math.floor(Math.random() * 6) + 10);
|
|
1512
1491
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
Math.random(),
|
|
1521
|
-
(Math.random() - 0.5) * 200,
|
|
1522
|
-
(Math.random() - 0.5) * 200,
|
|
1523
|
-
color
|
|
1524
|
-
);
|
|
1525
|
-
}, autoSplatInterval);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// Cleanup
|
|
1529
|
-
function cleanup() {
|
|
1530
|
-
cancelAnimationFrame(animationFrameId);
|
|
1531
|
-
if (observer) observer.disconnect();
|
|
1532
|
-
if (autoSplatTimer) clearInterval(autoSplatTimer);
|
|
1533
|
-
if (interactive) {
|
|
1534
|
-
window.removeEventListener("mousedown", handleMouseDown);
|
|
1535
|
-
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1536
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
1492
|
+
function handleFirstTouchStart(e: TouchEvent) {
|
|
1493
|
+
const touches = e.targetTouches;
|
|
1494
|
+
const pointer = pointers[0];
|
|
1495
|
+
for (let i = 0; i < touches.length; i++) {
|
|
1496
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1497
|
+
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
|
|
1498
|
+
}
|
|
1537
1499
|
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
1538
|
-
window.removeEventListener("touchstart", handleTouchStart);
|
|
1539
|
-
window.removeEventListener("touchmove", handleTouchMove);
|
|
1540
1500
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1501
|
+
|
|
1502
|
+
function handleTouchStart(e: TouchEvent) {
|
|
1503
|
+
const touches = e.targetTouches;
|
|
1504
|
+
const pointer = pointers[0];
|
|
1505
|
+
for (let i = 0; i < touches.length; i++) {
|
|
1506
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1507
|
+
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
|
|
1508
|
+
}
|
|
1544
1509
|
}
|
|
1545
|
-
}
|
|
1546
1510
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
);
|
|
1511
|
+
function handleTouchMove(e: TouchEvent) {
|
|
1512
|
+
const touches = e.targetTouches;
|
|
1513
|
+
const pointer = pointers[0];
|
|
1514
|
+
for (let i = 0; i < touches.length; i++) {
|
|
1515
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1516
|
+
updatePointerMoveData(pointer, posX, posY, pointer.color);
|
|
1553
1517
|
}
|
|
1554
|
-
activeInstance();
|
|
1555
1518
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1519
|
+
|
|
1520
|
+
// Add event listeners
|
|
1521
|
+
if (interactive) {
|
|
1522
|
+
window.addEventListener("mousedown", handleMouseDown);
|
|
1523
|
+
document.body.addEventListener("mousemove", handleFirstMouseMove);
|
|
1524
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
1525
|
+
document.body.addEventListener("touchstart", handleFirstTouchStart);
|
|
1526
|
+
window.addEventListener("touchstart", handleTouchStart, false);
|
|
1527
|
+
window.addEventListener("touchmove", handleTouchMove, false);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Pause animation when scrolled out of view
|
|
1531
|
+
let observer: IntersectionObserver | null = null;
|
|
1532
|
+
if (pauseWhenHidden) {
|
|
1533
|
+
observer = new IntersectionObserver(
|
|
1534
|
+
([entry]) => {
|
|
1535
|
+
const wasVisible = isVisible;
|
|
1536
|
+
isVisible = entry.isIntersecting;
|
|
1537
|
+
if (isVisible && !wasVisible) {
|
|
1538
|
+
lastUpdateTime = Date.now();
|
|
1539
|
+
animationFrameId = requestAnimationFrame(updateFrame);
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
{ threshold: 0 }
|
|
1543
|
+
);
|
|
1544
|
+
observer.observe(canvas);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Start animation
|
|
1548
|
+
updateFrame();
|
|
1549
|
+
if (splatOnMount) multipleSplats(Math.floor(Math.random() * 6) + 10);
|
|
1550
|
+
|
|
1551
|
+
// Auto-splat timer
|
|
1552
|
+
let autoSplatTimer: ReturnType<typeof setInterval> | null = null;
|
|
1553
|
+
if (autoSplat) {
|
|
1554
|
+
autoSplatTimer = setInterval(() => {
|
|
1555
|
+
const color = generateColor();
|
|
1556
|
+
splat(
|
|
1557
|
+
Math.random(),
|
|
1558
|
+
Math.random(),
|
|
1559
|
+
(Math.random() - 0.5) * 200,
|
|
1560
|
+
(Math.random() - 0.5) * 200,
|
|
1561
|
+
color
|
|
1562
|
+
);
|
|
1563
|
+
}, autoSplatInterval);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Cleanup
|
|
1567
|
+
function cleanup() {
|
|
1568
|
+
cancelAnimationFrame(animationFrameId);
|
|
1569
|
+
if (observer) observer.disconnect();
|
|
1570
|
+
if (autoSplatTimer) clearInterval(autoSplatTimer);
|
|
1571
|
+
if (interactive) {
|
|
1572
|
+
window.removeEventListener("mousedown", handleMouseDown);
|
|
1573
|
+
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1574
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
1575
|
+
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
1576
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
1577
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
1561
1578
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1579
|
+
if (contained) {
|
|
1580
|
+
window.removeEventListener("resize", updateCanvasRectCache);
|
|
1581
|
+
window.removeEventListener("scroll", updateCanvasRectCache);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1564
1584
|
|
|
1565
|
-
|
|
1585
|
+
return registerInstance(cleanup);
|
|
1586
|
+
}
|
|
1566
1587
|
});
|
|
1567
1588
|
</script>
|
|
1568
1589
|
|