@firecms/neat 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,9 @@
1
- import * as THREE from "three";
1
+ import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
2
+ import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
2
3
  console.info("%c🌈 Neat Gradients%c\n\nLicensed under MIT + The Commons Clause.\nFree for personal and commercial use.\nSelling this software or its derivatives is strictly prohibited.\nhttps://neat.firecms.co", "font-weight: bold; font-size: 14px; color: #FF5772;", "color: inherit;");
3
4
  const PLANE_WIDTH = 50;
4
5
  const PLANE_HEIGHT = 80;
5
- const WIREFRAME = true;
6
6
  const COLORS_COUNT = 6;
7
- const clock = new THREE.Clock();
8
7
  const LINK_ID = generateRandomString();
9
8
  export class NeatGradient {
10
9
  _ref;
@@ -26,6 +25,7 @@ export class NeatGradient {
26
25
  _colors = [];
27
26
  _wireframe = false;
28
27
  _backgroundColor = "#FFFFFF";
28
+ _backgroundColorRgb = [1, 1, 1];
29
29
  _backgroundAlpha = 1.0;
30
30
  // Flow field properties
31
31
  _flowDistortionA = 0;
@@ -33,18 +33,7 @@ export class NeatGradient {
33
33
  _flowScale = 1.0;
34
34
  _flowEase = 0.0;
35
35
  _flowEnabled = true;
36
- // Mouse interaction properties
37
- _mouseDistortionStrength = 0.0;
38
- _mouseDistortionRadius = 0.25;
39
- _mouseDecayRate = 0.96;
40
- _mouseDarken = 0.0;
41
- _mouse = new THREE.Vector2(-1000, -1000);
42
- _mouseFBO = null;
43
- _sceneMouse = null;
44
- _cameraMouse = null;
45
- _mouseObjects = [];
46
- _currentBrush = 0;
47
- _mouseBrushBaseScale = 1;
36
+ glState;
48
37
  // Texture generation properties
49
38
  _enableProceduralTexture = false;
50
39
  _textureVoidLikelihood = 0.45;
@@ -62,36 +51,29 @@ export class NeatGradient {
62
51
  _textureShapeSquiggles = 10;
63
52
  requestRef = -1;
64
53
  sizeObserver;
65
- sceneState;
66
- // Optimization: Cache uniforms to avoid lookups and object creation in render loop
67
- _cachedUniforms = null;
54
+ _initialized = false;
68
55
  _linkElement = null;
56
+ _cachedColorRgb = [];
69
57
  _yOffset = 0;
70
58
  _yOffsetWaveMultiplier = 0.004;
71
59
  _yOffsetColorMultiplier = 0.004;
72
60
  _yOffsetFlowMultiplier = 0.004;
73
- // For saving/restoring clear color
74
- _tempClearColor = new THREE.Color();
75
61
  // Performance optimizations
76
62
  _resizeTimeoutId = null;
77
63
  _textureNeedsUpdate = false;
78
- _lastColorUpdate = 0;
79
64
  _linkCheckCounter = 0;
80
- _mouseUpdateScheduled = false;
81
- _pendingMousePosition = null;
82
- _colorsChanged = true; // Track if colors need update
65
+ _colorsChanged = true;
66
+ _uniformsDirty = true;
67
+ _textureDirty = true;
83
68
  constructor(config) {
84
69
  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,
85
70
  // Flow field parameters
86
71
  flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
87
- // Mouse interaction
88
- mouseDistortionStrength = 0.0, mouseDistortionRadius = 0.25, mouseDecayRate = 0.96, mouseDarken = 0.0,
89
72
  // Texture generation
90
73
  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;
91
74
  this._ref = ref;
92
75
  this.destroy = this.destroy.bind(this);
93
76
  this._initScene = this._initScene.bind(this);
94
- this._buildMaterial = this._buildMaterial.bind(this);
95
77
  this.speed = speed;
96
78
  this.horizontalPressure = horizontalPressure;
97
79
  this.verticalPressure = verticalPressure;
@@ -121,11 +103,6 @@ export class NeatGradient {
121
103
  this.flowScale = flowScale;
122
104
  this.flowEase = flowEase;
123
105
  this.flowEnabled = flowEnabled;
124
- // Mouse interaction
125
- this.mouseDistortionStrength = mouseDistortionStrength;
126
- this.mouseDistortionRadius = mouseDistortionRadius;
127
- this.mouseDecayRate = mouseDecayRate;
128
- this.mouseDarken = mouseDarken;
129
106
  // Texture generation
130
107
  this.enableProceduralTexture = enableProceduralTexture;
131
108
  this.textureVoidLikelihood = textureVoidLikelihood;
@@ -140,14 +117,12 @@ export class NeatGradient {
140
117
  this._textureShapeCircles = textureShapeCircles;
141
118
  this._textureShapeBars = textureShapeBars;
142
119
  this._textureShapeSquiggles = textureShapeSquiggles;
143
- // FIX 1: Setup mouse resources BEFORE building the material/scene
144
- // This ensures u_mouse_texture isn't null during material compilation
145
- this._setupMouseInteraction();
146
- this.sceneState = this._initScene(resolution);
120
+ this.glState = this._initScene(resolution);
147
121
  injectSEO();
148
122
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
123
+ let lastTime = performance.now();
149
124
  const render = () => {
150
- const { renderer, camera, scene } = this.sceneState;
125
+ const { gl, program, locations, indexCount, indexType } = this.glState;
151
126
  // Optimization: check if cached link is still valid in DOM less frequently
152
127
  this._linkCheckCounter++;
153
128
  if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
@@ -156,134 +131,101 @@ export class NeatGradient {
156
131
  this._linkElement = addNeatLink(ref);
157
132
  }
158
133
  }
159
- // Update Uniforms efficiently without creating new objects
160
- if (this._cachedUniforms) {
161
- const u = this._cachedUniforms;
162
- tick += clock.getDelta() * this._speed;
163
- u.u_time.value = tick;
164
- u.u_resolution.value.set(this._ref.width, this._ref.height);
165
- u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
166
- // Directly assign simple values
167
- u.u_wave_frequency_x.value = this._waveFrequencyX;
168
- u.u_wave_frequency_y.value = this._waveFrequencyY;
169
- u.u_wave_amplitude.value = this._waveAmplitude;
170
- u.u_color_blending.value = this._colorBlending;
171
- u.u_shadows.value = this._shadows;
172
- u.u_highlights.value = this._highlights;
173
- u.u_saturation.value = this._saturation;
174
- u.u_brightness.value = this._brightness;
175
- u.u_grain_intensity.value = this._grainIntensity;
176
- u.u_grain_sparsity.value = this._grainSparsity;
177
- u.u_grain_speed.value = this._grainSpeed;
178
- u.u_grain_scale.value = this._grainScale;
179
- u.u_y_offset.value = this._yOffset;
180
- u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
181
- u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
182
- u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
183
- u.u_flow_distortion_a.value = this._flowDistortionA;
184
- u.u_flow_distortion_b.value = this._flowDistortionB;
185
- u.u_flow_scale.value = this._flowScale;
186
- u.u_flow_ease.value = this._flowEase;
187
- u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
188
- u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
189
- u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
190
- u.u_mouse_darken.value = this._mouseDarken;
191
- u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
134
+ if (this._initialized) {
135
+ const timeNow = performance.now();
136
+ tick += ((timeNow - lastTime) / 1000) * this._speed;
137
+ lastTime = timeNow;
138
+ gl.useProgram(program);
139
+ gl.uniform1f(locations.uniforms['u_time'], tick);
140
+ // Only upload static uniforms when they've been modified
141
+ if (this._uniformsDirty) {
142
+ gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
143
+ gl.uniform2f(locations.uniforms['u_color_pressure'], this._horizontalPressure, this._verticalPressure);
144
+ gl.uniform1f(locations.uniforms['u_wave_frequency_x'], this._waveFrequencyX);
145
+ gl.uniform1f(locations.uniforms['u_wave_frequency_y'], this._waveFrequencyY);
146
+ gl.uniform1f(locations.uniforms['u_wave_amplitude'], this._waveAmplitude);
147
+ gl.uniform1f(locations.uniforms['u_color_blending'], this._colorBlending);
148
+ gl.uniform1f(locations.uniforms['u_shadows'], this._shadows);
149
+ gl.uniform1f(locations.uniforms['u_highlights'], this._highlights);
150
+ gl.uniform1f(locations.uniforms['u_saturation'], this._saturation);
151
+ gl.uniform1f(locations.uniforms['u_brightness'], this._brightness);
152
+ gl.uniform1f(locations.uniforms['u_grain_intensity'], this._grainIntensity);
153
+ gl.uniform1f(locations.uniforms['u_grain_sparsity'], this._grainSparsity);
154
+ gl.uniform1f(locations.uniforms['u_grain_speed'], this._grainSpeed);
155
+ gl.uniform1f(locations.uniforms['u_grain_scale'], this._grainScale);
156
+ gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
157
+ gl.uniform1f(locations.uniforms['u_y_offset_wave_multiplier'], this._yOffsetWaveMultiplier);
158
+ gl.uniform1f(locations.uniforms['u_y_offset_color_multiplier'], this._yOffsetColorMultiplier);
159
+ gl.uniform1f(locations.uniforms['u_y_offset_flow_multiplier'], this._yOffsetFlowMultiplier);
160
+ gl.uniform1f(locations.uniforms['u_flow_distortion_a'], this._flowDistortionA);
161
+ gl.uniform1f(locations.uniforms['u_flow_distortion_b'], this._flowDistortionB);
162
+ gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
163
+ gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
164
+ gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
165
+ gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
166
+ gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
167
+ this._uniformsDirty = false;
168
+ }
192
169
  // Only regenerate procedural texture when needed
193
170
  if (this._textureNeedsUpdate && this._enableProceduralTexture) {
194
171
  if (this._proceduralTexture) {
195
- this._proceduralTexture.dispose();
172
+ gl.deleteTexture(this._proceduralTexture);
196
173
  }
197
- this._proceduralTexture = this._createProceduralTexture();
174
+ this._proceduralTexture = this._createProceduralTexture(gl);
198
175
  this._textureNeedsUpdate = false;
176
+ this._textureDirty = true;
177
+ }
178
+ // Procedural texture binding — only when texture changes
179
+ if (this._textureDirty && this._proceduralTexture) {
180
+ gl.activeTexture(gl.TEXTURE1);
181
+ gl.bindTexture(gl.TEXTURE_2D, this._proceduralTexture);
182
+ gl.uniform1i(locations.uniforms['u_procedural_texture'], 1);
183
+ this._textureDirty = false;
199
184
  }
200
- u.u_procedural_texture.value = this._proceduralTexture;
201
- u.u_texture_ease.value = this._textureEase;
202
- // Wireframe is a material property and must update every frame to avoid artifacts
203
- // @ts-ignore - access material safely
204
- this.sceneState.meshes[0].material.wireframe = this._wireframe;
205
- // Optimized Color Update: Update immediately on change, or throttle to 10 times per second
206
- const now = Date.now();
207
- const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
208
- if (shouldUpdate) {
209
- this._lastColorUpdate = now;
185
+ // Color update — only when colors have changed
186
+ if (this._colorsChanged) {
210
187
  this._colorsChanged = false;
211
- const shaderColors = u.u_colors.value;
212
188
  for (let i = 0; i < COLORS_COUNT; i++) {
213
189
  if (i < this._colors.length) {
214
190
  const c = this._colors[i];
215
- shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
216
- shaderColors[i].color.setStyle(c.color, "");
217
- shaderColors[i].influence = c.influence || 0;
191
+ const rgb = this._cachedColorRgb[i] || [0, 0, 0];
192
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], c.enabled ? 1.0 : 0.0);
193
+ gl.uniform3fv(locations.uniforms[`u_colors[${i}].color`], rgb);
194
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].influence`], c.influence || 0);
218
195
  }
219
196
  else {
220
- shaderColors[i].is_active = 0.0;
197
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], 0.0);
221
198
  }
222
199
  }
223
- u.u_colors_count.value = COLORS_COUNT;
200
+ gl.uniform1i(locations.uniforms['u_colors_count'], COLORS_COUNT);
224
201
  }
225
202
  }
226
- // Render mouse interaction to FBO - optimize by only rendering when needed
227
- if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
228
- let hasActiveBrushes = false;
229
- // Update mouse objects - decay rate controls how fast trails fade
230
- for (let i = 0; i < this._mouseObjects.length; i++) {
231
- const obj = this._mouseObjects[i];
232
- if (obj.mesh.visible) {
233
- hasActiveBrushes = true;
234
- obj.mesh.rotation.z += 0.01;
235
- if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
236
- // Decay only affects opacity
237
- obj.mesh.material.opacity *= this._mouseDecayRate;
238
- if (obj.mesh.material.opacity < 0.01) {
239
- obj.mesh.visible = false;
240
- }
241
- }
242
- }
243
- }
244
- // Only render FBO if there are active brushes
245
- if (hasActiveBrushes) {
246
- // Store current clear color (likely the main background color)
247
- renderer.getClearColor(this._tempClearColor);
248
- const oldClearAlpha = renderer.getClearAlpha();
249
- // Set clear color to Black/Transparent for the FBO.
250
- renderer.setClearColor(0x000000, 0.0);
251
- renderer.setRenderTarget(this._mouseFBO);
252
- renderer.clear();
253
- renderer.render(this._sceneMouse, this._cameraMouse);
254
- renderer.setRenderTarget(null);
255
- // Restore main background color for the actual scene render
256
- renderer.setClearColor(this._tempClearColor, oldClearAlpha);
257
- // Update mouse texture uniform
258
- if (this._cachedUniforms) {
259
- this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
260
- }
261
- }
203
+ // Draw scene
204
+ gl.clearColor(this._backgroundColorRgb[0], this._backgroundColorRgb[1], this._backgroundColorRgb[2], this._backgroundAlpha);
205
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
206
+ if (this._wireframe) {
207
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
208
+ gl.drawElements(gl.LINES, this.glState.wireframeIndexCount, indexType, 0);
209
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
210
+ }
211
+ else {
212
+ gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
262
213
  }
263
- // Ensure we set the clear color for the main scene explicitly before rendering
264
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
265
- renderer.render(scene, camera);
266
214
  this.requestRef = requestAnimationFrame(render);
267
215
  };
268
216
  const setSize = () => {
269
- const { renderer } = this.sceneState;
270
- const canvas = renderer.domElement;
271
- const width = canvas.clientWidth;
272
- const height = canvas.clientHeight;
273
- this.sceneState.renderer.setSize(width, height, false);
274
- updateCamera(this.sceneState.camera, width, height);
275
- // FIX 3: Update Mouse FBO and Camera on resize
276
- // If we don't do this, mouse coordinates map incorrectly after a resize
277
- if (this._mouseFBO && this._cameraMouse) {
278
- const fSize = height / 2;
279
- const aspect = width / height;
280
- this._mouseFBO.setSize(width / 2, height / 2);
281
- this._cameraMouse.left = -fSize * aspect;
282
- this._cameraMouse.right = fSize * aspect;
283
- this._cameraMouse.top = fSize;
284
- this._cameraMouse.bottom = -fSize;
285
- this._cameraMouse.updateProjectionMatrix();
286
- }
217
+ const { gl, camera } = this.glState;
218
+ const width = this._ref.clientWidth;
219
+ const height = this._ref.clientHeight;
220
+ // Handle high DPI displays properly without scaling buffer resolution, matching client width
221
+ this._ref.width = width;
222
+ this._ref.height = height;
223
+ gl.viewport(0, 0, width, height);
224
+ updateCamera(camera, width, height);
225
+ // Recompute projection matrix on resize
226
+ const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
227
+ gl.useProgram(this.glState.program);
228
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
287
229
  };
288
230
  // Debounce resize to prevent excessive operations
289
231
  this.sizeObserver = new ResizeObserver(() => {
@@ -299,197 +241,220 @@ export class NeatGradient {
299
241
  render();
300
242
  }
301
243
  destroy() {
302
- if (this) {
303
- cancelAnimationFrame(this.requestRef);
304
- this.sizeObserver.disconnect();
305
- // Clear resize timeout
306
- if (this._resizeTimeoutId !== null) {
307
- clearTimeout(this._resizeTimeoutId);
308
- this._resizeTimeoutId = null;
309
- }
310
- // Cleanup WebGL resources
311
- if (this.sceneState) {
312
- this.sceneState.renderer.dispose();
313
- this.sceneState.meshes.forEach(m => {
314
- m.geometry.dispose();
315
- if (Array.isArray(m.material))
316
- m.material.forEach(mat => mat.dispose());
317
- else
318
- m.material.dispose();
319
- });
320
- }
321
- if (this._mouseFBO)
322
- this._mouseFBO.dispose();
323
- if (this._proceduralTexture)
324
- this._proceduralTexture.dispose();
244
+ cancelAnimationFrame(this.requestRef);
245
+ this.sizeObserver.disconnect();
246
+ // Clear resize timeout
247
+ if (this._resizeTimeoutId !== null) {
248
+ clearTimeout(this._resizeTimeoutId);
249
+ this._resizeTimeoutId = null;
250
+ }
251
+ // Remove NEAT link
252
+ if (this._linkElement && this._linkElement.parentElement) {
253
+ this._linkElement.parentElement.removeChild(this._linkElement);
254
+ this._linkElement = null;
255
+ }
256
+ // Cleanup WebGL resources
257
+ if (this.glState) {
258
+ const gl = this.glState.gl;
259
+ gl.deleteProgram(this.glState.program);
260
+ gl.deleteBuffer(this.glState.buffers.position);
261
+ gl.deleteBuffer(this.glState.buffers.normal);
262
+ gl.deleteBuffer(this.glState.buffers.uv);
263
+ gl.deleteBuffer(this.glState.buffers.index);
264
+ gl.deleteBuffer(this.glState.buffers.wireframeIndex);
265
+ }
266
+ if (this._proceduralTexture && this.glState) {
267
+ this.glState.gl.deleteTexture(this._proceduralTexture);
325
268
  }
326
269
  }
327
270
  downloadAsPNG(filename = "neat.png") {
328
- console.log("Downloading as PNG", this._ref);
329
271
  const dataURL = this._ref.toDataURL("image/png");
330
- console.log("data", dataURL);
331
272
  downloadURI(dataURL, filename);
332
273
  }
333
274
  set speed(speed) {
275
+ this._uniformsDirty = true;
334
276
  this._speed = speed / 20;
335
277
  }
336
278
  set horizontalPressure(horizontalPressure) {
279
+ this._uniformsDirty = true;
337
280
  this._horizontalPressure = horizontalPressure / 4;
338
281
  }
339
282
  set verticalPressure(verticalPressure) {
283
+ this._uniformsDirty = true;
340
284
  this._verticalPressure = verticalPressure / 4;
341
285
  }
342
286
  set waveFrequencyX(waveFrequencyX) {
287
+ this._uniformsDirty = true;
343
288
  this._waveFrequencyX = waveFrequencyX * 0.04;
344
289
  }
345
290
  set waveFrequencyY(waveFrequencyY) {
291
+ this._uniformsDirty = true;
346
292
  this._waveFrequencyY = waveFrequencyY * 0.04;
347
293
  }
348
294
  set waveAmplitude(waveAmplitude) {
295
+ this._uniformsDirty = true;
349
296
  this._waveAmplitude = waveAmplitude * .75;
350
297
  }
351
298
  set colors(colors) {
299
+ this._uniformsDirty = true;
352
300
  this._colors = colors;
353
- this._colorsChanged = true; // Flag for immediate update
301
+ this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
302
+ this._colorsChanged = true;
354
303
  }
355
304
  set highlights(highlights) {
305
+ this._uniformsDirty = true;
356
306
  this._highlights = highlights / 100;
357
307
  }
358
308
  set shadows(shadows) {
309
+ this._uniformsDirty = true;
359
310
  this._shadows = shadows / 100;
360
311
  }
361
312
  set colorSaturation(colorSaturation) {
313
+ this._uniformsDirty = true;
362
314
  this._saturation = colorSaturation / 10;
363
315
  }
364
316
  set colorBrightness(colorBrightness) {
317
+ this._uniformsDirty = true;
365
318
  this._brightness = colorBrightness;
366
319
  }
367
320
  set colorBlending(colorBlending) {
321
+ this._uniformsDirty = true;
368
322
  this._colorBlending = colorBlending / 10;
369
323
  }
370
324
  set grainScale(grainScale) {
325
+ this._uniformsDirty = true;
371
326
  this._grainScale = grainScale == 0 ? 1 : grainScale;
372
327
  }
373
328
  set grainIntensity(grainIntensity) {
329
+ this._uniformsDirty = true;
374
330
  this._grainIntensity = grainIntensity;
375
331
  }
376
332
  set grainSparsity(grainSparsity) {
333
+ this._uniformsDirty = true;
377
334
  this._grainSparsity = grainSparsity;
378
335
  }
379
336
  set grainSpeed(grainSpeed) {
337
+ this._uniformsDirty = true;
380
338
  this._grainSpeed = grainSpeed;
381
339
  }
382
340
  set wireframe(wireframe) {
341
+ this._uniformsDirty = true;
383
342
  this._wireframe = wireframe;
384
343
  }
385
344
  set resolution(resolution) {
386
- this.sceneState = this._initScene(resolution);
345
+ this._uniformsDirty = true;
346
+ if (this.glState) {
347
+ const gl = this.glState.gl;
348
+ gl.deleteProgram(this.glState.program);
349
+ gl.deleteBuffer(this.glState.buffers.position);
350
+ gl.deleteBuffer(this.glState.buffers.normal);
351
+ gl.deleteBuffer(this.glState.buffers.uv);
352
+ gl.deleteBuffer(this.glState.buffers.index);
353
+ gl.deleteBuffer(this.glState.buffers.wireframeIndex);
354
+ }
355
+ this.glState = this._initScene(resolution);
387
356
  }
388
357
  set backgroundColor(backgroundColor) {
358
+ this._uniformsDirty = true;
389
359
  this._backgroundColor = backgroundColor;
360
+ this._backgroundColorRgb = this._hexToRgb(backgroundColor);
390
361
  }
391
362
  set backgroundAlpha(backgroundAlpha) {
363
+ this._uniformsDirty = true;
392
364
  this._backgroundAlpha = backgroundAlpha;
393
365
  }
394
366
  set yOffset(yOffset) {
367
+ this._uniformsDirty = true;
395
368
  this._yOffset = yOffset;
396
369
  }
397
370
  get yOffsetWaveMultiplier() {
398
371
  return this._yOffsetWaveMultiplier * 1000;
399
372
  }
400
373
  set yOffsetWaveMultiplier(value) {
374
+ this._uniformsDirty = true;
401
375
  this._yOffsetWaveMultiplier = value / 1000;
402
376
  }
403
377
  get yOffsetColorMultiplier() {
404
378
  return this._yOffsetColorMultiplier * 1000;
405
379
  }
406
380
  set yOffsetColorMultiplier(value) {
381
+ this._uniformsDirty = true;
407
382
  this._yOffsetColorMultiplier = value / 1000;
408
383
  }
409
384
  get yOffsetFlowMultiplier() {
410
385
  return this._yOffsetFlowMultiplier * 1000;
411
386
  }
412
387
  set yOffsetFlowMultiplier(value) {
388
+ this._uniformsDirty = true;
413
389
  this._yOffsetFlowMultiplier = value / 1000;
414
390
  }
415
391
  set flowDistortionA(value) {
392
+ this._uniformsDirty = true;
416
393
  this._flowDistortionA = value;
417
394
  }
418
395
  set flowDistortionB(value) {
396
+ this._uniformsDirty = true;
419
397
  this._flowDistortionB = value;
420
398
  }
421
399
  set flowScale(value) {
400
+ this._uniformsDirty = true;
422
401
  this._flowScale = value;
423
402
  }
424
403
  set flowEase(value) {
404
+ this._uniformsDirty = true;
425
405
  this._flowEase = value;
426
406
  }
427
407
  set flowEnabled(value) {
408
+ this._uniformsDirty = true;
428
409
  this._flowEnabled = value;
429
410
  }
430
411
  get flowEnabled() {
431
412
  return this._flowEnabled;
432
413
  }
433
- set mouseDistortionStrength(value) {
434
- this._mouseDistortionStrength = Math.max(0, value);
435
- }
436
- set mouseDistortionRadius(value) {
437
- // Clamp to a sane range in UV space
438
- this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
439
- // Update brush scale when radius changes
440
- this._updateBrushScale();
441
- }
442
- _updateBrushScale() {
443
- if (!this._mouseObjects || this._mouseObjects.length === 0)
444
- return;
445
- // Radius directly controls the brush scale
446
- // Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
447
- this._mouseBrushBaseScale = this._mouseDistortionRadius;
448
- }
449
- set mouseDecayRate(value) {
450
- // Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
451
- this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
452
- }
453
- set mouseDarken(value) {
454
- this._mouseDarken = value;
455
- }
456
414
  set enableProceduralTexture(value) {
415
+ this._uniformsDirty = true;
457
416
  this._enableProceduralTexture = value;
458
417
  if (value && !this._proceduralTexture) {
459
418
  this._textureNeedsUpdate = true;
460
419
  }
461
420
  }
462
421
  set textureVoidLikelihood(value) {
422
+ this._uniformsDirty = true;
463
423
  this._textureVoidLikelihood = value;
464
424
  if (this._enableProceduralTexture) {
465
425
  this._textureNeedsUpdate = true;
466
426
  }
467
427
  }
468
428
  set textureVoidWidthMin(value) {
429
+ this._uniformsDirty = true;
469
430
  this._textureVoidWidthMin = value;
470
431
  if (this._enableProceduralTexture) {
471
432
  this._textureNeedsUpdate = true;
472
433
  }
473
434
  }
474
435
  set textureVoidWidthMax(value) {
436
+ this._uniformsDirty = true;
475
437
  this._textureVoidWidthMax = value;
476
438
  if (this._enableProceduralTexture) {
477
439
  this._textureNeedsUpdate = true;
478
440
  }
479
441
  }
480
442
  set textureBandDensity(value) {
443
+ this._uniformsDirty = true;
481
444
  this._textureBandDensity = value;
482
445
  if (this._enableProceduralTexture) {
483
446
  this._textureNeedsUpdate = true;
484
447
  }
485
448
  }
486
449
  set textureColorBlending(value) {
450
+ this._uniformsDirty = true;
487
451
  this._textureColorBlending = value;
488
452
  if (this._enableProceduralTexture) {
489
453
  this._textureNeedsUpdate = true;
490
454
  }
491
455
  }
492
456
  set textureSeed(value) {
457
+ this._uniformsDirty = true;
493
458
  this._textureSeed = value;
494
459
  if (this._enableProceduralTexture) {
495
460
  this._textureNeedsUpdate = true;
@@ -499,222 +464,195 @@ export class NeatGradient {
499
464
  return this._textureEase;
500
465
  }
501
466
  set textureEase(value) {
467
+ this._uniformsDirty = true;
502
468
  this._textureEase = value;
503
469
  }
504
470
  set proceduralBackgroundColor(value) {
471
+ this._uniformsDirty = true;
505
472
  this._proceduralBackgroundColor = value;
506
473
  if (this._enableProceduralTexture) {
507
474
  this._textureNeedsUpdate = true;
508
475
  }
509
476
  }
510
477
  set textureShapeTriangles(value) {
478
+ this._uniformsDirty = true;
511
479
  this._textureShapeTriangles = value;
512
480
  if (this._enableProceduralTexture)
513
481
  this._textureNeedsUpdate = true;
514
482
  }
515
483
  set textureShapeCircles(value) {
484
+ this._uniformsDirty = true;
516
485
  this._textureShapeCircles = value;
517
486
  if (this._enableProceduralTexture)
518
487
  this._textureNeedsUpdate = true;
519
488
  }
520
489
  set textureShapeBars(value) {
490
+ this._uniformsDirty = true;
521
491
  this._textureShapeBars = value;
522
492
  if (this._enableProceduralTexture)
523
493
  this._textureNeedsUpdate = true;
524
494
  }
525
495
  set textureShapeSquiggles(value) {
496
+ this._uniformsDirty = true;
526
497
  this._textureShapeSquiggles = value;
527
498
  if (this._enableProceduralTexture)
528
499
  this._textureNeedsUpdate = true;
529
500
  }
501
+ _hexToRgb(hex) {
502
+ const bigint = parseInt(hex.replace('#', ''), 16);
503
+ return [
504
+ ((bigint >> 16) & 255) / 255.0,
505
+ ((bigint >> 8) & 255) / 255.0,
506
+ (bigint & 255) / 255.0
507
+ ];
508
+ }
530
509
  _initScene(resolution) {
531
- const width = this._ref.width, height = this._ref.height;
532
- // Cleanup existing renderer if needed
533
- if (this.sceneState && this.sceneState.renderer) {
534
- this.sceneState.renderer.dispose();
535
- this.sceneState.meshes.forEach(m => {
536
- m.geometry.dispose();
537
- if (Array.isArray(m.material))
538
- m.material.forEach(mat => mat.dispose());
539
- else
540
- m.material.dispose();
541
- });
510
+ const width = this._ref.clientWidth;
511
+ const height = this._ref.clientHeight;
512
+ const gl = this._ref.getContext("webgl2", { alpha: true, preserveDrawingBuffer: true, antialias: true }) ||
513
+ this._ref.getContext("webgl", { alpha: true, preserveDrawingBuffer: true, antialias: true });
514
+ if (!gl) {
515
+ throw new Error("WebGL not supported");
542
516
  }
543
- const renderer = new THREE.WebGLRenderer({
544
- // antialias: true,
545
- alpha: true,
546
- preserveDrawingBuffer: true,
547
- canvas: this._ref
548
- });
549
- renderer.setClearColor(0xFF0000, .5);
550
- renderer.setSize(width, height, false);
551
- const meshes = [];
552
- const scene = new THREE.Scene();
553
- const material = this._buildMaterial(width, height);
554
- const geo = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
555
- const plane = new THREE.Mesh(geo, material);
556
- plane.rotation.x = -Math.PI / 3.5;
557
- plane.position.z = -1;
558
- meshes.push(plane);
559
- scene.add(plane);
560
- const camera = new THREE.OrthographicCamera(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
561
- // const camera = new THREE.PerspectiveCamera( 1000, window.innerWidth / window.innerHeight, 1, 1000000 );
562
- camera.position.z = 5;
517
+ const ext = gl.getExtension("OES_standard_derivatives");
518
+ gl.getExtension("OES_element_index_uint");
519
+ gl.viewport(0, 0, width, height);
520
+ // Generate plane geometry with Uint32Array for large meshes
521
+ const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
522
+ const positionBuffer = gl.createBuffer();
523
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
524
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
525
+ const normalBuffer = gl.createBuffer();
526
+ gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
527
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
528
+ const uvBuffer = gl.createBuffer();
529
+ gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
530
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
531
+ const indexBuffer = gl.createBuffer();
532
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
533
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
534
+ const wireframeIndexBuffer = gl.createBuffer();
535
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeIndexBuffer);
536
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
537
+ // Rebind the triangle index buffer as default
538
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
539
+ const vertShaderSourceCombined = buildVertUniforms() + "\n" + buildNoise() + "\n" + buildColorFunctions() + "\n" + vertexShaderSource;
540
+ const vertShader = gl.createShader(gl.VERTEX_SHADER);
541
+ gl.shaderSource(vertShader, vertShaderSourceCombined);
542
+ gl.compileShader(vertShader);
543
+ if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
544
+ console.log("VERTEX_SHADER_ERROR_START");
545
+ console.log("Vertex shader error: ", gl.getShaderInfoLog(vertShader));
546
+ console.log("GL Error Code:", gl.getError());
547
+ console.log("Vertex Shader Source Dump:");
548
+ console.log(vertShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
549
+ console.log("VERTEX_SHADER_ERROR_END");
550
+ }
551
+ const fragShaderSourceCombined = buildFragUniforms() + "\n" + buildColorFunctions() + "\n" + buildNoise() + "\n" + fragmentShaderSource;
552
+ const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
553
+ gl.shaderSource(fragShader, fragShaderSourceCombined);
554
+ gl.compileShader(fragShader);
555
+ if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
556
+ console.log("FRAGMENT_SHADER_ERROR_START");
557
+ console.log("Fragment shader error: ", gl.getShaderInfoLog(fragShader));
558
+ console.log("GL Error Code:", gl.getError());
559
+ console.log("Fragment Shader Source Dump:");
560
+ console.log(fragShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
561
+ console.log("FRAGMENT_SHADER_ERROR_END");
562
+ }
563
+ const program = gl.createProgram();
564
+ gl.attachShader(program, vertShader);
565
+ gl.attachShader(program, fragShader);
566
+ gl.linkProgram(program);
567
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
568
+ console.log("PROGRAM_LINK_ERROR_START");
569
+ console.log("Program linking error: ", gl.getProgramInfoLog(program));
570
+ console.log("GL Error Code:", gl.getError());
571
+ console.log("PROGRAM_LINK_ERROR_END");
572
+ }
573
+ gl.useProgram(program);
574
+ const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
575
+ camera.position = [0, 0, 5];
563
576
  updateCamera(camera, width, height);
564
- return {
565
- renderer,
566
- camera,
567
- scene,
568
- meshes,
569
- resolution
577
+ // Define attributes
578
+ const aPosition = gl.getAttribLocation(program, "position");
579
+ const aNormal = gl.getAttribLocation(program, "normal");
580
+ const aUv = gl.getAttribLocation(program, "uv");
581
+ gl.enableVertexAttribArray(aPosition);
582
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
583
+ gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
584
+ gl.enableVertexAttribArray(aNormal);
585
+ gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
586
+ gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
587
+ gl.enableVertexAttribArray(aUv);
588
+ gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
589
+ gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
590
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
591
+ const modelViewMatrix = new Matrix4();
592
+ // The View Matrix is the inverse of the Camera's position
593
+ // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
594
+ modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
595
+ // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
596
+ modelViewMatrix.translate(0, 0, -1);
597
+ modelViewMatrix.rotateX(-Math.PI / 3.5);
598
+ const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
599
+ gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
600
+ const projLoc = gl.getUniformLocation(program, "projectionMatrix");
601
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
602
+ const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
603
+ gl.uniform1f(planeWidthLoc, PLANE_WIDTH);
604
+ const planeHeightLoc = gl.getUniformLocation(program, "u_plane_height");
605
+ gl.uniform1f(planeHeightLoc, PLANE_HEIGHT);
606
+ const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
607
+ gl.uniform1i(colorsCountLoc, COLORS_COUNT);
608
+ const uniformsList = [
609
+ "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
610
+ "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
611
+ "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
612
+ "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
613
+ "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
614
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending"
615
+ ];
616
+ const locations = {
617
+ attributes: { position: aPosition, normal: aNormal, uv: aUv },
618
+ uniforms: {}
570
619
  };
571
- }
572
- _buildMaterial(width, height) {
573
- // Initialize stable array structure for colors
574
- // We create 6 objects and just update them in the render loop to avoid GC
575
- const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
576
- is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
577
- color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
578
- influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
579
- }));
580
- const uniforms = {
581
- u_time: { value: 0 },
582
- u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
583
- u_wave_frequency_x: { value: this._waveFrequencyX },
584
- u_wave_frequency_y: { value: this._waveFrequencyY },
585
- u_wave_amplitude: { value: this._waveAmplitude },
586
- u_resolution: { value: new THREE.Vector2(width, height) },
587
- u_colors: { value: colors },
588
- u_colors_count: { value: this._colors.length },
589
- u_plane_width: { value: PLANE_WIDTH },
590
- u_plane_height: { value: PLANE_HEIGHT },
591
- u_shadows: { value: this._shadows },
592
- u_highlights: { value: this._highlights },
593
- u_grain_intensity: { value: this._grainIntensity },
594
- u_grain_sparsity: { value: this._grainSparsity },
595
- u_grain_scale: { value: this._grainScale },
596
- u_grain_speed: { value: this._grainSpeed },
597
- // Flow field
598
- u_flow_distortion_a: { value: this._flowDistortionA },
599
- u_flow_distortion_b: { value: this._flowDistortionB },
600
- u_flow_scale: { value: this._flowScale },
601
- u_flow_ease: { value: this._flowEase },
602
- u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
603
- // Y offset multipliers
604
- u_y_offset: { value: this._yOffset },
605
- u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
606
- u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
607
- u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
608
- // Mouse interaction
609
- u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
610
- u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
611
- u_mouse_darken: { value: this._mouseDarken },
612
- u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
613
- // Procedural texture
614
- u_procedural_texture: { value: this._proceduralTexture },
615
- u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
616
- u_texture_ease: { value: this._textureEase },
617
- u_saturation: { value: this._saturation },
618
- u_brightness: { value: this._brightness },
619
- u_color_blending: { value: this._colorBlending }
620
- };
621
- const material = new THREE.ShaderMaterial({
622
- uniforms: uniforms,
623
- vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
624
- fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
620
+ uniformsList.forEach(name => {
621
+ locations.uniforms[name] = gl.getUniformLocation(program, name);
625
622
  });
626
- // Cache the uniforms object for direct access in render loop
627
- this._cachedUniforms = uniforms;
628
- material.wireframe = WIREFRAME;
629
- return material;
630
- }
631
- _setupMouseInteraction() {
632
- if (!this._ref)
633
- return;
634
- const width = this._ref.width;
635
- const height = this._ref.height;
636
- // Create mouse FBO
637
- this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
638
- // Create mouse scene and camera
639
- this._sceneMouse = new THREE.Scene();
640
- const fSize = height / 2;
641
- const aspect = width / height;
642
- // FIX 4: Ensure near plane allows viewing objects at Z=0
643
- // Near -100 is safer for objects at 0
644
- this._cameraMouse = new THREE.OrthographicCamera(-fSize * aspect, fSize * aspect, fSize, -fSize, 0, 10000);
645
- this._cameraMouse.position.set(0, 0, 100);
646
- // Create brush texture - More visible and impactful
647
- const brushCanvas = document.createElement('canvas');
648
- brushCanvas.width = 128;
649
- brushCanvas.height = 128;
650
- const bCtx = brushCanvas.getContext('2d');
651
- if (bCtx) {
652
- const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
653
- // Match reference implementation's stronger gradient
654
- grd.addColorStop(0, 'rgba(255,255,255,0.8)');
655
- grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
656
- grd.addColorStop(1, 'rgba(255,255,255,0)');
657
- bCtx.fillStyle = grd;
658
- bCtx.fillRect(0, 0, 128, 128);
623
+ // Add colors uniforms manually
624
+ for (let i = 0; i < COLORS_COUNT; i++) {
625
+ locations.uniforms[`u_colors[${i}].is_active`] = gl.getUniformLocation(program, `u_colors[${i}].is_active`);
626
+ locations.uniforms[`u_colors[${i}].color`] = gl.getUniformLocation(program, `u_colors[${i}].color`);
627
+ locations.uniforms[`u_colors[${i}].influence`] = gl.getUniformLocation(program, `u_colors[${i}].influence`);
659
628
  }
660
- const brushTex = new THREE.CanvasTexture(brushCanvas);
661
- const brushMat = new THREE.MeshBasicMaterial({
662
- map: brushTex,
663
- transparent: true,
664
- opacity: 1.0,
665
- depthTest: false,
666
- blending: THREE.AdditiveBlending // Additive blending for better accumulation
667
- });
668
- // Brush geometry size - will be scaled by radius parameter
669
- const brushGeo = new THREE.PlaneGeometry(200, 200);
670
- // Create brush pool
671
- const brushPoolSize = 50;
672
- for (let i = 0; i < brushPoolSize; i++) {
673
- const m = new THREE.Mesh(brushGeo, brushMat.clone());
674
- m.visible = false;
675
- this._sceneMouse.add(m);
676
- this._mouseObjects.push({ mesh: m, active: false });
677
- }
678
- // Initialize brush scale based on current radius
679
- this._updateBrushScale();
680
- // Add mouse move listener
681
- this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
682
- }
683
- _onMouseMove(e) {
684
- if (!this._ref || !this._sceneMouse)
685
- return;
686
- const rect = this._ref.getBoundingClientRect();
687
- const width = this._ref.width;
688
- const height = this._ref.height;
689
- // Store pending mouse position
690
- this._pendingMousePosition = {
691
- x: e.clientX - rect.left - width / 2,
692
- y: -(e.clientY - rect.top - height / 2)
629
+ this._initialized = true;
630
+ // New program needs all uniforms re-uploaded on first frame
631
+ this._uniformsDirty = true;
632
+ this._colorsChanged = true;
633
+ this._textureDirty = true;
634
+ // Enable alpha blending
635
+ gl.enable(gl.BLEND);
636
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
637
+ gl.enable(gl.DEPTH_TEST);
638
+ return {
639
+ gl,
640
+ program,
641
+ buffers: {
642
+ position: positionBuffer,
643
+ normal: normalBuffer,
644
+ uv: uvBuffer,
645
+ index: indexBuffer,
646
+ wireframeIndex: wireframeIndexBuffer
647
+ },
648
+ locations,
649
+ camera,
650
+ indexCount: index.length,
651
+ wireframeIndexCount: wireframeIndex.length,
652
+ indexType: (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT
693
653
  };
694
- // Batch mouse updates using requestAnimationFrame
695
- if (!this._mouseUpdateScheduled) {
696
- this._mouseUpdateScheduled = true;
697
- requestAnimationFrame(() => {
698
- this._mouseUpdateScheduled = false;
699
- if (!this._pendingMousePosition)
700
- return;
701
- this._mouse.x = this._pendingMousePosition.x;
702
- this._mouse.y = this._pendingMousePosition.y;
703
- const brush = this._mouseObjects[this._currentBrush];
704
- brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
705
- brush.active = true;
706
- brush.mesh.visible = true;
707
- brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
708
- brush.mesh.rotation.z = Math.random() * Math.PI * 2;
709
- if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
710
- brush.mesh.material.opacity = 1.0;
711
- }
712
- this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
713
- this._pendingMousePosition = null;
714
- });
715
- }
716
654
  }
717
- _createProceduralTexture() {
655
+ _createProceduralTexture(gl) {
718
656
  // Texture size - 1024 provides good balance between quality and performance
719
657
  // Reduced from 2048 for better performance
720
658
  const texSize = 1024;
@@ -723,7 +661,7 @@ export class NeatGradient {
723
661
  sourceCanvas.height = texSize;
724
662
  const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
725
663
  if (!sCtx)
726
- return new THREE.Texture();
664
+ return null;
727
665
  let seed = this._textureSeed;
728
666
  const baseSeed = this._textureSeed;
729
667
  function random() {
@@ -736,7 +674,7 @@ export class NeatGradient {
736
674
  };
737
675
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
738
676
  if (colors.length === 0)
739
- return new THREE.Texture();
677
+ return null;
740
678
  // Helper functions
741
679
  function hexToRgb(hex) {
742
680
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -747,7 +685,7 @@ export class NeatGradient {
747
685
  };
748
686
  }
749
687
  function rgbToHex(r, g, b) {
750
- return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
688
+ return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1).padStart(6, '0');
751
689
  }
752
690
  const getInterColor = () => {
753
691
  const c1 = colors[Math.floor(random() * colors.length)];
@@ -827,7 +765,7 @@ export class NeatGradient {
827
765
  canvas.height = texSize;
828
766
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
829
767
  if (!ctx)
830
- return new THREE.Texture();
768
+ return null;
831
769
  // Start filled with the chosen void color so gaps show that color
832
770
  ctx.fillStyle = baseColor;
833
771
  ctx.fillRect(0, 0, texSize, texSize);
@@ -862,519 +800,24 @@ export class NeatGradient {
862
800
  }
863
801
  // void segments: leave as baseColor
864
802
  }
865
- const tex = new THREE.CanvasTexture(canvas);
866
- // Use mipmapping for better quality when texture is scaled
867
- tex.minFilter = THREE.LinearMipmapLinearFilter;
868
- tex.magFilter = THREE.LinearFilter;
869
- tex.wrapS = THREE.RepeatWrapping;
870
- tex.wrapT = THREE.RepeatWrapping;
871
- // Enable anisotropic filtering for much better quality when texture is stretched
872
- // 16 is a commonly supported value that dramatically improves quality
873
- tex.anisotropy = 16;
874
- // Ensure mipmaps are generated
875
- tex.needsUpdate = true;
876
- return tex;
877
- }
878
- }
879
- function updateCamera(camera, width, height) {
880
- const viewPortAreaRatio = 1000000;
881
- const areaViewPort = width * height;
882
- const targetPlaneArea = areaViewPort / viewPortAreaRatio *
883
- PLANE_WIDTH * PLANE_HEIGHT / 1.5;
884
- const ratio = width / height;
885
- const targetWidth = Math.sqrt(targetPlaneArea * ratio);
886
- const targetHeight = targetPlaneArea / targetWidth;
887
- let left = -PLANE_WIDTH / 2;
888
- let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
889
- let top = PLANE_HEIGHT / 4;
890
- let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
891
- // Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
892
- if (ratio < 1) {
893
- // Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
894
- const horizontalScale = ratio;
895
- left = left * horizontalScale;
896
- right = right * horizontalScale;
897
- // Zoom out slightly on mobile (1.1 = 10% zoom out)
898
- const mobileZoomFactor = 1.05;
899
- left = left * mobileZoomFactor;
900
- right = right * mobileZoomFactor;
901
- top = top * mobileZoomFactor;
902
- bottom = bottom * mobileZoomFactor;
903
- }
904
- const near = -100;
905
- const far = 1000;
906
- if (camera instanceof THREE.OrthographicCamera) {
907
- camera.left = left;
908
- camera.right = right;
909
- camera.top = top;
910
- camera.bottom = bottom;
911
- camera.near = near;
912
- camera.far = far;
913
- camera.updateProjectionMatrix();
914
- }
915
- else if (camera instanceof THREE.PerspectiveCamera) {
916
- camera.aspect = width / height;
917
- camera.updateProjectionMatrix();
918
- }
919
- }
920
- // Cache shader strings to avoid repeated concatenation
921
- let cachedVertexShader = null;
922
- let cachedFragmentShader = null;
923
- function buildVertexShader() {
924
- if (cachedVertexShader)
925
- return cachedVertexShader;
926
- cachedVertexShader = `
927
- void main() {
928
- vUv = uv;
929
-
930
- // SCROLLING LOGIC
931
- // Separate multipliers for wave, color, and flow offsets
932
- float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
933
- float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
934
- float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
935
-
936
- // 1. DISPLACEMENT (WAVES)
937
- // We add waveOffset to Y to scroll the wave pattern
938
- v_displacement_amount = cnoise( vec3(
939
- u_wave_frequency_x * position.x + u_time,
940
- u_wave_frequency_y * (position.y + waveOffset) + u_time,
941
- u_time
942
- ));
943
-
944
- // 2. FLOW FIELD
945
- // Apply flow offset to scroll the flow field mask
946
- vec2 baseUv = vUv;
947
- baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
948
- vec2 flowUv = baseUv;
949
-
950
- if (u_flow_enabled > 0.5) {
951
- if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
952
- vec2 ppp = -1.0 + 2.0 * baseUv;
953
- ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
954
- ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
955
- ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
956
- ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
957
-
958
- float r = length(ppp);
959
- flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
960
- }
961
- }
962
-
963
- // Pass the standard flow UV to fragment shader (for mouse/texture)
964
- vFlowUv = flowUv;
965
-
966
- // 3. COLOR MIXING
967
- // We take the computed flow UVs and apply the color offset
968
- // Scale by plane height to match wave offset speed (world space vs UV space)
969
- vec3 color = u_colors[0].color;
970
- vec2 adjustedUv = flowUv;
971
- adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
972
-
973
- vec2 noise_cord = adjustedUv * u_color_pressure;
974
- const float minNoise = .0;
975
- const float maxNoise = .9;
976
-
977
- for (int i = 1; i < u_colors_count; i++) {
978
- if(u_colors[i].is_active > 0.5){
979
- float noiseFlow = (1. + float(i)) / 30.;
980
- float noiseSpeed = (1. + float(i)) * 0.11;
981
- float noiseSeed = 13. + float(i) * 7.;
982
-
983
- float noise = snoise(
984
- vec3(
985
- noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
986
- noise_cord.y * u_color_pressure.y,
987
- u_time * noiseSpeed
988
- ) + noiseSeed
989
- ) - (.1 * float(i)) + (.5 * u_color_blending);
990
-
991
- noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
992
- color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
993
- }
994
- }
995
-
996
- v_color = color;
997
-
998
- // 4. VERTEX POSITION
999
- vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
1000
- gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
1001
- v_new_position = gl_Position;
1002
- }
1003
- `;
1004
- return cachedVertexShader;
1005
- }
1006
- function buildFragmentShader() {
1007
- if (cachedFragmentShader)
1008
- return cachedFragmentShader;
1009
- cachedFragmentShader = `
1010
- float random(vec2 p) {
1011
- return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
1012
- }
1013
-
1014
- float fbm(vec3 x) {
1015
- float value = 0.0;
1016
- float amplitude = 0.5;
1017
- float frequency = 1.0;
1018
- for (int i = 0; i < 4; i++) {
1019
- value += amplitude * snoise(x * frequency);
1020
- frequency *= 2.0;
1021
- amplitude *= 0.5;
1022
- }
1023
- return value;
1024
- }
1025
-
1026
- void main() {
1027
- // MOUSE DISTORTION
1028
- vec2 finalUv = vFlowUv;
1029
-
1030
- if (u_mouse_distortion_strength > 0.0) {
1031
- vec4 mouseColor = texture2D(u_mouse_texture, vUv);
1032
- float mouseValue = mouseColor.r;
1033
-
1034
- if (mouseValue > 0.001) {
1035
- float distortionAmount = mouseValue * u_mouse_distortion_strength;
1036
- vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
1037
- finalUv -= mouseDisp;
803
+ const tex = gl.createTexture();
804
+ gl.bindTexture(gl.TEXTURE_2D, tex);
805
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
806
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
807
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
808
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
809
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
810
+ gl.generateMipmap(gl.TEXTURE_2D);
811
+ const ext = gl.getExtension('EXT_texture_filter_anisotropic') ||
812
+ gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
813
+ gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
814
+ if (ext) {
815
+ const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
816
+ gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, max));
1038
817
  }
818
+ return tex;
1039
819
  }
1040
-
1041
- vec3 baseColor;
1042
-
1043
- if (u_enable_procedural_texture > 0.5) {
1044
- // Calculate flow field distance for ease effect
1045
- vec2 ppp = -1.0 + 2.0 * finalUv;
1046
- ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1047
- ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1048
- ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1049
- ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1050
- float r = length(ppp); // Flow distance
1051
-
1052
- // Ease blending: 0 = topographic (flow), 1 = image (UV)
1053
- float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
1054
- float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
1055
- vec2 texUv = vec2(vx, vy);
1056
-
1057
- // PARALLAX SCROLLING
1058
- // We manually apply a smaller offset here to make the texture lag behind
1059
- float parallaxFactor = 0.25; // 25% speed of the color mixing
1060
- texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
1061
-
1062
- texUv *= 1.5; // Tiling scale
1063
-
1064
- vec4 texSample = texture2D(u_procedural_texture, texUv);
1065
- baseColor = texSample.rgb;
1066
- } else {
1067
- baseColor = v_color;
1068
- }
1069
-
1070
- vec3 color = baseColor;
1071
-
1072
- // Post-processing
1073
- color += pow(v_displacement_amount, 1.0) * u_highlights;
1074
- color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
1075
- color = saturation(color, 1.0 + u_saturation);
1076
- color = color * u_brightness;
1077
-
1078
- // Grain
1079
- vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
1080
- float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
1081
-
1082
- grain = grain * 0.5 + 0.5;
1083
- grain -= 0.5;
1084
- grain = (grain > u_grain_sparsity) ? grain : 0.0;
1085
- grain *= u_grain_intensity;
1086
-
1087
- color += vec3(grain);
1088
-
1089
- gl_FragColor = vec4(color, 1.0);
1090
- }
1091
- `;
1092
- return cachedFragmentShader;
1093
- }
1094
- // Cache uniforms string as well
1095
- let cachedUniformsShader = null;
1096
- const buildUniforms = () => {
1097
- if (cachedUniformsShader)
1098
- return cachedUniformsShader;
1099
- cachedUniformsShader = `
1100
- precision highp float;
1101
-
1102
- struct Color {
1103
- float is_active;
1104
- vec3 color;
1105
- float value;
1106
- };
1107
-
1108
- uniform float u_grain_intensity;
1109
- uniform float u_grain_sparsity;
1110
- uniform float u_grain_scale;
1111
- uniform float u_grain_speed;
1112
- uniform float u_time;
1113
-
1114
- uniform float u_wave_amplitude;
1115
- uniform float u_wave_frequency_x;
1116
- uniform float u_wave_frequency_y;
1117
-
1118
- uniform vec2 u_color_pressure;
1119
-
1120
- uniform float u_plane_width;
1121
- uniform float u_plane_height;
1122
-
1123
- uniform float u_shadows;
1124
- uniform float u_highlights;
1125
- uniform float u_saturation;
1126
- uniform float u_brightness;
1127
-
1128
- uniform float u_color_blending;
1129
-
1130
- uniform int u_colors_count;
1131
- uniform Color u_colors[6];
1132
- uniform vec2 u_resolution;
1133
-
1134
- uniform float u_y_offset;
1135
- uniform float u_y_offset_wave_multiplier;
1136
- uniform float u_y_offset_color_multiplier;
1137
- uniform float u_y_offset_flow_multiplier;
1138
-
1139
- // Flow field uniforms
1140
- uniform float u_flow_distortion_a;
1141
- uniform float u_flow_distortion_b;
1142
- uniform float u_flow_scale;
1143
- uniform float u_flow_ease;
1144
- uniform float u_flow_enabled;
1145
-
1146
- // Mouse interaction uniforms
1147
- uniform float u_mouse_distortion_strength;
1148
- uniform float u_mouse_distortion_radius;
1149
- uniform float u_mouse_darken;
1150
- uniform sampler2D u_mouse_texture;
1151
-
1152
- // Procedural texture uniforms
1153
- uniform sampler2D u_procedural_texture;
1154
- uniform float u_enable_procedural_texture;
1155
- uniform float u_texture_ease;
1156
-
1157
- varying vec2 vUv;
1158
- varying vec2 vFlowUv;
1159
- varying vec4 v_new_position;
1160
- varying vec3 v_color;
1161
- varying float v_displacement_amount;
1162
-
1163
- `;
1164
- return cachedUniformsShader;
1165
- };
1166
- // Cache noise functions as well
1167
- let cachedNoiseShader = null;
1168
- const buildNoise = () => {
1169
- if (cachedNoiseShader)
1170
- return cachedNoiseShader;
1171
- cachedNoiseShader = `
1172
-
1173
- // 1. REPLACEMENT PERMUTE:
1174
- // Uses a hash function (fract/sin) instead of a modular lookup table.
1175
- vec4 permute(vec4 x) {
1176
- return floor(fract(sin(x) * 43758.5453123) * 289.0);
1177
- }
1178
-
1179
- // Taylor Inverse Sqrt
1180
- vec4 taylorInvSqrt(vec4 r) {
1181
- return 1.79284291400159 - 0.85373472095314 * r;
1182
- }
1183
-
1184
- // Fade function
1185
- vec3 fade(vec3 t) {
1186
- return t*t*t*(t*(t*6.0-15.0)+10.0);
1187
- }
1188
-
1189
- // 3D Simplex Noise
1190
- float snoise(vec3 v) {
1191
- const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
1192
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
1193
-
1194
- // First corner
1195
- vec3 i = floor(v + dot(v, C.yyy) );
1196
- vec3 x0 = v - i + dot(i, C.xxx) ;
1197
-
1198
- // Other corners
1199
- vec3 g = step(x0.yzx, x0.xyz);
1200
- vec3 l = 1.0 - g;
1201
- vec3 i1 = min( g.xyz, l.zxy );
1202
- vec3 i2 = max( g.xyz, l.zxy );
1203
-
1204
- vec3 x1 = x0 - i1 + C.xxx;
1205
- vec3 x2 = x0 - i2 + C.yyy;
1206
- vec3 x3 = x0 - D.yyy;
1207
-
1208
- // Permutations
1209
- vec4 p = permute( permute( permute(
1210
- i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
1211
- + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
1212
- + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
1213
-
1214
- // Gradients
1215
- float n_ = 0.142857142857; // 1.0/7.0
1216
- vec3 ns = n_ * D.wyz - D.xzx;
1217
-
1218
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
1219
-
1220
- vec4 x_ = floor(j * ns.z);
1221
- vec4 y_ = floor(j - 7.0 * x_ );
1222
-
1223
- vec4 x = x_ *ns.x + ns.yyyy;
1224
- vec4 y = y_ *ns.x + ns.yyyy;
1225
- vec4 h = 1.0 - abs(x) - abs(y);
1226
-
1227
- vec4 b0 = vec4( x.xy, y.xy );
1228
- vec4 b1 = vec4( x.zw, y.zw );
1229
-
1230
- vec4 s0 = floor(b0)*2.0 + 1.0;
1231
- vec4 s1 = floor(b1)*2.0 + 1.0;
1232
- vec4 sh = -step(h, vec4(0.0));
1233
-
1234
- vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
1235
- vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
1236
-
1237
- vec3 p0 = vec3(a0.xy,h.x);
1238
- vec3 p1 = vec3(a0.zw,h.y);
1239
- vec3 p2 = vec3(a1.xy,h.z);
1240
- vec3 p3 = vec3(a1.zw,h.w);
1241
-
1242
- // Normalise gradients
1243
- vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
1244
- p0 *= norm.x;
1245
- p1 *= norm.y;
1246
- p2 *= norm.z;
1247
- p3 *= norm.w;
1248
-
1249
- // Mix final noise value
1250
- vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
1251
- m = m * m;
1252
- return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
1253
- dot(p2,x2), dot(p3,x3) ) );
1254
820
  }
1255
-
1256
- // Classic Perlin noise
1257
- float cnoise(vec3 P)
1258
- {
1259
- vec3 Pi0 = floor(P);
1260
- vec3 Pi1 = Pi0 + vec3(1.0);
1261
-
1262
- vec3 Pf0 = fract(P);
1263
- vec3 Pf1 = Pf0 - vec3(1.0);
1264
- vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
1265
- vec4 iy = vec4(Pi0.yy, Pi1.yy);
1266
- vec4 iz0 = Pi0.zzzz;
1267
- vec4 iz1 = Pi1.zzzz;
1268
-
1269
- vec4 ixy = permute(permute(ix) + iy);
1270
- vec4 ixy0 = permute(ixy + iz0);
1271
- vec4 ixy1 = permute(ixy + iz1);
1272
-
1273
- vec4 gx0 = ixy0 * (1.0 / 7.0);
1274
- vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
1275
- gx0 = fract(gx0);
1276
- vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
1277
- vec4 sz0 = step(gz0, vec4(0.0));
1278
- gx0 -= sz0 * (step(0.0, gx0) - 0.5);
1279
- gy0 -= sz0 * (step(0.0, gy0) - 0.5);
1280
-
1281
- vec4 gx1 = ixy1 * (1.0 / 7.0);
1282
- vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
1283
- gx1 = fract(gx1);
1284
- vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
1285
- vec4 sz1 = step(gz1, vec4(0.0));
1286
- gx1 -= sz1 * (step(0.0, gx1) - 0.5);
1287
- gy1 -= sz1 * (step(0.0, gy1) - 0.5);
1288
-
1289
- vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
1290
- vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
1291
- vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
1292
- vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
1293
- vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
1294
- vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
1295
- vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
1296
- vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
1297
-
1298
- vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
1299
- g000 *= norm0.x;
1300
- g010 *= norm0.y;
1301
- g100 *= norm0.z;
1302
- g110 *= norm0.w;
1303
- vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
1304
- g001 *= norm1.x;
1305
- g011 *= norm1.y;
1306
- g101 *= norm1.z;
1307
- g111 *= norm1.w;
1308
-
1309
- float n000 = dot(g000, Pf0);
1310
- float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
1311
- float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
1312
- float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
1313
- float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
1314
- float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
1315
- float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
1316
- float n111 = dot(g111, Pf1);
1317
-
1318
- vec3 fade_xyz = fade(Pf0);
1319
- vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
1320
- vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
1321
- float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
1322
- return 2.2 * n_xyz;
1323
- }
1324
- `;
1325
- return cachedNoiseShader;
1326
- };
1327
- // Cache color functions as well
1328
- let cachedColorFunctionsShader = null;
1329
- const buildColorFunctions = () => {
1330
- if (cachedColorFunctionsShader)
1331
- return cachedColorFunctionsShader;
1332
- cachedColorFunctionsShader = `
1333
-
1334
- vec3 saturation(vec3 rgb, float adjustment) {
1335
- const vec3 W = vec3(0.2125, 0.7154, 0.0721);
1336
- vec3 intensity = vec3(dot(rgb, W));
1337
- return mix(intensity, rgb, adjustment);
1338
- }
1339
-
1340
- float saturation(vec3 rgb)
1341
- {
1342
- vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
1343
- vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
1344
- vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
1345
-
1346
- float d = q.x - min(q.w, q.y);
1347
- float e = 1.0e-10;
1348
- return abs(6.0 * d + e);
1349
- }
1350
-
1351
- // get saturation of a color in values between 0 and 1
1352
- float getSaturation(vec3 color) {
1353
- float max = max(color.r, max(color.g, color.b));
1354
- float min = min(color.r, min(color.g, color.b));
1355
- return (max - min) / max;
1356
- }
1357
-
1358
- vec3 rgb2hsv(vec3 c)
1359
- {
1360
- vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
1361
- vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
1362
- vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
1363
-
1364
- float d = q.x - min(q.w, q.y);
1365
- float e = 1.0e-10;
1366
- return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
1367
- }
1368
-
1369
- vec3 hsv2rgb(vec3 c)
1370
- {
1371
- vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
1372
- vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
1373
- return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
1374
- }
1375
- `;
1376
- return cachedColorFunctionsShader;
1377
- };
1378
821
  const setLinkStyles = (link) => {
1379
822
  link.id = LINK_ID;
1380
823
  link.href = "https://neat.firecms.co";
@@ -1391,21 +834,27 @@ const setLinkStyles = (link) => {
1391
834
  link.style.fontWeight = "bold";
1392
835
  link.style.textDecoration = "none";
1393
836
  link.style.zIndex = "10000";
837
+ link.style.pointerEvents = "auto";
838
+ link.setAttribute("data-n", "1");
1394
839
  link.innerHTML = "NEAT";
1395
840
  };
1396
841
  const addNeatLink = (ref) => {
1397
- const existingLinks = ref.parentElement?.getElementsByTagName("a");
1398
- if (existingLinks) {
1399
- for (let i = 0; i < existingLinks.length; i++) {
1400
- if (existingLinks[i].id === LINK_ID) {
1401
- setLinkStyles(existingLinks[i]);
1402
- return existingLinks[i];
1403
- }
842
+ const parent = ref.parentElement;
843
+ // Ensure parent has position so absolute link is positioned relative to it
844
+ if (parent && getComputedStyle(parent).position === "static") {
845
+ parent.style.position = "relative";
846
+ }
847
+ // Search parent for existing neat link (survives HMR where LINK_ID changes)
848
+ if (parent) {
849
+ const existing = parent.querySelector('a[data-n]');
850
+ if (existing) {
851
+ setLinkStyles(existing);
852
+ return existing;
1404
853
  }
1405
854
  }
1406
855
  const link = document.createElement("a");
1407
856
  setLinkStyles(link);
1408
- ref.parentElement?.appendChild(link);
857
+ parent?.appendChild(link);
1409
858
  return link;
1410
859
  };
1411
860
  function getElapsedSecondsInLastHour() {