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