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