@firecms/neat 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 {
@@ -19,17 +19,66 @@ export class NeatGradient {
19
19
  _brightness = -1;
20
20
  _grainScale = -1;
21
21
  _grainIntensity = -1;
22
+ _grainSparsity = -1;
22
23
  _grainSpeed = -1;
23
24
  _colorBlending = -1;
24
25
  _colors = [];
25
26
  _wireframe = false;
26
27
  _backgroundColor = "#FFFFFF";
27
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;
28
62
  requestRef = -1;
29
63
  sizeObserver;
30
64
  sceneState;
65
+ // Optimization: Cache uniforms to avoid lookups and object creation in render loop
66
+ _cachedUniforms = null;
67
+ _linkElement = null;
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();
31
74
  constructor(config) {
32
- 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, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed } = 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;
33
82
  this._ref = ref;
34
83
  this.destroy = this.destroy.bind(this);
35
84
  this._initScene = this._initScene.bind(this);
@@ -43,6 +92,7 @@ export class NeatGradient {
43
92
  this.colorBlending = colorBlending;
44
93
  this.grainScale = grainScale;
45
94
  this.grainIntensity = grainIntensity;
95
+ this.grainSparsity = grainSparsity;
46
96
  this.grainSpeed = grainSpeed;
47
97
  this.colors = colors;
48
98
  this.shadows = shadows;
@@ -52,71 +102,142 @@ export class NeatGradient {
52
102
  this.wireframe = wireframe;
53
103
  this.backgroundColor = backgroundColor;
54
104
  this.backgroundAlpha = backgroundAlpha;
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();
55
137
  this.sceneState = this._initScene(resolution);
56
138
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
57
139
  const render = () => {
58
- 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
59
142
  if (Math.floor(tick * 10) % 5 === 0) {
60
- addNeatLink(ref);
143
+ if (!this._linkElement || !document.contains(this._linkElement)) {
144
+ this._linkElement = addNeatLink(ref);
145
+ }
61
146
  }
62
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
63
- meshes.forEach((mesh) => {
64
- const width = this._ref.width, height = this._ref.height;
65
- const colors = [
66
- ...this._colors.map(color => {
67
- let threeColor = new THREE.Color();
68
- threeColor.setStyle(color.color, "");
69
- return ({
70
- is_active: color.enabled,
71
- color: threeColor,
72
- influence: color.influence
73
- });
74
- }),
75
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
76
- is_active: false,
77
- color: new THREE.Color(0x000000)
78
- }))
79
- ];
147
+ // Update Uniforms efficiently without creating new objects
148
+ if (this._cachedUniforms) {
149
+ const u = this._cachedUniforms;
80
150
  tick += clock.getDelta() * this._speed;
81
- // @ts-ignore
82
- mesh.material.uniforms.u_time.value = tick;
83
- // @ts-ignore
84
- mesh.material.uniforms.u_resolution = { value: new THREE.Vector2(width, height) };
85
- // @ts-ignore
86
- mesh.material.uniforms.u_color_pressure = { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) };
87
- // @ts-ignore
88
- mesh.material.uniforms.u_wave_frequency_x = { value: this._waveFrequencyX };
89
- // @ts-ignore
90
- mesh.material.uniforms.u_wave_frequency_y = { value: this._waveFrequencyY };
91
- // @ts-ignore
92
- mesh.material.uniforms.u_wave_amplitude = { value: this._waveAmplitude };
93
- // @ts-ignore
94
- mesh.material.uniforms.u_plane_width = { value: PLANE_WIDTH };
95
- // @ts-ignore
96
- mesh.material.uniforms.u_plane_height = { value: PLANE_HEIGHT };
97
- // @ts-ignore
98
- mesh.material.uniforms.u_color_blending = { value: this._colorBlending };
99
- // @ts-ignore
100
- mesh.material.uniforms.u_colors = { value: colors };
101
- // @ts-ignore
102
- mesh.material.uniforms.u_colors_count = { value: COLORS_COUNT };
103
- // @ts-ignore
104
- mesh.material.uniforms.u_shadows = { value: this._shadows };
105
- // @ts-ignore
106
- mesh.material.uniforms.u_highlights = { value: this._highlights };
107
- // @ts-ignore
108
- mesh.material.uniforms.u_saturation = { value: this._saturation };
109
- // @ts-ignore
110
- mesh.material.uniforms.u_brightness = { value: this._brightness };
111
- // @ts-ignore
112
- mesh.material.uniforms.u_grain_intensity = { value: this._grainIntensity };
113
- // @ts-ignore
114
- mesh.material.uniforms.u_grain_speed = { value: this._grainSpeed };
115
- // @ts-ignore
116
- mesh.material.uniforms.u_grain_scale = { value: this._grainScale };
117
- // @ts-ignore
118
- mesh.material.wireframe = this._wireframe;
119
- });
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);
120
241
  renderer.render(scene, camera);
121
242
  this.requestRef = requestAnimationFrame(render);
122
243
  };
@@ -127,6 +248,18 @@ export class NeatGradient {
127
248
  const height = canvas.clientHeight;
128
249
  this.sceneState.renderer.setSize(width, height, false);
129
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
+ }
130
263
  };
131
264
  this.sizeObserver = new ResizeObserver(entries => {
132
265
  setSize();
@@ -138,8 +271,29 @@ export class NeatGradient {
138
271
  if (this) {
139
272
  cancelAnimationFrame(this.requestRef);
140
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();
141
289
  }
142
290
  }
291
+ downloadAsPNG(filename = "neat.png") {
292
+ console.log("Downloading as PNG", this._ref);
293
+ const dataURL = this._ref.toDataURL("image/png");
294
+ console.log("data", dataURL);
295
+ downloadURI(dataURL, filename);
296
+ }
143
297
  set speed(speed) {
144
298
  this._speed = speed / 20;
145
299
  }
@@ -182,6 +336,9 @@ export class NeatGradient {
182
336
  set grainIntensity(grainIntensity) {
183
337
  this._grainIntensity = grainIntensity;
184
338
  }
339
+ set grainSparsity(grainSparsity) {
340
+ this._grainSparsity = grainSparsity;
341
+ }
185
342
  set grainSpeed(grainSpeed) {
186
343
  this._grainSpeed = grainSpeed;
187
344
  }
@@ -197,11 +354,159 @@ export class NeatGradient {
197
354
  set backgroundAlpha(backgroundAlpha) {
198
355
  this._backgroundAlpha = backgroundAlpha;
199
356
  }
357
+ set yOffset(yOffset) {
358
+ this._yOffset = yOffset;
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
+ }
200
493
  _initScene(resolution) {
201
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
+ }
202
506
  const renderer = new THREE.WebGLRenderer({
203
507
  // antialias: true,
204
508
  alpha: true,
509
+ preserveDrawingBuffer: true,
205
510
  canvas: this._ref
206
511
  });
207
512
  renderer.setClearColor(0xFF0000, .5);
@@ -228,17 +533,13 @@ export class NeatGradient {
228
533
  };
229
534
  }
230
535
  _buildMaterial(width, height) {
231
- const colors = [
232
- ...this._colors.map(color => ({
233
- is_active: color.enabled,
234
- color: new THREE.Color(color.color),
235
- influence: color.influence
236
- })),
237
- ...Array.from({ length: COLORS_COUNT - this._colors.length }).map(() => ({
238
- is_active: false,
239
- color: new THREE.Color(0x000000)
240
- }))
241
- ];
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
+ }));
242
543
  const uniforms = {
243
544
  u_time: { value: 0 },
244
545
  u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
@@ -253,17 +554,275 @@ export class NeatGradient {
253
554
  u_shadows: { value: this._shadows },
254
555
  u_highlights: { value: this._highlights },
255
556
  u_grain_intensity: { value: this._grainIntensity },
557
+ u_grain_sparsity: { value: this._grainSparsity },
256
558
  u_grain_scale: { value: this._grainScale },
257
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 }
258
583
  };
259
584
  const material = new THREE.ShaderMaterial({
260
585
  uniforms: uniforms,
261
586
  vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
262
587
  fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
263
588
  });
589
+ // Cache the uniforms object for direct access in render loop
590
+ this._cachedUniforms = uniforms;
264
591
  material.wireframe = WIREFRAME;
265
592
  return material;
266
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
+ }
267
826
  }
268
827
  function updateCamera(camera, width, height) {
269
828
  const viewPortAreaRatio = 1000000;
@@ -295,36 +854,62 @@ function updateCamera(camera, width, height) {
295
854
  }
296
855
  function buildVertexShader() {
297
856
  return `
298
-
299
857
  void main() {
300
-
301
858
  vUv = uv;
302
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
303
868
  v_displacement_amount = cnoise( vec3(
304
869
  u_wave_frequency_x * position.x + u_time,
305
- u_wave_frequency_y * position.y + u_time,
870
+ u_wave_frequency_y * (position.y + waveOffset) + u_time,
306
871
  u_time
307
872
  ));
308
-
309
- vec3 color;
310
873
 
311
- // float t = mod(u_base_color, 100.0);
312
- color = u_colors[0].color;
313
-
314
- vec2 noise_cord = vUv * u_color_pressure;
315
-
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
+ }
892
+
893
+ // Pass the standard flow UV to fragment shader (for mouse/texture)
894
+ vFlowUv = flowUv;
895
+
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
902
+
903
+ vec2 noise_cord = adjustedUv * u_color_pressure;
316
904
  const float minNoise = .0;
317
905
  const float maxNoise = .9;
318
-
906
+
319
907
  for (int i = 1; i < u_colors_count; i++) {
320
-
321
- if(u_colors[i].is_active == 1.0){
908
+ if(u_colors[i].is_active > 0.5){
322
909
  float noiseFlow = (1. + float(i)) / 30.;
323
910
  float noiseSpeed = (1. + float(i)) * 0.11;
324
911
  float noiseSeed = 13. + float(i) * 7.;
325
-
326
- int reverseIndex = u_colors_count - i;
327
-
912
+
328
913
  float noise = snoise(
329
914
  vec3(
330
915
  noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
@@ -332,25 +917,17 @@ void main() {
332
917
  u_time * noiseSpeed
333
918
  ) + noiseSeed
334
919
  ) - (.1 * float(i)) + (.5 * u_color_blending);
335
-
920
+
336
921
  noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
337
- vec3 nextColor = u_colors[i].color;
338
- color = mix(color, nextColor, smoothstep(0.0, u_color_blending, noise));
339
-
340
- // vec3 colorOklab = oklab2rgb(color);
341
- // vec3 nextColorOklab = oklab2rgb(nextColor);
342
- // vec3 mixColor = mix(colorOklab, nextColorOklab, smoothstep(0.0, u_color_blending, noise));
343
- // color = rgb2oklab(mixColor);
344
-
922
+ color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
345
923
  }
346
-
347
924
  }
348
-
925
+
349
926
  v_color = color;
350
-
927
+
928
+ // 4. VERTEX POSITION
351
929
  vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
352
- gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
353
-
930
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
354
931
  v_new_position = gl_Position;
355
932
  }
356
933
  `;
@@ -365,7 +942,6 @@ float fbm(vec3 x) {
365
942
  float value = 0.0;
366
943
  float amplitude = 0.5;
367
944
  float frequency = 1.0;
368
-
369
945
  for (int i = 0; i < 4; i++) {
370
946
  value += amplitude * snoise(x * frequency);
371
947
  frequency *= 2.0;
@@ -373,35 +949,73 @@ float fbm(vec3 x) {
373
949
  }
374
950
  return value;
375
951
  }
952
+
953
+ void main() {
954
+ // MOUSE DISTORTION
955
+ vec2 finalUv = vFlowUv;
376
956
 
377
- void main(){
378
- vec3 color = v_color;
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
+ }
379
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
380
1000
  color += pow(v_displacement_amount, 1.0) * u_highlights;
381
1001
  color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
382
1002
  color = saturation(color, 1.0 + u_saturation);
383
1003
  color = color * u_brightness;
384
-
385
- // Generate grain using fbm
386
- vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
387
1004
 
388
- float grain = (u_grain_speed != 0.0)
389
- ? fbm(vec3(noiseCoords, u_time * u_grain_speed))
390
- : fbm(vec3(noiseCoords, 0.0));
1005
+ // Grain
1006
+ vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
1007
+ float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
391
1008
 
392
- // Center the grain around zero
393
1009
  grain = grain * 0.5 + 0.5;
394
1010
  grain -= 0.5;
395
-
396
- // Apply grain intensity
1011
+ grain = (grain > u_grain_sparsity) ? grain : 0.0;
397
1012
  grain *= u_grain_intensity;
398
1013
 
399
- // Add grain to color
400
1014
  color += vec3(grain);
401
-
402
- gl_FragColor = vec4(color,1.0);
1015
+
1016
+ gl_FragColor = vec4(color, 1.0);
403
1017
  }
404
- `;
1018
+ `;
405
1019
  }
406
1020
  const buildUniforms = () => `
407
1021
  precision highp float;
@@ -413,6 +1027,7 @@ struct Color {
413
1027
  };
414
1028
 
415
1029
  uniform float u_grain_intensity;
1030
+ uniform float u_grain_sparsity;
416
1031
  uniform float u_grain_scale;
417
1032
  uniform float u_grain_speed;
418
1033
  uniform float u_time;
@@ -434,10 +1049,34 @@ uniform float u_brightness;
434
1049
  uniform float u_color_blending;
435
1050
 
436
1051
  uniform int u_colors_count;
437
- uniform Color u_colors[5];
1052
+ uniform Color u_colors[6];
438
1053
  uniform vec2 u_resolution;
439
1054
 
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;
1077
+
440
1078
  varying vec2 vUv;
1079
+ varying vec2 vFlowUv;
441
1080
  varying vec4 v_new_position;
442
1081
  varying vec3 v_color;
443
1082
  varying float v_displacement_amount;
@@ -445,69 +1084,55 @@ varying float v_displacement_amount;
445
1084
  `;
446
1085
  const buildNoise = () => `
447
1086
 
448
- vec3 mod289(vec3 x)
449
- {
450
- return x - floor(x * (1.0 / 289.0)) * 289.0;
451
- }
452
-
453
- vec4 mod289(vec4 x)
454
- {
455
- return x - floor(x * (1.0 / 289.0)) * 289.0;
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);
456
1091
  }
457
1092
 
458
- vec4 permute(vec4 x)
459
- {
460
- return mod289(((x*34.0)+1.0)*x);
461
- }
462
-
463
- vec4 taylorInvSqrt(vec4 r)
464
- {
1093
+ // Taylor Inverse Sqrt
1094
+ vec4 taylorInvSqrt(vec4 r) {
465
1095
  return 1.79284291400159 - 0.85373472095314 * r;
466
1096
  }
467
1097
 
1098
+ // Fade function
468
1099
  vec3 fade(vec3 t) {
469
1100
  return t*t*t*(t*(t*6.0-15.0)+10.0);
470
1101
  }
471
1102
 
472
- float snoise(vec3 v)
473
- {
1103
+ // 3D Simplex Noise
1104
+ float snoise(vec3 v) {
474
1105
  const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
475
1106
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
476
1107
 
477
- // First corner
1108
+ // First corner
478
1109
  vec3 i = floor(v + dot(v, C.yyy) );
479
1110
  vec3 x0 = v - i + dot(i, C.xxx) ;
480
1111
 
481
- // Other corners
1112
+ // Other corners
482
1113
  vec3 g = step(x0.yzx, x0.xyz);
483
1114
  vec3 l = 1.0 - g;
484
1115
  vec3 i1 = min( g.xyz, l.zxy );
485
1116
  vec3 i2 = max( g.xyz, l.zxy );
486
1117
 
487
- // x0 = x0 - 0.0 + 0.0 * C.xxx;
488
- // x1 = x0 - i1 + 1.0 * C.xxx;
489
- // x2 = x0 - i2 + 2.0 * C.xxx;
490
- // x3 = x0 - 1.0 + 3.0 * C.xxx;
491
1118
  vec3 x1 = x0 - i1 + C.xxx;
492
- vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
493
- 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;
494
1121
 
495
- // Permutations
496
- i = mod289(i);
1122
+ // Permutations
497
1123
  vec4 p = permute( permute( permute(
498
1124
  i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
499
1125
  + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
500
1126
  + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
501
1127
 
502
- // Gradients: 7x7 points over a square, mapped onto an octahedron.
503
- // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
1128
+ // Gradients
504
1129
  float n_ = 0.142857142857; // 1.0/7.0
505
1130
  vec3 ns = n_ * D.wyz - D.xzx;
506
1131
 
507
- 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);
508
1133
 
509
1134
  vec4 x_ = floor(j * ns.z);
510
- vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
1135
+ vec4 y_ = floor(j - 7.0 * x_ );
511
1136
 
512
1137
  vec4 x = x_ *ns.x + ns.yyyy;
513
1138
  vec4 y = y_ *ns.x + ns.yyyy;
@@ -516,8 +1141,6 @@ float snoise(vec3 v)
516
1141
  vec4 b0 = vec4( x.xy, y.xy );
517
1142
  vec4 b1 = vec4( x.zw, y.zw );
518
1143
 
519
- //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
520
- //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
521
1144
  vec4 s0 = floor(b0)*2.0 + 1.0;
522
1145
  vec4 s1 = floor(b1)*2.0 + 1.0;
523
1146
  vec4 sh = -step(h, vec4(0.0));
@@ -530,14 +1153,14 @@ float snoise(vec3 v)
530
1153
  vec3 p2 = vec3(a1.xy,h.z);
531
1154
  vec3 p3 = vec3(a1.zw,h.w);
532
1155
 
533
- //Normalise gradients
1156
+ // Normalise gradients
534
1157
  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
535
1158
  p0 *= norm.x;
536
1159
  p1 *= norm.y;
537
1160
  p2 *= norm.z;
538
1161
  p3 *= norm.w;
539
1162
 
540
- // Mix final noise value
1163
+ // Mix final noise value
541
1164
  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
542
1165
  m = m * m;
543
1166
  return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
@@ -547,12 +1170,11 @@ float snoise(vec3 v)
547
1170
  // Classic Perlin noise
548
1171
  float cnoise(vec3 P)
549
1172
  {
550
- vec3 Pi0 = floor(P); // Integer part for indexing
551
- vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
552
- Pi0 = mod289(Pi0);
553
- Pi1 = mod289(Pi1);
554
- vec3 Pf0 = fract(P); // Fractional part for interpolation
555
- 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);
556
1178
  vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
557
1179
  vec4 iy = vec4(Pi0.yy, Pi1.yy);
558
1180
  vec4 iz0 = Pi0.zzzz;
@@ -613,47 +1235,7 @@ float cnoise(vec3 P)
613
1235
  float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
614
1236
  return 2.2 * n_xyz;
615
1237
  }
616
-
617
- // YUV to RGB matrix
618
- mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983,
619
- 1.0, -0.39465, -0.58060,
620
- 1.0, 2.03211, 0.0);
621
-
622
- // RGB to YUV matrix
623
- mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722,
624
- -0.09991, -0.33609, 0.43600,
625
- 0.615, -0.5586, -0.05639);
626
-
627
- vec3 oklab2rgb(vec3 linear)
628
- {
629
- const mat3 im1 = mat3(0.4121656120, 0.2118591070, 0.0883097947,
630
- 0.5362752080, 0.6807189584, 0.2818474174,
631
- 0.0514575653, 0.1074065790, 0.6302613616);
632
-
633
- const mat3 im2 = mat3(+0.2104542553, +1.9779984951, +0.0259040371,
634
- +0.7936177850, -2.4285922050, +0.7827717662,
635
- -0.0040720468, +0.4505937099, -0.8086757660);
636
-
637
- vec3 lms = im1 * linear;
638
-
639
- return im2 * (sign(lms) * pow(abs(lms), vec3(1.0/3.0)));
640
- }
641
-
642
- vec3 rgb2oklab(vec3 oklab)
643
- {
644
- const mat3 m1 = mat3(+1.000000000, +1.000000000, +1.000000000,
645
- +0.396337777, -0.105561346, -0.089484178,
646
- +0.215803757, -0.063854173, -1.291485548);
647
-
648
- const mat3 m2 = mat3(+4.076724529, -1.268143773, -0.004111989,
649
- -3.307216883, +2.609332323, -0.703476310,
650
- +0.230759054, -0.341134429, +1.706862569);
651
- vec3 lms = m1 * oklab;
652
-
653
- return m2 * (lms * lms * lms);
654
- }
655
-
656
- `;
1238
+ `;
657
1239
  const buildColorFunctions = () => `
658
1240
 
659
1241
  vec3 saturation(vec3 rgb, float adjustment) {
@@ -722,13 +1304,14 @@ const addNeatLink = (ref) => {
722
1304
  for (let i = 0; i < existingLinks.length; i++) {
723
1305
  if (existingLinks[i].id === LINK_ID) {
724
1306
  setLinkStyles(existingLinks[i]);
725
- return;
1307
+ return existingLinks[i];
726
1308
  }
727
1309
  }
728
1310
  }
729
1311
  const link = document.createElement("a");
730
1312
  setLinkStyles(link);
731
1313
  ref.parentElement?.appendChild(link);
1314
+ return link;
732
1315
  };
733
1316
  function getElapsedSecondsInLastHour() {
734
1317
  const now = new Date();
@@ -745,4 +1328,12 @@ function generateRandomString(length = 6) {
745
1328
  }
746
1329
  return result;
747
1330
  }
1331
+ function downloadURI(uri, name) {
1332
+ const link = document.createElement("a");
1333
+ link.download = name;
1334
+ link.href = uri;
1335
+ document.body.appendChild(link);
1336
+ link.click();
1337
+ document.body.removeChild(link);
1338
+ }
748
1339
  //# sourceMappingURL=NeatGradient.js.map