@firecms/neat 0.4.0 → 0.5.1

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.
@@ -2,7 +2,7 @@ import * as THREE from "three";
2
2
  const PLANE_WIDTH = 50;
3
3
  const PLANE_HEIGHT = 80;
4
4
  const WIREFRAME = true;
5
- const COLORS_COUNT = 5;
5
+ const COLORS_COUNT = 6;
6
6
  const clock = new THREE.Clock();
7
7
  const LINK_ID = generateRandomString();
8
8
  export class NeatGradient {
@@ -26,12 +26,67 @@ export class NeatGradient {
26
26
  _wireframe = false;
27
27
  _backgroundColor = "#FFFFFF";
28
28
  _backgroundAlpha = 1.0;
29
+ // Flow field properties
30
+ _flowDistortionA = 0;
31
+ _flowDistortionB = 0;
32
+ _flowScale = 1.0;
33
+ _flowEase = 0.0;
34
+ _flowEnabled = true;
35
+ // Mouse interaction properties
36
+ _mouseDistortionStrength = 0.0;
37
+ _mouseDistortionRadius = 0.25;
38
+ _mouseDecayRate = 0.96;
39
+ _mouseDarken = 0.0;
40
+ _mouse = new THREE.Vector2(-1000, -1000);
41
+ _mouseFBO = null;
42
+ _sceneMouse = null;
43
+ _cameraMouse = null;
44
+ _mouseObjects = [];
45
+ _currentBrush = 0;
46
+ _mouseBrushBaseScale = 1;
47
+ // Texture generation properties
48
+ _enableProceduralTexture = false;
49
+ _textureVoidLikelihood = 0.45;
50
+ _textureVoidWidthMin = 200;
51
+ _textureVoidWidthMax = 486;
52
+ _textureBandDensity = 2.15;
53
+ _textureColorBlending = 0.01;
54
+ _textureSeed = 333;
55
+ _textureEase = 0.5;
56
+ _proceduralTexture = null;
57
+ _proceduralBackgroundColor = "#000000";
58
+ _textureShapeTriangles = 20;
59
+ _textureShapeCircles = 15;
60
+ _textureShapeBars = 15;
61
+ _textureShapeSquiggles = 10;
29
62
  requestRef = -1;
30
63
  sizeObserver;
31
64
  sceneState;
65
+ // Optimization: Cache uniforms to avoid lookups and object creation in render loop
66
+ _cachedUniforms = null;
67
+ _linkElement = null;
32
68
  _yOffset = 0;
69
+ _yOffsetWaveMultiplier = 0.004;
70
+ _yOffsetColorMultiplier = 0.004;
71
+ _yOffsetFlowMultiplier = 0.004;
72
+ // For saving/restoring clear color
73
+ _tempClearColor = new THREE.Color();
74
+ // Performance optimizations
75
+ _resizeTimeoutId = null;
76
+ _textureNeedsUpdate = false;
77
+ _lastColorUpdate = 0;
78
+ _linkCheckCounter = 0;
79
+ _mouseUpdateScheduled = false;
80
+ _pendingMousePosition = null;
81
+ _colorsChanged = true; // Track if colors need update
33
82
  constructor(config) {
34
- const { ref, speed = 4, horizontalPressure = 3, verticalPressure = 3, waveFrequencyX = 5, waveFrequencyY = 5, waveAmplitude = 3, colors, highlights = 4, shadows = 4, colorSaturation = 0, colorBrightness = 1, colorBlending = 5, grainScale = 2, grainIntensity = 0.55, grainSparsity = 0.0, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed, yOffset = 0 } = config;
83
+ const { ref, speed = 4, horizontalPressure = 3, verticalPressure = 3, waveFrequencyX = 5, waveFrequencyY = 5, waveAmplitude = 3, colors, highlights = 4, shadows = 4, colorSaturation = 0, colorBrightness = 1, colorBlending = 5, grainScale = 2, grainIntensity = 0.55, grainSparsity = 0.0, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed, yOffset = 0, yOffsetWaveMultiplier = 4, yOffsetColorMultiplier = 4, yOffsetFlowMultiplier = 4,
84
+ // Flow field parameters
85
+ flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
86
+ // Mouse interaction
87
+ mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
88
+ // Texture generation
89
+ enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, } = config;
35
90
  this._ref = ref;
36
91
  this.destroy = this.destroy.bind(this);
37
92
  this._initScene = this._initScene.bind(this);
@@ -56,75 +111,155 @@ export class NeatGradient {
56
111
  this.backgroundColor = backgroundColor;
57
112
  this.backgroundAlpha = backgroundAlpha;
58
113
  this.yOffset = yOffset;
114
+ this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
115
+ this.yOffsetColorMultiplier = yOffsetColorMultiplier;
116
+ this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
117
+ // Flow field
118
+ this.flowDistortionA = flowDistortionA;
119
+ this.flowDistortionB = flowDistortionB;
120
+ this.flowScale = flowScale;
121
+ this.flowEase = flowEase;
122
+ this.flowEnabled = flowEnabled;
123
+ // Mouse interaction
124
+ this.mouseDistortionStrength = mouseDistortionStrength;
125
+ this.mouseDistortionRadius = mouseDistortionRadius;
126
+ this.mouseDecayRate = mouseDecayRate;
127
+ this.mouseDarken = mouseDarken;
128
+ // Texture generation
129
+ this.enableProceduralTexture = enableProceduralTexture;
130
+ this.textureVoidLikelihood = textureVoidLikelihood;
131
+ this.textureVoidWidthMin = textureVoidWidthMin;
132
+ this.textureVoidWidthMax = textureVoidWidthMax;
133
+ this.textureBandDensity = textureBandDensity;
134
+ this.textureColorBlending = textureColorBlending;
135
+ this.textureSeed = textureSeed;
136
+ this.textureEase = textureEase;
137
+ this._proceduralBackgroundColor = proceduralBackgroundColor;
138
+ this._textureShapeTriangles = textureShapeTriangles;
139
+ this._textureShapeCircles = textureShapeCircles;
140
+ this._textureShapeBars = textureShapeBars;
141
+ this._textureShapeSquiggles = textureShapeSquiggles;
142
+ // FIX 1: Setup mouse resources BEFORE building the material/scene
143
+ // This ensures u_mouse_texture isn't null during material compilation
144
+ this._setupMouseInteraction();
59
145
  this.sceneState = this._initScene(resolution);
60
146
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
61
147
  const render = () => {
62
- const { renderer, camera, scene, meshes } = this.sceneState;
63
- if (Math.floor(tick * 10) % 5 === 0) {
64
- addNeatLink(ref);
148
+ const { renderer, camera, scene } = this.sceneState;
149
+ // Optimization: check if cached link is still valid in DOM less frequently
150
+ this._linkCheckCounter++;
151
+ if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
152
+ this._linkCheckCounter = 0;
153
+ if (!this._linkElement || !document.contains(this._linkElement)) {
154
+ this._linkElement = addNeatLink(ref);
155
+ }
65
156
  }
66
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
67
- meshes.forEach((mesh) => {
68
- const width = this._ref.width, height = this._ref.height;
69
- const colors = [
70
- ...this._colors.map(color => {
71
- let threeColor = new THREE.Color();
72
- threeColor.setStyle(color.color, "");
73
- return ({
74
- is_active: color.enabled,
75
- color: threeColor,
76
- influence: color.influence
77
- });
78
- }),
79
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
80
- is_active: false,
81
- color: new THREE.Color(0x000000)
82
- }))
83
- ];
157
+ // Update Uniforms efficiently without creating new objects
158
+ if (this._cachedUniforms) {
159
+ const u = this._cachedUniforms;
84
160
  tick += clock.getDelta() * this._speed;
85
- // @ts-ignore
86
- mesh.material.uniforms.u_time.value = tick;
87
- // @ts-ignore
88
- mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
89
- // @ts-ignore
90
- mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
91
- // @ts-ignore
92
- mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
93
- // @ts-ignore
94
- mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
95
- // @ts-ignore
96
- mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
97
- // @ts-ignore
98
- mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
99
- // @ts-ignore
100
- mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
101
- // @ts-ignore
102
- mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
103
- // @ts-ignore
104
- mesh.material.uniforms.u_colors = { value: colors };
105
- // @ts-ignore
106
- mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
107
- // @ts-ignore
108
- mesh.material.uniforms.u_shadows = { value: this._shadows };
109
- // @ts-ignore
110
- mesh.material.uniforms.u_highlights = { value: this._highlights };
111
- // @ts-ignore
112
- mesh.material.uniforms.u_saturation = { value: this._saturation };
113
- // @ts-ignore
114
- mesh.material.uniforms.u_brightness = { value: this._brightness };
115
- // @ts-ignore
116
- mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
117
- // @ts-ignore
118
- mesh.material.uniforms.u_grain_sparsity = { value: this._grainSparsity };
119
- // @ts-ignore
120
- mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
121
- // @ts-ignore
122
- mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
123
- // @ts-ignore
124
- mesh.material.uniforms.u_y_offset = { value: this._yOffset };
125
- // @ts-ignore
126
- mesh.material.wireframe = this._wireframe;
127
- });
161
+ u.u_time.value = tick;
162
+ u.u_resolution.value.set(this._ref.width, this._ref.height);
163
+ u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
164
+ // Directly assign simple values
165
+ u.u_wave_frequency_x.value = this._waveFrequencyX;
166
+ u.u_wave_frequency_y.value = this._waveFrequencyY;
167
+ u.u_wave_amplitude.value = this._waveAmplitude;
168
+ u.u_color_blending.value = this._colorBlending;
169
+ u.u_shadows.value = this._shadows;
170
+ u.u_highlights.value = this._highlights;
171
+ u.u_saturation.value = this._saturation;
172
+ u.u_brightness.value = this._brightness;
173
+ u.u_grain_intensity.value = this._grainIntensity;
174
+ u.u_grain_sparsity.value = this._grainSparsity;
175
+ u.u_grain_speed.value = this._grainSpeed;
176
+ u.u_grain_scale.value = this._grainScale;
177
+ u.u_y_offset.value = this._yOffset;
178
+ u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
179
+ u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
180
+ u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
181
+ u.u_flow_distortion_a.value = this._flowDistortionA;
182
+ u.u_flow_distortion_b.value = this._flowDistortionB;
183
+ u.u_flow_scale.value = this._flowScale;
184
+ u.u_flow_ease.value = this._flowEase;
185
+ u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
186
+ u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
187
+ u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
188
+ u.u_mouse_darken.value = this._mouseDarken;
189
+ u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
190
+ // Only regenerate procedural texture when needed
191
+ if (this._textureNeedsUpdate && this._enableProceduralTexture) {
192
+ if (this._proceduralTexture) {
193
+ this._proceduralTexture.dispose();
194
+ }
195
+ this._proceduralTexture = this._createProceduralTexture();
196
+ this._textureNeedsUpdate = false;
197
+ }
198
+ u.u_procedural_texture.value = this._proceduralTexture;
199
+ u.u_texture_ease.value = this._textureEase;
200
+ // Wireframe is a material property and must update every frame to avoid artifacts
201
+ // @ts-ignore - access material safely
202
+ this.sceneState.meshes[0].material.wireframe = this._wireframe;
203
+ // Optimized Color Update: Update immediately on change, or throttle to 10 times per second
204
+ const now = Date.now();
205
+ const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
206
+ if (shouldUpdate) {
207
+ this._lastColorUpdate = now;
208
+ this._colorsChanged = false;
209
+ const shaderColors = u.u_colors.value;
210
+ for (let i = 0; i < COLORS_COUNT; i++) {
211
+ if (i < this._colors.length) {
212
+ const c = this._colors[i];
213
+ shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
214
+ shaderColors[i].color.setStyle(c.color, "");
215
+ shaderColors[i].influence = c.influence || 0;
216
+ }
217
+ else {
218
+ shaderColors[i].is_active = 0.0;
219
+ }
220
+ }
221
+ u.u_colors_count.value = COLORS_COUNT;
222
+ }
223
+ }
224
+ // Render mouse interaction to FBO - optimize by only rendering when needed
225
+ if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
226
+ let hasActiveBrushes = false;
227
+ // Update mouse objects - decay rate controls how fast trails fade
228
+ for (let i = 0; i < this._mouseObjects.length; i++) {
229
+ const obj = this._mouseObjects[i];
230
+ if (obj.mesh.visible) {
231
+ hasActiveBrushes = true;
232
+ obj.mesh.rotation.z += 0.01;
233
+ if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
234
+ // Decay only affects opacity
235
+ obj.mesh.material.opacity *= this._mouseDecayRate;
236
+ if (obj.mesh.material.opacity < 0.01) {
237
+ obj.mesh.visible = false;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ // Only render FBO if there are active brushes
243
+ if (hasActiveBrushes) {
244
+ // Store current clear color (likely the main background color)
245
+ renderer.getClearColor(this._tempClearColor);
246
+ const oldClearAlpha = renderer.getClearAlpha();
247
+ // Set clear color to Black/Transparent for the FBO.
248
+ renderer.setClearColor(0x000000, 0.0);
249
+ renderer.setRenderTarget(this._mouseFBO);
250
+ renderer.clear();
251
+ renderer.render(this._sceneMouse, this._cameraMouse);
252
+ renderer.setRenderTarget(null);
253
+ // Restore main background color for the actual scene render
254
+ renderer.setClearColor(this._tempClearColor, oldClearAlpha);
255
+ // Update mouse texture uniform
256
+ if (this._cachedUniforms) {
257
+ this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
258
+ }
259
+ }
260
+ }
261
+ // Ensure we set the clear color for the main scene explicitly before rendering
262
+ renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
128
263
  renderer.render(scene, camera);
129
264
  this.requestRef = requestAnimationFrame(render);
130
265
  };
@@ -135,9 +270,28 @@ export class NeatGradient {
135
270
  const height = canvas.clientHeight;
136
271
  this.sceneState.renderer.setSize(width, height, false);
137
272
  updateCamera(this.sceneState.camera, width, height);
273
+ // FIX 3: Update Mouse FBO and Camera on resize
274
+ // If we don't do this, mouse coordinates map incorrectly after a resize
275
+ if (this._mouseFBO && this._cameraMouse) {
276
+ const fSize = height / 2;
277
+ const aspect = width / height;
278
+ this._mouseFBO.setSize(width / 2, height / 2);
279
+ this._cameraMouse.left = -fSize * aspect;
280
+ this._cameraMouse.right = fSize * aspect;
281
+ this._cameraMouse.top = fSize;
282
+ this._cameraMouse.bottom = -fSize;
283
+ this._cameraMouse.updateProjectionMatrix();
284
+ }
138
285
  };
139
- this.sizeObserver = new ResizeObserver(entries => {
140
- setSize();
286
+ // Debounce resize to prevent excessive operations
287
+ this.sizeObserver = new ResizeObserver(() => {
288
+ if (this._resizeTimeoutId !== null) {
289
+ clearTimeout(this._resizeTimeoutId);
290
+ }
291
+ this._resizeTimeoutId = window.setTimeout(() => {
292
+ setSize();
293
+ this._resizeTimeoutId = null;
294
+ }, 100); // Wait 100ms after last resize event
141
295
  });
142
296
  this.sizeObserver.observe(ref);
143
297
  render();
@@ -146,6 +300,26 @@ export class NeatGradient {
146
300
  if (this) {
147
301
  cancelAnimationFrame(this.requestRef);
148
302
  this.sizeObserver.disconnect();
303
+ // Clear resize timeout
304
+ if (this._resizeTimeoutId !== null) {
305
+ clearTimeout(this._resizeTimeoutId);
306
+ this._resizeTimeoutId = null;
307
+ }
308
+ // Cleanup WebGL resources
309
+ if (this.sceneState) {
310
+ this.sceneState.renderer.dispose();
311
+ this.sceneState.meshes.forEach(m => {
312
+ m.geometry.dispose();
313
+ if (Array.isArray(m.material))
314
+ m.material.forEach(mat => mat.dispose());
315
+ else
316
+ m.material.dispose();
317
+ });
318
+ }
319
+ if (this._mouseFBO)
320
+ this._mouseFBO.dispose();
321
+ if (this._proceduralTexture)
322
+ this._proceduralTexture.dispose();
149
323
  }
150
324
  }
151
325
  downloadAsPNG(filename = "neat.png") {
@@ -174,6 +348,7 @@ export class NeatGradient {
174
348
  }
175
349
  set colors(colors) {
176
350
  this._colors = colors;
351
+ this._colorsChanged = true; // Flag for immediate update
177
352
  }
178
353
  set highlights(highlights) {
179
354
  this._highlights = highlights / 100;
@@ -217,8 +392,152 @@ export class NeatGradient {
217
392
  set yOffset(yOffset) {
218
393
  this._yOffset = yOffset;
219
394
  }
395
+ get yOffsetWaveMultiplier() {
396
+ return this._yOffsetWaveMultiplier * 1000;
397
+ }
398
+ set yOffsetWaveMultiplier(value) {
399
+ this._yOffsetWaveMultiplier = value / 1000;
400
+ }
401
+ get yOffsetColorMultiplier() {
402
+ return this._yOffsetColorMultiplier * 1000;
403
+ }
404
+ set yOffsetColorMultiplier(value) {
405
+ this._yOffsetColorMultiplier = value / 1000;
406
+ }
407
+ get yOffsetFlowMultiplier() {
408
+ return this._yOffsetFlowMultiplier * 1000;
409
+ }
410
+ set yOffsetFlowMultiplier(value) {
411
+ this._yOffsetFlowMultiplier = value / 1000;
412
+ }
413
+ set flowDistortionA(value) {
414
+ this._flowDistortionA = value;
415
+ }
416
+ set flowDistortionB(value) {
417
+ this._flowDistortionB = value;
418
+ }
419
+ set flowScale(value) {
420
+ this._flowScale = value;
421
+ }
422
+ set flowEase(value) {
423
+ this._flowEase = value;
424
+ }
425
+ set flowEnabled(value) {
426
+ this._flowEnabled = value;
427
+ }
428
+ get flowEnabled() {
429
+ return this._flowEnabled;
430
+ }
431
+ set mouseDistortionStrength(value) {
432
+ this._mouseDistortionStrength = Math.max(0, value);
433
+ }
434
+ set mouseDistortionRadius(value) {
435
+ // Clamp to a sane range in UV space
436
+ this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
437
+ // Update brush scale when radius changes
438
+ this._updateBrushScale();
439
+ }
440
+ _updateBrushScale() {
441
+ if (!this._mouseObjects || this._mouseObjects.length === 0)
442
+ return;
443
+ // Radius directly controls the brush scale
444
+ // Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
445
+ this._mouseBrushBaseScale = this._mouseDistortionRadius;
446
+ }
447
+ set mouseDecayRate(value) {
448
+ // Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
449
+ this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
450
+ }
451
+ set mouseDarken(value) {
452
+ this._mouseDarken = value;
453
+ }
454
+ set enableProceduralTexture(value) {
455
+ this._enableProceduralTexture = value;
456
+ if (value && !this._proceduralTexture) {
457
+ this._textureNeedsUpdate = true;
458
+ }
459
+ }
460
+ set textureVoidLikelihood(value) {
461
+ this._textureVoidLikelihood = value;
462
+ if (this._enableProceduralTexture) {
463
+ this._textureNeedsUpdate = true;
464
+ }
465
+ }
466
+ set textureVoidWidthMin(value) {
467
+ this._textureVoidWidthMin = value;
468
+ if (this._enableProceduralTexture) {
469
+ this._textureNeedsUpdate = true;
470
+ }
471
+ }
472
+ set textureVoidWidthMax(value) {
473
+ this._textureVoidWidthMax = value;
474
+ if (this._enableProceduralTexture) {
475
+ this._textureNeedsUpdate = true;
476
+ }
477
+ }
478
+ set textureBandDensity(value) {
479
+ this._textureBandDensity = value;
480
+ if (this._enableProceduralTexture) {
481
+ this._textureNeedsUpdate = true;
482
+ }
483
+ }
484
+ set textureColorBlending(value) {
485
+ this._textureColorBlending = value;
486
+ if (this._enableProceduralTexture) {
487
+ this._textureNeedsUpdate = true;
488
+ }
489
+ }
490
+ set textureSeed(value) {
491
+ this._textureSeed = value;
492
+ if (this._enableProceduralTexture) {
493
+ this._textureNeedsUpdate = true;
494
+ }
495
+ }
496
+ get textureEase() {
497
+ return this._textureEase;
498
+ }
499
+ set textureEase(value) {
500
+ this._textureEase = value;
501
+ }
502
+ set proceduralBackgroundColor(value) {
503
+ this._proceduralBackgroundColor = value;
504
+ if (this._enableProceduralTexture) {
505
+ this._textureNeedsUpdate = true;
506
+ }
507
+ }
508
+ set textureShapeTriangles(value) {
509
+ this._textureShapeTriangles = value;
510
+ if (this._enableProceduralTexture)
511
+ this._textureNeedsUpdate = true;
512
+ }
513
+ set textureShapeCircles(value) {
514
+ this._textureShapeCircles = value;
515
+ if (this._enableProceduralTexture)
516
+ this._textureNeedsUpdate = true;
517
+ }
518
+ set textureShapeBars(value) {
519
+ this._textureShapeBars = value;
520
+ if (this._enableProceduralTexture)
521
+ this._textureNeedsUpdate = true;
522
+ }
523
+ set textureShapeSquiggles(value) {
524
+ this._textureShapeSquiggles = value;
525
+ if (this._enableProceduralTexture)
526
+ this._textureNeedsUpdate = true;
527
+ }
220
528
  _initScene(resolution) {
221
529
  const width = this._ref.width, height = this._ref.height;
530
+ // Cleanup existing renderer if needed
531
+ if (this.sceneState && this.sceneState.renderer) {
532
+ this.sceneState.renderer.dispose();
533
+ this.sceneState.meshes.forEach(m => {
534
+ m.geometry.dispose();
535
+ if (Array.isArray(m.material))
536
+ m.material.forEach(mat => mat.dispose());
537
+ else
538
+ m.material.dispose();
539
+ });
540
+ }
222
541
  const renderer = new THREE.WebGLRenderer({
223
542
  // antialias: true,
224
543
  alpha: true,
@@ -249,17 +568,13 @@ export class NeatGradient {
249
568
  };
250
569
  }
251
570
  _buildMaterial(width, height) {
252
- const colors = [
253
- ...this._colors.map(color => ({
254
- is_active: color.enabled,
255
- color: new THREE.Color(color.color),
256
- influence: color.influence
257
- })),
258
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
259
- is_active: false,
260
- color: new THREE.Color(0x000000)
261
- }))
262
- ];
571
+ // Initialize stable array structure for colors
572
+ // We create 6 objects and just update them in the render loop to avoid GC
573
+ const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
574
+ is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
575
+ color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
576
+ influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
577
+ }));
263
578
  const uniforms = {
264
579
  u_time: { value: 0 },
265
580
  u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
@@ -277,15 +592,287 @@ export class NeatGradient {
277
592
  u_grain_sparsity: { value: this._grainSparsity },
278
593
  u_grain_scale: { value: this._grainScale },
279
594
  u_grain_speed: { value: this._grainSpeed },
595
+ // Flow field
596
+ u_flow_distortion_a: { value: this._flowDistortionA },
597
+ u_flow_distortion_b: { value: this._flowDistortionB },
598
+ u_flow_scale: { value: this._flowScale },
599
+ u_flow_ease: { value: this._flowEase },
600
+ u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
601
+ // Y offset multipliers
602
+ u_y_offset: { value: this._yOffset },
603
+ u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
604
+ u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
605
+ u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
606
+ // Mouse interaction
607
+ u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
608
+ u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
609
+ u_mouse_darken: { value: this._mouseDarken },
610
+ u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
611
+ // Procedural texture
612
+ u_procedural_texture: { value: this._proceduralTexture },
613
+ u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
614
+ u_texture_ease: { value: this._textureEase },
615
+ u_saturation: { value: this._saturation },
616
+ u_brightness: { value: this._brightness },
617
+ u_color_blending: { value: this._colorBlending }
280
618
  };
281
619
  const material = new THREE.ShaderMaterial({
282
620
  uniforms: uniforms,
283
621
  vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
284
622
  fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
285
623
  });
624
+ // Cache the uniforms object for direct access in render loop
625
+ this._cachedUniforms = uniforms;
286
626
  material.wireframe = WIREFRAME;
287
627
  return material;
288
628
  }
629
+ _setupMouseInteraction() {
630
+ if (!this._ref)
631
+ return;
632
+ const width = this._ref.width;
633
+ const height = this._ref.height;
634
+ // Create mouse FBO
635
+ this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
636
+ // Create mouse scene and camera
637
+ this._sceneMouse = new THREE.Scene();
638
+ const fSize = height / 2;
639
+ const aspect = width / height;
640
+ // FIX 4: Ensure near plane allows viewing objects at Z=0
641
+ // Near -100 is safer for objects at 0
642
+ this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
643
+ this._cameraMouse.position.set(0, 0, 100);
644
+ // Create brush texture - More visible and impactful
645
+ const brushCanvas = document.createElement('canvas');
646
+ brushCanvas.width = 128;
647
+ brushCanvas.height = 128;
648
+ const bCtx = brushCanvas.getContext('2d');
649
+ if (bCtx) {
650
+ const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
651
+ // Match reference implementation's stronger gradient
652
+ grd.addColorStop(0, 'rgba(255,255,255,0.8)');
653
+ grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
654
+ grd.addColorStop(1, 'rgba(255,255,255,0)');
655
+ bCtx.fillStyle = grd;
656
+ bCtx.fillRect(0, 0, 128, 128);
657
+ }
658
+ const brushTex = new THREE.CanvasTexture(brushCanvas);
659
+ const brushMat = new THREE.MeshBasicMaterial({
660
+ map: brushTex,
661
+ transparent: true,
662
+ opacity: 1.0,
663
+ depthTest: false,
664
+ blending: THREE.AdditiveBlending // Additive blending for better accumulation
665
+ });
666
+ // Brush geometry size - will be scaled by radius parameter
667
+ const brushGeo = new THREE.PlaneGeometry(200, 200);
668
+ // Create brush pool
669
+ const brushPoolSize = 50;
670
+ for (let i = 0; i < brushPoolSize; i++) {
671
+ const m = new THREE.Mesh(brushGeo, brushMat.clone());
672
+ m.visible = false;
673
+ this._sceneMouse.add(m);
674
+ this._mouseObjects.push({ mesh: m, active: false });
675
+ }
676
+ // Initialize brush scale based on current radius
677
+ this._updateBrushScale();
678
+ // Add mouse move listener
679
+ this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
680
+ }
681
+ _onMouseMove(e) {
682
+ if (!this._ref || !this._sceneMouse)
683
+ return;
684
+ const rect = this._ref.getBoundingClientRect();
685
+ const width = this._ref.width;
686
+ const height = this._ref.height;
687
+ // Store pending mouse position
688
+ this._pendingMousePosition = {
689
+ x: e.clientX - rect.left - width / 2,
690
+ y: -(e.clientY - rect.top - height / 2)
691
+ };
692
+ // Batch mouse updates using requestAnimationFrame
693
+ if (!this._mouseUpdateScheduled) {
694
+ this._mouseUpdateScheduled = true;
695
+ requestAnimationFrame(() => {
696
+ this._mouseUpdateScheduled = false;
697
+ if (!this._pendingMousePosition)
698
+ return;
699
+ this._mouse.x = this._pendingMousePosition.x;
700
+ this._mouse.y = this._pendingMousePosition.y;
701
+ const brush = this._mouseObjects[this._currentBrush];
702
+ brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
703
+ brush.active = true;
704
+ brush.mesh.visible = true;
705
+ brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
706
+ brush.mesh.rotation.z = Math.random() * Math.PI * 2;
707
+ if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
708
+ brush.mesh.material.opacity = 1.0;
709
+ }
710
+ this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
711
+ this._pendingMousePosition = null;
712
+ });
713
+ }
714
+ }
715
+ _createProceduralTexture() {
716
+ // Texture size - 1024 provides good balance between quality and performance
717
+ // Reduced from 2048 for better performance
718
+ const texSize = 1024;
719
+ const sourceCanvas = document.createElement('canvas');
720
+ sourceCanvas.width = texSize;
721
+ sourceCanvas.height = texSize;
722
+ const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
723
+ if (!sCtx)
724
+ return new THREE.Texture();
725
+ let seed = this._textureSeed;
726
+ const baseSeed = this._textureSeed;
727
+ function random() {
728
+ const x = Math.sin(seed++) * 10000;
729
+ return x - Math.floor(x);
730
+ }
731
+ // Helper to reset seed for isolated shape generation
732
+ const setSeed = (offset) => {
733
+ seed = baseSeed + offset;
734
+ };
735
+ const colors = this._colors.filter(c => c.enabled).map(c => c.color);
736
+ if (colors.length === 0)
737
+ return new THREE.Texture();
738
+ // Helper functions
739
+ function hexToRgb(hex) {
740
+ const bigint = parseInt(hex.replace('#', ''), 16);
741
+ return {
742
+ r: (bigint >> 16) & 255,
743
+ g: (bigint >> 8) & 255,
744
+ b: bigint & 255
745
+ };
746
+ }
747
+ function rgbToHex(r, g, b) {
748
+ return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
749
+ }
750
+ const getInterColor = () => {
751
+ const c1 = colors[Math.floor(random() * colors.length)];
752
+ const c2 = colors[Math.floor(random() * colors.length)];
753
+ const mix = random() * this._textureColorBlending;
754
+ const rgb1 = hexToRgb(c1);
755
+ const rgb2 = hexToRgb(c2);
756
+ const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
757
+ const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
758
+ const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
759
+ return rgbToHex(r, g, b);
760
+ };
761
+ // === SOURCE CANVAS ===
762
+ // Base with procedural background color so even sparse areas pick it up
763
+ const baseColor = this._proceduralBackgroundColor || "#000000";
764
+ sCtx.fillStyle = baseColor;
765
+ sCtx.fillRect(0, 0, texSize, texSize);
766
+ // Then lay a vertical gradient of mixed colors on top for richness
767
+ const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
768
+ bgGrad.addColorStop(0, getInterColor());
769
+ bgGrad.addColorStop(1, getInterColor());
770
+ sCtx.fillStyle = bgGrad;
771
+ sCtx.fillRect(0, 0, texSize, texSize);
772
+ // Triangles: use configurable count
773
+ for (let i = 0; i < this._textureShapeTriangles; i++) {
774
+ sCtx.fillStyle = getInterColor();
775
+ sCtx.beginPath();
776
+ const x = random() * texSize;
777
+ const y = random() * texSize;
778
+ const s = 100 + random() * 300;
779
+ sCtx.moveTo(x, y);
780
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
781
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
782
+ sCtx.fill();
783
+ }
784
+ // Circles / rings: use configurable count
785
+ for (let i = 0; i < this._textureShapeCircles; i++) {
786
+ sCtx.strokeStyle = getInterColor();
787
+ sCtx.lineWidth = 10 + random() * 50;
788
+ sCtx.beginPath();
789
+ const x = random() * texSize;
790
+ const y = random() * texSize;
791
+ const r = 50 + random() * 150;
792
+ sCtx.arc(x, y, r, 0, Math.PI * 2);
793
+ sCtx.stroke();
794
+ }
795
+ // Bars: use configurable count
796
+ for (let i = 0; i < this._textureShapeBars; i++) {
797
+ sCtx.fillStyle = getInterColor();
798
+ sCtx.save();
799
+ sCtx.translate(random() * texSize, random() * texSize);
800
+ sCtx.rotate(random() * Math.PI);
801
+ sCtx.fillRect(-150, -25, 300, 50);
802
+ sCtx.restore();
803
+ }
804
+ // Squiggles: use configurable count
805
+ sCtx.lineWidth = 15;
806
+ sCtx.lineCap = 'round';
807
+ for (let i = 0; i < this._textureShapeSquiggles; i++) {
808
+ sCtx.strokeStyle = getInterColor();
809
+ sCtx.beginPath();
810
+ let x = random() * texSize;
811
+ let y = random() * texSize;
812
+ sCtx.moveTo(x, y);
813
+ for (let j = 0; j < 4; j++) {
814
+ sCtx.bezierCurveTo(x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300);
815
+ x += (random() - 0.5) * 300;
816
+ y += (random() - 0.5) * 300;
817
+ }
818
+ sCtx.stroke();
819
+ }
820
+ // === MASKED CANVAS ===
821
+ // Masking: Seed isolation
822
+ setSeed(50000);
823
+ const canvas = document.createElement('canvas');
824
+ canvas.width = texSize;
825
+ canvas.height = texSize;
826
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
827
+ if (!ctx)
828
+ return new THREE.Texture();
829
+ // Start filled with the chosen void color so gaps show that color
830
+ ctx.fillStyle = baseColor;
831
+ ctx.fillRect(0, 0, texSize, texSize);
832
+ // Determine layout segments (matter vs void)
833
+ let layoutHead = 0;
834
+ const segments = [];
835
+ while (layoutHead < texSize) {
836
+ const isVoid = random() < this._textureVoidLikelihood;
837
+ if (isVoid) {
838
+ const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
839
+ segments.push({ type: 'void', x: layoutHead, width: w });
840
+ layoutHead += w;
841
+ }
842
+ else {
843
+ const w = 50 + random() * 200;
844
+ segments.push({ type: 'matter', x: layoutHead, width: w });
845
+ layoutHead += w;
846
+ }
847
+ }
848
+ // Render only matter bands from the source into the masked canvas
849
+ for (const seg of segments) {
850
+ if (seg.type === 'matter') {
851
+ const startX = seg.x;
852
+ const endX = Math.min(seg.x + seg.width, texSize);
853
+ let currentX = startX;
854
+ while (currentX < endX) {
855
+ const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
856
+ const sourceX = Math.floor(random() * texSize);
857
+ ctx.drawImage(sourceCanvas, sourceX, 0, stripeWidth, texSize, currentX, 0, stripeWidth, texSize);
858
+ currentX += stripeWidth;
859
+ }
860
+ }
861
+ // void segments: leave as baseColor
862
+ }
863
+ const tex = new THREE.CanvasTexture(canvas);
864
+ // Use mipmapping for better quality when texture is scaled
865
+ tex.minFilter = THREE.LinearMipmapLinearFilter;
866
+ tex.magFilter = THREE.LinearFilter;
867
+ tex.wrapS = THREE.RepeatWrapping;
868
+ tex.wrapT = THREE.RepeatWrapping;
869
+ // Enable anisotropic filtering for much better quality when texture is stretched
870
+ // 16 is a commonly supported value that dramatically improves quality
871
+ tex.anisotropy = 16;
872
+ // Ensure mipmaps are generated
873
+ tex.needsUpdate = true;
874
+ return tex;
875
+ }
289
876
  }
290
877
  function updateCamera(camera, width, height) {
291
878
  const viewPortAreaRatio = 1000000;
@@ -295,10 +882,23 @@ function updateCamera(camera, width, height) {
295
882
  const ratio = width / height;
296
883
  const targetWidth = Math.sqrt(targetPlaneArea * ratio);
297
884
  const targetHeight = targetPlaneArea / targetWidth;
298
- const left = -PLANE_WIDTH / 2;
299
- const right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
300
- const top = PLANE_HEIGHT / 4;
301
- const bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
885
+ let left = -PLANE_WIDTH / 2;
886
+ let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
887
+ let top = PLANE_HEIGHT / 4;
888
+ let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
889
+ // Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
890
+ if (ratio < 1) {
891
+ // Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
892
+ const horizontalScale = ratio;
893
+ left = left * horizontalScale;
894
+ right = right * horizontalScale;
895
+ // Zoom out slightly on mobile (1.1 = 10% zoom out)
896
+ const mobileZoomFactor = 1.05;
897
+ left = left * mobileZoomFactor;
898
+ right = right * mobileZoomFactor;
899
+ top = top * mobileZoomFactor;
900
+ bottom = bottom * mobileZoomFactor;
901
+ }
302
902
  const near = -100;
303
903
  const far = 1000;
304
904
  if (camera instanceof THREE.OrthographicCamera) {
@@ -315,43 +915,69 @@ function updateCamera(camera, width, height) {
315
915
  camera.updateProjectionMatrix();
316
916
  }
317
917
  }
918
+ // Cache shader strings to avoid repeated concatenation
919
+ let cachedVertexShader = null;
920
+ let cachedFragmentShader = null;
318
921
  function buildVertexShader() {
319
- return `
320
-
922
+ if (cachedVertexShader)
923
+ return cachedVertexShader;
924
+ cachedVertexShader = `
321
925
  void main() {
322
-
323
926
  vUv = uv;
324
927
 
928
+ // SCROLLING LOGIC
929
+ // Separate multipliers for wave, color, and flow offsets
930
+ float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
931
+ float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
932
+ float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
933
+
934
+ // 1. DISPLACEMENT (WAVES)
935
+ // We add waveOffset to Y to scroll the wave pattern
325
936
  v_displacement_amount = cnoise( vec3(
326
937
  u_wave_frequency_x * position.x + u_time,
327
- u_wave_frequency_y * position.y + u_time,
938
+ u_wave_frequency_y * (position.y + waveOffset) + u_time,
328
939
  u_time
329
940
  ));
330
941
 
331
- vec3 color;
942
+ // 2. FLOW FIELD
943
+ // Apply flow offset to scroll the flow field mask
944
+ vec2 baseUv = vUv;
945
+ baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
946
+ vec2 flowUv = baseUv;
947
+
948
+ if (u_flow_enabled > 0.5) {
949
+ if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
950
+ vec2 ppp = -1.0 + 2.0 * baseUv;
951
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
952
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
953
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
954
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
955
+
956
+ float r = length(ppp);
957
+ flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
958
+ }
959
+ }
332
960
 
333
- // float t = mod(u_base_color, 100.0);
334
- color = u_colors[0].color;
961
+ // Pass the standard flow UV to fragment shader (for mouse/texture)
962
+ vFlowUv = flowUv;
335
963
 
336
- // Apply y_offset to the noise coordinates
337
- vec2 noise_cord = vUv * u_color_pressure;
338
- // Apply the y-offset to shift the pattern vertically (1:1 pixel ratio)
339
- // Scale the offset to match the UV coordinate space
340
- float scaledOffset = u_y_offset / u_resolution.y;
341
- noise_cord.y -= scaledOffset;
964
+ // 3. COLOR MIXING
965
+ // We take the computed flow UVs and apply the color offset
966
+ // Scale by plane height to match wave offset speed (world space vs UV space)
967
+ vec3 color = u_colors[0].color;
968
+ vec2 adjustedUv = flowUv;
969
+ adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
342
970
 
971
+ vec2 noise_cord = adjustedUv * u_color_pressure;
343
972
  const float minNoise = .0;
344
973
  const float maxNoise = .9;
345
974
 
346
975
  for (int i = 1; i < u_colors_count; i++) {
347
-
348
- if(u_colors[i].is_active == 1.0){
976
+ if(u_colors[i].is_active > 0.5){
349
977
  float noiseFlow = (1. + float(i)) / 30.;
350
978
  float noiseSpeed = (1. + float(i)) * 0.11;
351
979
  float noiseSeed = 13. + float(i) * 7.;
352
980
 
353
- int reverseIndex = u_colors_count - i;
354
-
355
981
  float noise = snoise(
356
982
  vec3(
357
983
  noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
@@ -361,22 +987,24 @@ void main() {
361
987
  ) - (.1 * float(i)) + (.5 * u_color_blending);
362
988
 
363
989
  noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
364
- vec3 nextColor = u_colors[i].color;
365
- color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
990
+ color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
366
991
  }
367
992
  }
368
993
 
369
994
  v_color = color;
370
995
 
996
+ // 4. VERTEX POSITION
371
997
  vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
372
998
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
373
-
374
999
  v_new_position = gl_Position;
375
1000
  }
376
1001
  `;
1002
+ return cachedVertexShader;
377
1003
  }
378
1004
  function buildFragmentShader() {
379
- return `
1005
+ if (cachedFragmentShader)
1006
+ return cachedFragmentShader;
1007
+ cachedFragmentShader = `
380
1008
  float random(vec2 p) {
381
1009
  return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
382
1010
  }
@@ -394,34 +1022,79 @@ float fbm(vec3 x) {
394
1022
  }
395
1023
 
396
1024
  void main() {
397
- vec3 color = v_color;
1025
+ // MOUSE DISTORTION
1026
+ vec2 finalUv = vFlowUv;
1027
+
1028
+ if (u_mouse_distortion_strength > 0.0) {
1029
+ vec4 mouseColor = texture2D(u_mouse_texture, vUv);
1030
+ float mouseValue = mouseColor.r;
1031
+
1032
+ if (mouseValue > 0.001) {
1033
+ float distortionAmount = mouseValue * u_mouse_distortion_strength;
1034
+ vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
1035
+ finalUv -= mouseDisp;
1036
+ }
1037
+ }
1038
+
1039
+ vec3 baseColor;
1040
+
1041
+ if (u_enable_procedural_texture > 0.5) {
1042
+ // Calculate flow field distance for ease effect
1043
+ vec2 ppp = -1.0 + 2.0 * finalUv;
1044
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1045
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1046
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1047
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1048
+ float r = length(ppp); // Flow distance
1049
+
1050
+ // Ease blending: 0 = topographic (flow), 1 = image (UV)
1051
+ float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
1052
+ float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
1053
+ vec2 texUv = vec2(vx, vy);
1054
+
1055
+ // PARALLAX SCROLLING
1056
+ // We manually apply a smaller offset here to make the texture lag behind
1057
+ float parallaxFactor = 0.25; // 25% speed of the color mixing
1058
+ texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
1059
+
1060
+ texUv *= 1.5; // Tiling scale
1061
+
1062
+ vec4 texSample = texture2D(u_procedural_texture, texUv);
1063
+ baseColor = texSample.rgb;
1064
+ } else {
1065
+ baseColor = v_color;
1066
+ }
1067
+
1068
+ vec3 color = baseColor;
1069
+
1070
+ // Post-processing
398
1071
  color += pow(v_displacement_amount, 1.0) * u_highlights;
399
1072
  color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
400
1073
  color = saturation(color, 1.0 + u_saturation);
401
1074
  color = color * u_brightness;
402
1075
 
403
- // Generate grain using fbm
1076
+ // Grain
404
1077
  vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
405
1078
  float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
406
1079
 
407
- // Center the grain around zero
408
1080
  grain = grain * 0.5 + 0.5;
409
1081
  grain -= 0.5;
410
-
411
- // Add sparsity control
412
1082
  grain = (grain > u_grain_sparsity) ? grain : 0.0;
413
-
414
- // Apply grain intensity
415
1083
  grain *= u_grain_intensity;
416
1084
 
417
- // Add grain to color
418
1085
  color += vec3(grain);
419
1086
 
420
1087
  gl_FragColor = vec4(color, 1.0);
421
1088
  }
422
1089
  `;
1090
+ return cachedFragmentShader;
423
1091
  }
424
- const buildUniforms = () => `
1092
+ // Cache uniforms string as well
1093
+ let cachedUniformsShader = null;
1094
+ const buildUniforms = () => {
1095
+ if (cachedUniformsShader)
1096
+ return cachedUniformsShader;
1097
+ cachedUniformsShader = `
425
1098
  precision highp float;
426
1099
 
427
1100
  struct Color {
@@ -453,82 +1126,97 @@ uniform float u_brightness;
453
1126
  uniform float u_color_blending;
454
1127
 
455
1128
  uniform int u_colors_count;
456
- uniform Color u_colors[5];
1129
+ uniform Color u_colors[6];
457
1130
  uniform vec2 u_resolution;
458
1131
 
459
1132
  uniform float u_y_offset;
1133
+ uniform float u_y_offset_wave_multiplier;
1134
+ uniform float u_y_offset_color_multiplier;
1135
+ uniform float u_y_offset_flow_multiplier;
1136
+
1137
+ // Flow field uniforms
1138
+ uniform float u_flow_distortion_a;
1139
+ uniform float u_flow_distortion_b;
1140
+ uniform float u_flow_scale;
1141
+ uniform float u_flow_ease;
1142
+ uniform float u_flow_enabled;
1143
+
1144
+ // Mouse interaction uniforms
1145
+ uniform float u_mouse_distortion_strength;
1146
+ uniform float u_mouse_distortion_radius;
1147
+ uniform float u_mouse_darken;
1148
+ uniform sampler2D u_mouse_texture;
1149
+
1150
+ // Procedural texture uniforms
1151
+ uniform sampler2D u_procedural_texture;
1152
+ uniform float u_enable_procedural_texture;
1153
+ uniform float u_texture_ease;
460
1154
 
461
1155
  varying vec2 vUv;
1156
+ varying vec2 vFlowUv;
462
1157
  varying vec4 v_new_position;
463
1158
  varying vec3 v_color;
464
1159
  varying float v_displacement_amount;
465
1160
 
466
1161
  `;
467
- const buildNoise = () => `
468
-
469
- vec3 mod289(vec3 x)
470
- {
471
- return x - floor(x * (1.0 / 289.0)) * 289.0;
472
- }
473
-
474
- vec4 mod289(vec4 x)
475
- {
476
- return x - floor(x * (1.0 / 289.0)) * 289.0;
477
- }
478
-
479
- vec4 permute(vec4 x)
480
- {
481
- return mod289(((x*34.0)+1.0)*x);
1162
+ return cachedUniformsShader;
1163
+ };
1164
+ // Cache noise functions as well
1165
+ let cachedNoiseShader = null;
1166
+ const buildNoise = () => {
1167
+ if (cachedNoiseShader)
1168
+ return cachedNoiseShader;
1169
+ cachedNoiseShader = `
1170
+
1171
+ // 1. REPLACEMENT PERMUTE:
1172
+ // Uses a hash function (fract/sin) instead of a modular lookup table.
1173
+ vec4 permute(vec4 x) {
1174
+ return floor(fract(sin(x) * 43758.5453123) * 289.0);
482
1175
  }
483
1176
 
484
- vec4 taylorInvSqrt(vec4 r)
485
- {
1177
+ // Taylor Inverse Sqrt
1178
+ vec4 taylorInvSqrt(vec4 r) {
486
1179
  return 1.79284291400159 - 0.85373472095314 * r;
487
1180
  }
488
1181
 
1182
+ // Fade function
489
1183
  vec3 fade(vec3 t) {
490
1184
  return t*t*t*(t*(t*6.0-15.0)+10.0);
491
1185
  }
492
1186
 
493
- float snoise(vec3 v)
494
- {
1187
+ // 3D Simplex Noise
1188
+ float snoise(vec3 v) {
495
1189
  const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
496
1190
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
497
1191
 
498
- // First corner
1192
+ // First corner
499
1193
  vec3 i = floor(v + dot(v, C.yyy) );
500
1194
  vec3 x0 = v - i + dot(i, C.xxx) ;
501
1195
 
502
- // Other corners
1196
+ // Other corners
503
1197
  vec3 g = step(x0.yzx, x0.xyz);
504
1198
  vec3 l = 1.0 - g;
505
1199
  vec3 i1 = min( g.xyz, l.zxy );
506
1200
  vec3 i2 = max( g.xyz, l.zxy );
507
1201
 
508
- // x0 = x0 - 0.0 + 0.0 * C.xxx;
509
- // x1 = x0 - i1 + 1.0 * C.xxx;
510
- // x2 = x0 - i2 + 2.0 * C.xxx;
511
- // x3 = x0 - 1.0 + 3.0 * C.xxx;
512
1202
  vec3 x1 = x0 - i1 + C.xxx;
513
- vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
514
- vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
1203
+ vec3 x2 = x0 - i2 + C.yyy;
1204
+ vec3 x3 = x0 - D.yyy;
515
1205
 
516
- // Permutations
517
- i = mod289(i);
1206
+ // Permutations
518
1207
  vec4 p = permute( permute( permute(
519
1208
  i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
520
1209
  + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
521
1210
  + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
522
1211
 
523
- // Gradients: 7x7 points over a square, mapped onto an octahedron.
524
- // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
1212
+ // Gradients
525
1213
  float n_ = 0.142857142857; // 1.0/7.0
526
1214
  vec3 ns = n_ * D.wyz - D.xzx;
527
1215
 
528
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
1216
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
529
1217
 
530
1218
  vec4 x_ = floor(j * ns.z);
531
- vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
1219
+ vec4 y_ = floor(j - 7.0 * x_ );
532
1220
 
533
1221
  vec4 x = x_ *ns.x + ns.yyyy;
534
1222
  vec4 y = y_ *ns.x + ns.yyyy;
@@ -537,8 +1225,6 @@ float snoise(vec3 v)
537
1225
  vec4 b0 = vec4( x.xy, y.xy );
538
1226
  vec4 b1 = vec4( x.zw, y.zw );
539
1227
 
540
- //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
541
- //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
542
1228
  vec4 s0 = floor(b0)*2.0 + 1.0;
543
1229
  vec4 s1 = floor(b1)*2.0 + 1.0;
544
1230
  vec4 sh = -step(h, vec4(0.0));
@@ -551,14 +1237,14 @@ float snoise(vec3 v)
551
1237
  vec3 p2 = vec3(a1.xy,h.z);
552
1238
  vec3 p3 = vec3(a1.zw,h.w);
553
1239
 
554
- //Normalise gradients
1240
+ // Normalise gradients
555
1241
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
556
1242
  p0 *= norm.x;
557
1243
  p1 *= norm.y;
558
1244
  p2 *= norm.z;
559
1245
  p3 *= norm.w;
560
1246
 
561
- // Mix final noise value
1247
+ // Mix final noise value
562
1248
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
563
1249
  m = m * m;
564
1250
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
@@ -568,12 +1254,11 @@ float snoise(vec3 v)
568
1254
  // Classic Perlin noise
569
1255
  float cnoise(vec3 P)
570
1256
  {
571
- vec3 Pi0 = floor(P); // Integer part for indexing
572
- vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
573
- Pi0 = mod289(Pi0);
574
- Pi1 = mod289(Pi1);
575
- vec3 Pf0 = fract(P); // Fractional part for interpolation
576
- vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
1257
+ vec3 Pi0 = floor(P);
1258
+ vec3 Pi1 = Pi0 + vec3(1.0);
1259
+
1260
+ vec3 Pf0 = fract(P);
1261
+ vec3 Pf1 = Pf0 - vec3(1.0);
577
1262
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
578
1263
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
579
1264
  vec4 iz0 = Pi0.zzzz;
@@ -634,48 +1319,15 @@ float cnoise(vec3 P)
634
1319
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
635
1320
  return 2.2 * n_xyz;
636
1321
  }
637
-
638
- // YUV to RGB matrix
639
- mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
640
- 1.0, -0.39465, -0.58060,
641
- 1.0, 2.03211, 0.0);
642
-
643
- // RGB to YUV matrix
644
- mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
645
- -0.09991, -0.33609, 0.43600,
646
- 0.615, -0.5586, -0.05639);
647
-
648
- vec3 oklab2rgb(vec3 linear)
649
- {
650
- const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
651
- 0.5362752080, 0.6807189584, 0.2818474174,
652
- 0.0514575653, 0.1074065790, 0.6302613616);
653
-
654
- const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
655
- +0.7936177850, -2.4285922050, +0.7827717662,
656
- -0.0040720468, +0.4505937099, -0.8086757660);
657
-
658
- vec3 lms = im1 * linear;
659
-
660
- return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
661
- }
662
-
663
- vec3 rgb2oklab(vec3 oklab)
664
- {
665
- const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
666
- +0.396337777, -0.105561346, -0.089484178,
667
- +0.215803757, -0.063854173, -1.291485548);
668
-
669
- const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
670
- -3.307216883, +2.609332323, -0.703476310,
671
- +0.230759054, -0.341134429, +1.706862569);
672
- vec3 lms = m1 * oklab;
673
-
674
- return m2 * (lms * lms * lms);
675
- }
676
-
677
- `;
678
- const buildColorFunctions = () => `
1322
+ `;
1323
+ return cachedNoiseShader;
1324
+ };
1325
+ // Cache color functions as well
1326
+ let cachedColorFunctionsShader = null;
1327
+ const buildColorFunctions = () => {
1328
+ if (cachedColorFunctionsShader)
1329
+ return cachedColorFunctionsShader;
1330
+ cachedColorFunctionsShader = `
679
1331
 
680
1332
  vec3 saturation(vec3 rgb, float adjustment) {
681
1333
  const vec3 W = vec3(0.2125, 0.7154, 0.0721);
@@ -719,6 +1371,8 @@ vec3 hsv2rgb(vec3 c)
719
1371
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
720
1372
  }
721
1373
  `;
1374
+ return cachedColorFunctionsShader;
1375
+ };
722
1376
  const setLinkStyles = (link) => {
723
1377
  link.id = LINK_ID;
724
1378
  link.href = "https://neat.firecms.co";
@@ -743,13 +1397,14 @@ const addNeatLink = (ref) => {
743
1397
  for (let i = 0; i < existingLinks.length; i++) {
744
1398
  if (existingLinks[i].id === LINK_ID) {
745
1399
  setLinkStyles(existingLinks[i]);
746
- return;
1400
+ return existingLinks[i];
747
1401
  }
748
1402
  }
749
1403
  }
750
1404
  const link = document.createElement("a");
751
1405
  setLinkStyles(link);
752
1406
  ref.parentElement?.appendChild(link);
1407
+ return link;
753
1408
  };
754
1409
  function getElapsedSecondsInLastHour() {
755
1410
  const now = new Date();