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