@firecms/neat 0.4.0 → 0.5.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.
@@ -4,7 +4,7 @@ const PLANE_WIDTH = 50;
4
4
  const PLANE_HEIGHT = 80;
5
5
 
6
6
  const WIREFRAME = true;
7
- const COLORS_COUNT = 5;
7
+ const COLORS_COUNT = 6;
8
8
 
9
9
  const clock = new THREE.Clock();
10
10
 
@@ -18,6 +18,16 @@ type SceneState = {
18
18
  resolution: number
19
19
  }
20
20
 
21
+ // Interface for the Uniforms to avoid @ts-ignore and improve access speed
22
+ interface NeatUniforms {
23
+ [key: string]: THREE.IUniform;
24
+ u_time: { value: number };
25
+ u_resolution: { value: THREE.Vector2 };
26
+ u_color_pressure: { value: THREE.Vector2 };
27
+ u_colors: { value: { is_active: number; color: THREE.Color; influence: number }[] };
28
+ u_mouse_texture: { value: THREE.Texture | null };
29
+ }
30
+
21
31
  export type NeatConfig = {
22
32
  resolution?: number;
23
33
  speed?: number;
@@ -40,6 +50,37 @@ export type NeatConfig = {
40
50
  backgroundColor?: string;
41
51
  backgroundAlpha?: number;
42
52
  yOffset?: number;
53
+ yOffsetWaveMultiplier?: number;
54
+ yOffsetColorMultiplier?: number;
55
+ yOffsetFlowMultiplier?: number;
56
+ // Flow field parameters
57
+ flowDistortionA?: number;
58
+ flowDistortionB?: number;
59
+ flowScale?: number;
60
+ flowEase?: number;
61
+ flowEnabled?: boolean;
62
+ // Mouse interaction
63
+ /** Strength of mouse-driven distortion */
64
+ mouseDistortionStrength?: number;
65
+ /** Radius / area of mouse-driven distortion in UV space (0–1-ish) */
66
+ mouseDistortionRadius?: number;
67
+ /** How quickly mouse trails decay/fade (0.9=slow/wobbly, 0.99=fast/sharp) */
68
+ mouseDecayRate?: number;
69
+ mouseDarken?: number;
70
+ // Texture generation
71
+ enableProceduralTexture?: boolean;
72
+ textureVoidLikelihood?: number;
73
+ textureVoidWidthMin?: number;
74
+ textureVoidWidthMax?: number;
75
+ textureBandDensity?: number;
76
+ textureColorBlending?: number;
77
+ textureSeed?: number;
78
+ textureEase?: number;
79
+ proceduralBackgroundColor?: string;
80
+ textureShapeTriangles?: number;
81
+ textureShapeCircles?: number;
82
+ textureShapeBars?: number;
83
+ textureShapeSquiggles?: number;
43
84
  };
44
85
 
45
86
  export type NeatColor = {
@@ -86,11 +127,58 @@ export class NeatGradient implements NeatController {
86
127
  private _backgroundColor: string = "#FFFFFF";
87
128
  private _backgroundAlpha: number = 1.0;
88
129
 
130
+ // Flow field properties
131
+ private _flowDistortionA: number = 0;
132
+ private _flowDistortionB: number = 0;
133
+ private _flowScale: number = 1.0;
134
+ private _flowEase: number = 0.0;
135
+ private _flowEnabled: boolean = true;
136
+
137
+ // Mouse interaction properties
138
+ private _mouseDistortionStrength: number = 0.0;
139
+ private _mouseDistortionRadius: number = 0.25;
140
+ private _mouseDecayRate: number = 0.96;
141
+ private _mouseDarken: number = 0.0;
142
+ private _mouse: THREE.Vector2 = new THREE.Vector2(-1000, -1000);
143
+ private _mouseFBO: THREE.WebGLRenderTarget | null = null;
144
+ private _sceneMouse: THREE.Scene | null = null;
145
+ private _cameraMouse: THREE.OrthographicCamera | null = null;
146
+ private _mouseObjects: Array<{ mesh: THREE.Mesh, active: boolean }> = [];
147
+ private _currentBrush: number = 0;
148
+ private _mouseBrushBaseScale: number = 1;
149
+
150
+ // Texture generation properties
151
+ private _enableProceduralTexture: boolean = false;
152
+ private _textureVoidLikelihood: number = 0.45;
153
+ private _textureVoidWidthMin: number = 200;
154
+ private _textureVoidWidthMax: number = 486;
155
+ private _textureBandDensity: number = 2.15;
156
+ private _textureColorBlending: number = 0.01;
157
+ private _textureSeed: number = 333;
158
+ private _textureEase: number = 0.5;
159
+ private _proceduralTexture: THREE.Texture | null = null;
160
+ private _proceduralBackgroundColor: string = "#000000";
161
+
162
+ private _textureShapeTriangles: number = 20;
163
+ private _textureShapeCircles: number = 15;
164
+ private _textureShapeBars: number = 15;
165
+ private _textureShapeSquiggles: number = 10;
166
+
89
167
  private requestRef: number = -1;
90
168
  private sizeObserver: ResizeObserver;
91
169
  private sceneState: SceneState;
92
170
 
171
+ // Optimization: Cache uniforms to avoid lookups and object creation in render loop
172
+ private _cachedUniforms: NeatUniforms | null = null;
173
+ private _linkElement: HTMLAnchorElement | null = null;
174
+
93
175
  private _yOffset: number = 0;
176
+ private _yOffsetWaveMultiplier: number = 0.004;
177
+ private _yOffsetColorMultiplier: number = 0.004;
178
+ private _yOffsetFlowMultiplier: number = 0.004;
179
+
180
+ // For saving/restoring clear color
181
+ private _tempClearColor = new THREE.Color();
94
182
 
95
183
  constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
96
184
 
@@ -117,7 +205,35 @@ export class NeatGradient implements NeatController {
117
205
  backgroundAlpha = 1.0,
118
206
  resolution = 1,
119
207
  seed,
120
- yOffset = 0
208
+ yOffset = 0,
209
+ yOffsetWaveMultiplier = 4,
210
+ yOffsetColorMultiplier = 4,
211
+ yOffsetFlowMultiplier = 4,
212
+ // Flow field parameters
213
+ flowDistortionA = 0,
214
+ flowDistortionB = 0,
215
+ flowScale = 1.0,
216
+ flowEase = 0.0,
217
+ flowEnabled = true,
218
+ // Mouse interaction
219
+ mouseDistortionStrength = 0.0,
220
+ mouseDistortionRadius = 0.25,
221
+ mouseDecayRate = 0.96,
222
+ mouseDarken = 0.0,
223
+ // Texture generation
224
+ enableProceduralTexture = false,
225
+ textureVoidLikelihood = 0.45,
226
+ textureVoidWidthMin = 200,
227
+ textureVoidWidthMax = 486,
228
+ textureBandDensity = 2.15,
229
+ textureColorBlending = 0.01,
230
+ textureSeed = 333,
231
+ textureEase = 0.5,
232
+ proceduralBackgroundColor = "#000000",
233
+ textureShapeTriangles = 20,
234
+ textureShapeCircles = 15,
235
+ textureShapeBars = 15,
236
+ textureShapeSquiggles = 10,
121
237
  } = config;
122
238
 
123
239
 
@@ -147,84 +263,165 @@ export class NeatGradient implements NeatController {
147
263
  this.backgroundColor = backgroundColor;
148
264
  this.backgroundAlpha = backgroundAlpha;
149
265
  this.yOffset = yOffset;
150
-
266
+ this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
267
+ this.yOffsetColorMultiplier = yOffsetColorMultiplier;
268
+ this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
269
+
270
+ // Flow field
271
+ this.flowDistortionA = flowDistortionA;
272
+ this.flowDistortionB = flowDistortionB;
273
+ this.flowScale = flowScale;
274
+ this.flowEase = flowEase;
275
+ this.flowEnabled = flowEnabled;
276
+
277
+ // Mouse interaction
278
+ this.mouseDistortionStrength = mouseDistortionStrength;
279
+ this.mouseDistortionRadius = mouseDistortionRadius;
280
+ this.mouseDecayRate = mouseDecayRate;
281
+ this.mouseDarken = mouseDarken;
282
+
283
+ // Texture generation
284
+ this.enableProceduralTexture = enableProceduralTexture;
285
+ this.textureVoidLikelihood = textureVoidLikelihood;
286
+ this.textureVoidWidthMin = textureVoidWidthMin;
287
+ this.textureVoidWidthMax = textureVoidWidthMax;
288
+ this.textureBandDensity = textureBandDensity;
289
+ this.textureColorBlending = textureColorBlending;
290
+ this.textureSeed = textureSeed;
291
+ this.textureEase = textureEase;
292
+ this._proceduralBackgroundColor = proceduralBackgroundColor;
293
+
294
+ this._textureShapeTriangles = textureShapeTriangles;
295
+ this._textureShapeCircles = textureShapeCircles;
296
+ this._textureShapeBars = textureShapeBars;
297
+ this._textureShapeSquiggles = textureShapeSquiggles;
298
+
299
+ // FIX 1: Setup mouse resources BEFORE building the material/scene
300
+ // This ensures u_mouse_texture isn't null during material compilation
301
+ this._setupMouseInteraction();
151
302
  this.sceneState = this._initScene(resolution);
152
303
 
153
304
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
305
+
154
306
  const render = () => {
155
307
 
156
- const { renderer, camera, scene, meshes } = this.sceneState;
308
+ const { renderer, camera, scene } = this.sceneState;
309
+
310
+ // Optimization: check if cached link is still valid in DOM, otherwise search
157
311
  if (Math.floor(tick * 10) % 5 === 0) {
158
- addNeatLink(ref);
312
+ if (!this._linkElement || !document.contains(this._linkElement)) {
313
+ this._linkElement = addNeatLink(ref);
314
+ }
159
315
  }
160
316
 
161
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
162
- meshes.forEach((mesh) => {
163
-
164
- const width = this._ref.width,
165
- height = this._ref.height;
166
-
167
- const colors = [
168
- ...this._colors.map(color => {
169
- let threeColor = new THREE.Color();
170
- threeColor.setStyle(color.color, "");
171
- return ({
172
- is_active: color.enabled,
173
- color: threeColor,
174
- influence: color.influence
175
- });
176
- }),
177
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
178
- is_active: false,
179
- color: new THREE.Color(0x000000)
180
- }))
181
- ];
317
+ // Update Uniforms efficiently without creating new objects
318
+ if (this._cachedUniforms) {
319
+ const u = this._cachedUniforms;
182
320
 
183
321
  tick += clock.getDelta() * this._speed;
184
- // @ts-ignore
185
- mesh.material.uniforms.u_time.value = tick;
186
- // @ts-ignore
187
- mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
188
- // @ts-ignore
189
- mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
190
- // @ts-ignore
191
- mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
192
- // @ts-ignore
193
- mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
194
- // @ts-ignore
195
- mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
196
- // @ts-ignore
197
- mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
198
- // @ts-ignore
199
- mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
200
- // @ts-ignore
201
- mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
202
- // @ts-ignore
203
- mesh.material.uniforms.u_colors = { value: colors };
204
- // @ts-ignore
205
- mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
206
- // @ts-ignore
207
- mesh.material.uniforms.u_shadows = { value: this._shadows };
208
- // @ts-ignore
209
- mesh.material.uniforms.u_highlights = { value: this._highlights };
210
- // @ts-ignore
211
- mesh.material.uniforms.u_saturation = { value: this._saturation };
212
- // @ts-ignore
213
- mesh.material.uniforms.u_brightness = { value: this._brightness };
214
- // @ts-ignore
215
- mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
216
- // @ts-ignore
217
- mesh.material.uniforms.u_grain_sparsity = { value: this._grainSparsity };
218
- // @ts-ignore
219
- mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
220
- // @ts-ignore
221
- mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
222
- // @ts-ignore
223
- mesh.material.uniforms.u_y_offset = { value: this._yOffset };
224
- // @ts-ignore
225
- mesh.material.wireframe = this._wireframe;
226
- });
227
322
 
323
+ u.u_time.value = tick;
324
+ u.u_resolution.value.set(this._ref.width, this._ref.height);
325
+ u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
326
+
327
+ // Directly assign simple values
328
+ u.u_wave_frequency_x.value = this._waveFrequencyX;
329
+ u.u_wave_frequency_y.value = this._waveFrequencyY;
330
+ u.u_wave_amplitude.value = this._waveAmplitude;
331
+ u.u_color_blending.value = this._colorBlending;
332
+ u.u_shadows.value = this._shadows;
333
+ u.u_highlights.value = this._highlights;
334
+ u.u_saturation.value = this._saturation;
335
+ u.u_brightness.value = this._brightness;
336
+ u.u_grain_intensity.value = this._grainIntensity;
337
+ u.u_grain_sparsity.value = this._grainSparsity;
338
+ u.u_grain_speed.value = this._grainSpeed;
339
+ u.u_grain_scale.value = this._grainScale;
340
+ u.u_y_offset.value = this._yOffset;
341
+ u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
342
+ u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
343
+ u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
344
+ u.u_flow_distortion_a.value = this._flowDistortionA;
345
+ u.u_flow_distortion_b.value = this._flowDistortionB;
346
+ u.u_flow_scale.value = this._flowScale;
347
+ u.u_flow_ease.value = this._flowEase;
348
+ u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
349
+ u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
350
+ u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
351
+ u.u_mouse_darken.value = this._mouseDarken;
352
+ u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
353
+ u.u_procedural_texture.value = this._proceduralTexture;
354
+ u.u_texture_ease.value = this._textureEase;
355
+
356
+ // Optimized Color Update: Update the existing array objects instead of recreating array
357
+ const shaderColors = u.u_colors.value;
358
+ for(let i = 0; i < COLORS_COUNT; i++) {
359
+ if (i < this._colors.length) {
360
+ const c = this._colors[i];
361
+ shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
362
+ shaderColors[i].color.setStyle(c.color, "");
363
+ shaderColors[i].influence = c.influence || 0;
364
+ } else {
365
+ shaderColors[i].is_active = 0.0;
366
+ }
367
+ }
368
+
369
+ u.u_colors_count.value = COLORS_COUNT;
370
+ // Wireframe is a material property, not a uniform
371
+ // @ts-ignore - access material safely
372
+ this.sceneState.meshes[0].material.wireframe = this._wireframe;
373
+ }
374
+
375
+ // Render mouse interaction to FBO
376
+ if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
377
+ let hasActiveBrushes = false;
378
+
379
+ // Update mouse objects - decay rate controls how fast trails fade
380
+ for(let i = 0; i < this._mouseObjects.length; i++) {
381
+ const obj = this._mouseObjects[i];
382
+ if (obj.mesh.visible) {
383
+ hasActiveBrushes = true;
384
+ obj.mesh.rotation.z += 0.01;
385
+ if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
386
+ // Decay only affects opacity
387
+ obj.mesh.material.opacity *= this._mouseDecayRate;
388
+
389
+ if (obj.mesh.material.opacity < 0.01) {
390
+ obj.mesh.visible = false;
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ // FIX 2: Handle FBO Clearing correctly
397
+ // Store current clear color (likely the main background color)
398
+ renderer.getClearColor(this._tempClearColor);
399
+ const oldClearAlpha = renderer.getClearAlpha();
400
+
401
+ // Set clear color to Black/Transparent for the FBO.
402
+ // Important: If we use the main background color (e.g. White), the FBO
403
+ // will be white, causing 100% distortion everywhere.
404
+ renderer.setClearColor(0x000000, 0.0);
405
+
406
+ renderer.setRenderTarget(this._mouseFBO);
407
+ renderer.clear();
408
+
409
+ if (hasActiveBrushes) {
410
+ renderer.render(this._sceneMouse, this._cameraMouse);
411
+ }
412
+ renderer.setRenderTarget(null);
413
+
414
+ // Restore main background color for the actual scene render
415
+ renderer.setClearColor(this._tempClearColor, oldClearAlpha);
416
+
417
+ // Update mouse texture uniform
418
+ if (this._cachedUniforms) {
419
+ this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
420
+ }
421
+ }
422
+
423
+ // Ensure we set the clear color for the main scene explicitly before rendering
424
+ renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
228
425
  renderer.render(scene, camera);
229
426
  this.requestRef = requestAnimationFrame(render);
230
427
  };
@@ -238,6 +435,19 @@ export class NeatGradient implements NeatController {
238
435
 
239
436
  this.sceneState.renderer.setSize(width, height, false);
240
437
  updateCamera(this.sceneState.camera, width, height);
438
+
439
+ // FIX 3: Update Mouse FBO and Camera on resize
440
+ // If we don't do this, mouse coordinates map incorrectly after a resize
441
+ if (this._mouseFBO && this._cameraMouse) {
442
+ const fSize = height / 2;
443
+ const aspect = width / height;
444
+ this._mouseFBO.setSize(width / 2, height / 2);
445
+ this._cameraMouse.left = -fSize * aspect;
446
+ this._cameraMouse.right = fSize * aspect;
447
+ this._cameraMouse.top = fSize;
448
+ this._cameraMouse.bottom = -fSize;
449
+ this._cameraMouse.updateProjectionMatrix();
450
+ }
241
451
  };
242
452
 
243
453
  this.sizeObserver = new ResizeObserver(entries => {
@@ -254,6 +464,18 @@ export class NeatGradient implements NeatController {
254
464
  if (this) {
255
465
  cancelAnimationFrame(this.requestRef);
256
466
  this.sizeObserver.disconnect();
467
+
468
+ // Cleanup WebGL resources
469
+ if (this.sceneState) {
470
+ this.sceneState.renderer.dispose();
471
+ this.sceneState.meshes.forEach(m => {
472
+ m.geometry.dispose();
473
+ if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
474
+ else m.material.dispose();
475
+ });
476
+ }
477
+ if (this._mouseFBO) this._mouseFBO.dispose();
478
+ if (this._proceduralTexture) this._proceduralTexture.dispose();
257
479
  }
258
480
  }
259
481
 
@@ -348,11 +570,178 @@ export class NeatGradient implements NeatController {
348
570
  this._yOffset = yOffset;
349
571
  }
350
572
 
573
+ get yOffsetWaveMultiplier(): number {
574
+ return this._yOffsetWaveMultiplier * 1000;
575
+ }
576
+
577
+ set yOffsetWaveMultiplier(value: number) {
578
+ this._yOffsetWaveMultiplier = value / 1000;
579
+ }
580
+
581
+ get yOffsetColorMultiplier(): number {
582
+ return this._yOffsetColorMultiplier * 1000;
583
+ }
584
+
585
+ set yOffsetColorMultiplier(value: number) {
586
+ this._yOffsetColorMultiplier = value / 1000;
587
+ }
588
+
589
+ get yOffsetFlowMultiplier(): number {
590
+ return this._yOffsetFlowMultiplier * 1000;
591
+ }
592
+
593
+ set yOffsetFlowMultiplier(value: number) {
594
+ this._yOffsetFlowMultiplier = value / 1000;
595
+ }
596
+
597
+ set flowDistortionA(value: number) {
598
+ this._flowDistortionA = value;
599
+ }
600
+
601
+ set flowDistortionB(value: number) {
602
+ this._flowDistortionB = value;
603
+ }
604
+
605
+ set flowScale(value: number) {
606
+ this._flowScale = value;
607
+ }
608
+
609
+ set flowEase(value: number) {
610
+ this._flowEase = value;
611
+ }
612
+
613
+ set flowEnabled(value: boolean) {
614
+ this._flowEnabled = value;
615
+ }
616
+
617
+ get flowEnabled(): boolean {
618
+ return this._flowEnabled;
619
+ }
620
+
621
+
622
+ set mouseDistortionStrength(value: number) {
623
+ this._mouseDistortionStrength = Math.max(0, value);
624
+ }
625
+
626
+ set mouseDistortionRadius(value: number) {
627
+ // Clamp to a sane range in UV space
628
+ this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
629
+ // Update brush scale when radius changes
630
+ this._updateBrushScale();
631
+ }
632
+
633
+ _updateBrushScale() {
634
+ if (!this._mouseObjects || this._mouseObjects.length === 0) return;
635
+ // Radius directly controls the brush scale
636
+ // Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
637
+ this._mouseBrushBaseScale = this._mouseDistortionRadius;
638
+ }
639
+
640
+ set mouseDecayRate(value: number) {
641
+ // Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
642
+ this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
643
+ }
644
+
645
+ set mouseDarken(value: number) {
646
+ this._mouseDarken = value;
647
+ }
648
+
649
+ set enableProceduralTexture(value: boolean) {
650
+ this._enableProceduralTexture = value;
651
+ if (value && !this._proceduralTexture) {
652
+ this._proceduralTexture = this._createProceduralTexture();
653
+ }
654
+ }
655
+
656
+ set textureVoidLikelihood(value: number) {
657
+ this._textureVoidLikelihood = value;
658
+ if (this._enableProceduralTexture) {
659
+ this._proceduralTexture = this._createProceduralTexture();
660
+ }
661
+ }
662
+
663
+ set textureVoidWidthMin(value: number) {
664
+ this._textureVoidWidthMin = value;
665
+ if (this._enableProceduralTexture) {
666
+ this._proceduralTexture = this._createProceduralTexture();
667
+ }
668
+ }
669
+
670
+ set textureVoidWidthMax(value: number) {
671
+ this._textureVoidWidthMax = value;
672
+ if (this._enableProceduralTexture) {
673
+ this._proceduralTexture = this._createProceduralTexture();
674
+ }
675
+ }
676
+
677
+ set textureBandDensity(value: number) {
678
+ this._textureBandDensity = value;
679
+ if (this._enableProceduralTexture) {
680
+ this._proceduralTexture = this._createProceduralTexture();
681
+ }
682
+ }
683
+
684
+ set textureColorBlending(value: number) {
685
+ this._textureColorBlending = value;
686
+ if (this._enableProceduralTexture) {
687
+ this._proceduralTexture = this._createProceduralTexture();
688
+ }
689
+ }
690
+
691
+ set textureSeed(value: number) {
692
+ this._textureSeed = value;
693
+ if (this._enableProceduralTexture) {
694
+ this._proceduralTexture = this._createProceduralTexture();
695
+ }
696
+ }
697
+
698
+ get textureEase(): number {
699
+ return this._textureEase;
700
+ }
701
+
702
+ set textureEase(value: number) {
703
+ this._textureEase = value;
704
+ }
705
+
706
+ set proceduralBackgroundColor(value: string) {
707
+ this._proceduralBackgroundColor = value;
708
+ if (this._enableProceduralTexture) {
709
+ this._proceduralTexture = this._createProceduralTexture();
710
+ }
711
+ }
712
+
713
+ set textureShapeTriangles(value: number) {
714
+ this._textureShapeTriangles = value;
715
+ if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
716
+ }
717
+ set textureShapeCircles(value: number) {
718
+ this._textureShapeCircles = value;
719
+ if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
720
+ }
721
+ set textureShapeBars(value: number) {
722
+ this._textureShapeBars = value;
723
+ if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
724
+ }
725
+ set textureShapeSquiggles(value: number) {
726
+ this._textureShapeSquiggles = value;
727
+ if (this._enableProceduralTexture) this._proceduralTexture = this._createProceduralTexture();
728
+ }
729
+
351
730
  _initScene(resolution: number): SceneState {
352
731
 
353
732
  const width = this._ref.width,
354
733
  height = this._ref.height;
355
734
 
735
+ // Cleanup existing renderer if needed
736
+ if (this.sceneState && this.sceneState.renderer) {
737
+ this.sceneState.renderer.dispose();
738
+ this.sceneState.meshes.forEach(m => {
739
+ m.geometry.dispose();
740
+ if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
741
+ else m.material.dispose();
742
+ });
743
+ }
744
+
356
745
  const renderer = new THREE.WebGLRenderer({
357
746
  // antialias: true,
358
747
  alpha: true,
@@ -392,17 +781,13 @@ export class NeatGradient implements NeatController {
392
781
 
393
782
  _buildMaterial(width: number, height: number) {
394
783
 
395
- const colors = [
396
- ...this._colors.map(color => ({
397
- is_active: color.enabled,
398
- color: new THREE.Color(color.color),
399
- influence: color.influence
400
- })),
401
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
402
- is_active: false,
403
- color: new THREE.Color(0x000000)
404
- }))
405
- ];
784
+ // Initialize stable array structure for colors
785
+ // We create 6 objects and just update them in the render loop to avoid GC
786
+ const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
787
+ is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
788
+ color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
789
+ influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
790
+ }));
406
791
 
407
792
  const uniforms = {
408
793
  u_time: { value: 0 },
@@ -421,6 +806,29 @@ export class NeatGradient implements NeatController {
421
806
  u_grain_sparsity: { value: this._grainSparsity },
422
807
  u_grain_scale: { value: this._grainScale },
423
808
  u_grain_speed: { value: this._grainSpeed },
809
+ // Flow field
810
+ u_flow_distortion_a: { value: this._flowDistortionA },
811
+ u_flow_distortion_b: { value: this._flowDistortionB },
812
+ u_flow_scale: { value: this._flowScale },
813
+ u_flow_ease: { value: this._flowEase },
814
+ u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
815
+ // Y offset multipliers
816
+ u_y_offset: { value: this._yOffset },
817
+ u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
818
+ u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
819
+ u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
820
+ // Mouse interaction
821
+ u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
822
+ u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
823
+ u_mouse_darken: { value: this._mouseDarken },
824
+ u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
825
+ // Procedural texture
826
+ u_procedural_texture: { value: this._proceduralTexture },
827
+ u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
828
+ u_texture_ease: { value: this._textureEase },
829
+ u_saturation: { value: this._saturation },
830
+ u_brightness: { value: this._brightness },
831
+ u_color_blending: { value: this._colorBlending }
424
832
  };
425
833
 
426
834
  const material = new THREE.ShaderMaterial({
@@ -429,10 +837,287 @@ export class NeatGradient implements NeatController {
429
837
  fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
430
838
  });
431
839
 
840
+ // Cache the uniforms object for direct access in render loop
841
+ this._cachedUniforms = uniforms as unknown as NeatUniforms;
842
+
432
843
  material.wireframe = WIREFRAME;
433
844
  return material;
434
845
  }
435
846
 
847
+ _setupMouseInteraction() {
848
+ if (!this._ref) return;
849
+
850
+ const width = this._ref.width;
851
+ const height = this._ref.height;
852
+
853
+ // Create mouse FBO
854
+ this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
855
+
856
+ // Create mouse scene and camera
857
+ this._sceneMouse = new THREE.Scene();
858
+ const fSize = height / 2;
859
+ const aspect = width / height;
860
+
861
+ // FIX 4: Ensure near plane allows viewing objects at Z=0
862
+ // Near -100 is safer for objects at 0
863
+ this._cameraMouse = new THREE.OrthographicCamera(
864
+ -fSize * aspect, fSize * aspect,
865
+ fSize, -fSize,
866
+ 0, 10000
867
+ );
868
+ this._cameraMouse.position.set(0, 0, 100);
869
+
870
+ // Create brush texture - More visible and impactful
871
+ const brushCanvas = document.createElement('canvas');
872
+ brushCanvas.width = 128;
873
+ brushCanvas.height = 128;
874
+ const bCtx = brushCanvas.getContext('2d');
875
+ if (bCtx) {
876
+ const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
877
+ // Match reference implementation's stronger gradient
878
+ grd.addColorStop(0, 'rgba(255,255,255,0.8)');
879
+ grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
880
+ grd.addColorStop(1, 'rgba(255,255,255,0)');
881
+ bCtx.fillStyle = grd;
882
+ bCtx.fillRect(0, 0, 128, 128);
883
+ }
884
+ const brushTex = new THREE.CanvasTexture(brushCanvas);
885
+ const brushMat = new THREE.MeshBasicMaterial({
886
+ map: brushTex,
887
+ transparent: true,
888
+ opacity: 1.0,
889
+ depthTest: false,
890
+ blending: THREE.AdditiveBlending // Additive blending for better accumulation
891
+ });
892
+ // Brush geometry size - will be scaled by radius parameter
893
+ const brushGeo = new THREE.PlaneGeometry(200, 200);
894
+
895
+ // Create brush pool
896
+ const brushPoolSize = 50;
897
+ for (let i = 0; i < brushPoolSize; i++) {
898
+ const m = new THREE.Mesh(brushGeo, brushMat.clone());
899
+ m.visible = false;
900
+ this._sceneMouse!.add(m);
901
+ this._mouseObjects.push({ mesh: m, active: false });
902
+ }
903
+
904
+ // Initialize brush scale based on current radius
905
+ this._updateBrushScale();
906
+
907
+ // Add mouse move listener
908
+ this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
909
+ }
910
+
911
+ _onMouseMove(e: MouseEvent) {
912
+ if (!this._ref || !this._sceneMouse) return;
913
+ const rect = this._ref.getBoundingClientRect();
914
+ const width = this._ref.width;
915
+ const height = this._ref.height;
916
+
917
+ this._mouse.x = e.clientX - rect.left - width / 2;
918
+ this._mouse.y = -(e.clientY - rect.top - height / 2);
919
+
920
+ const brush = this._mouseObjects[this._currentBrush];
921
+ brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
922
+ brush.active = true;
923
+ brush.mesh.visible = true;
924
+ brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
925
+ brush.mesh.rotation.z = Math.random() * Math.PI * 2;
926
+ if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
927
+ brush.mesh.material.opacity = 1.0;
928
+ }
929
+ this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
930
+ }
931
+
932
+ _createProceduralTexture(): THREE.Texture {
933
+ // Texture size - 1024 provides good balance between quality and performance
934
+ // Can be increased to 2048 for even better quality if needed
935
+ const texSize = 1024;
936
+ const sourceCanvas = document.createElement('canvas');
937
+ sourceCanvas.width = texSize;
938
+ sourceCanvas.height = texSize;
939
+ const sCtx = sourceCanvas.getContext('2d');
940
+ if (!sCtx) return new THREE.Texture();
941
+
942
+ let seed = this._textureSeed;
943
+ const baseSeed = this._textureSeed;
944
+
945
+ function random() {
946
+ const x = Math.sin(seed++) * 10000;
947
+ return x - Math.floor(x);
948
+ }
949
+
950
+ // Helper to reset seed for isolated shape generation
951
+ const setSeed = (offset: number) => {
952
+ seed = baseSeed + offset;
953
+ };
954
+
955
+ const colors = this._colors.filter(c => c.enabled).map(c => c.color);
956
+ if (colors.length === 0) return new THREE.Texture();
957
+
958
+ // Helper functions
959
+ function hexToRgb(hex: string) {
960
+ const bigint = parseInt(hex.replace('#', ''), 16);
961
+ return {
962
+ r: (bigint >> 16) & 255,
963
+ g: (bigint >> 8) & 255,
964
+ b: bigint & 255
965
+ };
966
+ }
967
+
968
+ function rgbToHex(r: number, g: number, b: number) {
969
+ return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
970
+ }
971
+
972
+ const getInterColor = () => {
973
+ const c1 = colors[Math.floor(random() * colors.length)];
974
+ const c2 = colors[Math.floor(random() * colors.length)];
975
+ const mix = random() * this._textureColorBlending;
976
+ const rgb1 = hexToRgb(c1);
977
+ const rgb2 = hexToRgb(c2);
978
+ const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
979
+ const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
980
+ const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
981
+ return rgbToHex(r, g, b);
982
+ };
983
+
984
+ // === SOURCE CANVAS ===
985
+ // Base with procedural background color so even sparse areas pick it up
986
+ const baseColor = this._proceduralBackgroundColor || "#000000";
987
+ sCtx.fillStyle = baseColor;
988
+ sCtx.fillRect(0, 0, texSize, texSize);
989
+
990
+ // Then lay a vertical gradient of mixed colors on top for richness
991
+ const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
992
+ bgGrad.addColorStop(0, getInterColor());
993
+ bgGrad.addColorStop(1, getInterColor());
994
+ sCtx.fillStyle = bgGrad;
995
+ sCtx.fillRect(0, 0, texSize, texSize);
996
+
997
+ // Triangles: use configurable count
998
+ for (let i = 0; i < this._textureShapeTriangles; i++) {
999
+ sCtx.fillStyle = getInterColor();
1000
+ sCtx.beginPath();
1001
+ const x = random() * texSize;
1002
+ const y = random() * texSize;
1003
+ const s = 100 + random() * 300;
1004
+ sCtx.moveTo(x, y);
1005
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1006
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1007
+ sCtx.fill();
1008
+ }
1009
+
1010
+ // Circles / rings: use configurable count
1011
+ for (let i = 0; i < this._textureShapeCircles; i++) {
1012
+ sCtx.strokeStyle = getInterColor();
1013
+ sCtx.lineWidth = 10 + random() * 50;
1014
+ sCtx.beginPath();
1015
+ const x = random() * texSize;
1016
+ const y = random() * texSize;
1017
+ const r = 50 + random() * 150;
1018
+ sCtx.arc(x, y, r, 0, Math.PI * 2);
1019
+ sCtx.stroke();
1020
+ }
1021
+
1022
+ // Bars: use configurable count
1023
+ for (let i = 0; i < this._textureShapeBars; i++) {
1024
+ sCtx.fillStyle = getInterColor();
1025
+ sCtx.save();
1026
+ sCtx.translate(random() * texSize, random() * texSize);
1027
+ sCtx.rotate(random() * Math.PI);
1028
+ sCtx.fillRect(-150, -25, 300, 50);
1029
+ sCtx.restore();
1030
+ }
1031
+
1032
+ // Squiggles: use configurable count
1033
+ sCtx.lineWidth = 15;
1034
+ sCtx.lineCap = 'round';
1035
+ for (let i = 0; i < this._textureShapeSquiggles; i++) {
1036
+ sCtx.strokeStyle = getInterColor();
1037
+ sCtx.beginPath();
1038
+ let x = random() * texSize;
1039
+ let y = random() * texSize;
1040
+ sCtx.moveTo(x, y);
1041
+ for (let j = 0; j < 4; j++) {
1042
+ sCtx.bezierCurveTo(
1043
+ x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1044
+ x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1045
+ x + (random() - 0.5) * 300, y + (random() - 0.5) * 300
1046
+ );
1047
+ x += (random() - 0.5) * 300;
1048
+ y += (random() - 0.5) * 300;
1049
+ }
1050
+ sCtx.stroke();
1051
+ }
1052
+
1053
+ // === MASKED CANVAS ===
1054
+ // Masking: Seed isolation
1055
+ setSeed(50000);
1056
+ const canvas = document.createElement('canvas');
1057
+ canvas.width = texSize;
1058
+ canvas.height = texSize;
1059
+ const ctx = canvas.getContext('2d');
1060
+ if (!ctx) return new THREE.Texture();
1061
+
1062
+ // Start filled with the chosen void color so gaps show that color
1063
+ ctx.fillStyle = baseColor;
1064
+ ctx.fillRect(0, 0, texSize, texSize);
1065
+
1066
+ // Determine layout segments (matter vs void)
1067
+ let layoutHead = 0;
1068
+ const segments: Array<{ type: 'void' | 'matter', x: number, width: number }> = [];
1069
+
1070
+ while (layoutHead < texSize) {
1071
+ const isVoid = random() < this._textureVoidLikelihood;
1072
+ if (isVoid) {
1073
+ const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
1074
+ segments.push({ type: 'void', x: layoutHead, width: w });
1075
+ layoutHead += w;
1076
+ } else {
1077
+ const w = 50 + random() * 200;
1078
+ segments.push({ type: 'matter', x: layoutHead, width: w });
1079
+ layoutHead += w;
1080
+ }
1081
+ }
1082
+
1083
+ // Render only matter bands from the source into the masked canvas
1084
+ for (const seg of segments) {
1085
+ if (seg.type === 'matter') {
1086
+ const startX = seg.x;
1087
+ const endX = Math.min(seg.x + seg.width, texSize);
1088
+ let currentX = startX;
1089
+
1090
+ while (currentX < endX) {
1091
+ const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
1092
+ const sourceX = Math.floor(random() * texSize);
1093
+ ctx.drawImage(
1094
+ sourceCanvas,
1095
+ sourceX, 0, stripeWidth, texSize,
1096
+ currentX, 0, stripeWidth, texSize
1097
+ );
1098
+ currentX += stripeWidth;
1099
+ }
1100
+ }
1101
+ // void segments: leave as baseColor
1102
+ }
1103
+
1104
+ const tex = new THREE.CanvasTexture(canvas);
1105
+ // Use mipmapping for better quality when texture is scaled
1106
+ tex.minFilter = THREE.LinearMipmapLinearFilter;
1107
+ tex.magFilter = THREE.LinearFilter;
1108
+ tex.wrapS = THREE.RepeatWrapping;
1109
+ tex.wrapT = THREE.RepeatWrapping;
1110
+
1111
+ // Enable anisotropic filtering for much better quality when texture is stretched
1112
+ // 16 is a commonly supported value that dramatically improves quality
1113
+ tex.anisotropy = 16;
1114
+
1115
+ // Ensure mipmaps are generated
1116
+ tex.needsUpdate = true;
1117
+
1118
+ return tex;
1119
+ }
1120
+
436
1121
 
437
1122
  }
438
1123
 
@@ -475,41 +1160,62 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
475
1160
 
476
1161
  function buildVertexShader() {
477
1162
  return `
478
-
479
1163
  void main() {
480
-
481
1164
  vUv = uv;
482
1165
 
1166
+ // SCROLLING LOGIC
1167
+ // Separate multipliers for wave, color, and flow offsets
1168
+ float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
1169
+ float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
1170
+ float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
1171
+
1172
+ // 1. DISPLACEMENT (WAVES)
1173
+ // We add waveOffset to Y to scroll the wave pattern
483
1174
  v_displacement_amount = cnoise( vec3(
484
1175
  u_wave_frequency_x * position.x + u_time,
485
- u_wave_frequency_y * position.y + u_time,
1176
+ u_wave_frequency_y * (position.y + waveOffset) + u_time,
486
1177
  u_time
487
1178
  ));
488
1179
 
489
- vec3 color;
1180
+ // 2. FLOW FIELD
1181
+ // Apply flow offset to scroll the flow field mask
1182
+ vec2 baseUv = vUv;
1183
+ baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
1184
+ vec2 flowUv = baseUv;
1185
+
1186
+ if (u_flow_enabled > 0.5) {
1187
+ if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
1188
+ vec2 ppp = -1.0 + 2.0 * baseUv;
1189
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1190
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1191
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1192
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1193
+
1194
+ float r = length(ppp);
1195
+ flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
1196
+ }
1197
+ }
490
1198
 
491
- // float t = mod(u_base_color, 100.0);
492
- color = u_colors[0].color;
1199
+ // Pass the standard flow UV to fragment shader (for mouse/texture)
1200
+ vFlowUv = flowUv;
493
1201
 
494
- // Apply y_offset to the noise coordinates
495
- vec2 noise_cord = vUv * u_color_pressure;
496
- // Apply the y-offset to shift the pattern vertically (1:1 pixel ratio)
497
- // Scale the offset to match the UV coordinate space
498
- float scaledOffset = u_y_offset / u_resolution.y;
499
- noise_cord.y -= scaledOffset;
1202
+ // 3. COLOR MIXING
1203
+ // We take the computed flow UVs and apply the color offset
1204
+ // Scale by plane height to match wave offset speed (world space vs UV space)
1205
+ vec3 color = u_colors[0].color;
1206
+ vec2 adjustedUv = flowUv;
1207
+ adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
500
1208
 
1209
+ vec2 noise_cord = adjustedUv * u_color_pressure;
501
1210
  const float minNoise = .0;
502
1211
  const float maxNoise = .9;
503
1212
 
504
1213
  for (int i = 1; i < u_colors_count; i++) {
505
-
506
- if(u_colors[i].is_active == 1.0){
1214
+ if(u_colors[i].is_active > 0.5){
507
1215
  float noiseFlow = (1. + float(i)) / 30.;
508
1216
  float noiseSpeed = (1. + float(i)) * 0.11;
509
1217
  float noiseSeed = 13. + float(i) * 7.;
510
1218
 
511
- int reverseIndex = u_colors_count - i;
512
-
513
1219
  float noise = snoise(
514
1220
  vec3(
515
1221
  noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
@@ -519,16 +1225,15 @@ void main() {
519
1225
  ) - (.1 * float(i)) + (.5 * u_color_blending);
520
1226
 
521
1227
  noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
522
- vec3 nextColor = u_colors[i].color;
523
- color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
1228
+ color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
524
1229
  }
525
1230
  }
526
1231
 
527
1232
  v_color = color;
528
1233
 
1234
+ // 4. VERTEX POSITION
529
1235
  vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
530
1236
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
531
-
532
1237
  v_new_position = gl_Position;
533
1238
  }
534
1239
  `;
@@ -553,34 +1258,72 @@ float fbm(vec3 x) {
553
1258
  }
554
1259
 
555
1260
  void main() {
556
- vec3 color = v_color;
1261
+ // MOUSE DISTORTION
1262
+ vec2 finalUv = vFlowUv;
1263
+
1264
+ if (u_mouse_distortion_strength > 0.0) {
1265
+ vec4 mouseColor = texture2D(u_mouse_texture, vUv);
1266
+ float mouseValue = mouseColor.r;
1267
+
1268
+ if (mouseValue > 0.001) {
1269
+ float distortionAmount = mouseValue * u_mouse_distortion_strength;
1270
+ vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
1271
+ finalUv -= mouseDisp;
1272
+ }
1273
+ }
1274
+
1275
+ vec3 baseColor;
1276
+
1277
+ if (u_enable_procedural_texture > 0.5) {
1278
+ // Calculate flow field distance for ease effect
1279
+ vec2 ppp = -1.0 + 2.0 * finalUv;
1280
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1281
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1282
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1283
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1284
+ float r = length(ppp); // Flow distance
1285
+
1286
+ // Ease blending: 0 = topographic (flow), 1 = image (UV)
1287
+ float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
1288
+ float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
1289
+ vec2 texUv = vec2(vx, vy);
1290
+
1291
+ // PARALLAX SCROLLING
1292
+ // We manually apply a smaller offset here to make the texture lag behind
1293
+ float parallaxFactor = 0.25; // 25% speed of the color mixing
1294
+ texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
1295
+
1296
+ texUv *= 1.5; // Tiling scale
1297
+
1298
+ vec4 texSample = texture2D(u_procedural_texture, texUv);
1299
+ baseColor = texSample.rgb;
1300
+ } else {
1301
+ baseColor = v_color;
1302
+ }
1303
+
1304
+ vec3 color = baseColor;
1305
+
1306
+ // Post-processing
557
1307
  color += pow(v_displacement_amount, 1.0) * u_highlights;
558
1308
  color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
559
1309
  color = saturation(color, 1.0 + u_saturation);
560
1310
  color = color * u_brightness;
561
1311
 
562
- // Generate grain using fbm
1312
+ // Grain
563
1313
  vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
564
1314
  float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
565
1315
 
566
- // Center the grain around zero
567
1316
  grain = grain * 0.5 + 0.5;
568
1317
  grain -= 0.5;
569
-
570
- // Add sparsity control
571
1318
  grain = (grain > u_grain_sparsity) ? grain : 0.0;
572
-
573
- // Apply grain intensity
574
1319
  grain *= u_grain_intensity;
575
1320
 
576
- // Add grain to color
577
1321
  color += vec3(grain);
578
1322
 
579
1323
  gl_FragColor = vec4(color, 1.0);
580
1324
  }
581
1325
  `;
582
1326
  }
583
-
584
1327
  const buildUniforms = () => `
585
1328
  precision highp float;
586
1329
 
@@ -613,12 +1356,34 @@ uniform float u_brightness;
613
1356
  uniform float u_color_blending;
614
1357
 
615
1358
  uniform int u_colors_count;
616
- uniform Color u_colors[5];
1359
+ uniform Color u_colors[6];
617
1360
  uniform vec2 u_resolution;
618
1361
 
619
1362
  uniform float u_y_offset;
1363
+ uniform float u_y_offset_wave_multiplier;
1364
+ uniform float u_y_offset_color_multiplier;
1365
+ uniform float u_y_offset_flow_multiplier;
1366
+
1367
+ // Flow field uniforms
1368
+ uniform float u_flow_distortion_a;
1369
+ uniform float u_flow_distortion_b;
1370
+ uniform float u_flow_scale;
1371
+ uniform float u_flow_ease;
1372
+ uniform float u_flow_enabled;
1373
+
1374
+ // Mouse interaction uniforms
1375
+ uniform float u_mouse_distortion_strength;
1376
+ uniform float u_mouse_distortion_radius;
1377
+ uniform float u_mouse_darken;
1378
+ uniform sampler2D u_mouse_texture;
1379
+
1380
+ // Procedural texture uniforms
1381
+ uniform sampler2D u_procedural_texture;
1382
+ uniform float u_enable_procedural_texture;
1383
+ uniform float u_texture_ease;
620
1384
 
621
1385
  varying vec2 vUv;
1386
+ varying vec2 vFlowUv;
622
1387
  varying vec4 v_new_position;
623
1388
  varying vec3 v_color;
624
1389
  varying float v_displacement_amount;
@@ -627,69 +1392,55 @@ varying float v_displacement_amount;
627
1392
 
628
1393
  const buildNoise = () => `
629
1394
 
630
- vec3 mod289(vec3 x)
631
- {
632
- return x - floor(x * (1.0 / 289.0)) * 289.0;
633
- }
634
-
635
- vec4 mod289(vec4 x)
636
- {
637
- return x - floor(x * (1.0 / 289.0)) * 289.0;
1395
+ // 1. REPLACEMENT PERMUTE:
1396
+ // Uses a hash function (fract/sin) instead of a modular lookup table.
1397
+ vec4 permute(vec4 x) {
1398
+ return floor(fract(sin(x) * 43758.5453123) * 289.0);
638
1399
  }
639
1400
 
640
- vec4 permute(vec4 x)
641
- {
642
- return mod289(((x*34.0)+1.0)*x);
643
- }
644
-
645
- vec4 taylorInvSqrt(vec4 r)
646
- {
1401
+ // Taylor Inverse Sqrt
1402
+ vec4 taylorInvSqrt(vec4 r) {
647
1403
  return 1.79284291400159 - 0.85373472095314 * r;
648
1404
  }
649
1405
 
1406
+ // Fade function
650
1407
  vec3 fade(vec3 t) {
651
1408
  return t*t*t*(t*(t*6.0-15.0)+10.0);
652
1409
  }
653
1410
 
654
- float snoise(vec3 v)
655
- {
1411
+ // 3D Simplex Noise
1412
+ float snoise(vec3 v) {
656
1413
  const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
657
1414
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
658
1415
 
659
- // First corner
1416
+ // First corner
660
1417
  vec3 i = floor(v + dot(v, C.yyy) );
661
1418
  vec3 x0 = v - i + dot(i, C.xxx) ;
662
1419
 
663
- // Other corners
1420
+ // Other corners
664
1421
  vec3 g = step(x0.yzx, x0.xyz);
665
1422
  vec3 l = 1.0 - g;
666
1423
  vec3 i1 = min( g.xyz, l.zxy );
667
1424
  vec3 i2 = max( g.xyz, l.zxy );
668
1425
 
669
- // x0 = x0 - 0.0 + 0.0 * C.xxx;
670
- // x1 = x0 - i1 + 1.0 * C.xxx;
671
- // x2 = x0 - i2 + 2.0 * C.xxx;
672
- // x3 = x0 - 1.0 + 3.0 * C.xxx;
673
1426
  vec3 x1 = x0 - i1 + C.xxx;
674
- vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
675
- vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
1427
+ vec3 x2 = x0 - i2 + C.yyy;
1428
+ vec3 x3 = x0 - D.yyy;
676
1429
 
677
- // Permutations
678
- i = mod289(i);
1430
+ // Permutations
679
1431
  vec4 p = permute( permute( permute(
680
1432
  i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
681
1433
  + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
682
1434
  + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
683
1435
 
684
- // Gradients: 7x7 points over a square, mapped onto an octahedron.
685
- // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
1436
+ // Gradients
686
1437
  float n_ = 0.142857142857; // 1.0/7.0
687
1438
  vec3 ns = n_ * D.wyz - D.xzx;
688
1439
 
689
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
1440
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
690
1441
 
691
1442
  vec4 x_ = floor(j * ns.z);
692
- vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
1443
+ vec4 y_ = floor(j - 7.0 * x_ );
693
1444
 
694
1445
  vec4 x = x_ *ns.x + ns.yyyy;
695
1446
  vec4 y = y_ *ns.x + ns.yyyy;
@@ -698,8 +1449,6 @@ float snoise(vec3 v)
698
1449
  vec4 b0 = vec4( x.xy, y.xy );
699
1450
  vec4 b1 = vec4( x.zw, y.zw );
700
1451
 
701
- //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
702
- //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
703
1452
  vec4 s0 = floor(b0)*2.0 + 1.0;
704
1453
  vec4 s1 = floor(b1)*2.0 + 1.0;
705
1454
  vec4 sh = -step(h, vec4(0.0));
@@ -712,14 +1461,14 @@ float snoise(vec3 v)
712
1461
  vec3 p2 = vec3(a1.xy,h.z);
713
1462
  vec3 p3 = vec3(a1.zw,h.w);
714
1463
 
715
- //Normalise gradients
1464
+ // Normalise gradients
716
1465
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
717
1466
  p0 *= norm.x;
718
1467
  p1 *= norm.y;
719
1468
  p2 *= norm.z;
720
1469
  p3 *= norm.w;
721
1470
 
722
- // Mix final noise value
1471
+ // Mix final noise value
723
1472
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
724
1473
  m = m * m;
725
1474
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
@@ -729,12 +1478,11 @@ float snoise(vec3 v)
729
1478
  // Classic Perlin noise
730
1479
  float cnoise(vec3 P)
731
1480
  {
732
- vec3 Pi0 = floor(P); // Integer part for indexing
733
- vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
734
- Pi0 = mod289(Pi0);
735
- Pi1 = mod289(Pi1);
736
- vec3 Pf0 = fract(P); // Fractional part for interpolation
737
- vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
1481
+ vec3 Pi0 = floor(P);
1482
+ vec3 Pi1 = Pi0 + vec3(1.0);
1483
+
1484
+ vec3 Pf0 = fract(P);
1485
+ vec3 Pf1 = Pf0 - vec3(1.0);
738
1486
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
739
1487
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
740
1488
  vec4 iz0 = Pi0.zzzz;
@@ -795,47 +1543,7 @@ float cnoise(vec3 P)
795
1543
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
796
1544
  return 2.2 * n_xyz;
797
1545
  }
798
-
799
- // YUV to RGB matrix
800
- mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
801
- 1.0, -0.39465, -0.58060,
802
- 1.0, 2.03211, 0.0);
803
-
804
- // RGB to YUV matrix
805
- mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
806
- -0.09991, -0.33609, 0.43600,
807
- 0.615, -0.5586, -0.05639);
808
-
809
- vec3 oklab2rgb(vec3 linear)
810
- {
811
- const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
812
- 0.5362752080, 0.6807189584, 0.2818474174,
813
- 0.0514575653, 0.1074065790, 0.6302613616);
814
-
815
- const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
816
- +0.7936177850, -2.4285922050, +0.7827717662,
817
- -0.0040720468, +0.4505937099, -0.8086757660);
818
-
819
- vec3 lms = im1 * linear;
820
-
821
- return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
822
- }
823
-
824
- vec3 rgb2oklab(vec3 oklab)
825
- {
826
- const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
827
- +0.396337777, -0.105561346, -0.089484178,
828
- +0.215803757, -0.063854173, -1.291485548);
829
-
830
- const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
831
- -3.307216883, +2.609332323, -0.703476310,
832
- +0.230759054, -0.341134429, +1.706862569);
833
- vec3 lms = m1 * oklab;
834
-
835
- return m2 * (lms * lms * lms);
836
- }
837
-
838
- `;
1546
+ `;
839
1547
 
840
1548
  const buildColorFunctions = () => `
841
1549
 
@@ -882,7 +1590,6 @@ vec3 hsv2rgb(vec3 c)
882
1590
  }
883
1591
  `;
884
1592
 
885
-
886
1593
  const setLinkStyles = (link: HTMLAnchorElement) => {
887
1594
  link.id = LINK_ID;
888
1595
  link.href = "https://neat.firecms.co";
@@ -902,19 +1609,20 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
902
1609
  link.innerHTML = "NEAT";
903
1610
  }
904
1611
 
905
- const addNeatLink = (ref: HTMLCanvasElement) => {
1612
+ const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
906
1613
  const existingLinks = ref.parentElement?.getElementsByTagName("a");
907
1614
  if (existingLinks) {
908
1615
  for (let i = 0; i < existingLinks.length; i++) {
909
1616
  if (existingLinks[i].id === LINK_ID) {
910
1617
  setLinkStyles(existingLinks[i]);
911
- return;
1618
+ return existingLinks[i];
912
1619
  }
913
1620
  }
914
1621
  }
915
1622
  const link = document.createElement("a");
916
1623
  setLinkStyles(link);
917
1624
  ref.parentElement?.appendChild(link);
1625
+ return link;
918
1626
  }
919
1627
 
920
1628
  function getElapsedSecondsInLastHour() {