@firecms/neat 0.3.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;
@@ -34,10 +44,43 @@ export type NeatConfig = {
34
44
  colorBlending?: number;
35
45
  grainScale?: number;
36
46
  grainIntensity?: number;
47
+ grainSparsity?: number;
37
48
  grainSpeed?: number;
38
49
  wireframe?: boolean;
39
50
  backgroundColor?: string;
40
51
  backgroundAlpha?: number;
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;
41
84
  };
42
85
 
43
86
  export type NeatColor = {
@@ -73,6 +116,7 @@ export class NeatGradient implements NeatController {
73
116
 
74
117
  private _grainScale: number = -1;
75
118
  private _grainIntensity: number = -1;
119
+ private _grainSparsity: number = -1;
76
120
  private _grainSpeed: number = -1;
77
121
 
78
122
  private _colorBlending: number = -1;
@@ -83,10 +127,59 @@ export class NeatGradient implements NeatController {
83
127
  private _backgroundColor: string = "#FFFFFF";
84
128
  private _backgroundAlpha: number = 1.0;
85
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
+
86
167
  private requestRef: number = -1;
87
168
  private sizeObserver: ResizeObserver;
88
169
  private sceneState: SceneState;
89
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
+
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();
182
+
90
183
  constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
91
184
 
92
185
  const {
@@ -105,12 +198,42 @@ export class NeatGradient implements NeatController {
105
198
  colorBlending = 5,
106
199
  grainScale = 2,
107
200
  grainIntensity = 0.55,
201
+ grainSparsity = 0.0,
108
202
  grainSpeed = 0.1,
109
203
  wireframe = false,
110
204
  backgroundColor = "#FFFFFF",
111
205
  backgroundAlpha = 1.0,
112
206
  resolution = 1,
113
- seed
207
+ seed,
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,
114
237
  } = config;
115
238
 
116
239
 
@@ -129,6 +252,7 @@ export class NeatGradient implements NeatController {
129
252
  this.colorBlending = colorBlending;
130
253
  this.grainScale = grainScale;
131
254
  this.grainIntensity = grainIntensity;
255
+ this.grainSparsity = grainSparsity;
132
256
  this.grainSpeed = grainSpeed;
133
257
  this.colors = colors;
134
258
  this.shadows = shadows;
@@ -138,80 +262,166 @@ export class NeatGradient implements NeatController {
138
262
  this.wireframe = wireframe;
139
263
  this.backgroundColor = backgroundColor;
140
264
  this.backgroundAlpha = backgroundAlpha;
141
-
265
+ this.yOffset = yOffset;
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();
142
302
  this.sceneState = this._initScene(resolution);
143
303
 
144
304
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
305
+
145
306
  const render = () => {
146
307
 
147
- 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
148
311
  if (Math.floor(tick * 10) % 5 === 0) {
149
- addNeatLink(ref);
312
+ if (!this._linkElement || !document.contains(this._linkElement)) {
313
+ this._linkElement = addNeatLink(ref);
314
+ }
150
315
  }
151
316
 
152
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
153
- meshes.forEach((mesh) => {
154
-
155
- const width = this._ref.width,
156
- height = this._ref.height;
157
-
158
- const colors = [
159
- ...this._colors.map(color => {
160
- let threeColor = new THREE.Color();
161
- threeColor.setStyle(color.color, "");
162
- return ({
163
- is_active: color.enabled,
164
- color: threeColor,
165
- influence: color.influence
166
- });
167
- }),
168
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
169
- is_active: false,
170
- color: new THREE.Color(0x000000)
171
- }))
172
- ];
317
+ // Update Uniforms efficiently without creating new objects
318
+ if (this._cachedUniforms) {
319
+ const u = this._cachedUniforms;
173
320
 
174
321
  tick += clock.getDelta() * this._speed;
175
- // @ts-ignore
176
- mesh.material.uniforms.u_time.value = tick;
177
- // @ts-ignore
178
- mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
179
- // @ts-ignore
180
- mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
181
- // @ts-ignore
182
- mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
183
- // @ts-ignore
184
- mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
185
- // @ts-ignore
186
- mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
187
- // @ts-ignore
188
- mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
189
- // @ts-ignore
190
- mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
191
- // @ts-ignore
192
- mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
193
- // @ts-ignore
194
- mesh.material.uniforms.u_colors = { value: colors };
195
- // @ts-ignore
196
- mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
197
- // @ts-ignore
198
- mesh.material.uniforms.u_shadows = { value: this._shadows };
199
- // @ts-ignore
200
- mesh.material.uniforms.u_highlights = { value: this._highlights };
201
- // @ts-ignore
202
- mesh.material.uniforms.u_saturation = { value: this._saturation };
203
- // @ts-ignore
204
- mesh.material.uniforms.u_brightness = { value: this._brightness };
205
- // @ts-ignore
206
- mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
207
- // @ts-ignore
208
- mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
209
- // @ts-ignore
210
- mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
211
- // @ts-ignore
212
- mesh.material.wireframe = this._wireframe;
213
- });
214
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);
215
425
  renderer.render(scene, camera);
216
426
  this.requestRef = requestAnimationFrame(render);
217
427
  };
@@ -225,6 +435,19 @@ export class NeatGradient implements NeatController {
225
435
 
226
436
  this.sceneState.renderer.setSize(width, height, false);
227
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
+ }
228
451
  };
229
452
 
230
453
  this.sizeObserver = new ResizeObserver(entries => {
@@ -241,9 +464,28 @@ export class NeatGradient implements NeatController {
241
464
  if (this) {
242
465
  cancelAnimationFrame(this.requestRef);
243
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();
244
479
  }
245
480
  }
246
481
 
482
+ downloadAsPNG(filename = "neat.png") {
483
+ console.log("Downloading as PNG", this._ref);
484
+ const dataURL = this._ref.toDataURL("image/png");
485
+ console.log("data", dataURL);
486
+ downloadURI(dataURL, filename);
487
+ }
488
+
247
489
  set speed(speed: number) {
248
490
  this._speed = speed / 20;
249
491
  }
@@ -300,6 +542,10 @@ export class NeatGradient implements NeatController {
300
542
  this._grainIntensity = grainIntensity;
301
543
  }
302
544
 
545
+ set grainSparsity(grainSparsity: number) {
546
+ this._grainSparsity = grainSparsity;
547
+ }
548
+
303
549
  set grainSpeed(grainSpeed: number) {
304
550
  this._grainSpeed = grainSpeed;
305
551
  }
@@ -320,14 +566,186 @@ export class NeatGradient implements NeatController {
320
566
  this._backgroundAlpha = backgroundAlpha;
321
567
  }
322
568
 
569
+ set yOffset(yOffset: number) {
570
+ this._yOffset = yOffset;
571
+ }
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
+
323
730
  _initScene(resolution: number): SceneState {
324
731
 
325
732
  const width = this._ref.width,
326
733
  height = this._ref.height;
327
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
+
328
745
  const renderer = new THREE.WebGLRenderer({
329
746
  // antialias: true,
330
747
  alpha: true,
748
+ preserveDrawingBuffer: true,
331
749
  canvas: this._ref
332
750
  });
333
751
 
@@ -363,17 +781,13 @@ export class NeatGradient implements NeatController {
363
781
 
364
782
  _buildMaterial(width: number, height: number) {
365
783
 
366
- const colors = [
367
- ...this._colors.map(color => ({
368
- is_active: color.enabled,
369
- color: new THREE.Color(color.color),
370
- influence: color.influence
371
- })),
372
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
373
- is_active: false,
374
- color: new THREE.Color(0x000000)
375
- }))
376
- ];
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
+ }));
377
791
 
378
792
  const uniforms = {
379
793
  u_time: { value: 0 },
@@ -389,8 +803,32 @@ export class NeatGradient implements NeatController {
389
803
  u_shadows: { value: this._shadows },
390
804
  u_highlights: { value: this._highlights },
391
805
  u_grain_intensity: { value: this._grainIntensity },
806
+ u_grain_sparsity: { value: this._grainSparsity },
392
807
  u_grain_scale: { value: this._grainScale },
393
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 }
394
832
  };
395
833
 
396
834
  const material = new THREE.ShaderMaterial({
@@ -399,10 +837,287 @@ export class NeatGradient implements NeatController {
399
837
  fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
400
838
  });
401
839
 
840
+ // Cache the uniforms object for direct access in render loop
841
+ this._cachedUniforms = uniforms as unknown as NeatUniforms;
842
+
402
843
  material.wireframe = WIREFRAME;
403
844
  return material;
404
845
  }
405
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
+
406
1121
 
407
1122
  }
408
1123
 
@@ -445,36 +1160,62 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
445
1160
 
446
1161
  function buildVertexShader() {
447
1162
  return `
448
-
449
1163
  void main() {
450
-
451
1164
  vUv = uv;
452
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
453
1174
  v_displacement_amount = cnoise( vec3(
454
1175
  u_wave_frequency_x * position.x + u_time,
455
- u_wave_frequency_y * position.y + u_time,
1176
+ u_wave_frequency_y * (position.y + waveOffset) + u_time,
456
1177
  u_time
457
1178
  ));
458
-
459
- vec3 color;
460
1179
 
461
- // float t = mod(u_base_color, 100.0);
462
- color = u_colors[0].color;
463
-
464
- vec2 noise_cord = vUv * u_color_pressure;
465
-
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
+ }
1198
+
1199
+ // Pass the standard flow UV to fragment shader (for mouse/texture)
1200
+ vFlowUv = flowUv;
1201
+
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
1208
+
1209
+ vec2 noise_cord = adjustedUv * u_color_pressure;
466
1210
  const float minNoise = .0;
467
1211
  const float maxNoise = .9;
468
-
1212
+
469
1213
  for (int i = 1; i < u_colors_count; i++) {
470
-
471
- if(u_colors[i].is_active == 1.0){
1214
+ if(u_colors[i].is_active > 0.5){
472
1215
  float noiseFlow = (1. + float(i)) / 30.;
473
1216
  float noiseSpeed = (1. + float(i)) * 0.11;
474
1217
  float noiseSeed = 13. + float(i) * 7.;
475
-
476
- int reverseIndex = u_colors_count - i;
477
-
1218
+
478
1219
  float noise = snoise(
479
1220
  vec3(
480
1221
  noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
@@ -482,25 +1223,17 @@ void main() {
482
1223
  u_time * noiseSpeed
483
1224
  ) + noiseSeed
484
1225
  ) - (.1 * float(i)) + (.5 * u_color_blending);
485
-
1226
+
486
1227
  noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
487
- vec3 nextColor = u_colors[i].color;
488
- color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
489
-
490
- // vec3 colorOklab = oklab2rgb(color);
491
- // vec3 nextColorOklab = oklab2rgb(nextColor);
492
- // vec3 mixColor = mix(colorOklab, nextColorOklab, smoothstep(0.0, u_color_blending, noise));
493
- // color = rgb2oklab(mixColor);
494
-
1228
+ color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
495
1229
  }
496
-
497
1230
  }
498
-
1231
+
499
1232
  v_color = color;
500
-
1233
+
1234
+ // 4. VERTEX POSITION
501
1235
  vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
502
- gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
503
-
1236
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
504
1237
  v_new_position = gl_Position;
505
1238
  }
506
1239
  `;
@@ -516,7 +1249,6 @@ float fbm(vec3 x) {
516
1249
  float value = 0.0;
517
1250
  float amplitude = 0.5;
518
1251
  float frequency = 1.0;
519
-
520
1252
  for (int i = 0; i < 4; i++) {
521
1253
  value += amplitude * snoise(x * frequency);
522
1254
  frequency *= 2.0;
@@ -524,37 +1256,74 @@ float fbm(vec3 x) {
524
1256
  }
525
1257
  return value;
526
1258
  }
1259
+
1260
+ void main() {
1261
+ // MOUSE DISTORTION
1262
+ vec2 finalUv = vFlowUv;
527
1263
 
528
- void main(){
529
- vec3 color = v_color;
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
+ }
530
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
531
1307
  color += pow(v_displacement_amount, 1.0) * u_highlights;
532
1308
  color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
533
1309
  color = saturation(color, 1.0 + u_saturation);
534
1310
  color = color * u_brightness;
535
-
536
- // Generate grain using fbm
537
- vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
538
1311
 
539
- float grain = (u_grain_speed != 0.0)
540
- ? fbm(vec3(noiseCoords, u_time * u_grain_speed))
541
- : fbm(vec3(noiseCoords, 0.0));
1312
+ // Grain
1313
+ vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
1314
+ float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
542
1315
 
543
- // Center the grain around zero
544
1316
  grain = grain * 0.5 + 0.5;
545
1317
  grain -= 0.5;
546
-
547
- // Apply grain intensity
1318
+ grain = (grain > u_grain_sparsity) ? grain : 0.0;
548
1319
  grain *= u_grain_intensity;
549
1320
 
550
- // Add grain to color
551
1321
  color += vec3(grain);
552
-
553
- gl_FragColor = vec4(color,1.0);
1322
+
1323
+ gl_FragColor = vec4(color, 1.0);
554
1324
  }
555
- `;
1325
+ `;
556
1326
  }
557
-
558
1327
  const buildUniforms = () => `
559
1328
  precision highp float;
560
1329
 
@@ -565,6 +1334,7 @@ struct Color {
565
1334
  };
566
1335
 
567
1336
  uniform float u_grain_intensity;
1337
+ uniform float u_grain_sparsity;
568
1338
  uniform float u_grain_scale;
569
1339
  uniform float u_grain_speed;
570
1340
  uniform float u_time;
@@ -586,10 +1356,34 @@ uniform float u_brightness;
586
1356
  uniform float u_color_blending;
587
1357
 
588
1358
  uniform int u_colors_count;
589
- uniform Color u_colors[5];
1359
+ uniform Color u_colors[6];
590
1360
  uniform vec2 u_resolution;
591
1361
 
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;
1384
+
592
1385
  varying vec2 vUv;
1386
+ varying vec2 vFlowUv;
593
1387
  varying vec4 v_new_position;
594
1388
  varying vec3 v_color;
595
1389
  varying float v_displacement_amount;
@@ -598,69 +1392,55 @@ varying float v_displacement_amount;
598
1392
 
599
1393
  const buildNoise = () => `
600
1394
 
601
- vec3 mod289(vec3 x)
602
- {
603
- 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);
604
1399
  }
605
1400
 
606
- vec4 mod289(vec4 x)
607
- {
608
- return x - floor(x * (1.0 / 289.0)) * 289.0;
609
- }
610
-
611
- vec4 permute(vec4 x)
612
- {
613
- return mod289(((x*34.0)+1.0)*x);
614
- }
615
-
616
- vec4 taylorInvSqrt(vec4 r)
617
- {
1401
+ // Taylor Inverse Sqrt
1402
+ vec4 taylorInvSqrt(vec4 r) {
618
1403
  return 1.79284291400159 - 0.85373472095314 * r;
619
1404
  }
620
1405
 
1406
+ // Fade function
621
1407
  vec3 fade(vec3 t) {
622
1408
  return t*t*t*(t*(t*6.0-15.0)+10.0);
623
1409
  }
624
1410
 
625
- float snoise(vec3 v)
626
- {
1411
+ // 3D Simplex Noise
1412
+ float snoise(vec3 v) {
627
1413
  const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
628
1414
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
629
1415
 
630
- // First corner
1416
+ // First corner
631
1417
  vec3 i = floor(v + dot(v, C.yyy) );
632
1418
  vec3 x0 = v - i + dot(i, C.xxx) ;
633
1419
 
634
- // Other corners
1420
+ // Other corners
635
1421
  vec3 g = step(x0.yzx, x0.xyz);
636
1422
  vec3 l = 1.0 - g;
637
1423
  vec3 i1 = min( g.xyz, l.zxy );
638
1424
  vec3 i2 = max( g.xyz, l.zxy );
639
1425
 
640
- // x0 = x0 - 0.0 + 0.0 * C.xxx;
641
- // x1 = x0 - i1 + 1.0 * C.xxx;
642
- // x2 = x0 - i2 + 2.0 * C.xxx;
643
- // x3 = x0 - 1.0 + 3.0 * C.xxx;
644
1426
  vec3 x1 = x0 - i1 + C.xxx;
645
- vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
646
- 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;
647
1429
 
648
- // Permutations
649
- i = mod289(i);
1430
+ // Permutations
650
1431
  vec4 p = permute( permute( permute(
651
1432
  i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
652
1433
  + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
653
1434
  + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
654
1435
 
655
- // Gradients: 7x7 points over a square, mapped onto an octahedron.
656
- // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
1436
+ // Gradients
657
1437
  float n_ = 0.142857142857; // 1.0/7.0
658
1438
  vec3 ns = n_ * D.wyz - D.xzx;
659
1439
 
660
- 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);
661
1441
 
662
1442
  vec4 x_ = floor(j * ns.z);
663
- vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
1443
+ vec4 y_ = floor(j - 7.0 * x_ );
664
1444
 
665
1445
  vec4 x = x_ *ns.x + ns.yyyy;
666
1446
  vec4 y = y_ *ns.x + ns.yyyy;
@@ -669,8 +1449,6 @@ float snoise(vec3 v)
669
1449
  vec4 b0 = vec4( x.xy, y.xy );
670
1450
  vec4 b1 = vec4( x.zw, y.zw );
671
1451
 
672
- //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
673
- //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
674
1452
  vec4 s0 = floor(b0)*2.0 + 1.0;
675
1453
  vec4 s1 = floor(b1)*2.0 + 1.0;
676
1454
  vec4 sh = -step(h, vec4(0.0));
@@ -683,14 +1461,14 @@ float snoise(vec3 v)
683
1461
  vec3 p2 = vec3(a1.xy,h.z);
684
1462
  vec3 p3 = vec3(a1.zw,h.w);
685
1463
 
686
- //Normalise gradients
1464
+ // Normalise gradients
687
1465
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
688
1466
  p0 *= norm.x;
689
1467
  p1 *= norm.y;
690
1468
  p2 *= norm.z;
691
1469
  p3 *= norm.w;
692
1470
 
693
- // Mix final noise value
1471
+ // Mix final noise value
694
1472
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
695
1473
  m = m * m;
696
1474
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
@@ -700,12 +1478,11 @@ float snoise(vec3 v)
700
1478
  // Classic Perlin noise
701
1479
  float cnoise(vec3 P)
702
1480
  {
703
- vec3 Pi0 = floor(P); // Integer part for indexing
704
- vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
705
- Pi0 = mod289(Pi0);
706
- Pi1 = mod289(Pi1);
707
- vec3 Pf0 = fract(P); // Fractional part for interpolation
708
- 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);
709
1486
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
710
1487
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
711
1488
  vec4 iz0 = Pi0.zzzz;
@@ -766,47 +1543,7 @@ float cnoise(vec3 P)
766
1543
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
767
1544
  return 2.2 * n_xyz;
768
1545
  }
769
-
770
- // YUV to RGB matrix
771
- mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
772
- 1.0, -0.39465, -0.58060,
773
- 1.0, 2.03211, 0.0);
774
-
775
- // RGB to YUV matrix
776
- mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
777
- -0.09991, -0.33609, 0.43600,
778
- 0.615, -0.5586, -0.05639);
779
-
780
- vec3 oklab2rgb(vec3 linear)
781
- {
782
- const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
783
- 0.5362752080, 0.6807189584, 0.2818474174,
784
- 0.0514575653, 0.1074065790, 0.6302613616);
785
-
786
- const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
787
- +0.7936177850, -2.4285922050, +0.7827717662,
788
- -0.0040720468, +0.4505937099, -0.8086757660);
789
-
790
- vec3 lms = im1 * linear;
791
-
792
- return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
793
- }
794
-
795
- vec3 rgb2oklab(vec3 oklab)
796
- {
797
- const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
798
- +0.396337777, -0.105561346, -0.089484178,
799
- +0.215803757, -0.063854173, -1.291485548);
800
-
801
- const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
802
- -3.307216883, +2.609332323, -0.703476310,
803
- +0.230759054, -0.341134429, +1.706862569);
804
- vec3 lms = m1 * oklab;
805
-
806
- return m2 * (lms * lms * lms);
807
- }
808
-
809
- `;
1546
+ `;
810
1547
 
811
1548
  const buildColorFunctions = () => `
812
1549
 
@@ -853,7 +1590,6 @@ vec3 hsv2rgb(vec3 c)
853
1590
  }
854
1591
  `;
855
1592
 
856
-
857
1593
  const setLinkStyles = (link: HTMLAnchorElement) => {
858
1594
  link.id = LINK_ID;
859
1595
  link.href = "https://neat.firecms.co";
@@ -873,19 +1609,20 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
873
1609
  link.innerHTML = "NEAT";
874
1610
  }
875
1611
 
876
- const addNeatLink = (ref: HTMLCanvasElement) => {
1612
+ const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
877
1613
  const existingLinks = ref.parentElement?.getElementsByTagName("a");
878
1614
  if (existingLinks) {
879
1615
  for (let i = 0; i < existingLinks.length; i++) {
880
1616
  if (existingLinks[i].id === LINK_ID) {
881
1617
  setLinkStyles(existingLinks[i]);
882
- return;
1618
+ return existingLinks[i];
883
1619
  }
884
1620
  }
885
1621
  }
886
1622
  const link = document.createElement("a");
887
1623
  setLinkStyles(link);
888
1624
  ref.parentElement?.appendChild(link);
1625
+ return link;
889
1626
  }
890
1627
 
891
1628
  function getElapsedSecondsInLastHour() {
@@ -904,3 +1641,12 @@ function generateRandomString(length: number = 6): string {
904
1641
  }
905
1642
  return result;
906
1643
  }
1644
+
1645
+ function downloadURI(uri: string, name: string) {
1646
+ const link = document.createElement("a");
1647
+ link.download = name;
1648
+ link.href = uri;
1649
+ document.body.appendChild(link);
1650
+ link.click();
1651
+ document.body.removeChild(link);
1652
+ }