Fluid Cursor Effect

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

1
'use client';
2
import { useEffect } from 'react';
3
4
import fluidCursor from '@/hooks/use-FluidCursor';
5
6
const FluidCursor = () => {
7
useEffect(() => {
8
fluidCursor();
9
}, []);
10
11
return (
12
<div className='fixed top-0 left-0 z-2'>
13
<canvas id='fluid' className='w-screen h-screen' />
14
</div>
15
);
16
};
17
export default FluidCursor;
18

useFluidCursor

1
// @ts-nocheck
2
const useFluidCursor = () => {
3
const canvas = document.getElementById('fluid');
4
resizeCanvas();
5
6
//try to adjust settings
7
8
let config = {
9
SIM_RESOLUTION: 128,
10
DYE_RESOLUTION: 1440,
11
CAPTURE_RESOLUTION: 512,
12
DENSITY_DISSIPATION: 3.5,
13
VELOCITY_DISSIPATION: 2,
14
PRESSURE: 0.1,
15
PRESSURE_ITERATIONS: 20,
16
CURL: 3,
17
SPLAT_RADIUS: 0.2,
18
SPLAT_FORCE: 6000,
19
SHADING: true,
20
COLOR_UPDATE_SPEED: 10,
21
PAUSED: false,
22
BACK_COLOR: { r: 0.5, g: 0, b: 0 },
23
TRANSPARENT: true,
24
};
25
26
function pointerPrototype() {
27
this.id = -1;
28
this.texcoordX = 0;
29
this.texcoordY = 0;
30
this.prevTexcoordX = 0;
31
this.prevTexcoordY = 0;
32
this.deltaX = 0;
33
this.deltaY = 0;
34
this.down = false;
35
this.moved = false;
36
this.color = [0, 0, 0];
37
}
38
39
const pointers = [];
40
pointers.push(new pointerPrototype());
41
42
const { gl, ext } = getWebGLContext(canvas);
43
44
if (!ext.supportLinearFiltering) {
45
config.DYE_RESOLUTION = 256;
46
config.SHADING = false;
47
}
48
49
function getWebGLContext(canvas) {
50
const params = {
51
alpha: true,
52
depth: false,
53
stencil: false,
54
antialias: false,
55
preserveDrawingBuffer: false,
56
};
57
58
let gl = canvas.getContext('webgl2', params);
59
const isWebGL2 = !!gl;
60
if (!isWebGL2)
61
gl =
62
canvas.getContext('webgl', params) ||
63
canvas.getContext('experimental-webgl', params);
64
65
let halfFloat;
66
let supportLinearFiltering;
67
if (isWebGL2) {
68
gl.getExtension('EXT_color_buffer_float');
69
supportLinearFiltering = gl.getExtension('OES_texture_float_linear');
70
} else {
71
halfFloat = gl.getExtension('OES_texture_half_float');
72
supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear');
73
}
74
75
gl.clearColor(0.0, 0.0, 0.0, 1.0);
76
77
const halfFloatTexType = isWebGL2
78
? gl.HALF_FLOAT
79
: halfFloat.HALF_FLOAT_OES;
80
let formatRGBA;
81
let formatRG;
82
let formatR;
83
84
if (isWebGL2) {
85
formatRGBA = getSupportedFormat(
86
gl,
87
gl.RGBA16F,
88
gl.RGBA,
89
halfFloatTexType
90
);
91
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
92
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
93
} else {
94
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
95
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
96
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
97
}
98
99
return {
100
gl,
101
ext: {
102
formatRGBA,
103
formatRG,
104
formatR,
105
halfFloatTexType,
106
supportLinearFiltering,
107
},
108
};
109
}
110
111
function getSupportedFormat(gl, internalFormat, format, type) {
112
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
113
switch (internalFormat) {
114
case gl.R16F:
115
return getSupportedFormat(gl, gl.RG16F, gl.RG, type);
116
case gl.RG16F:
117
return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type);
118
default:
119
return null;
120
}
121
}
122
123
return {
124
internalFormat,
125
format,
126
};
127
}
128
129
function supportRenderTextureFormat(gl, internalFormat, format, type) {
130
const texture = gl.createTexture();
131
gl.bindTexture(gl.TEXTURE_2D, texture);
132
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
133
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
134
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
135
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
136
gl.texImage2D(
137
gl.TEXTURE_2D,
138
0,
139
internalFormat,
140
4,
141
4,
142
0,
143
format,
144
type,
145
null
146
);
147
148
const fbo = gl.createFramebuffer();
149
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
150
gl.framebufferTexture2D(
151
gl.FRAMEBUFFER,
152
gl.COLOR_ATTACHMENT0,
153
gl.TEXTURE_2D,
154
texture,
155
0
156
);
157
158
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
159
return status == gl.FRAMEBUFFER_COMPLETE;
160
}
161
162
class Material {
163
constructor(vertexShader, fragmentShaderSource) {
164
this.vertexShader = vertexShader;
165
this.fragmentShaderSource = fragmentShaderSource;
166
this.programs = [];
167
this.activeProgram = null;
168
this.uniforms = [];
169
}
170
171
setKeywords(keywords) {
172
let hash = 0;
173
for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]);
174
175
let program = this.programs[hash];
176
if (program == null) {
177
let fragmentShader = compileShader(
178
gl.FRAGMENT_SHADER,
179
this.fragmentShaderSource,
180
keywords
181
);
182
program = createProgram(this.vertexShader, fragmentShader);
183
this.programs[hash] = program;
184
}
185
186
if (program == this.activeProgram) return;
187
188
this.uniforms = getUniforms(program);
189
this.activeProgram = program;
190
}
191
192
bind() {
193
gl.useProgram(this.activeProgram);
194
}
195
}
196
197
class Program {
198
constructor(vertexShader, fragmentShader) {
199
this.uniforms = {};
200
this.program = createProgram(vertexShader, fragmentShader);
201
this.uniforms = getUniforms(this.program);
202
}
203
204
bind() {
205
gl.useProgram(this.program);
206
}
207
}
208
209
function createProgram(vertexShader, fragmentShader) {
210
let program = gl.createProgram();
211
gl.attachShader(program, vertexShader);
212
gl.attachShader(program, fragmentShader);
213
gl.linkProgram(program);
214
215
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
216
console.trace(gl.getProgramInfoLog(program));
217
218
return program;
219
}
220
221
function getUniforms(program) {
222
let uniforms = [];
223
let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
224
for (let i = 0; i < uniformCount; i++) {
225
let uniformName = gl.getActiveUniform(program, i).name;
226
uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
227
}
228
return uniforms;
229
}
230
231
function compileShader(type, source, keywords) {
232
source = addKeywords(source, keywords);
233
234
const shader = gl.createShader(type);
235
gl.shaderSource(shader, source);
236
gl.compileShader(shader);
237
238
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
239
console.trace(gl.getShaderInfoLog(shader));
240
241
return shader;
242
}
243
244
function addKeywords(source, keywords) {
245
if (keywords == null) return source;
246
let keywordsString = '';
247
keywords.forEach((keyword) => {
248
keywordsString += '#define ' + keyword + '\n';
249
});
250
251
return keywordsString + source;
252
}
253
254
const baseVertexShader = compileShader(
255
gl.VERTEX_SHADER,
256
`
257
precision highp float;
258
259
attribute vec2 aPosition;
260
varying vec2 vUv;
261
varying vec2 vL;
262
varying vec2 vR;
263
varying vec2 vT;
264
varying vec2 vB;
265
uniform vec2 texelSize;
266
267
void main () {
268
vUv = aPosition * 0.5 + 0.5;
269
vL = vUv - vec2(texelSize.x, 0.0);
270
vR = vUv + vec2(texelSize.x, 0.0);
271
vT = vUv + vec2(0.0, texelSize.y);
272
vB = vUv - vec2(0.0, texelSize.y);
273
gl_Position = vec4(aPosition, 0.0, 1.0);
274
}
275
`
276
);
277
278
const blurVertexShader = compileShader(
279
gl.VERTEX_SHADER,
280
`
281
precision highp float;
282
283
attribute vec2 aPosition;
284
varying vec2 vUv;
285
varying vec2 vL;
286
varying vec2 vR;
287
uniform vec2 texelSize;
288
289
void main () {
290
vUv = aPosition * 0.5 + 0.5;
291
float offset = 1.33333333;
292
vL = vUv - texelSize * offset;
293
vR = vUv + texelSize * offset;
294
gl_Position = vec4(aPosition, 0.0, 1.0);
295
}
296
`
297
);
298
299
const blurShader = compileShader(
300
gl.FRAGMENT_SHADER,
301
`
302
precision mediump float;
303
precision mediump sampler2D;
304
305
varying vec2 vUv;
306
varying vec2 vL;
307
varying vec2 vR;
308
uniform sampler2D uTexture;
309
310
void main () {
311
vec4 sum = texture2D(uTexture, vUv) * 0.29411764;
312
sum += texture2D(uTexture, vL) * 0.35294117;
313
sum += texture2D(uTexture, vR) * 0.35294117;
314
gl_FragColor = sum;
315
}
316
`
317
);
318
319
const copyShader = compileShader(
320
gl.FRAGMENT_SHADER,
321
`
322
precision mediump float;
323
precision mediump sampler2D;
324
325
varying highp vec2 vUv;
326
uniform sampler2D uTexture;
327
328
void main () {
329
gl_FragColor = texture2D(uTexture, vUv);
330
}
331
`
332
);
333
334
const clearShader = compileShader(
335
gl.FRAGMENT_SHADER,
336
`
337
precision mediump float;
338
precision mediump sampler2D;
339
340
varying highp vec2 vUv;
341
uniform sampler2D uTexture;
342
uniform float value;
343
344
void main () {
345
gl_FragColor = value * texture2D(uTexture, vUv);
346
}
347
`
348
);
349
350
const colorShader = compileShader(
351
gl.FRAGMENT_SHADER,
352
`
353
precision mediump float;
354
355
uniform vec4 color;
356
357
void main () {
358
gl_FragColor = color;
359
}
360
`
361
);
362
363
const displayShaderSource = `
364
precision highp float;
365
precision highp sampler2D;
366
367
varying vec2 vUv;
368
varying vec2 vL;
369
varying vec2 vR;
370
varying vec2 vT;
371
varying vec2 vB;
372
uniform sampler2D uTexture;
373
uniform sampler2D uDithering;
374
uniform vec2 ditherScale;
375
uniform vec2 texelSize;
376
377
vec3 linearToGamma (vec3 color) {
378
color = max(color, vec3(0));
379
return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));
380
}
381
382
void main () {
383
vec3 c = texture2D(uTexture, vUv).rgb;
384
385
#ifdef SHADING
386
vec3 lc = texture2D(uTexture, vL).rgb;
387
vec3 rc = texture2D(uTexture, vR).rgb;
388
vec3 tc = texture2D(uTexture, vT).rgb;
389
vec3 bc = texture2D(uTexture, vB).rgb;
390
391
float dx = length(rc) - length(lc);
392
float dy = length(tc) - length(bc);
393
394
vec3 n = normalize(vec3(dx, dy, length(texelSize)));
395
vec3 l = vec3(0.0, 0.0, 1.0);
396
397
float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
398
c *= diffuse;
399
#endif
400
401
float a = max(c.r, max(c.g, c.b));
402
gl_FragColor = vec4(c, a);
403
}
404
`;
405
406
const splatShader = compileShader(
407
gl.FRAGMENT_SHADER,
408
`
409
precision highp float;
410
precision highp sampler2D;
411
412
varying vec2 vUv;
413
uniform sampler2D uTarget;
414
uniform float aspectRatio;
415
uniform vec3 color;
416
uniform vec2 point;
417
uniform float radius;
418
419
void main () {
420
vec2 p = vUv - point.xy;
421
p.x *= aspectRatio;
422
vec3 splat = exp(-dot(p, p) / radius) * color;
423
vec3 base = texture2D(uTarget, vUv).xyz;
424
gl_FragColor = vec4(base + splat, 1.0);
425
}
426
`
427
);
428
429
const advectionShader = compileShader(
430
gl.FRAGMENT_SHADER,
431
`
432
precision highp float;
433
precision highp sampler2D;
434
435
varying vec2 vUv;
436
uniform sampler2D uVelocity;
437
uniform sampler2D uSource;
438
uniform vec2 texelSize;
439
uniform vec2 dyeTexelSize;
440
uniform float dt;
441
uniform float dissipation;
442
443
vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
444
vec2 st = uv / tsize - 0.5;
445
446
vec2 iuv = floor(st);
447
vec2 fuv = fract(st);
448
449
vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
450
vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
451
vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
452
vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
453
454
return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
455
}
456
457
void main () {
458
#ifdef MANUAL_FILTERING
459
vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
460
vec4 result = bilerp(uSource, coord, dyeTexelSize);
461
#else
462
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
463
vec4 result = texture2D(uSource, coord);
464
#endif
465
float decay = 1.0 + dissipation * dt;
466
gl_FragColor = result / decay;
467
}`,
468
ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']
469
);
470
471
const divergenceShader = compileShader(
472
gl.FRAGMENT_SHADER,
473
`
474
precision mediump float;
475
precision mediump sampler2D;
476
477
varying highp vec2 vUv;
478
varying highp vec2 vL;
479
varying highp vec2 vR;
480
varying highp vec2 vT;
481
varying highp vec2 vB;
482
uniform sampler2D uVelocity;
483
484
void main () {
485
float L = texture2D(uVelocity, vL).x;
486
float R = texture2D(uVelocity, vR).x;
487
float T = texture2D(uVelocity, vT).y;
488
float B = texture2D(uVelocity, vB).y;
489
490
vec2 C = texture2D(uVelocity, vUv).xy;
491
if (vL.x < 0.0) { L = -C.x; }
492
if (vR.x > 1.0) { R = -C.x; }
493
if (vT.y > 1.0) { T = -C.y; }
494
if (vB.y < 0.0) { B = -C.y; }
495
496
float div = 0.5 * (R - L + T - B);
497
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
498
}
499
`
500
);
501
502
const curlShader = compileShader(
503
gl.FRAGMENT_SHADER,
504
`
505
precision mediump float;
506
precision mediump sampler2D;
507
508
varying highp vec2 vUv;
509
varying highp vec2 vL;
510
varying highp vec2 vR;
511
varying highp vec2 vT;
512
varying highp vec2 vB;
513
uniform sampler2D uVelocity;
514
515
void main () {
516
float L = texture2D(uVelocity, vL).y;
517
float R = texture2D(uVelocity, vR).y;
518
float T = texture2D(uVelocity, vT).x;
519
float B = texture2D(uVelocity, vB).x;
520
float vorticity = R - L - T + B;
521
gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
522
}
523
`
524
);
525
526
const vorticityShader = compileShader(
527
gl.FRAGMENT_SHADER,
528
`
529
precision highp float;
530
precision highp sampler2D;
531
532
varying vec2 vUv;
533
varying vec2 vL;
534
varying vec2 vR;
535
varying vec2 vT;
536
varying vec2 vB;
537
uniform sampler2D uVelocity;
538
uniform sampler2D uCurl;
539
uniform float curl;
540
uniform float dt;
541
542
void main () {
543
float L = texture2D(uCurl, vL).x;
544
float R = texture2D(uCurl, vR).x;
545
float T = texture2D(uCurl, vT).x;
546
float B = texture2D(uCurl, vB).x;
547
float C = texture2D(uCurl, vUv).x;
548
549
vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
550
force /= length(force) + 0.0001;
551
force *= curl * C;
552
force.y *= -1.0;
553
554
vec2 velocity = texture2D(uVelocity, vUv).xy;
555
velocity += force * dt;
556
velocity = min(max(velocity, -1000.0), 1000.0);
557
gl_FragColor = vec4(velocity, 0.0, 1.0);
558
}
559
`
560
);
561
562
const pressureShader = compileShader(
563
gl.FRAGMENT_SHADER,
564
`
565
precision mediump float;
566
precision mediump sampler2D;
567
568
varying highp vec2 vUv;
569
varying highp vec2 vL;
570
varying highp vec2 vR;
571
varying highp vec2 vT;
572
varying highp vec2 vB;
573
uniform sampler2D uPressure;
574
uniform sampler2D uDivergence;
575
576
void main () {
577
float L = texture2D(uPressure, vL).x;
578
float R = texture2D(uPressure, vR).x;
579
float T = texture2D(uPressure, vT).x;
580
float B = texture2D(uPressure, vB).x;
581
float C = texture2D(uPressure, vUv).x;
582
float divergence = texture2D(uDivergence, vUv).x;
583
float pressure = (L + R + B + T - divergence) * 0.25;
584
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
585
}
586
`
587
);
588
589
const gradientSubtractShader = compileShader(
590
gl.FRAGMENT_SHADER,
591
`
592
precision mediump float;
593
precision mediump sampler2D;
594
595
varying highp vec2 vUv;
596
varying highp vec2 vL;
597
varying highp vec2 vR;
598
varying highp vec2 vT;
599
varying highp vec2 vB;
600
uniform sampler2D uPressure;
601
uniform sampler2D uVelocity;
602
603
void main () {
604
float L = texture2D(uPressure, vL).x;
605
float R = texture2D(uPressure, vR).x;
606
float T = texture2D(uPressure, vT).x;
607
float B = texture2D(uPressure, vB).x;
608
vec2 velocity = texture2D(uVelocity, vUv).xy;
609
velocity.xy -= vec2(R - L, T - B);
610
gl_FragColor = vec4(velocity, 0.0, 1.0);
611
}
612
`
613
);
614
615
const blit = (() => {
616
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
617
gl.bufferData(
618
gl.ARRAY_BUFFER,
619
new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
620
gl.STATIC_DRAW
621
);
622
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
623
gl.bufferData(
624
gl.ELEMENT_ARRAY_BUFFER,
625
new Uint16Array([0, 1, 2, 0, 2, 3]),
626
gl.STATIC_DRAW
627
);
628
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
629
gl.enableVertexAttribArray(0);
630
631
return (target, clear = false) => {
632
if (target == null) {
633
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
634
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
635
} else {
636
gl.viewport(0, 0, target.width, target.height);
637
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
638
}
639
if (clear) {
640
gl.clearColor(0.0, 0.0, 0.0, 1.0);
641
gl.clear(gl.COLOR_BUFFER_BIT);
642
}
643
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
644
};
645
})();
646
647
let dye;
648
let velocity;
649
let divergence;
650
let curl;
651
let pressure;
652
653
const copyProgram = new Program(baseVertexShader, copyShader);
654
const clearProgram = new Program(baseVertexShader, clearShader);
655
const splatProgram = new Program(baseVertexShader, splatShader);
656
const advectionProgram = new Program(baseVertexShader, advectionShader);
657
const divergenceProgram = new Program(baseVertexShader, divergenceShader);
658
const curlProgram = new Program(baseVertexShader, curlShader);
659
const vorticityProgram = new Program(baseVertexShader, vorticityShader);
660
const pressureProgram = new Program(baseVertexShader, pressureShader);
661
const gradienSubtractProgram = new Program(
662
baseVertexShader,
663
gradientSubtractShader
664
);
665
666
const displayMaterial = new Material(baseVertexShader, displayShaderSource);
667
668
function initFramebuffers() {
669
let simRes = getResolution(config.SIM_RESOLUTION);
670
let dyeRes = getResolution(config.DYE_RESOLUTION);
671
672
const texType = ext.halfFloatTexType;
673
const rgba = ext.formatRGBA;
674
const rg = ext.formatRG;
675
const r = ext.formatR;
676
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
677
678
gl.disable(gl.BLEND);
679
680
if (dye == null)
681
dye = createDoubleFBO(
682
dyeRes.width,
683
dyeRes.height,
684
rgba.internalFormat,
685
rgba.format,
686
texType,
687
filtering
688
);
689
else
690
dye = resizeDoubleFBO(
691
dye,
692
dyeRes.width,
693
dyeRes.height,
694
rgba.internalFormat,
695
rgba.format,
696
texType,
697
filtering
698
);
699
700
if (velocity == null)
701
velocity = createDoubleFBO(
702
simRes.width,
703
simRes.height,
704
rg.internalFormat,
705
rg.format,
706
texType,
707
filtering
708
);
709
else
710
velocity = resizeDoubleFBO(
711
velocity,
712
simRes.width,
713
simRes.height,
714
rg.internalFormat,
715
rg.format,
716
texType,
717
filtering
718
);
719
720
divergence = createFBO(
721
simRes.width,
722
simRes.height,
723
r.internalFormat,
724
r.format,
725
texType,
726
gl.NEAREST
727
);
728
curl = createFBO(
729
simRes.width,
730
simRes.height,
731
r.internalFormat,
732
r.format,
733
texType,
734
gl.NEAREST
735
);
736
pressure = createDoubleFBO(
737
simRes.width,
738
simRes.height,
739
r.internalFormat,
740
r.format,
741
texType,
742
gl.NEAREST
743
);
744
}
745
746
function createFBO(w, h, internalFormat, format, type, param) {
747
gl.activeTexture(gl.TEXTURE0);
748
let texture = gl.createTexture();
749
gl.bindTexture(gl.TEXTURE_2D, texture);
750
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
751
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
752
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
753
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
754
gl.texImage2D(
755
gl.TEXTURE_2D,
756
0,
757
internalFormat,
758
w,
759
h,
760
0,
761
format,
762
type,
763
null
764
);
765
766
let fbo = gl.createFramebuffer();
767
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
768
gl.framebufferTexture2D(
769
gl.FRAMEBUFFER,
770
gl.COLOR_ATTACHMENT0,
771
gl.TEXTURE_2D,
772
texture,
773
0
774
);
775
gl.viewport(0, 0, w, h);
776
gl.clear(gl.COLOR_BUFFER_BIT);
777
778
let texelSizeX = 1.0 / w;
779
let texelSizeY = 1.0 / h;
780
781
return {
782
texture,
783
fbo,
784
width: w,
785
height: h,
786
texelSizeX,
787
texelSizeY,
788
attach(id) {
789
gl.activeTexture(gl.TEXTURE0 + id);
790
gl.bindTexture(gl.TEXTURE_2D, texture);
791
return id;
792
},
793
};
794
}
795
796
function createDoubleFBO(w, h, internalFormat, format, type, param) {
797
let fbo1 = createFBO(w, h, internalFormat, format, type, param);
798
let fbo2 = createFBO(w, h, internalFormat, format, type, param);
799
800
return {
801
width: w,
802
height: h,
803
texelSizeX: fbo1.texelSizeX,
804
texelSizeY: fbo1.texelSizeY,
805
get read() {
806
return fbo1;
807
},
808
set read(value) {
809
fbo1 = value;
810
},
811
get write() {
812
return fbo2;
813
},
814
set write(value) {
815
fbo2 = value;
816
},
817
swap() {
818
let temp = fbo1;
819
fbo1 = fbo2;
820
fbo2 = temp;
821
},
822
};
823
}
824
825
function resizeFBO(target, w, h, internalFormat, format, type, param) {
826
let newFBO = createFBO(w, h, internalFormat, format, type, param);
827
copyProgram.bind();
828
gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));
829
blit(newFBO);
830
return newFBO;
831
}
832
833
function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) {
834
if (target.width == w && target.height == h) return target;
835
target.read = resizeFBO(
836
target.read,
837
w,
838
h,
839
internalFormat,
840
format,
841
type,
842
param
843
);
844
target.write = createFBO(w, h, internalFormat, format, type, param);
845
target.width = w;
846
target.height = h;
847
target.texelSizeX = 1.0 / w;
848
target.texelSizeY = 1.0 / h;
849
return target;
850
}
851
852
function createTextureAsync(url) {
853
let texture = gl.createTexture();
854
gl.bindTexture(gl.TEXTURE_2D, texture);
855
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
856
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
857
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
858
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
859
gl.texImage2D(
860
gl.TEXTURE_2D,
861
0,
862
gl.RGB,
863
1,
864
1,
865
0,
866
gl.RGB,
867
gl.UNSIGNED_BYTE,
868
new Uint8Array([255, 255, 255])
869
);
870
871
let obj = {
872
texture,
873
width: 1,
874
height: 1,
875
attach(id) {
876
gl.activeTexture(gl.TEXTURE0 + id);
877
gl.bindTexture(gl.TEXTURE_2D, texture);
878
return id;
879
},
880
};
881
882
let image = new Image();
883
image.onload = () => {
884
obj.width = image.width;
885
obj.height = image.height;
886
gl.bindTexture(gl.TEXTURE_2D, texture);
887
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
888
};
889
image.src = url;
890
891
return obj;
892
}
893
894
function updateKeywords() {
895
let displayKeywords = [];
896
if (config.SHADING) displayKeywords.push('SHADING');
897
displayMaterial.setKeywords(displayKeywords);
898
}
899
900
updateKeywords();
901
initFramebuffers();
902
903
let lastUpdateTime = Date.now();
904
let colorUpdateTimer = 0.0;
905
906
function update() {
907
const dt = calcDeltaTime();
908
// console.log(dt)
909
if (resizeCanvas()) initFramebuffers();
910
updateColors(dt);
911
applyInputs();
912
step(dt);
913
render(null);
914
requestAnimationFrame(update);
915
}
916
917
function calcDeltaTime() {
918
let now = Date.now();
919
let dt = (now - lastUpdateTime) / 1000;
920
dt = Math.min(dt, 0.016666);
921
lastUpdateTime = now;
922
return dt;
923
}
924
925
function resizeCanvas() {
926
let width = scaleByPixelRatio(canvas.clientWidth);
927
let height = scaleByPixelRatio(canvas.clientHeight);
928
if (canvas.width != width || canvas.height != height) {
929
canvas.width = width;
930
canvas.height = height;
931
return true;
932
}
933
return false;
934
}
935
936
function updateColors(dt) {
937
colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;
938
if (colorUpdateTimer >= 1) {
939
colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
940
pointers.forEach((p) => {
941
p.color = generateColor();
942
});
943
}
944
}
945
946
function applyInputs() {
947
pointers.forEach((p) => {
948
if (p.moved) {
949
p.moved = false;
950
splatPointer(p);
951
}
952
});
953
}
954
955
function step(dt) {
956
gl.disable(gl.BLEND);
957
958
curlProgram.bind();
959
gl.uniform2f(
960
curlProgram.uniforms.texelSize,
961
velocity.texelSizeX,
962
velocity.texelSizeY
963
);
964
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
965
blit(curl);
966
967
vorticityProgram.bind();
968
gl.uniform2f(
969
vorticityProgram.uniforms.texelSize,
970
velocity.texelSizeX,
971
velocity.texelSizeY
972
);
973
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
974
gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1));
975
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
976
gl.uniform1f(vorticityProgram.uniforms.dt, dt);
977
blit(velocity.write);
978
velocity.swap();
979
980
divergenceProgram.bind();
981
gl.uniform2f(
982
divergenceProgram.uniforms.texelSize,
983
velocity.texelSizeX,
984
velocity.texelSizeY
985
);
986
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
987
blit(divergence);
988
989
clearProgram.bind();
990
gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0));
991
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
992
blit(pressure.write);
993
pressure.swap();
994
995
pressureProgram.bind();
996
gl.uniform2f(
997
pressureProgram.uniforms.texelSize,
998
velocity.texelSizeX,
999
velocity.texelSizeY
1000
);
1001
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0));
1002
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
1003
gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1));
1004
blit(pressure.write);
1005
pressure.swap();
1006
}
1007
1008
gradienSubtractProgram.bind();
1009
gl.uniform2f(
1010
gradienSubtractProgram.uniforms.texelSize,
1011
velocity.texelSizeX,
1012
velocity.texelSizeY
1013
);
1014
gl.uniform1i(
1015
gradienSubtractProgram.uniforms.uPressure,
1016
pressure.read.attach(0)
1017
);
1018
gl.uniform1i(
1019
gradienSubtractProgram.uniforms.uVelocity,
1020
velocity.read.attach(1)
1021
);
1022
blit(velocity.write);
1023
velocity.swap();
1024
1025
advectionProgram.bind();
1026
gl.uniform2f(
1027
advectionProgram.uniforms.texelSize,
1028
velocity.texelSizeX,
1029
velocity.texelSizeY
1030
);
1031
if (!ext.supportLinearFiltering)
1032
gl.uniform2f(
1033
advectionProgram.uniforms.dyeTexelSize,
1034
velocity.texelSizeX,
1035
velocity.texelSizeY
1036
);
1037
let velocityId = velocity.read.attach(0);
1038
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
1039
gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
1040
gl.uniform1f(advectionProgram.uniforms.dt, dt);
1041
gl.uniform1f(
1042
advectionProgram.uniforms.dissipation,
1043
config.VELOCITY_DISSIPATION
1044
);
1045
blit(velocity.write);
1046
velocity.swap();
1047
1048
if (!ext.supportLinearFiltering)
1049
gl.uniform2f(
1050
advectionProgram.uniforms.dyeTexelSize,
1051
dye.texelSizeX,
1052
dye.texelSizeY
1053
);
1054
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
1055
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
1056
gl.uniform1f(
1057
advectionProgram.uniforms.dissipation,
1058
config.DENSITY_DISSIPATION
1059
);
1060
blit(dye.write);
1061
dye.swap();
1062
}
1063
1064
function render(target) {
1065
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1066
gl.enable(gl.BLEND);
1067
drawDisplay(target);
1068
}
1069
1070
function drawDisplay(target) {
1071
let width = target == null ? gl.drawingBufferWidth : target.width;
1072
let height = target == null ? gl.drawingBufferHeight : target.height;
1073
1074
displayMaterial.bind();
1075
if (config.SHADING)
1076
gl.uniform2f(
1077
displayMaterial.uniforms.texelSize,
1078
1.0 / width,
1079
1.0 / height
1080
);
1081
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
1082
blit(target);
1083
}
1084
1085
function splatPointer(pointer) {
1086
let dx = pointer.deltaX * config.SPLAT_FORCE;
1087
let dy = pointer.deltaY * config.SPLAT_FORCE;
1088
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
1089
}
1090
1091
function clickSplat(pointer) {
1092
const color = generateColor();
1093
color.r *= 10.0;
1094
color.g *= 10.0;
1095
color.b *= 10.0;
1096
let dx = 10 * (Math.random() - 0.5);
1097
let dy = 30 * (Math.random() - 0.5);
1098
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
1099
}
1100
1101
function splat(x, y, dx, dy, color) {
1102
splatProgram.bind();
1103
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
1104
gl.uniform1f(
1105
splatProgram.uniforms.aspectRatio,
1106
canvas.width / canvas.height
1107
);
1108
gl.uniform2f(splatProgram.uniforms.point, x, y);
1109
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0);
1110
gl.uniform1f(
1111
splatProgram.uniforms.radius,
1112
correctRadius(config.SPLAT_RADIUS / 100.0)
1113
);
1114
blit(velocity.write);
1115
velocity.swap();
1116
1117
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
1118
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
1119
blit(dye.write);
1120
dye.swap();
1121
}
1122
1123
function correctRadius(radius) {
1124
let aspectRatio = canvas.width / canvas.height;
1125
if (aspectRatio > 1) radius *= aspectRatio;
1126
return radius;
1127
}
1128
1129
window.addEventListener('mousedown', (e) => {
1130
let pointer = pointers[0];
1131
let posX = scaleByPixelRatio(e.clientX);
1132
let posY = scaleByPixelRatio(e.clientY);
1133
updatePointerDownData(pointer, -1, posX, posY);
1134
clickSplat(pointer);
1135
});
1136
1137
document.body.addEventListener('mousemove', function handleFirstMouseMove(e) {
1138
let pointer = pointers[0];
1139
let posX = scaleByPixelRatio(e.clientX);
1140
let posY = scaleByPixelRatio(e.clientY);
1141
let color = generateColor();
1142
1143
update();
1144
updatePointerMoveData(pointer, posX, posY, color);
1145
1146
// Remove this event listener after the first mousemove event
1147
document.body.removeEventListener('mousemove', handleFirstMouseMove);
1148
});
1149
1150
window.addEventListener('mousemove', (e) => {
1151
let pointer = pointers[0];
1152
let posX = scaleByPixelRatio(e.clientX);
1153
let posY = scaleByPixelRatio(e.clientY);
1154
let color = pointer.color;
1155
1156
updatePointerMoveData(pointer, posX, posY, color);
1157
});
1158
1159
document.body.addEventListener(
1160
'touchstart',
1161
function handleFirstTouchStart(e) {
1162
const touches = e.targetTouches;
1163
let pointer = pointers[0];
1164
1165
for (let i = 0; i < touches.length; i++) {
1166
let posX = scaleByPixelRatio(touches[i].clientX);
1167
let posY = scaleByPixelRatio(touches[i].clientY);
1168
1169
update();
1170
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1171
}
1172
1173
// Remove this event listener after the first touchstart event
1174
document.body.removeEventListener('touchstart', handleFirstTouchStart);
1175
}
1176
);
1177
1178
window.addEventListener('touchstart', (e) => {
1179
const touches = e.targetTouches;
1180
let pointer = pointers[0];
1181
for (let i = 0; i < touches.length; i++) {
1182
let posX = scaleByPixelRatio(touches[i].clientX);
1183
let posY = scaleByPixelRatio(touches[i].clientY);
1184
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1185
}
1186
});
1187
1188
window.addEventListener(
1189
'touchmove',
1190
(e) => {
1191
const touches = e.targetTouches;
1192
let pointer = pointers[0];
1193
for (let i = 0; i < touches.length; i++) {
1194
let posX = scaleByPixelRatio(touches[i].clientX);
1195
let posY = scaleByPixelRatio(touches[i].clientY);
1196
updatePointerMoveData(pointer, posX, posY, pointer.color);
1197
}
1198
},
1199
false
1200
);
1201
1202
window.addEventListener('touchend', (e) => {
1203
const touches = e.changedTouches;
1204
let pointer = pointers[0];
1205
1206
for (let i = 0; i < touches.length; i++) {
1207
updatePointerUpData(pointer);
1208
}
1209
});
1210
1211
function updatePointerDownData(pointer, id, posX, posY) {
1212
pointer.id = id;
1213
pointer.down = true;
1214
pointer.moved = false;
1215
pointer.texcoordX = posX / canvas.width;
1216
pointer.texcoordY = 1.0 - posY / canvas.height;
1217
pointer.prevTexcoordX = pointer.texcoordX;
1218
pointer.prevTexcoordY = pointer.texcoordY;
1219
pointer.deltaX = 0;
1220
pointer.deltaY = 0;
1221
pointer.color = generateColor();
1222
}
1223
1224
function updatePointerMoveData(pointer, posX, posY, color) {
1225
// pointer.down = false;
1226
pointer.prevTexcoordX = pointer.texcoordX;
1227
pointer.prevTexcoordY = pointer.texcoordY;
1228
pointer.texcoordX = posX / canvas.width;
1229
pointer.texcoordY = 1.0 - posY / canvas.height;
1230
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
1231
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
1232
pointer.moved =
1233
Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
1234
pointer.color = color;
1235
}
1236
1237
function updatePointerUpData(pointer) {
1238
pointer.down = false;
1239
}
1240
1241
function correctDeltaX(delta) {
1242
let aspectRatio = canvas.width / canvas.height;
1243
if (aspectRatio < 1) delta *= aspectRatio;
1244
return delta;
1245
}
1246
1247
function correctDeltaY(delta) {
1248
let aspectRatio = canvas.width / canvas.height;
1249
if (aspectRatio > 1) delta /= aspectRatio;
1250
return delta;
1251
}
1252
1253
function generateColor() {
1254
let c = HSVtoRGB(Math.random(), 1.0, 1.0);
1255
c.r *= 0.15;
1256
c.g *= 0.15;
1257
c.b *= 0.15;
1258
return c;
1259
}
1260
1261
function HSVtoRGB(h, s, v) {
1262
let r, g, b, i, f, p, q, t;
1263
i = Math.floor(h * 6);
1264
f = h * 6 - i;
1265
p = v * (1 - s);
1266
q = v * (1 - f * s);
1267
t = v * (1 - (1 - f) * s);
1268
1269
switch (i % 6) {
1270
case 0:
1271
(r = v), (g = t), (b = p);
1272
break;
1273
case 1:
1274
(r = q), (g = v), (b = p);
1275
break;
1276
case 2:
1277
(r = p), (g = v), (b = t);
1278
break;
1279
case 3:
1280
(r = p), (g = q), (b = v);
1281
break;
1282
case 4:
1283
(r = t), (g = p), (b = v);
1284
break;
1285
case 5:
1286
(r = v), (g = p), (b = q);
1287
break;
1288
}
1289
1290
return {
1291
r,
1292
g,
1293
b,
1294
};
1295
}
1296
1297
function wrap(value, min, max) {
1298
const range = max - min;
1299
if (range == 0) return min;
1300
return ((value - min) % range) + min;
1301
}
1302
1303
function getResolution(resolution) {
1304
let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
1305
if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio;
1306
1307
const min = Math.round(resolution);
1308
const max = Math.round(resolution * aspectRatio);
1309
1310
if (gl.drawingBufferWidth > gl.drawingBufferHeight)
1311
return { width: max, height: min };
1312
else return { width: min, height: max };
1313
}
1314
1315
function scaleByPixelRatio(input) {
1316
const pixelRatio = window.devicePixelRatio || 1;
1317
return Math.floor(input * pixelRatio);
1318
}
1319
1320
function hashCode(s) {
1321
if (s.length == 0) return 0;
1322
let hash = 0;
1323
for (let i = 0; i < s.length; i++) {
1324
hash = (hash << 5) - hash + s.charCodeAt(i);
1325
hash |= 0; // Convert to 32bit integer
1326
}
1327
return hash;
1328
}
1329
};
1330
1331
export default useFluidCursor;