@firecms/neat 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,59 @@ 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();
33
74
  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;
75
+ 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,
76
+ // Flow field parameters
77
+ flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
78
+ // Mouse interaction
79
+ mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
80
+ // Texture generation
81
+ 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
82
  this._ref = ref;
36
83
  this.destroy = this.destroy.bind(this);
37
84
  this._initScene = this._initScene.bind(this);
@@ -56,75 +103,141 @@ export class NeatGradient {
56
103
  this.backgroundColor = backgroundColor;
57
104
  this.backgroundAlpha = backgroundAlpha;
58
105
  this.yOffset = yOffset;
106
+ this.yOffsetWaveMultiplier = yOffsetWaveMultiplier;
107
+ this.yOffsetColorMultiplier = yOffsetColorMultiplier;
108
+ this.yOffsetFlowMultiplier = yOffsetFlowMultiplier;
109
+ // Flow field
110
+ this.flowDistortionA = flowDistortionA;
111
+ this.flowDistortionB = flowDistortionB;
112
+ this.flowScale = flowScale;
113
+ this.flowEase = flowEase;
114
+ this.flowEnabled = flowEnabled;
115
+ // Mouse interaction
116
+ this.mouseDistortionStrength = mouseDistortionStrength;
117
+ this.mouseDistortionRadius = mouseDistortionRadius;
118
+ this.mouseDecayRate = mouseDecayRate;
119
+ this.mouseDarken = mouseDarken;
120
+ // Texture generation
121
+ this.enableProceduralTexture = enableProceduralTexture;
122
+ this.textureVoidLikelihood = textureVoidLikelihood;
123
+ this.textureVoidWidthMin = textureVoidWidthMin;
124
+ this.textureVoidWidthMax = textureVoidWidthMax;
125
+ this.textureBandDensity = textureBandDensity;
126
+ this.textureColorBlending = textureColorBlending;
127
+ this.textureSeed = textureSeed;
128
+ this.textureEase = textureEase;
129
+ this._proceduralBackgroundColor = proceduralBackgroundColor;
130
+ this._textureShapeTriangles = textureShapeTriangles;
131
+ this._textureShapeCircles = textureShapeCircles;
132
+ this._textureShapeBars = textureShapeBars;
133
+ this._textureShapeSquiggles = textureShapeSquiggles;
134
+ // FIX 1: Setup mouse resources BEFORE building the material/scene
135
+ // This ensures u_mouse_texture isn't null during material compilation
136
+ this._setupMouseInteraction();
59
137
  this.sceneState = this._initScene(resolution);
60
138
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
61
139
  const render = () => {
62
- const { renderer, camera, scene, meshes } = this.sceneState;
140
+ const { renderer, camera, scene } = this.sceneState;
141
+ // Optimization: check if cached link is still valid in DOM, otherwise search
63
142
  if (Math.floor(tick * 10) % 5 === 0) {
64
- addNeatLink(ref);
143
+ if (!this._linkElement || !document.contains(this._linkElement)) {
144
+ this._linkElement = addNeatLink(ref);
145
+ }
65
146
  }
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
- ];
147
+ // Update Uniforms efficiently without creating new objects
148
+ if (this._cachedUniforms) {
149
+ const u = this._cachedUniforms;
84
150
  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
- });
151
+ u.u_time.value = tick;
152
+ u.u_resolution.value.set(this._ref.width, this._ref.height);
153
+ u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
154
+ // Directly assign simple values
155
+ u.u_wave_frequency_x.value = this._waveFrequencyX;
156
+ u.u_wave_frequency_y.value = this._waveFrequencyY;
157
+ u.u_wave_amplitude.value = this._waveAmplitude;
158
+ u.u_color_blending.value = this._colorBlending;
159
+ u.u_shadows.value = this._shadows;
160
+ u.u_highlights.value = this._highlights;
161
+ u.u_saturation.value = this._saturation;
162
+ u.u_brightness.value = this._brightness;
163
+ u.u_grain_intensity.value = this._grainIntensity;
164
+ u.u_grain_sparsity.value = this._grainSparsity;
165
+ u.u_grain_speed.value = this._grainSpeed;
166
+ u.u_grain_scale.value = this._grainScale;
167
+ u.u_y_offset.value = this._yOffset;
168
+ u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
169
+ u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
170
+ u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
171
+ u.u_flow_distortion_a.value = this._flowDistortionA;
172
+ u.u_flow_distortion_b.value = this._flowDistortionB;
173
+ u.u_flow_scale.value = this._flowScale;
174
+ u.u_flow_ease.value = this._flowEase;
175
+ u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
176
+ u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
177
+ u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
178
+ u.u_mouse_darken.value = this._mouseDarken;
179
+ u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
180
+ u.u_procedural_texture.value = this._proceduralTexture;
181
+ u.u_texture_ease.value = this._textureEase;
182
+ // Optimized Color Update: Update the existing array objects instead of recreating array
183
+ const shaderColors = u.u_colors.value;
184
+ for (let i = 0; i < COLORS_COUNT; i++) {
185
+ if (i < this._colors.length) {
186
+ const c = this._colors[i];
187
+ shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
188
+ shaderColors[i].color.setStyle(c.color, "");
189
+ shaderColors[i].influence = c.influence || 0;
190
+ }
191
+ else {
192
+ shaderColors[i].is_active = 0.0;
193
+ }
194
+ }
195
+ u.u_colors_count.value = COLORS_COUNT;
196
+ // Wireframe is a material property, not a uniform
197
+ // @ts-ignore - access material safely
198
+ this.sceneState.meshes[0].material.wireframe = this._wireframe;
199
+ }
200
+ // Render mouse interaction to FBO
201
+ if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
202
+ let hasActiveBrushes = false;
203
+ // Update mouse objects - decay rate controls how fast trails fade
204
+ for (let i = 0; i < this._mouseObjects.length; i++) {
205
+ const obj = this._mouseObjects[i];
206
+ if (obj.mesh.visible) {
207
+ hasActiveBrushes = true;
208
+ obj.mesh.rotation.z += 0.01;
209
+ if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
210
+ // Decay only affects opacity
211
+ obj.mesh.material.opacity *= this._mouseDecayRate;
212
+ if (obj.mesh.material.opacity < 0.01) {
213
+ obj.mesh.visible = false;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ // FIX 2: Handle FBO Clearing correctly
219
+ // Store current clear color (likely the main background color)
220
+ renderer.getClearColor(this._tempClearColor);
221
+ const oldClearAlpha = renderer.getClearAlpha();
222
+ // Set clear color to Black/Transparent for the FBO.
223
+ // Important: If we use the main background color (e.g. White), the FBO
224
+ // will be white, causing 100% distortion everywhere.
225
+ renderer.setClearColor(0x000000, 0.0);
226
+ renderer.setRenderTarget(this._mouseFBO);
227
+ renderer.clear();
228
+ if (hasActiveBrushes) {
229
+ renderer.render(this._sceneMouse, this._cameraMouse);
230
+ }
231
+ renderer.setRenderTarget(null);
232
+ // Restore main background color for the actual scene render
233
+ renderer.setClearColor(this._tempClearColor, oldClearAlpha);
234
+ // Update mouse texture uniform
235
+ if (this._cachedUniforms) {
236
+ this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
237
+ }
238
+ }
239
+ // Ensure we set the clear color for the main scene explicitly before rendering
240
+ renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
128
241
  renderer.render(scene, camera);
129
242
  this.requestRef = requestAnimationFrame(render);
130
243
  };
@@ -135,6 +248,18 @@ export class NeatGradient {
135
248
  const height = canvas.clientHeight;
136
249
  this.sceneState.renderer.setSize(width, height, false);
137
250
  updateCamera(this.sceneState.camera, width, height);
251
+ // FIX 3: Update Mouse FBO and Camera on resize
252
+ // If we don't do this, mouse coordinates map incorrectly after a resize
253
+ if (this._mouseFBO && this._cameraMouse) {
254
+ const fSize = height / 2;
255
+ const aspect = width / height;
256
+ this._mouseFBO.setSize(width / 2, height / 2);
257
+ this._cameraMouse.left = -fSize * aspect;
258
+ this._cameraMouse.right = fSize * aspect;
259
+ this._cameraMouse.top = fSize;
260
+ this._cameraMouse.bottom = -fSize;
261
+ this._cameraMouse.updateProjectionMatrix();
262
+ }
138
263
  };
139
264
  this.sizeObserver = new ResizeObserver(entries => {
140
265
  setSize();
@@ -146,6 +271,21 @@ export class NeatGradient {
146
271
  if (this) {
147
272
  cancelAnimationFrame(this.requestRef);
148
273
  this.sizeObserver.disconnect();
274
+ // Cleanup WebGL resources
275
+ if (this.sceneState) {
276
+ this.sceneState.renderer.dispose();
277
+ this.sceneState.meshes.forEach(m => {
278
+ m.geometry.dispose();
279
+ if (Array.isArray(m.material))
280
+ m.material.forEach(mat => mat.dispose());
281
+ else
282
+ m.material.dispose();
283
+ });
284
+ }
285
+ if (this._mouseFBO)
286
+ this._mouseFBO.dispose();
287
+ if (this._proceduralTexture)
288
+ this._proceduralTexture.dispose();
149
289
  }
150
290
  }
151
291
  downloadAsPNG(filename = "neat.png") {
@@ -217,8 +357,152 @@ export class NeatGradient {
217
357
  set yOffset(yOffset) {
218
358
  this._yOffset = yOffset;
219
359
  }
360
+ get yOffsetWaveMultiplier() {
361
+ return this._yOffsetWaveMultiplier * 1000;
362
+ }
363
+ set yOffsetWaveMultiplier(value) {
364
+ this._yOffsetWaveMultiplier = value / 1000;
365
+ }
366
+ get yOffsetColorMultiplier() {
367
+ return this._yOffsetColorMultiplier * 1000;
368
+ }
369
+ set yOffsetColorMultiplier(value) {
370
+ this._yOffsetColorMultiplier = value / 1000;
371
+ }
372
+ get yOffsetFlowMultiplier() {
373
+ return this._yOffsetFlowMultiplier * 1000;
374
+ }
375
+ set yOffsetFlowMultiplier(value) {
376
+ this._yOffsetFlowMultiplier = value / 1000;
377
+ }
378
+ set flowDistortionA(value) {
379
+ this._flowDistortionA = value;
380
+ }
381
+ set flowDistortionB(value) {
382
+ this._flowDistortionB = value;
383
+ }
384
+ set flowScale(value) {
385
+ this._flowScale = value;
386
+ }
387
+ set flowEase(value) {
388
+ this._flowEase = value;
389
+ }
390
+ set flowEnabled(value) {
391
+ this._flowEnabled = value;
392
+ }
393
+ get flowEnabled() {
394
+ return this._flowEnabled;
395
+ }
396
+ set mouseDistortionStrength(value) {
397
+ this._mouseDistortionStrength = Math.max(0, value);
398
+ }
399
+ set mouseDistortionRadius(value) {
400
+ // Clamp to a sane range in UV space
401
+ this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
402
+ // Update brush scale when radius changes
403
+ this._updateBrushScale();
404
+ }
405
+ _updateBrushScale() {
406
+ if (!this._mouseObjects || this._mouseObjects.length === 0)
407
+ return;
408
+ // Radius directly controls the brush scale
409
+ // Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
410
+ this._mouseBrushBaseScale = this._mouseDistortionRadius;
411
+ }
412
+ set mouseDecayRate(value) {
413
+ // Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
414
+ this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
415
+ }
416
+ set mouseDarken(value) {
417
+ this._mouseDarken = value;
418
+ }
419
+ set enableProceduralTexture(value) {
420
+ this._enableProceduralTexture = value;
421
+ if (value && !this._proceduralTexture) {
422
+ this._proceduralTexture = this._createProceduralTexture();
423
+ }
424
+ }
425
+ set textureVoidLikelihood(value) {
426
+ this._textureVoidLikelihood = value;
427
+ if (this._enableProceduralTexture) {
428
+ this._proceduralTexture = this._createProceduralTexture();
429
+ }
430
+ }
431
+ set textureVoidWidthMin(value) {
432
+ this._textureVoidWidthMin = value;
433
+ if (this._enableProceduralTexture) {
434
+ this._proceduralTexture = this._createProceduralTexture();
435
+ }
436
+ }
437
+ set textureVoidWidthMax(value) {
438
+ this._textureVoidWidthMax = value;
439
+ if (this._enableProceduralTexture) {
440
+ this._proceduralTexture = this._createProceduralTexture();
441
+ }
442
+ }
443
+ set textureBandDensity(value) {
444
+ this._textureBandDensity = value;
445
+ if (this._enableProceduralTexture) {
446
+ this._proceduralTexture = this._createProceduralTexture();
447
+ }
448
+ }
449
+ set textureColorBlending(value) {
450
+ this._textureColorBlending = value;
451
+ if (this._enableProceduralTexture) {
452
+ this._proceduralTexture = this._createProceduralTexture();
453
+ }
454
+ }
455
+ set textureSeed(value) {
456
+ this._textureSeed = value;
457
+ if (this._enableProceduralTexture) {
458
+ this._proceduralTexture = this._createProceduralTexture();
459
+ }
460
+ }
461
+ get textureEase() {
462
+ return this._textureEase;
463
+ }
464
+ set textureEase(value) {
465
+ this._textureEase = value;
466
+ }
467
+ set proceduralBackgroundColor(value) {
468
+ this._proceduralBackgroundColor = value;
469
+ if (this._enableProceduralTexture) {
470
+ this._proceduralTexture = this._createProceduralTexture();
471
+ }
472
+ }
473
+ set textureShapeTriangles(value) {
474
+ this._textureShapeTriangles = value;
475
+ if (this._enableProceduralTexture)
476
+ this._proceduralTexture = this._createProceduralTexture();
477
+ }
478
+ set textureShapeCircles(value) {
479
+ this._textureShapeCircles = value;
480
+ if (this._enableProceduralTexture)
481
+ this._proceduralTexture = this._createProceduralTexture();
482
+ }
483
+ set textureShapeBars(value) {
484
+ this._textureShapeBars = value;
485
+ if (this._enableProceduralTexture)
486
+ this._proceduralTexture = this._createProceduralTexture();
487
+ }
488
+ set textureShapeSquiggles(value) {
489
+ this._textureShapeSquiggles = value;
490
+ if (this._enableProceduralTexture)
491
+ this._proceduralTexture = this._createProceduralTexture();
492
+ }
220
493
  _initScene(resolution) {
221
494
  const width = this._ref.width, height = this._ref.height;
495
+ // Cleanup existing renderer if needed
496
+ if (this.sceneState && this.sceneState.renderer) {
497
+ this.sceneState.renderer.dispose();
498
+ this.sceneState.meshes.forEach(m => {
499
+ m.geometry.dispose();
500
+ if (Array.isArray(m.material))
501
+ m.material.forEach(mat => mat.dispose());
502
+ else
503
+ m.material.dispose();
504
+ });
505
+ }
222
506
  const renderer = new THREE.WebGLRenderer({
223
507
  // antialias: true,
224
508
  alpha: true,
@@ -249,17 +533,13 @@ export class NeatGradient {
249
533
  };
250
534
  }
251
535
  _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
- ];
536
+ // Initialize stable array structure for colors
537
+ // We create 6 objects and just update them in the render loop to avoid GC
538
+ const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
539
+ is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
540
+ color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
541
+ influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
542
+ }));
263
543
  const uniforms = {
264
544
  u_time: { value: 0 },
265
545
  u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
@@ -277,15 +557,272 @@ export class NeatGradient {
277
557
  u_grain_sparsity: { value: this._grainSparsity },
278
558
  u_grain_scale: { value: this._grainScale },
279
559
  u_grain_speed: { value: this._grainSpeed },
560
+ // Flow field
561
+ u_flow_distortion_a: { value: this._flowDistortionA },
562
+ u_flow_distortion_b: { value: this._flowDistortionB },
563
+ u_flow_scale: { value: this._flowScale },
564
+ u_flow_ease: { value: this._flowEase },
565
+ u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
566
+ // Y offset multipliers
567
+ u_y_offset: { value: this._yOffset },
568
+ u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
569
+ u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
570
+ u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
571
+ // Mouse interaction
572
+ u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
573
+ u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
574
+ u_mouse_darken: { value: this._mouseDarken },
575
+ u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
576
+ // Procedural texture
577
+ u_procedural_texture: { value: this._proceduralTexture },
578
+ u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
579
+ u_texture_ease: { value: this._textureEase },
580
+ u_saturation: { value: this._saturation },
581
+ u_brightness: { value: this._brightness },
582
+ u_color_blending: { value: this._colorBlending }
280
583
  };
281
584
  const material = new THREE.ShaderMaterial({
282
585
  uniforms: uniforms,
283
586
  vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
284
587
  fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
285
588
  });
589
+ // Cache the uniforms object for direct access in render loop
590
+ this._cachedUniforms = uniforms;
286
591
  material.wireframe = WIREFRAME;
287
592
  return material;
288
593
  }
594
+ _setupMouseInteraction() {
595
+ if (!this._ref)
596
+ return;
597
+ const width = this._ref.width;
598
+ const height = this._ref.height;
599
+ // Create mouse FBO
600
+ this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
601
+ // Create mouse scene and camera
602
+ this._sceneMouse = new THREE.Scene();
603
+ const fSize = height / 2;
604
+ const aspect = width / height;
605
+ // FIX 4: Ensure near plane allows viewing objects at Z=0
606
+ // Near -100 is safer for objects at 0
607
+ this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
608
+ this._cameraMouse.position.set(0, 0, 100);
609
+ // Create brush texture - More visible and impactful
610
+ const brushCanvas = document.createElement('canvas');
611
+ brushCanvas.width = 128;
612
+ brushCanvas.height = 128;
613
+ const bCtx = brushCanvas.getContext('2d');
614
+ if (bCtx) {
615
+ const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
616
+ // Match reference implementation's stronger gradient
617
+ grd.addColorStop(0, 'rgba(255,255,255,0.8)');
618
+ grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
619
+ grd.addColorStop(1, 'rgba(255,255,255,0)');
620
+ bCtx.fillStyle = grd;
621
+ bCtx.fillRect(0, 0, 128, 128);
622
+ }
623
+ const brushTex = new THREE.CanvasTexture(brushCanvas);
624
+ const brushMat = new THREE.MeshBasicMaterial({
625
+ map: brushTex,
626
+ transparent: true,
627
+ opacity: 1.0,
628
+ depthTest: false,
629
+ blending: THREE.AdditiveBlending // Additive blending for better accumulation
630
+ });
631
+ // Brush geometry size - will be scaled by radius parameter
632
+ const brushGeo = new THREE.PlaneGeometry(200, 200);
633
+ // Create brush pool
634
+ const brushPoolSize = 50;
635
+ for (let i = 0; i < brushPoolSize; i++) {
636
+ const m = new THREE.Mesh(brushGeo, brushMat.clone());
637
+ m.visible = false;
638
+ this._sceneMouse.add(m);
639
+ this._mouseObjects.push({ mesh: m, active: false });
640
+ }
641
+ // Initialize brush scale based on current radius
642
+ this._updateBrushScale();
643
+ // Add mouse move listener
644
+ this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
645
+ }
646
+ _onMouseMove(e) {
647
+ if (!this._ref || !this._sceneMouse)
648
+ return;
649
+ const rect = this._ref.getBoundingClientRect();
650
+ const width = this._ref.width;
651
+ const height = this._ref.height;
652
+ this._mouse.x = e.clientX - rect.left - width / 2;
653
+ this._mouse.y = -(e.clientY - rect.top - height / 2);
654
+ const brush = this._mouseObjects[this._currentBrush];
655
+ brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
656
+ brush.active = true;
657
+ brush.mesh.visible = true;
658
+ brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
659
+ brush.mesh.rotation.z = Math.random() * Math.PI * 2;
660
+ if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
661
+ brush.mesh.material.opacity = 1.0;
662
+ }
663
+ this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
664
+ }
665
+ _createProceduralTexture() {
666
+ // Texture size - 1024 provides good balance between quality and performance
667
+ // Can be increased to 2048 for even better quality if needed
668
+ const texSize = 1024;
669
+ const sourceCanvas = document.createElement('canvas');
670
+ sourceCanvas.width = texSize;
671
+ sourceCanvas.height = texSize;
672
+ const sCtx = sourceCanvas.getContext('2d');
673
+ if (!sCtx)
674
+ return new THREE.Texture();
675
+ let seed = this._textureSeed;
676
+ const baseSeed = this._textureSeed;
677
+ function random() {
678
+ const x = Math.sin(seed++) * 10000;
679
+ return x - Math.floor(x);
680
+ }
681
+ // Helper to reset seed for isolated shape generation
682
+ const setSeed = (offset) => {
683
+ seed = baseSeed + offset;
684
+ };
685
+ const colors = this._colors.filter(c => c.enabled).map(c => c.color);
686
+ if (colors.length === 0)
687
+ return new THREE.Texture();
688
+ // Helper functions
689
+ function hexToRgb(hex) {
690
+ const bigint = parseInt(hex.replace('#', ''), 16);
691
+ return {
692
+ r: (bigint >> 16) & 255,
693
+ g: (bigint >> 8) & 255,
694
+ b: bigint & 255
695
+ };
696
+ }
697
+ function rgbToHex(r, g, b) {
698
+ return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
699
+ }
700
+ const getInterColor = () => {
701
+ const c1 = colors[Math.floor(random() * colors.length)];
702
+ const c2 = colors[Math.floor(random() * colors.length)];
703
+ const mix = random() * this._textureColorBlending;
704
+ const rgb1 = hexToRgb(c1);
705
+ const rgb2 = hexToRgb(c2);
706
+ const r = rgb1.r + (rgb2.r - rgb1.r) * mix;
707
+ const g = rgb1.g + (rgb2.g - rgb1.g) * mix;
708
+ const b = rgb1.b + (rgb2.b - rgb1.b) * mix;
709
+ return rgbToHex(r, g, b);
710
+ };
711
+ // === SOURCE CANVAS ===
712
+ // Base with procedural background color so even sparse areas pick it up
713
+ const baseColor = this._proceduralBackgroundColor || "#000000";
714
+ sCtx.fillStyle = baseColor;
715
+ sCtx.fillRect(0, 0, texSize, texSize);
716
+ // Then lay a vertical gradient of mixed colors on top for richness
717
+ const bgGrad = sCtx.createLinearGradient(0, 0, 0, texSize);
718
+ bgGrad.addColorStop(0, getInterColor());
719
+ bgGrad.addColorStop(1, getInterColor());
720
+ sCtx.fillStyle = bgGrad;
721
+ sCtx.fillRect(0, 0, texSize, texSize);
722
+ // Triangles: use configurable count
723
+ for (let i = 0; i < this._textureShapeTriangles; i++) {
724
+ sCtx.fillStyle = getInterColor();
725
+ sCtx.beginPath();
726
+ const x = random() * texSize;
727
+ const y = random() * texSize;
728
+ const s = 100 + random() * 300;
729
+ sCtx.moveTo(x, y);
730
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
731
+ sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
732
+ sCtx.fill();
733
+ }
734
+ // Circles / rings: use configurable count
735
+ for (let i = 0; i < this._textureShapeCircles; i++) {
736
+ sCtx.strokeStyle = getInterColor();
737
+ sCtx.lineWidth = 10 + random() * 50;
738
+ sCtx.beginPath();
739
+ const x = random() * texSize;
740
+ const y = random() * texSize;
741
+ const r = 50 + random() * 150;
742
+ sCtx.arc(x, y, r, 0, Math.PI * 2);
743
+ sCtx.stroke();
744
+ }
745
+ // Bars: use configurable count
746
+ for (let i = 0; i < this._textureShapeBars; i++) {
747
+ sCtx.fillStyle = getInterColor();
748
+ sCtx.save();
749
+ sCtx.translate(random() * texSize, random() * texSize);
750
+ sCtx.rotate(random() * Math.PI);
751
+ sCtx.fillRect(-150, -25, 300, 50);
752
+ sCtx.restore();
753
+ }
754
+ // Squiggles: use configurable count
755
+ sCtx.lineWidth = 15;
756
+ sCtx.lineCap = 'round';
757
+ for (let i = 0; i < this._textureShapeSquiggles; i++) {
758
+ sCtx.strokeStyle = getInterColor();
759
+ sCtx.beginPath();
760
+ let x = random() * texSize;
761
+ let y = random() * texSize;
762
+ sCtx.moveTo(x, y);
763
+ for (let j = 0; j < 4; j++) {
764
+ 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);
765
+ x += (random() - 0.5) * 300;
766
+ y += (random() - 0.5) * 300;
767
+ }
768
+ sCtx.stroke();
769
+ }
770
+ // === MASKED CANVAS ===
771
+ // Masking: Seed isolation
772
+ setSeed(50000);
773
+ const canvas = document.createElement('canvas');
774
+ canvas.width = texSize;
775
+ canvas.height = texSize;
776
+ const ctx = canvas.getContext('2d');
777
+ if (!ctx)
778
+ return new THREE.Texture();
779
+ // Start filled with the chosen void color so gaps show that color
780
+ ctx.fillStyle = baseColor;
781
+ ctx.fillRect(0, 0, texSize, texSize);
782
+ // Determine layout segments (matter vs void)
783
+ let layoutHead = 0;
784
+ const segments = [];
785
+ while (layoutHead < texSize) {
786
+ const isVoid = random() < this._textureVoidLikelihood;
787
+ if (isVoid) {
788
+ const w = this._textureVoidWidthMin + random() * (this._textureVoidWidthMax - this._textureVoidWidthMin);
789
+ segments.push({ type: 'void', x: layoutHead, width: w });
790
+ layoutHead += w;
791
+ }
792
+ else {
793
+ const w = 50 + random() * 200;
794
+ segments.push({ type: 'matter', x: layoutHead, width: w });
795
+ layoutHead += w;
796
+ }
797
+ }
798
+ // Render only matter bands from the source into the masked canvas
799
+ for (const seg of segments) {
800
+ if (seg.type === 'matter') {
801
+ const startX = seg.x;
802
+ const endX = Math.min(seg.x + seg.width, texSize);
803
+ let currentX = startX;
804
+ while (currentX < endX) {
805
+ const stripeWidth = (2 + random() * 20) / this._textureBandDensity;
806
+ const sourceX = Math.floor(random() * texSize);
807
+ ctx.drawImage(sourceCanvas, sourceX, 0, stripeWidth, texSize, currentX, 0, stripeWidth, texSize);
808
+ currentX += stripeWidth;
809
+ }
810
+ }
811
+ // void segments: leave as baseColor
812
+ }
813
+ const tex = new THREE.CanvasTexture(canvas);
814
+ // Use mipmapping for better quality when texture is scaled
815
+ tex.minFilter = THREE.LinearMipmapLinearFilter;
816
+ tex.magFilter = THREE.LinearFilter;
817
+ tex.wrapS = THREE.RepeatWrapping;
818
+ tex.wrapT = THREE.RepeatWrapping;
819
+ // Enable anisotropic filtering for much better quality when texture is stretched
820
+ // 16 is a commonly supported value that dramatically improves quality
821
+ tex.anisotropy = 16;
822
+ // Ensure mipmaps are generated
823
+ tex.needsUpdate = true;
824
+ return tex;
825
+ }
289
826
  }
290
827
  function updateCamera(camera, width, height) {
291
828
  const viewPortAreaRatio = 1000000;
@@ -317,41 +854,62 @@ function updateCamera(camera, width, height) {
317
854
  }
318
855
  function buildVertexShader() {
319
856
  return `
320
-
321
857
  void main() {
322
-
323
858
  vUv = uv;
324
859
 
860
+ // SCROLLING LOGIC
861
+ // Separate multipliers for wave, color, and flow offsets
862
+ float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
863
+ float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
864
+ float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
865
+
866
+ // 1. DISPLACEMENT (WAVES)
867
+ // We add waveOffset to Y to scroll the wave pattern
325
868
  v_displacement_amount = cnoise( vec3(
326
869
  u_wave_frequency_x * position.x + u_time,
327
- u_wave_frequency_y * position.y + u_time,
870
+ u_wave_frequency_y * (position.y + waveOffset) + u_time,
328
871
  u_time
329
872
  ));
330
873
 
331
- vec3 color;
874
+ // 2. FLOW FIELD
875
+ // Apply flow offset to scroll the flow field mask
876
+ vec2 baseUv = vUv;
877
+ baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
878
+ vec2 flowUv = baseUv;
879
+
880
+ if (u_flow_enabled > 0.5) {
881
+ if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
882
+ vec2 ppp = -1.0 + 2.0 * baseUv;
883
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
884
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
885
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
886
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
887
+
888
+ float r = length(ppp);
889
+ flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
890
+ }
891
+ }
332
892
 
333
- // float t = mod(u_base_color, 100.0);
334
- color = u_colors[0].color;
893
+ // Pass the standard flow UV to fragment shader (for mouse/texture)
894
+ vFlowUv = flowUv;
335
895
 
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;
896
+ // 3. COLOR MIXING
897
+ // We take the computed flow UVs and apply the color offset
898
+ // Scale by plane height to match wave offset speed (world space vs UV space)
899
+ vec3 color = u_colors[0].color;
900
+ vec2 adjustedUv = flowUv;
901
+ adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
342
902
 
903
+ vec2 noise_cord = adjustedUv * u_color_pressure;
343
904
  const float minNoise = .0;
344
905
  const float maxNoise = .9;
345
906
 
346
907
  for (int i = 1; i < u_colors_count; i++) {
347
-
348
- if(u_colors[i].is_active == 1.0){
908
+ if(u_colors[i].is_active > 0.5){
349
909
  float noiseFlow = (1. + float(i)) / 30.;
350
910
  float noiseSpeed = (1. + float(i)) * 0.11;
351
911
  float noiseSeed = 13. + float(i) * 7.;
352
912
 
353
- int reverseIndex = u_colors_count - i;
354
-
355
913
  float noise = snoise(
356
914
  vec3(
357
915
  noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
@@ -361,16 +919,15 @@ void main() {
361
919
  ) - (.1 * float(i)) + (.5 * u_color_blending);
362
920
 
363
921
  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));
922
+ color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
366
923
  }
367
924
  }
368
925
 
369
926
  v_color = color;
370
927
 
928
+ // 4. VERTEX POSITION
371
929
  vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
372
930
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
373
-
374
931
  v_new_position = gl_Position;
375
932
  }
376
933
  `;
@@ -394,27 +951,66 @@ float fbm(vec3 x) {
394
951
  }
395
952
 
396
953
  void main() {
397
- vec3 color = v_color;
954
+ // MOUSE DISTORTION
955
+ vec2 finalUv = vFlowUv;
956
+
957
+ if (u_mouse_distortion_strength > 0.0) {
958
+ vec4 mouseColor = texture2D(u_mouse_texture, vUv);
959
+ float mouseValue = mouseColor.r;
960
+
961
+ if (mouseValue > 0.001) {
962
+ float distortionAmount = mouseValue * u_mouse_distortion_strength;
963
+ vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
964
+ finalUv -= mouseDisp;
965
+ }
966
+ }
967
+
968
+ vec3 baseColor;
969
+
970
+ if (u_enable_procedural_texture > 0.5) {
971
+ // Calculate flow field distance for ease effect
972
+ vec2 ppp = -1.0 + 2.0 * finalUv;
973
+ ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
974
+ ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
975
+ ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
976
+ ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
977
+ float r = length(ppp); // Flow distance
978
+
979
+ // Ease blending: 0 = topographic (flow), 1 = image (UV)
980
+ float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
981
+ float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
982
+ vec2 texUv = vec2(vx, vy);
983
+
984
+ // PARALLAX SCROLLING
985
+ // We manually apply a smaller offset here to make the texture lag behind
986
+ float parallaxFactor = 0.25; // 25% speed of the color mixing
987
+ texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
988
+
989
+ texUv *= 1.5; // Tiling scale
990
+
991
+ vec4 texSample = texture2D(u_procedural_texture, texUv);
992
+ baseColor = texSample.rgb;
993
+ } else {
994
+ baseColor = v_color;
995
+ }
996
+
997
+ vec3 color = baseColor;
998
+
999
+ // Post-processing
398
1000
  color += pow(v_displacement_amount, 1.0) * u_highlights;
399
1001
  color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
400
1002
  color = saturation(color, 1.0 + u_saturation);
401
1003
  color = color * u_brightness;
402
1004
 
403
- // Generate grain using fbm
1005
+ // Grain
404
1006
  vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
405
1007
  float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
406
1008
 
407
- // Center the grain around zero
408
1009
  grain = grain * 0.5 + 0.5;
409
1010
  grain -= 0.5;
410
-
411
- // Add sparsity control
412
1011
  grain = (grain > u_grain_sparsity) ? grain : 0.0;
413
-
414
- // Apply grain intensity
415
1012
  grain *= u_grain_intensity;
416
1013
 
417
- // Add grain to color
418
1014
  color += vec3(grain);
419
1015
 
420
1016
  gl_FragColor = vec4(color, 1.0);
@@ -453,12 +1049,34 @@ uniform float u_brightness;
453
1049
  uniform float u_color_blending;
454
1050
 
455
1051
  uniform int u_colors_count;
456
- uniform Color u_colors[5];
1052
+ uniform Color u_colors[6];
457
1053
  uniform vec2 u_resolution;
458
1054
 
459
1055
  uniform float u_y_offset;
1056
+ uniform float u_y_offset_wave_multiplier;
1057
+ uniform float u_y_offset_color_multiplier;
1058
+ uniform float u_y_offset_flow_multiplier;
1059
+
1060
+ // Flow field uniforms
1061
+ uniform float u_flow_distortion_a;
1062
+ uniform float u_flow_distortion_b;
1063
+ uniform float u_flow_scale;
1064
+ uniform float u_flow_ease;
1065
+ uniform float u_flow_enabled;
1066
+
1067
+ // Mouse interaction uniforms
1068
+ uniform float u_mouse_distortion_strength;
1069
+ uniform float u_mouse_distortion_radius;
1070
+ uniform float u_mouse_darken;
1071
+ uniform sampler2D u_mouse_texture;
1072
+
1073
+ // Procedural texture uniforms
1074
+ uniform sampler2D u_procedural_texture;
1075
+ uniform float u_enable_procedural_texture;
1076
+ uniform float u_texture_ease;
460
1077
 
461
1078
  varying vec2 vUv;
1079
+ varying vec2 vFlowUv;
462
1080
  varying vec4 v_new_position;
463
1081
  varying vec3 v_color;
464
1082
  varying float v_displacement_amount;
@@ -466,69 +1084,55 @@ varying float v_displacement_amount;
466
1084
  `;
467
1085
  const buildNoise = () => `
468
1086
 
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);
1087
+ // 1. REPLACEMENT PERMUTE:
1088
+ // Uses a hash function (fract/sin) instead of a modular lookup table.
1089
+ vec4 permute(vec4 x) {
1090
+ return floor(fract(sin(x) * 43758.5453123) * 289.0);
482
1091
  }
483
1092
 
484
- vec4 taylorInvSqrt(vec4 r)
485
- {
1093
+ // Taylor Inverse Sqrt
1094
+ vec4 taylorInvSqrt(vec4 r) {
486
1095
  return 1.79284291400159 - 0.85373472095314 * r;
487
1096
  }
488
1097
 
1098
+ // Fade function
489
1099
  vec3 fade(vec3 t) {
490
1100
  return t*t*t*(t*(t*6.0-15.0)+10.0);
491
1101
  }
492
1102
 
493
- float snoise(vec3 v)
494
- {
1103
+ // 3D Simplex Noise
1104
+ float snoise(vec3 v) {
495
1105
  const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
496
1106
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
497
1107
 
498
- // First corner
1108
+ // First corner
499
1109
  vec3 i = floor(v + dot(v, C.yyy) );
500
1110
  vec3 x0 = v - i + dot(i, C.xxx) ;
501
1111
 
502
- // Other corners
1112
+ // Other corners
503
1113
  vec3 g = step(x0.yzx, x0.xyz);
504
1114
  vec3 l = 1.0 - g;
505
1115
  vec3 i1 = min( g.xyz, l.zxy );
506
1116
  vec3 i2 = max( g.xyz, l.zxy );
507
1117
 
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
1118
  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
1119
+ vec3 x2 = x0 - i2 + C.yyy;
1120
+ vec3 x3 = x0 - D.yyy;
515
1121
 
516
- // Permutations
517
- i = mod289(i);
1122
+ // Permutations
518
1123
  vec4 p = permute( permute( permute(
519
1124
  i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
520
1125
  + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
521
1126
  + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
522
1127
 
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)
1128
+ // Gradients
525
1129
  float n_ = 0.142857142857; // 1.0/7.0
526
1130
  vec3 ns = n_ * D.wyz - D.xzx;
527
1131
 
528
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
1132
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
529
1133
 
530
1134
  vec4 x_ = floor(j * ns.z);
531
- vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
1135
+ vec4 y_ = floor(j - 7.0 * x_ );
532
1136
 
533
1137
  vec4 x = x_ *ns.x + ns.yyyy;
534
1138
  vec4 y = y_ *ns.x + ns.yyyy;
@@ -537,8 +1141,6 @@ float snoise(vec3 v)
537
1141
  vec4 b0 = vec4( x.xy, y.xy );
538
1142
  vec4 b1 = vec4( x.zw, y.zw );
539
1143
 
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
1144
  vec4 s0 = floor(b0)*2.0 + 1.0;
543
1145
  vec4 s1 = floor(b1)*2.0 + 1.0;
544
1146
  vec4 sh = -step(h, vec4(0.0));
@@ -551,14 +1153,14 @@ float snoise(vec3 v)
551
1153
  vec3 p2 = vec3(a1.xy,h.z);
552
1154
  vec3 p3 = vec3(a1.zw,h.w);
553
1155
 
554
- //Normalise gradients
1156
+ // Normalise gradients
555
1157
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
556
1158
  p0 *= norm.x;
557
1159
  p1 *= norm.y;
558
1160
  p2 *= norm.z;
559
1161
  p3 *= norm.w;
560
1162
 
561
- // Mix final noise value
1163
+ // Mix final noise value
562
1164
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
563
1165
  m = m * m;
564
1166
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
@@ -568,12 +1170,11 @@ float snoise(vec3 v)
568
1170
  // Classic Perlin noise
569
1171
  float cnoise(vec3 P)
570
1172
  {
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
1173
+ vec3 Pi0 = floor(P);
1174
+ vec3 Pi1 = Pi0 + vec3(1.0);
1175
+
1176
+ vec3 Pf0 = fract(P);
1177
+ vec3 Pf1 = Pf0 - vec3(1.0);
577
1178
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
578
1179
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
579
1180
  vec4 iz0 = Pi0.zzzz;
@@ -634,47 +1235,7 @@ float cnoise(vec3 P)
634
1235
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
635
1236
  return 2.2 * n_xyz;
636
1237
  }
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
- `;
1238
+ `;
678
1239
  const buildColorFunctions = () => `
679
1240
 
680
1241
  vec3 saturation(vec3 rgb, float adjustment) {
@@ -743,13 +1304,14 @@ const addNeatLink = (ref) => {
743
1304
  for (let i = 0; i < existingLinks.length; i++) {
744
1305
  if (existingLinks[i].id === LINK_ID) {
745
1306
  setLinkStyles(existingLinks[i]);
746
- return;
1307
+ return existingLinks[i];
747
1308
  }
748
1309
  }
749
1310
  }
750
1311
  const link = document.createElement("a");
751
1312
  setLinkStyles(link);
752
1313
  ref.parentElement?.appendChild(link);
1314
+ return link;
753
1315
  };
754
1316
  function getElapsedSecondsInLastHour() {
755
1317
  const now = new Date();