@firecms/neat 0.6.0 → 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,4 +1,5 @@
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
 
3
4
  console.info(
4
5
  "%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",
@@ -8,30 +9,32 @@ console.info(
8
9
  const PLANE_WIDTH = 50;
9
10
  const PLANE_HEIGHT = 80;
10
11
 
11
- const WIREFRAME = true;
12
- const COLORS_COUNT = 6;
13
12
 
14
- const clock = new THREE.Clock();
13
+ const COLORS_COUNT = 6;
15
14
 
16
15
  const LINK_ID = generateRandomString();
17
16
 
18
- type SceneState = {
19
- renderer: THREE.WebGLRenderer,
20
- camera: THREE.Camera,
21
- scene: THREE.Scene,
22
- meshes: THREE.Mesh[],
23
- resolution: number
17
+ export interface WebGLState {
18
+ gl: WebGLRenderingContext | WebGL2RenderingContext;
19
+ program: WebGLProgram;
20
+ buffers: {
21
+ position: WebGLBuffer;
22
+ normal: WebGLBuffer;
23
+ uv: WebGLBuffer;
24
+ index: WebGLBuffer;
25
+ wireframeIndex: WebGLBuffer;
26
+ };
27
+ locations: {
28
+ attributes: Record<string, number>;
29
+ uniforms: Record<string, WebGLUniformLocation | null>;
30
+ };
31
+ camera: OrthographicCamera;
32
+ indexCount: number;
33
+ wireframeIndexCount: number;
34
+ indexType: number;
24
35
  }
25
36
 
26
- // Interface for the Uniforms to avoid @ts-ignore and improve access speed
27
- interface NeatUniforms {
28
- [key: string]: THREE.IUniform;
29
- u_time: { value: number };
30
- u_resolution: { value: THREE.Vector2 };
31
- u_color_pressure: { value: THREE.Vector2 };
32
- u_colors: { value: { is_active: number; color: THREE.Color; influence: number }[] };
33
- u_mouse_texture: { value: THREE.Texture | null };
34
- }
37
+
35
38
 
36
39
  export type NeatConfig = {
37
40
  resolution?: number;
@@ -64,14 +67,7 @@ export type NeatConfig = {
64
67
  flowScale?: number;
65
68
  flowEase?: number;
66
69
  flowEnabled?: boolean;
67
- // Mouse interaction
68
- /** Strength of mouse-driven distortion */
69
- mouseDistortionStrength?: number;
70
- /** Radius / area of mouse-driven distortion in UV space (0–1-ish) */
71
- mouseDistortionRadius?: number;
72
- /** How quickly mouse trails decay/fade (0.9=slow/wobbly, 0.99=fast/sharp) */
73
- mouseDecayRate?: number;
74
- mouseDarken?: number;
70
+
75
71
  // Texture generation
76
72
  enableProceduralTexture?: boolean;
77
73
  textureVoidLikelihood?: number;
@@ -130,6 +126,7 @@ export class NeatGradient implements NeatController {
130
126
  private _wireframe: boolean = false;
131
127
 
132
128
  private _backgroundColor: string = "#FFFFFF";
129
+ private _backgroundColorRgb: [number, number, number] = [1, 1, 1];
133
130
  private _backgroundAlpha: number = 1.0;
134
131
 
135
132
  // Flow field properties
@@ -139,18 +136,7 @@ export class NeatGradient implements NeatController {
139
136
  private _flowEase: number = 0.0;
140
137
  private _flowEnabled: boolean = true;
141
138
 
142
- // Mouse interaction properties
143
- private _mouseDistortionStrength: number = 0.0;
144
- private _mouseDistortionRadius: number = 0.25;
145
- private _mouseDecayRate: number = 0.96;
146
- private _mouseDarken: number = 0.0;
147
- private _mouse: THREE.Vector2 = new THREE.Vector2(-1000, -1000);
148
- private _mouseFBO: THREE.WebGLRenderTarget | null = null;
149
- private _sceneMouse: THREE.Scene | null = null;
150
- private _cameraMouse: THREE.OrthographicCamera | null = null;
151
- private _mouseObjects: Array<{ mesh: THREE.Mesh, active: boolean }> = [];
152
- private _currentBrush: number = 0;
153
- private _mouseBrushBaseScale: number = 1;
139
+ private glState!: WebGLState;
154
140
 
155
141
  // Texture generation properties
156
142
  private _enableProceduralTexture: boolean = false;
@@ -161,7 +147,7 @@ export class NeatGradient implements NeatController {
161
147
  private _textureColorBlending: number = 0.01;
162
148
  private _textureSeed: number = 333;
163
149
  private _textureEase: number = 0.5;
164
- private _proceduralTexture: THREE.Texture | null = null;
150
+ private _proceduralTexture: WebGLTexture | null = null;
165
151
  private _proceduralBackgroundColor: string = "#000000";
166
152
 
167
153
  private _textureShapeTriangles: number = 20;
@@ -171,28 +157,23 @@ export class NeatGradient implements NeatController {
171
157
 
172
158
  private requestRef: number = -1;
173
159
  private sizeObserver: ResizeObserver;
174
- private sceneState: SceneState;
175
160
 
176
- // Optimization: Cache uniforms to avoid lookups and object creation in render loop
177
- private _cachedUniforms: NeatUniforms | null = null;
161
+ private _initialized: boolean = false;
178
162
  private _linkElement: HTMLAnchorElement | null = null;
163
+ private _cachedColorRgb: [number, number, number][] = [];
179
164
 
180
165
  private _yOffset: number = 0;
181
166
  private _yOffsetWaveMultiplier: number = 0.004;
182
167
  private _yOffsetColorMultiplier: number = 0.004;
183
168
  private _yOffsetFlowMultiplier: number = 0.004;
184
169
 
185
- // For saving/restoring clear color
186
- private _tempClearColor = new THREE.Color();
187
-
188
170
  // Performance optimizations
189
171
  private _resizeTimeoutId: number | null = null;
190
172
  private _textureNeedsUpdate: boolean = false;
191
- private _lastColorUpdate: number = 0;
192
173
  private _linkCheckCounter: number = 0;
193
- private _mouseUpdateScheduled: boolean = false;
194
- private _pendingMousePosition: { x: number; y: number } | null = null;
195
- private _colorsChanged: boolean = true; // Track if colors need update
174
+ private _colorsChanged: boolean = true;
175
+ private _uniformsDirty: boolean = true;
176
+ private _textureDirty: boolean = true;
196
177
 
197
178
  constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
198
179
 
@@ -229,11 +210,7 @@ export class NeatGradient implements NeatController {
229
210
  flowScale = 1.0,
230
211
  flowEase = 0.0,
231
212
  flowEnabled = true,
232
- // Mouse interaction
233
- mouseDistortionStrength = 0.0,
234
- mouseDistortionRadius = 0.25,
235
- mouseDecayRate = 0.96,
236
- mouseDarken = 0.0,
213
+
237
214
  // Texture generation
238
215
  enableProceduralTexture = false,
239
216
  textureVoidLikelihood = 0.45,
@@ -255,7 +232,6 @@ export class NeatGradient implements NeatController {
255
232
 
256
233
  this.destroy = this.destroy.bind(this);
257
234
  this._initScene = this._initScene.bind(this);
258
- this._buildMaterial = this._buildMaterial.bind(this);
259
235
 
260
236
  this.speed = speed;
261
237
  this.horizontalPressure = horizontalPressure;
@@ -288,11 +264,7 @@ export class NeatGradient implements NeatController {
288
264
  this.flowEase = flowEase;
289
265
  this.flowEnabled = flowEnabled;
290
266
 
291
- // Mouse interaction
292
- this.mouseDistortionStrength = mouseDistortionStrength;
293
- this.mouseDistortionRadius = mouseDistortionRadius;
294
- this.mouseDecayRate = mouseDecayRate;
295
- this.mouseDarken = mouseDarken;
267
+
296
268
 
297
269
  // Texture generation
298
270
  this.enableProceduralTexture = enableProceduralTexture;
@@ -310,18 +282,17 @@ export class NeatGradient implements NeatController {
310
282
  this._textureShapeBars = textureShapeBars;
311
283
  this._textureShapeSquiggles = textureShapeSquiggles;
312
284
 
313
- // FIX 1: Setup mouse resources BEFORE building the material/scene
314
- // This ensures u_mouse_texture isn't null during material compilation
315
- this._setupMouseInteraction();
316
- this.sceneState = this._initScene(resolution);
285
+
286
+ this.glState = this._initScene(resolution);
317
287
 
318
288
  injectSEO();
319
289
 
320
290
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
291
+ let lastTime = performance.now();
321
292
 
322
293
  const render = () => {
323
294
 
324
- const { renderer, camera, scene } = this.sceneState;
295
+ const { gl, program, locations, indexCount, indexType } = this.glState;
325
296
 
326
297
  // Optimization: check if cached link is still valid in DOM less frequently
327
298
  this._linkCheckCounter++;
@@ -332,156 +303,127 @@ export class NeatGradient implements NeatController {
332
303
  }
333
304
  }
334
305
 
335
- // Update Uniforms efficiently without creating new objects
336
- if (this._cachedUniforms) {
337
- const u = this._cachedUniforms;
338
-
339
- tick += clock.getDelta() * this._speed;
340
-
341
- u.u_time.value = tick;
342
- u.u_resolution.value.set(this._ref.width, this._ref.height);
343
- u.u_color_pressure.value.set(this._horizontalPressure, this._verticalPressure);
344
-
345
- // Directly assign simple values
346
- u.u_wave_frequency_x.value = this._waveFrequencyX;
347
- u.u_wave_frequency_y.value = this._waveFrequencyY;
348
- u.u_wave_amplitude.value = this._waveAmplitude;
349
- u.u_color_blending.value = this._colorBlending;
350
- u.u_shadows.value = this._shadows;
351
- u.u_highlights.value = this._highlights;
352
- u.u_saturation.value = this._saturation;
353
- u.u_brightness.value = this._brightness;
354
- u.u_grain_intensity.value = this._grainIntensity;
355
- u.u_grain_sparsity.value = this._grainSparsity;
356
- u.u_grain_speed.value = this._grainSpeed;
357
- u.u_grain_scale.value = this._grainScale;
358
- u.u_y_offset.value = this._yOffset;
359
- u.u_y_offset_wave_multiplier.value = this._yOffsetWaveMultiplier;
360
- u.u_y_offset_color_multiplier.value = this._yOffsetColorMultiplier;
361
- u.u_y_offset_flow_multiplier.value = this._yOffsetFlowMultiplier;
362
- u.u_flow_distortion_a.value = this._flowDistortionA;
363
- u.u_flow_distortion_b.value = this._flowDistortionB;
364
- u.u_flow_scale.value = this._flowScale;
365
- u.u_flow_ease.value = this._flowEase;
366
- u.u_flow_enabled.value = this._flowEnabled ? 1.0 : 0.0;
367
- u.u_mouse_distortion_strength.value = this._mouseDistortionStrength;
368
- u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
369
- u.u_mouse_darken.value = this._mouseDarken;
370
- u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
306
+ if (this._initialized) {
307
+ const timeNow = performance.now();
308
+ tick += ((timeNow - lastTime) / 1000) * this._speed;
309
+ lastTime = timeNow;
310
+
311
+ gl.useProgram(program);
312
+
313
+ gl.uniform1f(locations.uniforms['u_time'], tick);
314
+
315
+ // Only upload static uniforms when they've been modified
316
+ if (this._uniformsDirty) {
317
+ gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
318
+ gl.uniform2f(locations.uniforms['u_color_pressure'], this._horizontalPressure, this._verticalPressure);
319
+
320
+ gl.uniform1f(locations.uniforms['u_wave_frequency_x'], this._waveFrequencyX);
321
+ gl.uniform1f(locations.uniforms['u_wave_frequency_y'], this._waveFrequencyY);
322
+ gl.uniform1f(locations.uniforms['u_wave_amplitude'], this._waveAmplitude);
323
+ gl.uniform1f(locations.uniforms['u_color_blending'], this._colorBlending);
324
+ gl.uniform1f(locations.uniforms['u_shadows'], this._shadows);
325
+ gl.uniform1f(locations.uniforms['u_highlights'], this._highlights);
326
+ gl.uniform1f(locations.uniforms['u_saturation'], this._saturation);
327
+ gl.uniform1f(locations.uniforms['u_brightness'], this._brightness);
328
+ gl.uniform1f(locations.uniforms['u_grain_intensity'], this._grainIntensity);
329
+ gl.uniform1f(locations.uniforms['u_grain_sparsity'], this._grainSparsity);
330
+ gl.uniform1f(locations.uniforms['u_grain_speed'], this._grainSpeed);
331
+ gl.uniform1f(locations.uniforms['u_grain_scale'], this._grainScale);
332
+ gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
333
+ gl.uniform1f(locations.uniforms['u_y_offset_wave_multiplier'], this._yOffsetWaveMultiplier);
334
+ gl.uniform1f(locations.uniforms['u_y_offset_color_multiplier'], this._yOffsetColorMultiplier);
335
+ gl.uniform1f(locations.uniforms['u_y_offset_flow_multiplier'], this._yOffsetFlowMultiplier);
336
+ gl.uniform1f(locations.uniforms['u_flow_distortion_a'], this._flowDistortionA);
337
+ gl.uniform1f(locations.uniforms['u_flow_distortion_b'], this._flowDistortionB);
338
+ gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
339
+ gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
340
+ gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
341
+
342
+ gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
343
+ gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
344
+
345
+ this._uniformsDirty = false;
346
+ }
371
347
 
372
348
  // Only regenerate procedural texture when needed
373
349
  if (this._textureNeedsUpdate && this._enableProceduralTexture) {
374
350
  if (this._proceduralTexture) {
375
- this._proceduralTexture.dispose();
351
+ gl.deleteTexture(this._proceduralTexture);
376
352
  }
377
- this._proceduralTexture = this._createProceduralTexture();
353
+ this._proceduralTexture = this._createProceduralTexture(gl);
378
354
  this._textureNeedsUpdate = false;
355
+ this._textureDirty = true;
379
356
  }
380
357
 
381
- u.u_procedural_texture.value = this._proceduralTexture;
382
- u.u_texture_ease.value = this._textureEase;
383
-
384
- // Wireframe is a material property and must update every frame to avoid artifacts
385
- // @ts-ignore - access material safely
386
- this.sceneState.meshes[0].material.wireframe = this._wireframe;
387
-
388
- // Optimized Color Update: Update immediately on change, or throttle to 10 times per second
389
- const now = Date.now();
390
- const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
358
+ // Procedural texture binding — only when texture changes
359
+ if (this._textureDirty && this._proceduralTexture) {
360
+ gl.activeTexture(gl.TEXTURE1);
361
+ gl.bindTexture(gl.TEXTURE_2D, this._proceduralTexture);
362
+ gl.uniform1i(locations.uniforms['u_procedural_texture'], 1);
363
+ this._textureDirty = false;
364
+ }
391
365
 
392
- if (shouldUpdate) {
393
- this._lastColorUpdate = now;
366
+ // Color update — only when colors have changed
367
+ if (this._colorsChanged) {
394
368
  this._colorsChanged = false;
395
369
 
396
- const shaderColors = u.u_colors.value;
397
370
  for (let i = 0; i < COLORS_COUNT; i++) {
398
371
  if (i < this._colors.length) {
399
372
  const c = this._colors[i];
400
- shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
401
- shaderColors[i].color.setStyle(c.color, "");
402
- shaderColors[i].influence = c.influence || 0;
373
+ const rgb = this._cachedColorRgb[i] || [0, 0, 0];
374
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], c.enabled ? 1.0 : 0.0);
375
+ gl.uniform3fv(locations.uniforms[`u_colors[${i}].color`], rgb);
376
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].influence`], c.influence || 0);
403
377
  } else {
404
- shaderColors[i].is_active = 0.0;
378
+ gl.uniform1f(locations.uniforms[`u_colors[${i}].is_active`], 0.0);
405
379
  }
406
380
  }
407
381
 
408
- u.u_colors_count.value = COLORS_COUNT;
382
+ gl.uniform1i(locations.uniforms['u_colors_count'], COLORS_COUNT);
409
383
  }
410
384
  }
411
385
 
412
- // Render mouse interaction to FBO - optimize by only rendering when needed
413
- if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
414
- let hasActiveBrushes = false;
415
-
416
- // Update mouse objects - decay rate controls how fast trails fade
417
- for (let i = 0; i < this._mouseObjects.length; i++) {
418
- const obj = this._mouseObjects[i];
419
- if (obj.mesh.visible) {
420
- hasActiveBrushes = true;
421
- obj.mesh.rotation.z += 0.01;
422
- if (obj.mesh.material instanceof THREE.MeshBasicMaterial) {
423
- // Decay only affects opacity
424
- obj.mesh.material.opacity *= this._mouseDecayRate;
425
-
426
- if (obj.mesh.material.opacity < 0.01) {
427
- obj.mesh.visible = false;
428
- }
429
- }
430
- }
431
- }
432
-
433
- // Only render FBO if there are active brushes
434
- if (hasActiveBrushes) {
435
- // Store current clear color (likely the main background color)
436
- renderer.getClearColor(this._tempClearColor);
437
- const oldClearAlpha = renderer.getClearAlpha();
438
-
439
- // Set clear color to Black/Transparent for the FBO.
440
- renderer.setClearColor(0x000000, 0.0);
441
-
442
- renderer.setRenderTarget(this._mouseFBO);
443
- renderer.clear();
444
- renderer.render(this._sceneMouse, this._cameraMouse);
445
- renderer.setRenderTarget(null);
446
386
 
447
- // Restore main background color for the actual scene render
448
- renderer.setClearColor(this._tempClearColor, oldClearAlpha);
387
+ // Draw scene
388
+ gl.clearColor(
389
+ this._backgroundColorRgb[0],
390
+ this._backgroundColorRgb[1],
391
+ this._backgroundColorRgb[2],
392
+ this._backgroundAlpha
393
+ );
394
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
449
395
 
450
- // Update mouse texture uniform
451
- if (this._cachedUniforms) {
452
- this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
453
- }
454
- }
396
+ if (this._wireframe) {
397
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
398
+ gl.drawElements(gl.LINES, this.glState.wireframeIndexCount, indexType, 0);
399
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
400
+ } else {
401
+ gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
455
402
  }
456
403
 
457
- // Ensure we set the clear color for the main scene explicitly before rendering
458
- renderer.setClearColor(this._backgroundColor, this._backgroundAlpha);
459
- renderer.render(scene, camera);
460
404
  this.requestRef = requestAnimationFrame(render);
461
405
  };
462
406
 
463
407
  const setSize = () => {
464
408
 
465
- const { renderer } = this.sceneState;
466
- const canvas = renderer.domElement;
467
- const width = canvas.clientWidth;
468
- const height = canvas.clientHeight;
469
-
470
- this.sceneState.renderer.setSize(width, height, false);
471
- updateCamera(this.sceneState.camera, width, height);
472
-
473
- // FIX 3: Update Mouse FBO and Camera on resize
474
- // If we don't do this, mouse coordinates map incorrectly after a resize
475
- if (this._mouseFBO && this._cameraMouse) {
476
- const fSize = height / 2;
477
- const aspect = width / height;
478
- this._mouseFBO.setSize(width / 2, height / 2);
479
- this._cameraMouse.left = -fSize * aspect;
480
- this._cameraMouse.right = fSize * aspect;
481
- this._cameraMouse.top = fSize;
482
- this._cameraMouse.bottom = -fSize;
483
- this._cameraMouse.updateProjectionMatrix();
484
- }
409
+ const { gl, camera } = this.glState;
410
+ const width = this._ref.clientWidth;
411
+ const height = this._ref.clientHeight;
412
+
413
+ // Handle high DPI displays properly without scaling buffer resolution, matching client width
414
+ this._ref.width = width;
415
+ this._ref.height = height;
416
+
417
+ gl.viewport(0, 0, width, height);
418
+
419
+ updateCamera(camera, width, height);
420
+
421
+
422
+
423
+ // Recompute projection matrix on resize
424
+ const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
425
+ gl.useProgram(this.glState.program);
426
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
485
427
  };
486
428
 
487
429
  // Debounce resize to prevent excessive operations
@@ -502,119 +444,155 @@ export class NeatGradient implements NeatController {
502
444
  }
503
445
 
504
446
  destroy() {
505
- if (this) {
506
- cancelAnimationFrame(this.requestRef);
507
- this.sizeObserver.disconnect();
447
+ cancelAnimationFrame(this.requestRef);
448
+ this.sizeObserver.disconnect();
508
449
 
509
- // Clear resize timeout
510
- if (this._resizeTimeoutId !== null) {
511
- clearTimeout(this._resizeTimeoutId);
512
- this._resizeTimeoutId = null;
513
- }
450
+ // Clear resize timeout
451
+ if (this._resizeTimeoutId !== null) {
452
+ clearTimeout(this._resizeTimeoutId);
453
+ this._resizeTimeoutId = null;
454
+ }
514
455
 
515
- // Cleanup WebGL resources
516
- if (this.sceneState) {
517
- this.sceneState.renderer.dispose();
518
- this.sceneState.meshes.forEach(m => {
519
- m.geometry.dispose();
520
- if (Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
521
- else m.material.dispose();
522
- });
523
- }
524
- if (this._mouseFBO) this._mouseFBO.dispose();
525
- if (this._proceduralTexture) this._proceduralTexture.dispose();
456
+ // Remove NEAT link
457
+ if (this._linkElement && this._linkElement.parentElement) {
458
+ this._linkElement.parentElement.removeChild(this._linkElement);
459
+ this._linkElement = null;
460
+ }
461
+
462
+ // Cleanup WebGL resources
463
+ if (this.glState) {
464
+ const gl = this.glState.gl;
465
+ gl.deleteProgram(this.glState.program);
466
+ gl.deleteBuffer(this.glState.buffers.position);
467
+ gl.deleteBuffer(this.glState.buffers.normal);
468
+ gl.deleteBuffer(this.glState.buffers.uv);
469
+ gl.deleteBuffer(this.glState.buffers.index);
470
+ gl.deleteBuffer(this.glState.buffers.wireframeIndex);
471
+ }
472
+ if (this._proceduralTexture && this.glState) {
473
+ this.glState.gl.deleteTexture(this._proceduralTexture);
526
474
  }
527
475
  }
528
476
 
529
477
  downloadAsPNG(filename = "neat.png") {
530
- console.log("Downloading as PNG", this._ref);
531
478
  const dataURL = this._ref.toDataURL("image/png");
532
- console.log("data", dataURL);
533
479
  downloadURI(dataURL, filename);
534
480
  }
535
481
 
536
482
  set speed(speed: number) {
483
+ this._uniformsDirty = true;
537
484
  this._speed = speed / 20;
538
485
  }
539
486
 
540
487
  set horizontalPressure(horizontalPressure: number) {
488
+ this._uniformsDirty = true;
541
489
  this._horizontalPressure = horizontalPressure / 4;
542
490
  }
543
491
 
544
492
  set verticalPressure(verticalPressure: number) {
493
+ this._uniformsDirty = true;
545
494
  this._verticalPressure = verticalPressure / 4;
546
495
  }
547
496
 
548
497
  set waveFrequencyX(waveFrequencyX: number) {
498
+ this._uniformsDirty = true;
549
499
  this._waveFrequencyX = waveFrequencyX * 0.04;
550
500
  }
551
501
 
552
502
  set waveFrequencyY(waveFrequencyY: number) {
503
+ this._uniformsDirty = true;
553
504
  this._waveFrequencyY = waveFrequencyY * 0.04;
554
505
  }
555
506
 
556
507
  set waveAmplitude(waveAmplitude: number) {
508
+ this._uniformsDirty = true;
557
509
  this._waveAmplitude = waveAmplitude * .75;
558
510
  }
559
511
 
560
512
  set colors(colors: NeatColor[]) {
513
+ this._uniformsDirty = true;
561
514
  this._colors = colors;
562
- this._colorsChanged = true; // Flag for immediate update
515
+ this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
516
+ this._colorsChanged = true;
563
517
  }
564
518
 
565
519
  set highlights(highlights: number) {
520
+ this._uniformsDirty = true;
566
521
  this._highlights = highlights / 100;
567
522
  }
568
523
 
569
524
  set shadows(shadows: number) {
525
+ this._uniformsDirty = true;
570
526
  this._shadows = shadows / 100;
571
527
  }
572
528
 
573
529
  set colorSaturation(colorSaturation: number) {
530
+ this._uniformsDirty = true;
574
531
  this._saturation = colorSaturation / 10;
575
532
  }
576
533
 
577
534
  set colorBrightness(colorBrightness: number) {
535
+ this._uniformsDirty = true;
578
536
  this._brightness = colorBrightness;
579
537
  }
580
538
 
581
539
  set colorBlending(colorBlending: number) {
540
+ this._uniformsDirty = true;
582
541
  this._colorBlending = colorBlending / 10;
583
542
  }
584
543
 
585
544
  set grainScale(grainScale: number) {
545
+ this._uniformsDirty = true;
586
546
  this._grainScale = grainScale == 0 ? 1 : grainScale;
587
547
  }
588
548
 
589
549
  set grainIntensity(grainIntensity: number) {
550
+ this._uniformsDirty = true;
590
551
  this._grainIntensity = grainIntensity;
591
552
  }
592
553
 
593
554
  set grainSparsity(grainSparsity: number) {
555
+ this._uniformsDirty = true;
594
556
  this._grainSparsity = grainSparsity;
595
557
  }
596
558
 
597
559
  set grainSpeed(grainSpeed: number) {
560
+ this._uniformsDirty = true;
598
561
  this._grainSpeed = grainSpeed;
599
562
  }
600
563
 
601
564
  set wireframe(wireframe: boolean) {
565
+ this._uniformsDirty = true;
602
566
  this._wireframe = wireframe;
603
567
  }
604
568
 
605
569
  set resolution(resolution: number) {
606
- this.sceneState = this._initScene(resolution);
570
+ this._uniformsDirty = true;
571
+ if (this.glState) {
572
+ const gl = this.glState.gl;
573
+ gl.deleteProgram(this.glState.program);
574
+ gl.deleteBuffer(this.glState.buffers.position);
575
+ gl.deleteBuffer(this.glState.buffers.normal);
576
+ gl.deleteBuffer(this.glState.buffers.uv);
577
+ gl.deleteBuffer(this.glState.buffers.index);
578
+ gl.deleteBuffer(this.glState.buffers.wireframeIndex);
579
+ }
580
+ this.glState = this._initScene(resolution);
607
581
  }
608
582
 
609
583
  set backgroundColor(backgroundColor: string) {
584
+ this._uniformsDirty = true;
610
585
  this._backgroundColor = backgroundColor;
586
+ this._backgroundColorRgb = this._hexToRgb(backgroundColor);
611
587
  }
612
588
 
613
589
  set backgroundAlpha(backgroundAlpha: number) {
590
+ this._uniformsDirty = true;
614
591
  this._backgroundAlpha = backgroundAlpha;
615
592
  }
616
593
 
617
594
  set yOffset(yOffset: number) {
595
+ this._uniformsDirty = true;
618
596
  this._yOffset = yOffset;
619
597
  }
620
598
 
@@ -623,6 +601,7 @@ export class NeatGradient implements NeatController {
623
601
  }
624
602
 
625
603
  set yOffsetWaveMultiplier(value: number) {
604
+ this._uniformsDirty = true;
626
605
  this._yOffsetWaveMultiplier = value / 1000;
627
606
  }
628
607
 
@@ -631,6 +610,7 @@ export class NeatGradient implements NeatController {
631
610
  }
632
611
 
633
612
  set yOffsetColorMultiplier(value: number) {
613
+ this._uniformsDirty = true;
634
614
  this._yOffsetColorMultiplier = value / 1000;
635
615
  }
636
616
 
@@ -639,26 +619,32 @@ export class NeatGradient implements NeatController {
639
619
  }
640
620
 
641
621
  set yOffsetFlowMultiplier(value: number) {
622
+ this._uniformsDirty = true;
642
623
  this._yOffsetFlowMultiplier = value / 1000;
643
624
  }
644
625
 
645
626
  set flowDistortionA(value: number) {
627
+ this._uniformsDirty = true;
646
628
  this._flowDistortionA = value;
647
629
  }
648
630
 
649
631
  set flowDistortionB(value: number) {
632
+ this._uniformsDirty = true;
650
633
  this._flowDistortionB = value;
651
634
  }
652
635
 
653
636
  set flowScale(value: number) {
637
+ this._uniformsDirty = true;
654
638
  this._flowScale = value;
655
639
  }
656
640
 
657
641
  set flowEase(value: number) {
642
+ this._uniformsDirty = true;
658
643
  this._flowEase = value;
659
644
  }
660
645
 
661
646
  set flowEnabled(value: boolean) {
647
+ this._uniformsDirty = true;
662
648
  this._flowEnabled = value;
663
649
  }
664
650
 
@@ -667,34 +653,9 @@ export class NeatGradient implements NeatController {
667
653
  }
668
654
 
669
655
 
670
- set mouseDistortionStrength(value: number) {
671
- this._mouseDistortionStrength = Math.max(0, value);
672
- }
673
-
674
- set mouseDistortionRadius(value: number) {
675
- // Clamp to a sane range in UV space
676
- this._mouseDistortionRadius = Math.max(0.01, Math.min(value, 1.0));
677
- // Update brush scale when radius changes
678
- this._updateBrushScale();
679
- }
680
-
681
- _updateBrushScale() {
682
- if (!this._mouseObjects || this._mouseObjects.length === 0) return;
683
- // Radius directly controls the brush scale
684
- // Base geometry is 200px, so radius 0.25 = 50px, radius 1.0 = 200px
685
- this._mouseBrushBaseScale = this._mouseDistortionRadius;
686
- }
687
-
688
- set mouseDecayRate(value: number) {
689
- // Clamp between 0.9 (slow decay, more wobble) and 0.99 (fast decay, less wobble)
690
- this._mouseDecayRate = Math.max(0.9, Math.min(value, 0.99));
691
- }
692
-
693
- set mouseDarken(value: number) {
694
- this._mouseDarken = value;
695
- }
696
656
 
697
657
  set enableProceduralTexture(value: boolean) {
658
+ this._uniformsDirty = true;
698
659
  this._enableProceduralTexture = value;
699
660
  if (value && !this._proceduralTexture) {
700
661
  this._textureNeedsUpdate = true;
@@ -702,6 +663,7 @@ export class NeatGradient implements NeatController {
702
663
  }
703
664
 
704
665
  set textureVoidLikelihood(value: number) {
666
+ this._uniformsDirty = true;
705
667
  this._textureVoidLikelihood = value;
706
668
  if (this._enableProceduralTexture) {
707
669
  this._textureNeedsUpdate = true;
@@ -709,6 +671,7 @@ export class NeatGradient implements NeatController {
709
671
  }
710
672
 
711
673
  set textureVoidWidthMin(value: number) {
674
+ this._uniformsDirty = true;
712
675
  this._textureVoidWidthMin = value;
713
676
  if (this._enableProceduralTexture) {
714
677
  this._textureNeedsUpdate = true;
@@ -716,6 +679,7 @@ export class NeatGradient implements NeatController {
716
679
  }
717
680
 
718
681
  set textureVoidWidthMax(value: number) {
682
+ this._uniformsDirty = true;
719
683
  this._textureVoidWidthMax = value;
720
684
  if (this._enableProceduralTexture) {
721
685
  this._textureNeedsUpdate = true;
@@ -723,6 +687,7 @@ export class NeatGradient implements NeatController {
723
687
  }
724
688
 
725
689
  set textureBandDensity(value: number) {
690
+ this._uniformsDirty = true;
726
691
  this._textureBandDensity = value;
727
692
  if (this._enableProceduralTexture) {
728
693
  this._textureNeedsUpdate = true;
@@ -730,6 +695,7 @@ export class NeatGradient implements NeatController {
730
695
  }
731
696
 
732
697
  set textureColorBlending(value: number) {
698
+ this._uniformsDirty = true;
733
699
  this._textureColorBlending = value;
734
700
  if (this._enableProceduralTexture) {
735
701
  this._textureNeedsUpdate = true;
@@ -737,6 +703,7 @@ export class NeatGradient implements NeatController {
737
703
  }
738
704
 
739
705
  set textureSeed(value: number) {
706
+ this._uniformsDirty = true;
740
707
  this._textureSeed = value;
741
708
  if (this._enableProceduralTexture) {
742
709
  this._textureNeedsUpdate = true;
@@ -748,10 +715,12 @@ export class NeatGradient implements NeatController {
748
715
  }
749
716
 
750
717
  set textureEase(value: number) {
718
+ this._uniformsDirty = true;
751
719
  this._textureEase = value;
752
720
  }
753
721
 
754
722
  set proceduralBackgroundColor(value: string) {
723
+ this._uniformsDirty = true;
755
724
  this._proceduralBackgroundColor = value;
756
725
  if (this._enableProceduralTexture) {
757
726
  this._textureNeedsUpdate = true;
@@ -759,244 +728,222 @@ export class NeatGradient implements NeatController {
759
728
  }
760
729
 
761
730
  set textureShapeTriangles(value: number) {
731
+ this._uniformsDirty = true;
762
732
  this._textureShapeTriangles = value;
763
733
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
764
734
  }
765
735
  set textureShapeCircles(value: number) {
736
+ this._uniformsDirty = true;
766
737
  this._textureShapeCircles = value;
767
738
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
768
739
  }
769
740
  set textureShapeBars(value: number) {
741
+ this._uniformsDirty = true;
770
742
  this._textureShapeBars = value;
771
743
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
772
744
  }
773
745
  set textureShapeSquiggles(value: number) {
746
+ this._uniformsDirty = true;
774
747
  this._textureShapeSquiggles = value;
775
748
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
776
749
  }
777
750
 
778
- _initScene(resolution: number): SceneState {
751
+ _hexToRgb(hex: string): [number, number, number] {
752
+ const bigint = parseInt(hex.replace('#', ''), 16);
753
+ return [
754
+ ((bigint >> 16) & 255) / 255.0,
755
+ ((bigint >> 8) & 255) / 255.0,
756
+ (bigint & 255) / 255.0
757
+ ];
758
+ }
779
759
 
780
- const width = this._ref.width,
781
- height = this._ref.height;
760
+ _initScene(resolution: number): WebGLState {
782
761
 
783
- // Cleanup existing renderer if needed
784
- if (this.sceneState && this.sceneState.renderer) {
785
- this.sceneState.renderer.dispose();
786
- this.sceneState.meshes.forEach(m => {
787
- m.geometry.dispose();
788
- if (Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
789
- else m.material.dispose();
790
- });
791
- }
762
+ const width = this._ref.clientWidth;
763
+ const height = this._ref.clientHeight;
792
764
 
793
- const renderer = new THREE.WebGLRenderer({
794
- // antialias: true,
795
- alpha: true,
796
- preserveDrawingBuffer: true,
797
- canvas: this._ref
798
- });
765
+ const gl = this._ref.getContext("webgl2", { alpha: true, preserveDrawingBuffer: true, antialias: true }) ||
766
+ this._ref.getContext("webgl", { alpha: true, preserveDrawingBuffer: true, antialias: true });
799
767
 
800
- renderer.setClearColor(0xFF0000, .5);
801
- renderer.setSize(width, height, false);
768
+ if (!gl) {
769
+ throw new Error("WebGL not supported");
770
+ }
802
771
 
803
- const meshes: THREE.Mesh[] = [];
772
+ const ext = gl.getExtension("OES_standard_derivatives");
773
+ gl.getExtension("OES_element_index_uint");
804
774
 
805
- const scene = new THREE.Scene();
775
+ gl.viewport(0, 0, width, height);
806
776
 
807
- const material = this._buildMaterial(width, height);
777
+ // Generate plane geometry with Uint32Array for large meshes
778
+ const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
808
779
 
809
- const geo = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
810
- const plane = new THREE.Mesh(geo, material);
811
- plane.rotation.x = -Math.PI / 3.5;
812
- plane.position.z = -1;
813
- meshes.push(plane);
814
- scene.add(plane);
780
+ const positionBuffer = gl.createBuffer()!;
781
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
782
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
815
783
 
816
- const camera = new THREE.OrthographicCamera(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
817
- // const camera = new THREE.PerspectiveCamera( 1000, window.innerWidth / window.innerHeight, 1, 1000000 );
818
- camera.position.z = 5;
819
- updateCamera(camera, width, height);
784
+ const normalBuffer = gl.createBuffer()!;
785
+ gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
786
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
820
787
 
821
- return {
822
- renderer,
823
- camera,
824
- scene,
825
- meshes,
826
- resolution
827
- };
828
- }
788
+ const uvBuffer = gl.createBuffer()!;
789
+ gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
790
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
829
791
 
830
- _buildMaterial(width: number, height: number) {
831
-
832
- // Initialize stable array structure for colors
833
- // We create 6 objects and just update them in the render loop to avoid GC
834
- const colors = Array.from({ length: COLORS_COUNT }).map((_, i) => ({
835
- is_active: i < this._colors.length ? (this._colors[i].enabled ? 1.0 : 0.0) : 0.0,
836
- color: new THREE.Color(i < this._colors.length ? this._colors[i].color : 0x000000),
837
- influence: i < this._colors.length ? (this._colors[i].influence || 0) : 0
838
- }));
839
-
840
- const uniforms = {
841
- u_time: { value: 0 },
842
- u_color_pressure: { value: new THREE.Vector2(this._horizontalPressure, this._verticalPressure) },
843
- u_wave_frequency_x: { value: this._waveFrequencyX },
844
- u_wave_frequency_y: { value: this._waveFrequencyY },
845
- u_wave_amplitude: { value: this._waveAmplitude },
846
- u_resolution: { value: new THREE.Vector2(width, height) },
847
- u_colors: { value: colors },
848
- u_colors_count: { value: this._colors.length },
849
- u_plane_width: { value: PLANE_WIDTH },
850
- u_plane_height: { value: PLANE_HEIGHT },
851
- u_shadows: { value: this._shadows },
852
- u_highlights: { value: this._highlights },
853
- u_grain_intensity: { value: this._grainIntensity },
854
- u_grain_sparsity: { value: this._grainSparsity },
855
- u_grain_scale: { value: this._grainScale },
856
- u_grain_speed: { value: this._grainSpeed },
857
- // Flow field
858
- u_flow_distortion_a: { value: this._flowDistortionA },
859
- u_flow_distortion_b: { value: this._flowDistortionB },
860
- u_flow_scale: { value: this._flowScale },
861
- u_flow_ease: { value: this._flowEase },
862
- u_flow_enabled: { value: this._flowEnabled ? 1.0 : 0.0 },
863
- // Y offset multipliers
864
- u_y_offset: { value: this._yOffset },
865
- u_y_offset_wave_multiplier: { value: this._yOffsetWaveMultiplier },
866
- u_y_offset_color_multiplier: { value: this._yOffsetColorMultiplier },
867
- u_y_offset_flow_multiplier: { value: this._yOffsetFlowMultiplier },
868
- // Mouse interaction
869
- u_mouse_distortion_strength: { value: this._mouseDistortionStrength },
870
- u_mouse_distortion_radius: { value: this._mouseDistortionRadius },
871
- u_mouse_darken: { value: this._mouseDarken },
872
- u_mouse_texture: { value: this._mouseFBO ? this._mouseFBO.texture : null },
873
- // Procedural texture
874
- u_procedural_texture: { value: this._proceduralTexture },
875
- u_enable_procedural_texture: { value: this._enableProceduralTexture ? 1.0 : 0.0 },
876
- u_texture_ease: { value: this._textureEase },
877
- u_saturation: { value: this._saturation },
878
- u_brightness: { value: this._brightness },
879
- u_color_blending: { value: this._colorBlending }
880
- };
792
+ const indexBuffer = gl.createBuffer()!;
793
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
794
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
881
795
 
882
- const material = new THREE.ShaderMaterial({
883
- uniforms: uniforms,
884
- vertexShader: buildUniforms() + buildNoise() + buildColorFunctions() + buildVertexShader(),
885
- fragmentShader: buildUniforms() + buildColorFunctions() + buildNoise() + buildFragmentShader()
886
- });
796
+ const wireframeIndexBuffer = gl.createBuffer()!;
797
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeIndexBuffer);
798
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
887
799
 
888
- // Cache the uniforms object for direct access in render loop
889
- this._cachedUniforms = uniforms as unknown as NeatUniforms;
800
+ // Rebind the triangle index buffer as default
801
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
890
802
 
891
- material.wireframe = WIREFRAME;
892
- return material;
893
- }
803
+ const vertShaderSourceCombined = buildVertUniforms() + "\n" + buildNoise() + "\n" + buildColorFunctions() + "\n" + vertexShaderSource;
804
+ const vertShader = gl.createShader(gl.VERTEX_SHADER)!;
805
+ gl.shaderSource(vertShader, vertShaderSourceCombined);
806
+ gl.compileShader(vertShader);
807
+ if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
808
+ console.log("VERTEX_SHADER_ERROR_START");
809
+ console.log("Vertex shader error: ", gl.getShaderInfoLog(vertShader));
810
+ console.log("GL Error Code:", gl.getError());
811
+ console.log("Vertex Shader Source Dump:");
812
+ console.log(vertShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
813
+ console.log("VERTEX_SHADER_ERROR_END");
814
+ }
894
815
 
895
- _setupMouseInteraction() {
896
- if (!this._ref) return;
897
-
898
- const width = this._ref.width;
899
- const height = this._ref.height;
900
-
901
- // Create mouse FBO
902
- this._mouseFBO = new THREE.WebGLRenderTarget(width / 2, height / 2);
903
-
904
- // Create mouse scene and camera
905
- this._sceneMouse = new THREE.Scene();
906
- const fSize = height / 2;
907
- const aspect = width / height;
908
-
909
- // FIX 4: Ensure near plane allows viewing objects at Z=0
910
- // Near -100 is safer for objects at 0
911
- this._cameraMouse = new THREE.OrthographicCamera(
912
- -fSize * aspect, fSize * aspect,
913
- fSize, -fSize,
914
- 0, 10000
915
- );
916
- this._cameraMouse.position.set(0, 0, 100);
917
-
918
- // Create brush texture - More visible and impactful
919
- const brushCanvas = document.createElement('canvas');
920
- brushCanvas.width = 128;
921
- brushCanvas.height = 128;
922
- const bCtx = brushCanvas.getContext('2d');
923
- if (bCtx) {
924
- const grd = bCtx.createRadialGradient(64, 64, 0, 64, 64, 64);
925
- // Match reference implementation's stronger gradient
926
- grd.addColorStop(0, 'rgba(255,255,255,0.8)');
927
- grd.addColorStop(0.5, 'rgba(255,255,255,0.4)');
928
- grd.addColorStop(1, 'rgba(255,255,255,0)');
929
- bCtx.fillStyle = grd;
930
- bCtx.fillRect(0, 0, 128, 128);
816
+ const fragShaderSourceCombined = buildFragUniforms() + "\n" + buildColorFunctions() + "\n" + buildNoise() + "\n" + fragmentShaderSource;
817
+ const fragShader = gl.createShader(gl.FRAGMENT_SHADER)!;
818
+ gl.shaderSource(fragShader, fragShaderSourceCombined);
819
+ gl.compileShader(fragShader);
820
+ if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
821
+ console.log("FRAGMENT_SHADER_ERROR_START");
822
+ console.log("Fragment shader error: ", gl.getShaderInfoLog(fragShader));
823
+ console.log("GL Error Code:", gl.getError());
824
+ console.log("Fragment Shader Source Dump:");
825
+ console.log(fragShaderSourceCombined.split('\n').map((line, i) => `${i + 1}: ${line}`).join('\n'));
826
+ console.log("FRAGMENT_SHADER_ERROR_END");
931
827
  }
932
- const brushTex = new THREE.CanvasTexture(brushCanvas);
933
- const brushMat = new THREE.MeshBasicMaterial({
934
- map: brushTex,
935
- transparent: true,
936
- opacity: 1.0,
937
- depthTest: false,
938
- blending: THREE.AdditiveBlending // Additive blending for better accumulation
939
- });
940
- // Brush geometry size - will be scaled by radius parameter
941
- const brushGeo = new THREE.PlaneGeometry(200, 200);
942
-
943
- // Create brush pool
944
- const brushPoolSize = 50;
945
- for (let i = 0; i < brushPoolSize; i++) {
946
- const m = new THREE.Mesh(brushGeo, brushMat.clone());
947
- m.visible = false;
948
- this._sceneMouse!.add(m);
949
- this._mouseObjects.push({ mesh: m, active: false });
828
+
829
+ const program = gl.createProgram()!;
830
+ gl.attachShader(program, vertShader);
831
+ gl.attachShader(program, fragShader);
832
+ gl.linkProgram(program);
833
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
834
+ console.log("PROGRAM_LINK_ERROR_START");
835
+ console.log("Program linking error: ", gl.getProgramInfoLog(program));
836
+ console.log("GL Error Code:", gl.getError());
837
+ console.log("PROGRAM_LINK_ERROR_END");
950
838
  }
951
839
 
952
- // Initialize brush scale based on current radius
953
- this._updateBrushScale();
840
+ gl.useProgram(program);
954
841
 
955
- // Add mouse move listener
956
- this._ref.addEventListener('mousemove', this._onMouseMove.bind(this));
957
- }
842
+ const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
843
+ camera.position = [0, 0, 5];
844
+ updateCamera(camera, width, height);
845
+
846
+ // Define attributes
847
+ const aPosition = gl.getAttribLocation(program, "position");
848
+ const aNormal = gl.getAttribLocation(program, "normal");
849
+ const aUv = gl.getAttribLocation(program, "uv");
850
+
851
+ gl.enableVertexAttribArray(aPosition);
852
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
853
+ gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
854
+
855
+ gl.enableVertexAttribArray(aNormal);
856
+ gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
857
+ gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
858
+
859
+ gl.enableVertexAttribArray(aUv);
860
+ gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
861
+ gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
862
+
863
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
864
+
865
+ const modelViewMatrix = new Matrix4();
866
+ // The View Matrix is the inverse of the Camera's position
867
+ // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
868
+ modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
869
+
870
+ // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
871
+ modelViewMatrix.translate(0, 0, -1);
872
+ modelViewMatrix.rotateX(-Math.PI / 3.5);
958
873
 
959
- _onMouseMove(e: MouseEvent) {
960
- if (!this._ref || !this._sceneMouse) return;
874
+ const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
875
+ gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
961
876
 
962
- const rect = this._ref.getBoundingClientRect();
963
- const width = this._ref.width;
964
- const height = this._ref.height;
877
+ const projLoc = gl.getUniformLocation(program, "projectionMatrix");
878
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
965
879
 
966
- // Store pending mouse position
967
- this._pendingMousePosition = {
968
- x: e.clientX - rect.left - width / 2,
969
- y: -(e.clientY - rect.top - height / 2)
880
+ const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
881
+ gl.uniform1f(planeWidthLoc, PLANE_WIDTH);
882
+
883
+ const planeHeightLoc = gl.getUniformLocation(program, "u_plane_height");
884
+ gl.uniform1f(planeHeightLoc, PLANE_HEIGHT);
885
+
886
+ const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
887
+ gl.uniform1i(colorsCountLoc, COLORS_COUNT);
888
+
889
+ const uniformsList = [
890
+ "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
891
+ "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
892
+ "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
893
+ "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
894
+ "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
895
+
896
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending"
897
+ ];
898
+
899
+ const locations: WebGLState["locations"] = {
900
+ attributes: { position: aPosition, normal: aNormal, uv: aUv },
901
+ uniforms: {}
970
902
  };
971
903
 
972
- // Batch mouse updates using requestAnimationFrame
973
- if (!this._mouseUpdateScheduled) {
974
- this._mouseUpdateScheduled = true;
975
- requestAnimationFrame(() => {
976
- this._mouseUpdateScheduled = false;
977
-
978
- if (!this._pendingMousePosition) return;
979
-
980
- this._mouse.x = this._pendingMousePosition.x;
981
- this._mouse.y = this._pendingMousePosition.y;
982
-
983
- const brush = this._mouseObjects[this._currentBrush];
984
- brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
985
- brush.active = true;
986
- brush.mesh.visible = true;
987
- brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
988
- brush.mesh.rotation.z = Math.random() * Math.PI * 2;
989
- if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
990
- brush.mesh.material.opacity = 1.0;
991
- }
992
- this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
904
+ uniformsList.forEach(name => {
905
+ locations.uniforms[name] = gl.getUniformLocation(program, name);
906
+ });
993
907
 
994
- this._pendingMousePosition = null;
995
- });
908
+ // Add colors uniforms manually
909
+ for (let i = 0; i < COLORS_COUNT; i++) {
910
+ locations.uniforms[`u_colors[${i}].is_active`] = gl.getUniformLocation(program, `u_colors[${i}].is_active`);
911
+ locations.uniforms[`u_colors[${i}].color`] = gl.getUniformLocation(program, `u_colors[${i}].color`);
912
+ locations.uniforms[`u_colors[${i}].influence`] = gl.getUniformLocation(program, `u_colors[${i}].influence`);
996
913
  }
914
+
915
+ this._initialized = true;
916
+ // New program needs all uniforms re-uploaded on first frame
917
+ this._uniformsDirty = true;
918
+ this._colorsChanged = true;
919
+ this._textureDirty = true;
920
+
921
+ // Enable alpha blending
922
+ gl.enable(gl.BLEND);
923
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
924
+ gl.enable(gl.DEPTH_TEST);
925
+
926
+ return {
927
+ gl,
928
+ program,
929
+ buffers: {
930
+ position: positionBuffer,
931
+ normal: normalBuffer,
932
+ uv: uvBuffer,
933
+ index: indexBuffer,
934
+ wireframeIndex: wireframeIndexBuffer
935
+ },
936
+ locations,
937
+ camera,
938
+ indexCount: index.length,
939
+ wireframeIndexCount: wireframeIndex.length,
940
+ indexType: (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT
941
+ };
997
942
  }
998
943
 
999
- _createProceduralTexture(): THREE.Texture {
944
+
945
+
946
+ _createProceduralTexture(gl: WebGLRenderingContext | WebGL2RenderingContext): WebGLTexture | null {
1000
947
  // Texture size - 1024 provides good balance between quality and performance
1001
948
  // Reduced from 2048 for better performance
1002
949
  const texSize = 1024;
@@ -1004,7 +951,7 @@ export class NeatGradient implements NeatController {
1004
951
  sourceCanvas.width = texSize;
1005
952
  sourceCanvas.height = texSize;
1006
953
  const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
1007
- if (!sCtx) return new THREE.Texture();
954
+ if (!sCtx) return null;
1008
955
 
1009
956
  let seed = this._textureSeed;
1010
957
  const baseSeed = this._textureSeed;
@@ -1020,7 +967,7 @@ export class NeatGradient implements NeatController {
1020
967
  };
1021
968
 
1022
969
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
1023
- if (colors.length === 0) return new THREE.Texture();
970
+ if (colors.length === 0) return null;
1024
971
 
1025
972
  // Helper functions
1026
973
  function hexToRgb(hex: string) {
@@ -1033,7 +980,7 @@ export class NeatGradient implements NeatController {
1033
980
  }
1034
981
 
1035
982
  function rgbToHex(r: number, g: number, b: number) {
1036
- return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);
983
+ return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1).padStart(6, '0');
1037
984
  }
1038
985
 
1039
986
  const getInterColor = () => {
@@ -1124,7 +1071,7 @@ export class NeatGradient implements NeatController {
1124
1071
  canvas.width = texSize;
1125
1072
  canvas.height = texSize;
1126
1073
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
1127
- if (!ctx) return new THREE.Texture();
1074
+ if (!ctx) return null;
1128
1075
 
1129
1076
  // Start filled with the chosen void color so gaps show that color
1130
1077
  ctx.fillStyle = baseColor;
@@ -1168,539 +1115,29 @@ export class NeatGradient implements NeatController {
1168
1115
  // void segments: leave as baseColor
1169
1116
  }
1170
1117
 
1171
- const tex = new THREE.CanvasTexture(canvas);
1172
- // Use mipmapping for better quality when texture is scaled
1173
- tex.minFilter = THREE.LinearMipmapLinearFilter;
1174
- tex.magFilter = THREE.LinearFilter;
1175
- tex.wrapS = THREE.RepeatWrapping;
1176
- tex.wrapT = THREE.RepeatWrapping;
1177
-
1178
- // Enable anisotropic filtering for much better quality when texture is stretched
1179
- // 16 is a commonly supported value that dramatically improves quality
1180
- tex.anisotropy = 16;
1181
-
1182
- // Ensure mipmaps are generated
1183
- tex.needsUpdate = true;
1184
-
1185
- return tex;
1186
- }
1187
-
1188
-
1189
- }
1190
-
1191
- function updateCamera(camera: THREE.Camera, width: number, height: number) {
1192
-
1193
- const viewPortAreaRatio = 1000000;
1194
- const areaViewPort = width * height;
1195
- const targetPlaneArea =
1196
- areaViewPort / viewPortAreaRatio *
1197
- PLANE_WIDTH * PLANE_HEIGHT / 1.5;
1198
-
1199
- const ratio = width / height;
1200
-
1201
- const targetWidth = Math.sqrt(targetPlaneArea * ratio);
1202
- const targetHeight = targetPlaneArea / targetWidth;
1203
-
1204
- let left = -PLANE_WIDTH / 2;
1205
- let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
1206
-
1207
- let top = PLANE_HEIGHT / 4;
1208
- let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
1209
-
1210
- // Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
1211
- if (ratio < 1) {
1212
- // Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
1213
- const horizontalScale = ratio;
1214
- left = left * horizontalScale;
1215
- right = right * horizontalScale;
1216
-
1217
- // Zoom out slightly on mobile (1.1 = 10% zoom out)
1218
- const mobileZoomFactor = 1.05;
1219
- left = left * mobileZoomFactor;
1220
- right = right * mobileZoomFactor;
1221
- top = top * mobileZoomFactor;
1222
- bottom = bottom * mobileZoomFactor;
1223
- }
1224
-
1225
- const near = -100;
1226
- const far = 1000;
1227
- if (camera instanceof THREE.OrthographicCamera) {
1228
- camera.left = left;
1229
- camera.right = right;
1230
- camera.top = top;
1231
- camera.bottom = bottom;
1232
- camera.near = near;
1233
- camera.far = far;
1234
- camera.updateProjectionMatrix();
1235
- } else if (camera instanceof THREE.PerspectiveCamera) {
1236
- camera.aspect = width / height;
1237
- camera.updateProjectionMatrix();
1238
- }
1239
-
1240
- }
1241
-
1242
-
1243
- // Cache shader strings to avoid repeated concatenation
1244
- let cachedVertexShader: string | null = null;
1245
- let cachedFragmentShader: string | null = null;
1246
-
1247
- function buildVertexShader() {
1248
- if (cachedVertexShader) return cachedVertexShader;
1249
- cachedVertexShader = `
1250
- void main() {
1251
- vUv = uv;
1252
-
1253
- // SCROLLING LOGIC
1254
- // Separate multipliers for wave, color, and flow offsets
1255
- float waveOffset = -u_y_offset * u_y_offset_wave_multiplier;
1256
- float colorOffset = -u_y_offset * u_y_offset_color_multiplier;
1257
- float flowOffset = -u_y_offset * u_y_offset_flow_multiplier;
1258
-
1259
- // 1. DISPLACEMENT (WAVES)
1260
- // We add waveOffset to Y to scroll the wave pattern
1261
- v_displacement_amount = cnoise( vec3(
1262
- u_wave_frequency_x * position.x + u_time,
1263
- u_wave_frequency_y * (position.y + waveOffset) + u_time,
1264
- u_time
1265
- ));
1266
-
1267
- // 2. FLOW FIELD
1268
- // Apply flow offset to scroll the flow field mask
1269
- vec2 baseUv = vUv;
1270
- baseUv.y += flowOffset / u_plane_height; // Scale to match wave speed
1271
- vec2 flowUv = baseUv;
1272
-
1273
- if (u_flow_enabled > 0.5) {
1274
- if (u_flow_ease > 0.0 || u_flow_distortion_a > 0.0) {
1275
- vec2 ppp = -1.0 + 2.0 * baseUv;
1276
- ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1277
- ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1278
- ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1279
- ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1280
-
1281
- float r = length(ppp);
1282
- flowUv = mix(baseUv, vec2(baseUv.x * (1.0 - u_flow_ease) + r * u_flow_ease, baseUv.y), u_flow_ease);
1283
- }
1284
- }
1285
-
1286
- // Pass the standard flow UV to fragment shader (for mouse/texture)
1287
- vFlowUv = flowUv;
1288
-
1289
- // 3. COLOR MIXING
1290
- // We take the computed flow UVs and apply the color offset
1291
- // Scale by plane height to match wave offset speed (world space vs UV space)
1292
- vec3 color = u_colors[0].color;
1293
- vec2 adjustedUv = flowUv;
1294
- adjustedUv.y += colorOffset / u_plane_height; // Scroll the color mixing pattern
1295
-
1296
- vec2 noise_cord = adjustedUv * u_color_pressure;
1297
- const float minNoise = .0;
1298
- const float maxNoise = .9;
1299
-
1300
- for (int i = 1; i < u_colors_count; i++) {
1301
- if(u_colors[i].is_active > 0.5){
1302
- float noiseFlow = (1. + float(i)) / 30.;
1303
- float noiseSpeed = (1. + float(i)) * 0.11;
1304
- float noiseSeed = 13. + float(i) * 7.;
1305
-
1306
- float noise = snoise(
1307
- vec3(
1308
- noise_cord.x * u_color_pressure.x + u_time * noiseFlow * 2.,
1309
- noise_cord.y * u_color_pressure.y,
1310
- u_time * noiseSpeed
1311
- ) + noiseSeed
1312
- ) - (.1 * float(i)) + (.5 * u_color_blending);
1313
-
1314
- noise = clamp(minNoise, maxNoise + float(i) * 0.02, noise);
1315
- color = mix(color, u_colors[i].color, smoothstep(0.0, u_color_blending, noise));
1118
+ const tex = gl.createTexture()!;
1119
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1120
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
1121
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
1122
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
1123
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
1124
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1125
+ gl.generateMipmap(gl.TEXTURE_2D);
1126
+
1127
+ const ext = gl.getExtension('EXT_texture_filter_anisotropic') ||
1128
+ gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
1129
+ gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
1130
+ if (ext) {
1131
+ const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
1132
+ gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, max));
1316
1133
  }
1317
- }
1318
-
1319
- v_color = color;
1320
-
1321
- // 4. VERTEX POSITION
1322
- vec3 newPosition = position + normal * v_displacement_amount * u_wave_amplitude;
1323
- gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
1324
- v_new_position = gl_Position;
1325
- }
1326
- `;
1327
- return cachedVertexShader;
1328
- }
1329
-
1330
- function buildFragmentShader() {
1331
- if (cachedFragmentShader) return cachedFragmentShader;
1332
- cachedFragmentShader = `
1333
- float random(vec2 p) {
1334
- return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
1335
- }
1336
-
1337
- float fbm(vec3 x) {
1338
- float value = 0.0;
1339
- float amplitude = 0.5;
1340
- float frequency = 1.0;
1341
- for (int i = 0; i < 4; i++) {
1342
- value += amplitude * snoise(x * frequency);
1343
- frequency *= 2.0;
1344
- amplitude *= 0.5;
1345
- }
1346
- return value;
1347
- }
1348
1134
 
1349
- void main() {
1350
- // MOUSE DISTORTION
1351
- vec2 finalUv = vFlowUv;
1352
-
1353
- if (u_mouse_distortion_strength > 0.0) {
1354
- vec4 mouseColor = texture2D(u_mouse_texture, vUv);
1355
- float mouseValue = mouseColor.r;
1356
-
1357
- if (mouseValue > 0.001) {
1358
- float distortionAmount = mouseValue * u_mouse_distortion_strength;
1359
- vec2 mouseDisp = vec2(distortionAmount, distortionAmount);
1360
- finalUv -= mouseDisp;
1361
- }
1362
- }
1363
-
1364
- vec3 baseColor;
1365
-
1366
- if (u_enable_procedural_texture > 0.5) {
1367
- // Calculate flow field distance for ease effect
1368
- vec2 ppp = -1.0 + 2.0 * finalUv;
1369
- ppp += 0.1 * cos((1.5 * u_flow_scale) * ppp.yx + 1.1 * u_time + vec2(0.1, 1.1));
1370
- ppp += 0.1 * cos((2.3 * u_flow_scale) * ppp.yx + 1.3 * u_time + vec2(3.2, 3.4));
1371
- ppp += 0.1 * cos((2.2 * u_flow_scale) * ppp.yx + 1.7 * u_time + vec2(1.8, 5.2));
1372
- ppp += u_flow_distortion_a * cos((u_flow_distortion_b * u_flow_scale) * ppp.yx + 1.4 * u_time + vec2(6.3, 3.9));
1373
- float r = length(ppp); // Flow distance
1374
-
1375
- // Ease blending: 0 = topographic (flow), 1 = image (UV)
1376
- float vx = (finalUv.x * u_texture_ease) + (r * (1.0 - u_texture_ease));
1377
- float vy = (finalUv.y * u_texture_ease) + (0.0 * (1.0 - u_texture_ease));
1378
- vec2 texUv = vec2(vx, vy);
1379
-
1380
- // PARALLAX SCROLLING
1381
- // We manually apply a smaller offset here to make the texture lag behind
1382
- float parallaxFactor = 0.25; // 25% speed of the color mixing
1383
- texUv.y -= (u_y_offset * u_y_offset_color_multiplier / u_plane_height) * parallaxFactor;
1384
-
1385
- texUv *= 1.5; // Tiling scale
1386
-
1387
- vec4 texSample = texture2D(u_procedural_texture, texUv);
1388
- baseColor = texSample.rgb;
1389
- } else {
1390
- baseColor = v_color;
1135
+ return tex;
1391
1136
  }
1392
1137
 
1393
- vec3 color = baseColor;
1394
-
1395
- // Post-processing
1396
- color += pow(v_displacement_amount, 1.0) * u_highlights;
1397
- color -= pow(1.0 - v_displacement_amount, 2.0) * u_shadows;
1398
- color = saturation(color, 1.0 + u_saturation);
1399
- color = color * u_brightness;
1400
-
1401
- // Grain
1402
- vec2 noiseCoords = gl_FragCoord.xy / u_grain_scale;
1403
- float grain = (u_grain_speed != 0.0) ? fbm(vec3(noiseCoords, u_time * u_grain_speed)) : fbm(vec3(noiseCoords, 0.0));
1404
-
1405
- grain = grain * 0.5 + 0.5;
1406
- grain -= 0.5;
1407
- grain = (grain > u_grain_sparsity) ? grain : 0.0;
1408
- grain *= u_grain_intensity;
1409
-
1410
- color += vec3(grain);
1411
-
1412
- gl_FragColor = vec4(color, 1.0);
1413
- }
1414
- `;
1415
- return cachedFragmentShader;
1416
- }
1417
-
1418
- // Cache uniforms string as well
1419
- let cachedUniformsShader: string | null = null;
1420
-
1421
- const buildUniforms = () => {
1422
- if (cachedUniformsShader) return cachedUniformsShader;
1423
- cachedUniformsShader = `
1424
- precision highp float;
1425
-
1426
- struct Color {
1427
- float is_active;
1428
- vec3 color;
1429
- float value;
1430
- };
1431
-
1432
- uniform float u_grain_intensity;
1433
- uniform float u_grain_sparsity;
1434
- uniform float u_grain_scale;
1435
- uniform float u_grain_speed;
1436
- uniform float u_time;
1437
-
1438
- uniform float u_wave_amplitude;
1439
- uniform float u_wave_frequency_x;
1440
- uniform float u_wave_frequency_y;
1441
-
1442
- uniform vec2 u_color_pressure;
1443
-
1444
- uniform float u_plane_width;
1445
- uniform float u_plane_height;
1446
-
1447
- uniform float u_shadows;
1448
- uniform float u_highlights;
1449
- uniform float u_saturation;
1450
- uniform float u_brightness;
1451
-
1452
- uniform float u_color_blending;
1453
-
1454
- uniform int u_colors_count;
1455
- uniform Color u_colors[6];
1456
- uniform vec2 u_resolution;
1457
-
1458
- uniform float u_y_offset;
1459
- uniform float u_y_offset_wave_multiplier;
1460
- uniform float u_y_offset_color_multiplier;
1461
- uniform float u_y_offset_flow_multiplier;
1462
-
1463
- // Flow field uniforms
1464
- uniform float u_flow_distortion_a;
1465
- uniform float u_flow_distortion_b;
1466
- uniform float u_flow_scale;
1467
- uniform float u_flow_ease;
1468
- uniform float u_flow_enabled;
1469
-
1470
- // Mouse interaction uniforms
1471
- uniform float u_mouse_distortion_strength;
1472
- uniform float u_mouse_distortion_radius;
1473
- uniform float u_mouse_darken;
1474
- uniform sampler2D u_mouse_texture;
1475
-
1476
- // Procedural texture uniforms
1477
- uniform sampler2D u_procedural_texture;
1478
- uniform float u_enable_procedural_texture;
1479
- uniform float u_texture_ease;
1480
-
1481
- varying vec2 vUv;
1482
- varying vec2 vFlowUv;
1483
- varying vec4 v_new_position;
1484
- varying vec3 v_color;
1485
- varying float v_displacement_amount;
1486
-
1487
- `;
1488
- return cachedUniformsShader;
1489
- };
1490
-
1491
- // Cache noise functions as well
1492
- let cachedNoiseShader: string | null = null;
1493
-
1494
- const buildNoise = () => {
1495
- if (cachedNoiseShader) return cachedNoiseShader;
1496
- cachedNoiseShader = `
1497
-
1498
- // 1. REPLACEMENT PERMUTE:
1499
- // Uses a hash function (fract/sin) instead of a modular lookup table.
1500
- vec4 permute(vec4 x) {
1501
- return floor(fract(sin(x) * 43758.5453123) * 289.0);
1502
- }
1503
-
1504
- // Taylor Inverse Sqrt
1505
- vec4 taylorInvSqrt(vec4 r) {
1506
- return 1.79284291400159 - 0.85373472095314 * r;
1507
- }
1508
-
1509
- // Fade function
1510
- vec3 fade(vec3 t) {
1511
- return t*t*t*(t*(t*6.0-15.0)+10.0);
1512
- }
1513
-
1514
- // 3D Simplex Noise
1515
- float snoise(vec3 v) {
1516
- const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
1517
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
1518
-
1519
- // First corner
1520
- vec3 i = floor(v + dot(v, C.yyy) );
1521
- vec3 x0 = v - i + dot(i, C.xxx) ;
1522
-
1523
- // Other corners
1524
- vec3 g = step(x0.yzx, x0.xyz);
1525
- vec3 l = 1.0 - g;
1526
- vec3 i1 = min( g.xyz, l.zxy );
1527
- vec3 i2 = max( g.xyz, l.zxy );
1528
-
1529
- vec3 x1 = x0 - i1 + C.xxx;
1530
- vec3 x2 = x0 - i2 + C.yyy;
1531
- vec3 x3 = x0 - D.yyy;
1532
-
1533
- // Permutations
1534
- vec4 p = permute( permute( permute(
1535
- i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
1536
- + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
1537
- + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
1538
-
1539
- // Gradients
1540
- float n_ = 0.142857142857; // 1.0/7.0
1541
- vec3 ns = n_ * D.wyz - D.xzx;
1542
-
1543
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
1544
-
1545
- vec4 x_ = floor(j * ns.z);
1546
- vec4 y_ = floor(j - 7.0 * x_ );
1547
-
1548
- vec4 x = x_ *ns.x + ns.yyyy;
1549
- vec4 y = y_ *ns.x + ns.yyyy;
1550
- vec4 h = 1.0 - abs(x) - abs(y);
1551
-
1552
- vec4 b0 = vec4( x.xy, y.xy );
1553
- vec4 b1 = vec4( x.zw, y.zw );
1554
-
1555
- vec4 s0 = floor(b0)*2.0 + 1.0;
1556
- vec4 s1 = floor(b1)*2.0 + 1.0;
1557
- vec4 sh = -step(h, vec4(0.0));
1558
-
1559
- vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
1560
- vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
1561
-
1562
- vec3 p0 = vec3(a0.xy,h.x);
1563
- vec3 p1 = vec3(a0.zw,h.y);
1564
- vec3 p2 = vec3(a1.xy,h.z);
1565
- vec3 p3 = vec3(a1.zw,h.w);
1566
-
1567
- // Normalise gradients
1568
- vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
1569
- p0 *= norm.x;
1570
- p1 *= norm.y;
1571
- p2 *= norm.z;
1572
- p3 *= norm.w;
1573
-
1574
- // Mix final noise value
1575
- vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
1576
- m = m * m;
1577
- return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
1578
- dot(p2,x2), dot(p3,x3) ) );
1579
- }
1580
-
1581
- // Classic Perlin noise
1582
- float cnoise(vec3 P)
1583
- {
1584
- vec3 Pi0 = floor(P);
1585
- vec3 Pi1 = Pi0 + vec3(1.0);
1586
-
1587
- vec3 Pf0 = fract(P);
1588
- vec3 Pf1 = Pf0 - vec3(1.0);
1589
- vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
1590
- vec4 iy = vec4(Pi0.yy, Pi1.yy);
1591
- vec4 iz0 = Pi0.zzzz;
1592
- vec4 iz1 = Pi1.zzzz;
1593
-
1594
- vec4 ixy = permute(permute(ix) + iy);
1595
- vec4 ixy0 = permute(ixy + iz0);
1596
- vec4 ixy1 = permute(ixy + iz1);
1597
-
1598
- vec4 gx0 = ixy0 * (1.0 / 7.0);
1599
- vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
1600
- gx0 = fract(gx0);
1601
- vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
1602
- vec4 sz0 = step(gz0, vec4(0.0));
1603
- gx0 -= sz0 * (step(0.0, gx0) - 0.5);
1604
- gy0 -= sz0 * (step(0.0, gy0) - 0.5);
1605
-
1606
- vec4 gx1 = ixy1 * (1.0 / 7.0);
1607
- vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
1608
- gx1 = fract(gx1);
1609
- vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
1610
- vec4 sz1 = step(gz1, vec4(0.0));
1611
- gx1 -= sz1 * (step(0.0, gx1) - 0.5);
1612
- gy1 -= sz1 * (step(0.0, gy1) - 0.5);
1613
-
1614
- vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
1615
- vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
1616
- vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
1617
- vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
1618
- vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
1619
- vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
1620
- vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
1621
- vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
1622
-
1623
- vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
1624
- g000 *= norm0.x;
1625
- g010 *= norm0.y;
1626
- g100 *= norm0.z;
1627
- g110 *= norm0.w;
1628
- vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
1629
- g001 *= norm1.x;
1630
- g011 *= norm1.y;
1631
- g101 *= norm1.z;
1632
- g111 *= norm1.w;
1633
-
1634
- float n000 = dot(g000, Pf0);
1635
- float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
1636
- float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
1637
- float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
1638
- float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
1639
- float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
1640
- float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
1641
- float n111 = dot(g111, Pf1);
1642
-
1643
- vec3 fade_xyz = fade(Pf0);
1644
- vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
1645
- vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
1646
- float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
1647
- return 2.2 * n_xyz;
1648
- }
1649
- `;
1650
- return cachedNoiseShader;
1651
- };
1652
-
1653
- // Cache color functions as well
1654
- let cachedColorFunctionsShader: string | null = null;
1655
1138
 
1656
- const buildColorFunctions = () => {
1657
- if (cachedColorFunctionsShader) return cachedColorFunctionsShader;
1658
- cachedColorFunctionsShader = `
1659
-
1660
- vec3 saturation(vec3 rgb, float adjustment) {
1661
- const vec3 W = vec3(0.2125, 0.7154, 0.0721);
1662
- vec3 intensity = vec3(dot(rgb, W));
1663
- return mix(intensity, rgb, adjustment);
1664
- }
1665
-
1666
- float saturation(vec3 rgb)
1667
- {
1668
- vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
1669
- vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
1670
- vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
1671
-
1672
- float d = q.x - min(q.w, q.y);
1673
- float e = 1.0e-10;
1674
- return abs(6.0 * d + e);
1675
- }
1676
-
1677
- // get saturation of a color in values between 0 and 1
1678
- float getSaturation(vec3 color) {
1679
- float max = max(color.r, max(color.g, color.b));
1680
- float min = min(color.r, min(color.g, color.b));
1681
- return (max - min) / max;
1682
- }
1683
-
1684
- vec3 rgb2hsv(vec3 c)
1685
- {
1686
- vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
1687
- vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
1688
- vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
1689
-
1690
- float d = q.x - min(q.w, q.y);
1691
- float e = 1.0e-10;
1692
- return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
1693
1139
  }
1694
1140
 
1695
- vec3 hsv2rgb(vec3 c)
1696
- {
1697
- vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
1698
- vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
1699
- return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
1700
- }
1701
- `;
1702
- return cachedColorFunctionsShader;
1703
- };
1704
1141
 
1705
1142
  const setLinkStyles = (link: HTMLAnchorElement) => {
1706
1143
  link.id = LINK_ID;
@@ -1718,22 +1155,28 @@ const setLinkStyles = (link: HTMLAnchorElement) => {
1718
1155
  link.style.fontWeight = "bold";
1719
1156
  link.style.textDecoration = "none";
1720
1157
  link.style.zIndex = "10000";
1158
+ link.style.pointerEvents = "auto";
1159
+ link.setAttribute("data-n", "1");
1721
1160
  link.innerHTML = "NEAT";
1722
1161
  }
1723
1162
 
1724
1163
  const addNeatLink = (ref: HTMLCanvasElement): HTMLAnchorElement => {
1725
- const existingLinks = ref.parentElement?.getElementsByTagName("a");
1726
- if (existingLinks) {
1727
- for (let i = 0; i < existingLinks.length; i++) {
1728
- if (existingLinks[i].id === LINK_ID) {
1729
- setLinkStyles(existingLinks[i]);
1730
- return existingLinks[i];
1731
- }
1164
+ const parent = ref.parentElement;
1165
+ // Ensure parent has position so absolute link is positioned relative to it
1166
+ if (parent && getComputedStyle(parent).position === "static") {
1167
+ parent.style.position = "relative";
1168
+ }
1169
+ // Search parent for existing neat link (survives HMR where LINK_ID changes)
1170
+ if (parent) {
1171
+ const existing = parent.querySelector('a[data-n]') as HTMLAnchorElement;
1172
+ if (existing) {
1173
+ setLinkStyles(existing);
1174
+ return existing;
1732
1175
  }
1733
1176
  }
1734
1177
  const link = document.createElement("a");
1735
1178
  setLinkStyles(link);
1736
- ref.parentElement?.appendChild(link);
1179
+ parent?.appendChild(link);
1737
1180
  return link;
1738
1181
  }
1739
1182