@firecms/neat 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
2
- import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
2
+ import { generatePlaneGeometry, generateSphereGeometry, generateTorusGeometry, generateCylinderGeometry, generateRibbonGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
3
3
 
4
4
  console.info(
5
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",
@@ -61,6 +61,7 @@ export class NeatGradient implements NeatController {
61
61
  private _grainSpeed: number = -1;
62
62
 
63
63
  private _colorBlending: number = -1;
64
+ private _resolution: number = 1;
64
65
 
65
66
  private _colors: NeatColor[] = [];
66
67
  private _wireframe: boolean = false;
@@ -87,6 +88,7 @@ export class NeatGradient implements NeatController {
87
88
  private _textureColorBlending: number = 0.01;
88
89
  private _textureSeed: number = 333;
89
90
  private _textureEase: number = 0.5;
91
+ private _transparentTextureVoid: boolean = false;
90
92
 
91
93
  // New effects
92
94
  private _domainWarpEnabled: boolean = false;
@@ -109,6 +111,35 @@ export class NeatGradient implements NeatController {
109
111
  private _bloomIntensity: number = 0;
110
112
  private _bloomThreshold: number = 0.7;
111
113
  private _chromaticAberration: number = 0;
114
+ private _silhouetteFade: number = 0.25;
115
+ private _cylinderFade: number = 0.08;
116
+ private _ribbonFade: number = 0.05;
117
+ private _flatShading: boolean = true;
118
+
119
+ // 3D Shapes config
120
+ private _shapeType: 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon' = 'plane';
121
+ private _shapeRotationX: number = 0;
122
+ private _shapeRotationY: number = 0;
123
+ private _shapeRotationZ: number = 0;
124
+ private _shapeAutoRotateSpeedX: number = 0;
125
+ private _shapeAutoRotateSpeedY: number = 0;
126
+ private _sphereRadius: number = 15;
127
+ private _torusRadius: number = 15;
128
+ private _torusTube: number = 5;
129
+ private _cylinderRadius: number = 10;
130
+ private _cylinderHeight: number = 40;
131
+ private _planeBend: number = 0;
132
+ private _planeTwist: number = 0;
133
+
134
+ // Camera settings
135
+ private _cameraLock: boolean = false;
136
+ private _cameraX: number = 0;
137
+ private _cameraY: number = 0;
138
+ private _cameraZ: number = 0;
139
+ private _cameraRotationX: number = 0;
140
+ private _cameraRotationY: number = 0;
141
+ private _cameraRotationZ: number = 0;
142
+ private _cameraZoom: number = 1.0;
112
143
 
113
144
  private _proceduralTexture: WebGLTexture | null = null;
114
145
  private _proceduralBackgroundColor: string = "#000000";
@@ -130,6 +161,12 @@ export class NeatGradient implements NeatController {
130
161
  private _yOffsetColorMultiplier: number = 0.004;
131
162
  private _yOffsetFlowMultiplier: number = 0.004;
132
163
 
164
+ // Cached offscreen canvases for procedural texture generation
165
+ private _sourceCanvas: HTMLCanvasElement | null = null;
166
+ private _sourceCtx: CanvasRenderingContext2D | null = null;
167
+ private _maskedCanvas: HTMLCanvasElement | null = null;
168
+ private _maskedCtx: CanvasRenderingContext2D | null = null;
169
+
133
170
  // Performance optimizations
134
171
  private _resizeTimeoutId: number | null = null;
135
172
  private _textureNeedsUpdate: boolean = false;
@@ -137,6 +174,11 @@ export class NeatGradient implements NeatController {
137
174
  private _colorsChanged: boolean = true;
138
175
  private _uniformsDirty: boolean = true;
139
176
  private _textureDirty: boolean = true;
177
+ private _yOffsetDirty: boolean = false;
178
+ private _modelViewMatrix: Matrix4 = new Matrix4();
179
+ private _isVisible: boolean = true;
180
+ private _visibilityObserver: IntersectionObserver | null = null;
181
+ private _visibilityHandler: (() => void) | null = null;
140
182
 
141
183
  constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
142
184
 
@@ -184,6 +226,7 @@ export class NeatGradient implements NeatController {
184
226
  textureSeed = 333,
185
227
  textureEase = 0.5,
186
228
  proceduralBackgroundColor = "#000000",
229
+ transparentTextureVoid = false,
187
230
  textureShapeTriangles = 20,
188
231
  textureShapeCircles = 15,
189
232
  textureShapeBars = 15,
@@ -192,7 +235,7 @@ export class NeatGradient implements NeatController {
192
235
  domainWarpEnabled = false,
193
236
  domainWarpIntensity = 0.5,
194
237
  domainWarpScale = 1.0,
195
- vignetteIntensity = 0.5,
238
+ vignetteIntensity = 0.0,
196
239
  vignetteRadius = 0.8,
197
240
  fresnelEnabled = false,
198
241
  fresnelPower = 2.0,
@@ -204,6 +247,35 @@ export class NeatGradient implements NeatController {
204
247
  bloomIntensity = 0.0,
205
248
  bloomThreshold = 0.7,
206
249
  chromaticAberration = 0.0,
250
+ silhouetteFade = 0.25,
251
+ cylinderFade = 0.08,
252
+ ribbonFade = 0.05,
253
+ flatShading = true,
254
+
255
+ // Camera configuration
256
+ cameraLock = false,
257
+ cameraX = 0,
258
+ cameraY = 0,
259
+ cameraZ = 0,
260
+ cameraRotationX = 0,
261
+ cameraRotationY = 0,
262
+ cameraRotationZ = 0,
263
+ cameraZoom = 1.0,
264
+
265
+ // 3D shapes default
266
+ shapeType = 'plane',
267
+ shapeRotationX = 0,
268
+ shapeRotationY = 0,
269
+ shapeRotationZ = 0,
270
+ shapeAutoRotateSpeedX = 0,
271
+ shapeAutoRotateSpeedY = 0,
272
+ sphereRadius = 15,
273
+ torusRadius = 15,
274
+ torusTube = 5,
275
+ cylinderRadius = 10,
276
+ cylinderHeight = 40,
277
+ planeBend = 0,
278
+ planeTwist = 0,
207
279
  } = config;
208
280
 
209
281
 
@@ -219,6 +291,7 @@ export class NeatGradient implements NeatController {
219
291
  this.waveFrequencyY = waveFrequencyY;
220
292
  this.waveAmplitude = waveAmplitude;
221
293
  this.colorBlending = colorBlending;
294
+ this._resolution = resolution;
222
295
  this.grainScale = grainScale;
223
296
  this.grainIntensity = grainIntensity;
224
297
  this.grainSparsity = grainSparsity;
@@ -255,6 +328,7 @@ export class NeatGradient implements NeatController {
255
328
  this.textureSeed = textureSeed;
256
329
  this.textureEase = textureEase;
257
330
  this._proceduralBackgroundColor = proceduralBackgroundColor;
331
+ this.transparentTextureVoid = transparentTextureVoid;
258
332
 
259
333
  this._textureShapeTriangles = textureShapeTriangles;
260
334
  this._textureShapeCircles = textureShapeCircles;
@@ -276,6 +350,33 @@ export class NeatGradient implements NeatController {
276
350
  this.bloomIntensity = bloomIntensity;
277
351
  this.bloomThreshold = bloomThreshold;
278
352
  this.chromaticAberration = chromaticAberration;
353
+ this.silhouetteFade = silhouetteFade;
354
+ this.cylinderFade = cylinderFade;
355
+ this.ribbonFade = ribbonFade;
356
+ this._flatShading = flatShading;
357
+
358
+ this._cameraLock = cameraLock;
359
+ this._cameraX = cameraX;
360
+ this._cameraY = cameraY;
361
+ this._cameraZ = cameraZ;
362
+ this._cameraRotationX = cameraRotationX;
363
+ this._cameraRotationY = cameraRotationY;
364
+ this._cameraRotationZ = cameraRotationZ;
365
+ this._cameraZoom = cameraZoom;
366
+
367
+ this._shapeType = shapeType;
368
+ this._shapeRotationX = shapeRotationX;
369
+ this._shapeRotationY = shapeRotationY;
370
+ this._shapeRotationZ = shapeRotationZ;
371
+ this._shapeAutoRotateSpeedX = shapeAutoRotateSpeedX;
372
+ this._shapeAutoRotateSpeedY = shapeAutoRotateSpeedY;
373
+ this._sphereRadius = sphereRadius;
374
+ this._torusRadius = torusRadius;
375
+ this._torusTube = torusTube;
376
+ this._cylinderRadius = cylinderRadius;
377
+ this._cylinderHeight = cylinderHeight;
378
+ this._planeBend = planeBend;
379
+ this._planeTwist = planeTwist;
279
380
 
280
381
  this.glState = this._initScene(resolution);
281
382
 
@@ -306,6 +407,52 @@ export class NeatGradient implements NeatController {
306
407
 
307
408
  gl.uniform1f(locations.uniforms['u_time'], tick);
308
409
 
410
+ // Update modelViewMatrix in every frame to support dynamic rotation and auto-rotation
411
+ const camera = this.glState.camera;
412
+ const modelViewMatrix = this._modelViewMatrix;
413
+ modelViewMatrix.identity();
414
+
415
+ // 1. Camera translation (default camera distance + displacement)
416
+ modelViewMatrix.translate(
417
+ -camera.position[0] - this._cameraX,
418
+ -camera.position[1] - this._cameraY,
419
+ -camera.position[2] - this._cameraZ
420
+ );
421
+ modelViewMatrix.translate(0, 0, -1);
422
+
423
+ // 2. Camera rotation (revolving around target)
424
+ modelViewMatrix.rotateX(-this._cameraRotationX);
425
+ modelViewMatrix.rotateY(-this._cameraRotationY);
426
+ modelViewMatrix.rotateZ(-this._cameraRotationZ);
427
+
428
+ let rx = this._shapeRotationX;
429
+ let ry = this._shapeRotationY;
430
+ let rz = this._shapeRotationZ;
431
+
432
+ if (this._shapeAutoRotateSpeedX !== 0) {
433
+ rx += tick * this._shapeAutoRotateSpeedX * 0.1;
434
+ }
435
+ if (this._shapeAutoRotateSpeedY !== 0) {
436
+ ry += tick * this._shapeAutoRotateSpeedY * 0.1;
437
+ }
438
+
439
+ if (this._shapeType === 'plane' || this._shapeType === 'ribbon') {
440
+ modelViewMatrix.rotateX(rx - Math.PI / 3.5);
441
+ } else {
442
+ modelViewMatrix.rotateX(rx);
443
+ }
444
+ modelViewMatrix.rotateY(ry);
445
+ modelViewMatrix.rotateZ(rz);
446
+
447
+ const mvLoc = locations.uniforms["modelViewMatrix"];
448
+ if (mvLoc) gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
449
+
450
+ // Fast path: only upload yOffset when it changed (scroll)
451
+ if (this._yOffsetDirty && !this._uniformsDirty) {
452
+ gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
453
+ this._yOffsetDirty = false;
454
+ }
455
+
309
456
  // Only upload static uniforms when they've been modified
310
457
  if (this._uniformsDirty) {
311
458
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -333,8 +480,16 @@ export class NeatGradient implements NeatController {
333
480
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
334
481
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
335
482
 
483
+ let shapeTypeVal = 0.0;
484
+ if (this._shapeType === 'sphere') shapeTypeVal = 1.0;
485
+ else if (this._shapeType === 'torus') shapeTypeVal = 2.0;
486
+ else if (this._shapeType === 'cylinder') shapeTypeVal = 3.0;
487
+ else if (this._shapeType === 'ribbon') shapeTypeVal = 4.0;
488
+ gl.uniform1f(locations.uniforms['u_shape_type'], shapeTypeVal);
489
+
336
490
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
337
491
  gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
492
+ gl.uniform1f(locations.uniforms['u_transparent_texture_void'], this._transparentTextureVoid ? 1.0 : 0.0);
338
493
 
339
494
  gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
340
495
  gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
@@ -355,8 +510,13 @@ export class NeatGradient implements NeatController {
355
510
  gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
356
511
  gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
357
512
  gl.uniform1f(locations.uniforms['u_chromatic_aberration'], this._chromaticAberration);
513
+ gl.uniform1f(locations.uniforms['u_silhouette_fade'], this._silhouetteFade);
514
+ gl.uniform1f(locations.uniforms['u_cylinder_fade'], this._cylinderFade);
515
+ gl.uniform1f(locations.uniforms['u_ribbon_fade'], this._ribbonFade);
516
+ gl.uniform1f(locations.uniforms['u_flat_shading'], this._flatShading ? 1.0 : 0.0);
358
517
 
359
518
  this._uniformsDirty = false;
519
+ this._yOffsetDirty = false;
360
520
  }
361
521
 
362
522
  // Only regenerate procedural texture when needed
@@ -415,8 +575,35 @@ export class NeatGradient implements NeatController {
415
575
  gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
416
576
  }
417
577
 
418
- this.requestRef = requestAnimationFrame(render);
578
+ if (this._isVisible) {
579
+ this.requestRef = requestAnimationFrame(render);
580
+ }
581
+ };
582
+
583
+ // Visibility optimization: pause rendering when off-screen or tab hidden
584
+ this._visibilityObserver = new IntersectionObserver((entries) => {
585
+ const wasVisible = this._isVisible;
586
+ this._isVisible = entries[0].isIntersecting && document.visibilityState !== 'hidden';
587
+ if (this._isVisible && !wasVisible) {
588
+ lastTime = performance.now(); // Avoid time jump after resume
589
+ this.requestRef = requestAnimationFrame(render);
590
+ }
591
+ }, { threshold: 0 });
592
+ this._visibilityObserver.observe(ref);
593
+
594
+ this._visibilityHandler = () => {
595
+ const wasVisible = this._isVisible;
596
+ if (document.visibilityState === 'hidden') {
597
+ this._isVisible = false;
598
+ } else {
599
+ this._isVisible = true;
600
+ if (!wasVisible) {
601
+ lastTime = performance.now();
602
+ this.requestRef = requestAnimationFrame(render);
603
+ }
604
+ }
419
605
  };
606
+ document.addEventListener('visibilitychange', this._visibilityHandler);
420
607
 
421
608
  const setSize = () => {
422
609
 
@@ -430,14 +617,14 @@ export class NeatGradient implements NeatController {
430
617
 
431
618
  gl.viewport(0, 0, width, height);
432
619
 
433
- updateCamera(camera, width, height);
620
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
434
621
 
435
622
 
436
623
 
437
624
  // Recompute projection matrix on resize
438
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
625
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
439
626
  gl.useProgram(this.glState.program);
440
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
627
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
441
628
  };
442
629
 
443
630
  // Debounce resize to prevent excessive operations
@@ -461,6 +648,16 @@ export class NeatGradient implements NeatController {
461
648
  cancelAnimationFrame(this.requestRef);
462
649
  this.sizeObserver.disconnect();
463
650
 
651
+ // Cleanup visibility observers
652
+ if (this._visibilityObserver) {
653
+ this._visibilityObserver.disconnect();
654
+ this._visibilityObserver = null;
655
+ }
656
+ if (this._visibilityHandler) {
657
+ document.removeEventListener('visibilitychange', this._visibilityHandler);
658
+ this._visibilityHandler = null;
659
+ }
660
+
464
661
  // Clear resize timeout
465
662
  if (this._resizeTimeoutId !== null) {
466
663
  clearTimeout(this._resizeTimeoutId);
@@ -493,36 +690,226 @@ export class NeatGradient implements NeatController {
493
690
  downloadURI(dataURL, filename);
494
691
  }
495
692
 
693
+ /**
694
+ * Records the canvas animation as a video with a NEAT watermark overlay.
695
+ * @param options.durationMs Recording duration in milliseconds (default 5000).
696
+ * @param options.filename Output file name (default "neat.firecms.co").
697
+ * @param options.width Output video width in pixels (default: current canvas width).
698
+ * @param options.height Output video height in pixels (default: current canvas height).
699
+ * @param options.format Preferred format: 'mp4' or 'webm' (default: best available).
700
+ * @param options.onProgress Callback with progress 0-1.
701
+ * @param options.onComplete Callback when recording finishes.
702
+ * @returns A stop function to end recording early.
703
+ */
704
+ recordVideo(options: {
705
+ durationMs?: number;
706
+ filename?: string;
707
+ width?: number;
708
+ height?: number;
709
+ format?: 'mp4' | 'webm';
710
+ onProgress?: (progress: number) => void;
711
+ onComplete?: () => void;
712
+ } = {}): () => void {
713
+ const {
714
+ durationMs = 5000,
715
+ filename = "neat.firecms.co",
716
+ format,
717
+ onProgress,
718
+ onComplete,
719
+ } = options;
720
+
721
+ const source = this._ref;
722
+ const width = options.width || source.width || source.clientWidth;
723
+ const height = options.height || source.height || source.clientHeight;
724
+
725
+ // Offscreen canvas that composites gradient + watermark each frame
726
+ const offscreen = document.createElement("canvas");
727
+ offscreen.width = width;
728
+ offscreen.height = height;
729
+ const ctx = offscreen.getContext("2d")!;
730
+
731
+ // Use captureStream(0) — only captures a frame when we explicitly
732
+ // call requestFrame() on the video track, so every composited frame
733
+ // is guaranteed to be captured.
734
+ const stream = offscreen.captureStream(0);
735
+ const videoTrack = stream.getVideoTracks()[0];
736
+
737
+ // Codec candidates ordered by preference
738
+ const mp4Candidates = [
739
+ "video/mp4;codecs=avc1",
740
+ "video/mp4;codecs=avc1,opus",
741
+ "video/mp4",
742
+ ];
743
+ const webmCandidates = [
744
+ "video/webm;codecs=vp9,opus",
745
+ "video/webm;codecs=vp9",
746
+ "video/webm;codecs=vp8,opus",
747
+ "video/webm",
748
+ ];
749
+
750
+ // Build candidate list based on preferred format
751
+ let candidates: string[];
752
+ if (format === 'mp4') candidates = [...mp4Candidates, ...webmCandidates];
753
+ else if (format === 'webm') candidates = [...webmCandidates, ...mp4Candidates];
754
+ else candidates = [...mp4Candidates, ...webmCandidates];
755
+
756
+ let mimeType = "video/webm";
757
+ for (const candidate of candidates) {
758
+ if (MediaRecorder.isTypeSupported(candidate)) {
759
+ mimeType = candidate;
760
+ break;
761
+ }
762
+ }
763
+
764
+ // Scale bitrate with pixel count: 8 Mbps baseline at 720p
765
+ const pixels = width * height;
766
+ const baseBitrate = 8_000_000;
767
+ const basePixels = 1280 * 720;
768
+ const videoBitsPerSecond = Math.round(baseBitrate * Math.max(1, pixels / basePixels));
769
+
770
+ const recorder = new MediaRecorder(stream, {
771
+ mimeType,
772
+ videoBitsPerSecond,
773
+ });
774
+
775
+ const chunks: Blob[] = [];
776
+ recorder.ondataavailable = (e) => {
777
+ if (e.data.size > 0) chunks.push(e.data);
778
+ };
779
+
780
+ let stopped = false;
781
+ let rafId: number;
782
+ const startTime = performance.now();
783
+ let lastProgressTime = 0;
784
+
785
+ // Composite loop: draw source canvas + watermark overlay on each frame
786
+ const drawFrame = () => {
787
+ if (stopped) return;
788
+
789
+ ctx.clearRect(0, 0, width, height);
790
+ ctx.drawImage(source, 0, 0, width, height);
791
+
792
+ // Watermark: "NEAT" in bottom-right corner
793
+ const fontSize = Math.max(14, Math.round(height * 0.025));
794
+ ctx.font = `bold ${fontSize}px "Sofia Sans", sans-serif`;
795
+ ctx.textAlign = "right";
796
+ ctx.textBaseline = "bottom";
797
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
798
+ ctx.shadowBlur = 4;
799
+ ctx.shadowOffsetX = 1;
800
+ ctx.shadowOffsetY = 1;
801
+ ctx.fillStyle = "rgba(255,255,255,0.7)";
802
+ ctx.fillText("NEAT", width - fontSize * 0.8, height - fontSize * 0.5);
803
+ ctx.shadowColor = "transparent";
804
+ ctx.shadowBlur = 0;
805
+ ctx.shadowOffsetX = 0;
806
+ ctx.shadowOffsetY = 0;
807
+
808
+ // Signal the stream to capture this frame
809
+ // @ts-ignore – requestFrame exists on CanvasCaptureMediaStreamTrack
810
+ if (videoTrack.requestFrame) videoTrack.requestFrame();
811
+
812
+ // Throttle progress to ~4 updates/sec to avoid flooding React state
813
+ if (onProgress) {
814
+ const now = performance.now();
815
+ if (now - lastProgressTime > 250) {
816
+ lastProgressTime = now;
817
+ onProgress(Math.min(0.99, (now - startTime) / durationMs));
818
+ }
819
+ }
820
+
821
+ rafId = requestAnimationFrame(drawFrame);
822
+ };
823
+
824
+ recorder.onstop = () => {
825
+ stopped = true;
826
+ cancelAnimationFrame(rafId);
827
+
828
+ // Use the correct file extension for the actual format
829
+ const isMP4 = mimeType.startsWith("video/mp4");
830
+ const ext = isMP4 ? ".mp4" : ".webm";
831
+ const blobType = isMP4 ? "video/mp4" : "video/webm";
832
+ const finalFilename = filename + ext;
833
+
834
+ const blob = new Blob(chunks, { type: blobType });
835
+ const url = URL.createObjectURL(blob);
836
+ downloadURI(url, finalFilename);
837
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
838
+
839
+ onProgress?.(1);
840
+ onComplete?.();
841
+ };
842
+
843
+ // Start drawing frames, then start recording
844
+ drawFrame();
845
+ recorder.start(100); // collect data every 100ms
846
+
847
+ // Auto-stop after the requested duration
848
+ const timeoutId = window.setTimeout(() => {
849
+ if (recorder.state === "recording") {
850
+ recorder.stop();
851
+ }
852
+ }, durationMs);
853
+
854
+ // Return a stop function for early termination
855
+ return () => {
856
+ clearTimeout(timeoutId);
857
+ if (recorder.state === "recording") {
858
+ recorder.stop();
859
+ }
860
+ };
861
+ }
862
+ get speed(): number {
863
+ return this._speed * 20;
864
+ }
496
865
  set speed(speed: number) {
497
866
  this._uniformsDirty = true;
498
867
  this._speed = speed / 20;
499
868
  }
500
869
 
870
+ get horizontalPressure(): number {
871
+ return this._horizontalPressure * 4;
872
+ }
501
873
  set horizontalPressure(horizontalPressure: number) {
502
874
  this._uniformsDirty = true;
503
875
  this._horizontalPressure = horizontalPressure / 4;
504
876
  }
505
877
 
878
+ get verticalPressure(): number {
879
+ return this._verticalPressure * 4;
880
+ }
506
881
  set verticalPressure(verticalPressure: number) {
507
882
  this._uniformsDirty = true;
508
883
  this._verticalPressure = verticalPressure / 4;
509
884
  }
510
885
 
886
+ get waveFrequencyX(): number {
887
+ return this._waveFrequencyX / 0.04;
888
+ }
511
889
  set waveFrequencyX(waveFrequencyX: number) {
512
890
  this._uniformsDirty = true;
513
891
  this._waveFrequencyX = waveFrequencyX * 0.04;
514
892
  }
515
893
 
894
+ get waveFrequencyY(): number {
895
+ return this._waveFrequencyY / 0.04;
896
+ }
516
897
  set waveFrequencyY(waveFrequencyY: number) {
517
898
  this._uniformsDirty = true;
518
899
  this._waveFrequencyY = waveFrequencyY * 0.04;
519
900
  }
520
901
 
902
+ get waveAmplitude(): number {
903
+ return this._waveAmplitude / 0.75;
904
+ }
521
905
  set waveAmplitude(waveAmplitude: number) {
522
906
  this._uniformsDirty = true;
523
907
  this._waveAmplitude = waveAmplitude * .75;
524
908
  }
525
909
 
910
+ get colors(): NeatColor[] {
911
+ return this._colors;
912
+ }
526
913
  set colors(colors: NeatColor[]) {
527
914
  this._uniformsDirty = true;
528
915
  this._colors = colors;
@@ -530,84 +917,120 @@ export class NeatGradient implements NeatController {
530
917
  this._colorsChanged = true;
531
918
  }
532
919
 
920
+ get highlights(): number {
921
+ return this._highlights * 100;
922
+ }
533
923
  set highlights(highlights: number) {
534
924
  this._uniformsDirty = true;
535
925
  this._highlights = highlights / 100;
536
926
  }
537
927
 
928
+ get shadows(): number {
929
+ return this._shadows * 100;
930
+ }
538
931
  set shadows(shadows: number) {
539
932
  this._uniformsDirty = true;
540
933
  this._shadows = shadows / 100;
541
934
  }
542
935
 
936
+ get colorSaturation(): number {
937
+ return this._saturation * 10;
938
+ }
543
939
  set colorSaturation(colorSaturation: number) {
544
940
  this._uniformsDirty = true;
545
941
  this._saturation = colorSaturation / 10;
546
942
  }
547
943
 
944
+ get colorBrightness(): number {
945
+ return this._brightness;
946
+ }
548
947
  set colorBrightness(colorBrightness: number) {
549
948
  this._uniformsDirty = true;
550
949
  this._brightness = colorBrightness;
551
950
  }
552
951
 
952
+ get colorBlending(): number {
953
+ return this._colorBlending * 10;
954
+ }
553
955
  set colorBlending(colorBlending: number) {
554
956
  this._uniformsDirty = true;
555
957
  this._colorBlending = colorBlending / 10;
556
958
  }
557
959
 
960
+ get grainScale(): number {
961
+ return this._grainScale;
962
+ }
558
963
  set grainScale(grainScale: number) {
559
964
  this._uniformsDirty = true;
560
965
  this._grainScale = grainScale == 0 ? 1 : grainScale;
561
966
  }
562
967
 
968
+ get grainIntensity(): number {
969
+ return this._grainIntensity;
970
+ }
563
971
  set grainIntensity(grainIntensity: number) {
564
972
  this._uniformsDirty = true;
565
973
  this._grainIntensity = grainIntensity;
566
974
  }
567
975
 
976
+ get grainSparsity(): number {
977
+ return this._grainSparsity;
978
+ }
568
979
  set grainSparsity(grainSparsity: number) {
569
980
  this._uniformsDirty = true;
570
981
  this._grainSparsity = grainSparsity;
571
982
  }
572
983
 
984
+ get grainSpeed(): number {
985
+ return this._grainSpeed;
986
+ }
573
987
  set grainSpeed(grainSpeed: number) {
574
988
  this._uniformsDirty = true;
575
989
  this._grainSpeed = grainSpeed;
576
990
  }
577
991
 
992
+ get wireframe(): boolean {
993
+ return this._wireframe;
994
+ }
578
995
  set wireframe(wireframe: boolean) {
579
996
  this._uniformsDirty = true;
580
997
  this._wireframe = wireframe;
581
998
  }
582
999
 
1000
+ get resolution(): number {
1001
+ return this._resolution;
1002
+ }
583
1003
  set resolution(resolution: number) {
584
- this._uniformsDirty = true;
585
- if (this.glState) {
586
- const gl = this.glState.gl;
587
- gl.deleteProgram(this.glState.program);
588
- gl.deleteBuffer(this.glState.buffers.position);
589
- gl.deleteBuffer(this.glState.buffers.normal);
590
- gl.deleteBuffer(this.glState.buffers.uv);
591
- gl.deleteBuffer(this.glState.buffers.index);
592
- gl.deleteBuffer(this.glState.buffers.wireframeIndex);
593
- }
594
- this.glState = this._initScene(resolution);
1004
+ if (this._resolution === resolution) return;
1005
+ this._resolution = resolution;
1006
+ this._updateGeometry();
595
1007
  }
596
1008
 
1009
+ get backgroundColor(): string {
1010
+ return this._backgroundColor;
1011
+ }
597
1012
  set backgroundColor(backgroundColor: string) {
598
1013
  this._uniformsDirty = true;
599
1014
  this._backgroundColor = backgroundColor;
600
1015
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
601
1016
  }
602
1017
 
1018
+ get backgroundAlpha(): number {
1019
+ return this._backgroundAlpha;
1020
+ }
603
1021
  set backgroundAlpha(backgroundAlpha: number) {
604
1022
  this._uniformsDirty = true;
605
1023
  this._backgroundAlpha = backgroundAlpha;
606
1024
  }
607
1025
 
1026
+ get yOffset(): number {
1027
+ return this._yOffset;
1028
+ }
608
1029
  set yOffset(yOffset: number) {
609
- this._uniformsDirty = true;
610
- this._yOffset = yOffset;
1030
+ if (this._yOffset !== yOffset) {
1031
+ this._yOffsetDirty = true;
1032
+ this._yOffset = yOffset;
1033
+ }
611
1034
  }
612
1035
 
613
1036
  get yOffsetWaveMultiplier(): number {
@@ -637,21 +1060,33 @@ export class NeatGradient implements NeatController {
637
1060
  this._yOffsetFlowMultiplier = value / 1000;
638
1061
  }
639
1062
 
1063
+ get flowDistortionA(): number {
1064
+ return this._flowDistortionA;
1065
+ }
640
1066
  set flowDistortionA(value: number) {
641
1067
  this._uniformsDirty = true;
642
1068
  this._flowDistortionA = value;
643
1069
  }
644
1070
 
1071
+ get flowDistortionB(): number {
1072
+ return this._flowDistortionB;
1073
+ }
645
1074
  set flowDistortionB(value: number) {
646
1075
  this._uniformsDirty = true;
647
1076
  this._flowDistortionB = value;
648
1077
  }
649
1078
 
1079
+ get flowScale(): number {
1080
+ return this._flowScale;
1081
+ }
650
1082
  set flowScale(value: number) {
651
1083
  this._uniformsDirty = true;
652
1084
  this._flowScale = value;
653
1085
  }
654
1086
 
1087
+ get flowEase(): number {
1088
+ return this._flowEase;
1089
+ }
655
1090
  set flowEase(value: number) {
656
1091
  this._uniformsDirty = true;
657
1092
  this._flowEase = value;
@@ -668,6 +1103,9 @@ export class NeatGradient implements NeatController {
668
1103
 
669
1104
 
670
1105
 
1106
+ get enableProceduralTexture(): boolean {
1107
+ return this._enableProceduralTexture;
1108
+ }
671
1109
  set enableProceduralTexture(value: boolean) {
672
1110
  this._uniformsDirty = true;
673
1111
  this._enableProceduralTexture = value;
@@ -676,6 +1114,9 @@ export class NeatGradient implements NeatController {
676
1114
  }
677
1115
  }
678
1116
 
1117
+ get textureVoidLikelihood(): number {
1118
+ return this._textureVoidLikelihood;
1119
+ }
679
1120
  set textureVoidLikelihood(value: number) {
680
1121
  this._uniformsDirty = true;
681
1122
  this._textureVoidLikelihood = value;
@@ -684,6 +1125,9 @@ export class NeatGradient implements NeatController {
684
1125
  }
685
1126
  }
686
1127
 
1128
+ get textureVoidWidthMin(): number {
1129
+ return this._textureVoidWidthMin;
1130
+ }
687
1131
  set textureVoidWidthMin(value: number) {
688
1132
  this._uniformsDirty = true;
689
1133
  this._textureVoidWidthMin = value;
@@ -692,6 +1136,9 @@ export class NeatGradient implements NeatController {
692
1136
  }
693
1137
  }
694
1138
 
1139
+ get textureVoidWidthMax(): number {
1140
+ return this._textureVoidWidthMax;
1141
+ }
695
1142
  set textureVoidWidthMax(value: number) {
696
1143
  this._uniformsDirty = true;
697
1144
  this._textureVoidWidthMax = value;
@@ -700,6 +1147,9 @@ export class NeatGradient implements NeatController {
700
1147
  }
701
1148
  }
702
1149
 
1150
+ get textureBandDensity(): number {
1151
+ return this._textureBandDensity;
1152
+ }
703
1153
  set textureBandDensity(value: number) {
704
1154
  this._uniformsDirty = true;
705
1155
  this._textureBandDensity = value;
@@ -708,6 +1158,9 @@ export class NeatGradient implements NeatController {
708
1158
  }
709
1159
  }
710
1160
 
1161
+ get textureColorBlending(): number {
1162
+ return this._textureColorBlending;
1163
+ }
711
1164
  set textureColorBlending(value: number) {
712
1165
  this._uniformsDirty = true;
713
1166
  this._textureColorBlending = value;
@@ -716,6 +1169,9 @@ export class NeatGradient implements NeatController {
716
1169
  }
717
1170
  }
718
1171
 
1172
+ get textureSeed(): number {
1173
+ return this._textureSeed;
1174
+ }
719
1175
  set textureSeed(value: number) {
720
1176
  this._uniformsDirty = true;
721
1177
  this._textureSeed = value;
@@ -733,6 +1189,21 @@ export class NeatGradient implements NeatController {
733
1189
  this._textureEase = value;
734
1190
  }
735
1191
 
1192
+ get transparentTextureVoid(): boolean {
1193
+ return this._transparentTextureVoid;
1194
+ }
1195
+
1196
+ set transparentTextureVoid(value: boolean) {
1197
+ this._uniformsDirty = true;
1198
+ this._transparentTextureVoid = value;
1199
+ if (this._enableProceduralTexture) {
1200
+ this._textureNeedsUpdate = true;
1201
+ }
1202
+ }
1203
+
1204
+ get proceduralBackgroundColor(): string {
1205
+ return this._proceduralBackgroundColor;
1206
+ }
736
1207
  set proceduralBackgroundColor(value: string) {
737
1208
  this._uniformsDirty = true;
738
1209
  this._proceduralBackgroundColor = value;
@@ -741,27 +1212,93 @@ export class NeatGradient implements NeatController {
741
1212
  }
742
1213
  }
743
1214
 
1215
+ get textureShapeTriangles(): number {
1216
+ return this._textureShapeTriangles;
1217
+ }
744
1218
  set textureShapeTriangles(value: number) {
745
1219
  this._uniformsDirty = true;
746
1220
  this._textureShapeTriangles = value;
747
1221
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
748
1222
  }
1223
+ get textureShapeCircles(): number {
1224
+ return this._textureShapeCircles;
1225
+ }
749
1226
  set textureShapeCircles(value: number) {
750
1227
  this._uniformsDirty = true;
751
1228
  this._textureShapeCircles = value;
752
1229
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
753
1230
  }
1231
+ get textureShapeBars(): number {
1232
+ return this._textureShapeBars;
1233
+ }
754
1234
  set textureShapeBars(value: number) {
755
1235
  this._uniformsDirty = true;
756
1236
  this._textureShapeBars = value;
757
1237
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
758
1238
  }
1239
+ get textureShapeSquiggles(): number {
1240
+ return this._textureShapeSquiggles;
1241
+ }
759
1242
  set textureShapeSquiggles(value: number) {
760
1243
  this._uniformsDirty = true;
761
1244
  this._textureShapeSquiggles = value;
762
1245
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
763
1246
  }
764
1247
 
1248
+ _updateGeometry() {
1249
+ if (!this.glState) return;
1250
+ const gl = this.glState.gl;
1251
+ const resolution = this._resolution || 1;
1252
+
1253
+ let geometry;
1254
+ if (this._shapeType === 'sphere') {
1255
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
1256
+ } else if (this._shapeType === 'torus') {
1257
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
1258
+ } else if (this._shapeType === 'cylinder') {
1259
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
1260
+ } else if (this._shapeType === 'ribbon') {
1261
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
1262
+ } else {
1263
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1264
+ }
1265
+ const { position, normal, uv, index, wireframeIndex } = geometry;
1266
+
1267
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.position);
1268
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
1269
+
1270
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.normal);
1271
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
1272
+
1273
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.uv);
1274
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
1275
+
1276
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
1277
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
1278
+
1279
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
1280
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
1281
+
1282
+ // Restore default bound element buffer
1283
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
1284
+
1285
+ this.glState.indexCount = index.length;
1286
+ this.glState.wireframeIndexCount = wireframeIndex.length;
1287
+ this.glState.indexType = (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
1288
+
1289
+ // Keep camera updated with the new shapeType and dimensions
1290
+ const width = this._ref.clientWidth;
1291
+ const height = this._ref.clientHeight;
1292
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1293
+
1294
+ // Recompute projection matrix
1295
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1296
+ gl.useProgram(this.glState.program);
1297
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1298
+
1299
+ this._uniformsDirty = true;
1300
+ }
1301
+
765
1302
  _hexToRgb(hex: string): [number, number, number] {
766
1303
  const bigint = parseInt(hex.replace('#', ''), 16);
767
1304
  return [
@@ -788,8 +1325,20 @@ export class NeatGradient implements NeatController {
788
1325
 
789
1326
  gl.viewport(0, 0, width, height);
790
1327
 
791
- // Generate plane geometry with Uint32Array for large meshes
792
- const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1328
+ // Generate parametric geometry based on shapeType
1329
+ let geometry;
1330
+ if (this._shapeType === 'sphere') {
1331
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
1332
+ } else if (this._shapeType === 'torus') {
1333
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
1334
+ } else if (this._shapeType === 'cylinder') {
1335
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
1336
+ } else if (this._shapeType === 'ribbon') {
1337
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
1338
+ } else {
1339
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1340
+ }
1341
+ const { position, normal, uv, index, wireframeIndex } = geometry;
793
1342
 
794
1343
  const positionBuffer = gl.createBuffer()!;
795
1344
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
@@ -855,7 +1404,7 @@ export class NeatGradient implements NeatController {
855
1404
 
856
1405
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
857
1406
  camera.position = [0, 0, 5];
858
- updateCamera(camera, width, height);
1407
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
859
1408
 
860
1409
  // Define attributes
861
1410
  const aPosition = gl.getAttribLocation(program, "position");
@@ -876,17 +1425,7 @@ export class NeatGradient implements NeatController {
876
1425
 
877
1426
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
878
1427
 
879
- const modelViewMatrix = new Matrix4();
880
- // The View Matrix is the inverse of the Camera's position
881
- // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
882
- modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
883
-
884
- // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
885
- modelViewMatrix.translate(0, 0, -1);
886
- modelViewMatrix.rotateX(-Math.PI / 3.5);
887
-
888
- const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
889
- gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
1428
+ // modelViewMatrix is set dynamically in the render loop
890
1429
 
891
1430
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
892
1431
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
@@ -901,18 +1440,20 @@ export class NeatGradient implements NeatController {
901
1440
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
902
1441
 
903
1442
  const uniformsList = [
1443
+ "projectionMatrix", "modelViewMatrix",
904
1444
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
905
1445
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
906
1446
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
907
1447
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
908
1448
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
909
1449
 
910
- "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending",
1450
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_transparent_texture_void", "u_saturation", "u_brightness", "u_color_blending",
911
1451
  "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
912
1452
  "u_vignette_intensity", "u_vignette_radius",
913
1453
  "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
914
1454
  "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
915
- "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration"
1455
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1456
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade", "u_flat_shading"
916
1457
  ];
917
1458
 
918
1459
  const locations: WebGLState["locations"] = {
@@ -966,10 +1507,15 @@ export class NeatGradient implements NeatController {
966
1507
  // Texture size - 1024 provides good balance between quality and performance
967
1508
  // Reduced from 2048 for better performance
968
1509
  const texSize = 1024;
969
- const sourceCanvas = document.createElement('canvas');
970
- sourceCanvas.width = texSize;
971
- sourceCanvas.height = texSize;
972
- const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
1510
+
1511
+ if (!this._sourceCanvas) {
1512
+ this._sourceCanvas = document.createElement('canvas');
1513
+ this._sourceCanvas.width = texSize;
1514
+ this._sourceCanvas.height = texSize;
1515
+ this._sourceCtx = this._sourceCanvas.getContext('2d');
1516
+ }
1517
+ const sourceCanvas = this._sourceCanvas;
1518
+ const sCtx = this._sourceCtx;
973
1519
  if (!sCtx) return null;
974
1520
 
975
1521
  let seed = this._textureSeed;
@@ -988,6 +1534,10 @@ export class NeatGradient implements NeatController {
988
1534
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
989
1535
  if (colors.length === 0) return null;
990
1536
 
1537
+ const shouldTile = this._shapeType !== 'plane';
1538
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1539
+ const dys = shouldTile ? [-1, 0, 1] : [0];
1540
+
991
1541
  // Helper functions
992
1542
  function hexToRgb(hex: string) {
993
1543
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -1029,72 +1579,133 @@ export class NeatGradient implements NeatController {
1029
1579
 
1030
1580
  // Triangles: use configurable count
1031
1581
  for (let i = 0; i < this._textureShapeTriangles; i++) {
1032
- sCtx.fillStyle = getInterColor();
1033
- sCtx.beginPath();
1582
+ const fillStyle = getInterColor();
1034
1583
  const x = random() * texSize;
1035
1584
  const y = random() * texSize;
1036
1585
  const s = 100 + random() * 300;
1037
- sCtx.moveTo(x, y);
1038
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1039
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1040
- sCtx.fill();
1586
+ const x1 = (random() - 0.5) * s;
1587
+ const y1 = (random() - 0.5) * s;
1588
+ const x2 = (random() - 0.5) * s;
1589
+ const y2 = (random() - 0.5) * s;
1590
+
1591
+ for (const dx of dxs) {
1592
+ for (const dy of dys) {
1593
+ sCtx.fillStyle = fillStyle;
1594
+ sCtx.beginPath();
1595
+ const tx = x + dx * texSize;
1596
+ const ty = y + dy * texSize;
1597
+ sCtx.moveTo(tx, ty);
1598
+ sCtx.lineTo(tx + x1, ty + y1);
1599
+ sCtx.lineTo(tx + x2, ty + y2);
1600
+ sCtx.fill();
1601
+ }
1602
+ }
1041
1603
  }
1042
1604
 
1043
1605
  // Circles / rings: use configurable count
1044
1606
  for (let i = 0; i < this._textureShapeCircles; i++) {
1045
- sCtx.strokeStyle = getInterColor();
1046
- sCtx.lineWidth = 10 + random() * 50;
1047
- sCtx.beginPath();
1607
+ const strokeStyle = getInterColor();
1608
+ const lineWidth = 10 + random() * 50;
1048
1609
  const x = random() * texSize;
1049
1610
  const y = random() * texSize;
1050
1611
  const r = 50 + random() * 150;
1051
- sCtx.arc(x, y, r, 0, Math.PI * 2);
1052
- sCtx.stroke();
1612
+
1613
+ for (const dx of dxs) {
1614
+ for (const dy of dys) {
1615
+ sCtx.strokeStyle = strokeStyle;
1616
+ sCtx.lineWidth = lineWidth;
1617
+ sCtx.beginPath();
1618
+ sCtx.arc(x + dx * texSize, y + dy * texSize, r, 0, Math.PI * 2);
1619
+ sCtx.stroke();
1620
+ }
1621
+ }
1053
1622
  }
1054
1623
 
1055
1624
  // Bars: use configurable count
1056
1625
  for (let i = 0; i < this._textureShapeBars; i++) {
1057
- sCtx.fillStyle = getInterColor();
1058
- sCtx.save();
1059
- sCtx.translate(random() * texSize, random() * texSize);
1060
- sCtx.rotate(random() * Math.PI);
1061
- sCtx.fillRect(-150, -25, 300, 50);
1062
- sCtx.restore();
1626
+ const fillStyle = getInterColor();
1627
+ const x = random() * texSize;
1628
+ const y = random() * texSize;
1629
+ const rot = random() * Math.PI;
1630
+
1631
+ for (const dx of dxs) {
1632
+ for (const dy of dys) {
1633
+ sCtx.fillStyle = fillStyle;
1634
+ sCtx.save();
1635
+ sCtx.translate(x + dx * texSize, y + dy * texSize);
1636
+ sCtx.rotate(rot);
1637
+ sCtx.fillRect(-150, -25, 300, 50);
1638
+ sCtx.restore();
1639
+ }
1640
+ }
1063
1641
  }
1064
1642
 
1065
1643
  // Squiggles: use configurable count
1066
1644
  sCtx.lineWidth = 15;
1067
1645
  sCtx.lineCap = 'round';
1068
1646
  for (let i = 0; i < this._textureShapeSquiggles; i++) {
1069
- sCtx.strokeStyle = getInterColor();
1070
- sCtx.beginPath();
1071
- let x = random() * texSize;
1072
- let y = random() * texSize;
1073
- sCtx.moveTo(x, y);
1647
+ const strokeStyle = getInterColor();
1648
+ const x = random() * texSize;
1649
+ const y = random() * texSize;
1650
+
1651
+ const curves: Array<{ cx1: number, cy1: number, cx2: number, cy2: number, ex: number, ey: number }> = [];
1652
+ let cx = 0;
1653
+ let cy = 0;
1074
1654
  for (let j = 0; j < 4; j++) {
1075
- sCtx.bezierCurveTo(
1076
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1077
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1078
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300
1079
- );
1080
- x += (random() - 0.5) * 300;
1081
- y += (random() - 0.5) * 300;
1655
+ const ex = cx + (random() - 0.5) * 300;
1656
+ const ey = cy + (random() - 0.5) * 300;
1657
+ curves.push({
1658
+ cx1: cx + (random() - 0.5) * 300,
1659
+ cy1: cy + (random() - 0.5) * 300,
1660
+ cx2: cx + (random() - 0.5) * 300,
1661
+ cy2: cy + (random() - 0.5) * 300,
1662
+ ex: ex,
1663
+ ey: ey
1664
+ });
1665
+ cx = ex;
1666
+ cy = ey;
1667
+ }
1668
+
1669
+ for (const dx of dxs) {
1670
+ for (const dy of dys) {
1671
+ sCtx.strokeStyle = strokeStyle;
1672
+ sCtx.beginPath();
1673
+ const tx = x + dx * texSize;
1674
+ const ty = y + dy * texSize;
1675
+ sCtx.moveTo(tx, ty);
1676
+
1677
+ for (const curve of curves) {
1678
+ sCtx.bezierCurveTo(
1679
+ tx + curve.cx1, ty + curve.cy1,
1680
+ tx + curve.cx2, ty + curve.cy2,
1681
+ tx + curve.ex, ty + curve.ey
1682
+ );
1683
+ }
1684
+ sCtx.stroke();
1685
+ }
1082
1686
  }
1083
- sCtx.stroke();
1084
1687
  }
1085
1688
 
1086
1689
  // === MASKED CANVAS ===
1087
1690
  // Masking: Seed isolation
1088
1691
  setSeed(50000);
1089
- const canvas = document.createElement('canvas');
1090
- canvas.width = texSize;
1091
- canvas.height = texSize;
1092
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1692
+ if (!this._maskedCanvas) {
1693
+ this._maskedCanvas = document.createElement('canvas');
1694
+ this._maskedCanvas.width = texSize;
1695
+ this._maskedCanvas.height = texSize;
1696
+ this._maskedCtx = this._maskedCanvas.getContext('2d');
1697
+ }
1698
+ const canvas = this._maskedCanvas;
1699
+ const ctx = this._maskedCtx;
1093
1700
  if (!ctx) return null;
1094
1701
 
1095
1702
  // Start filled with the chosen void color so gaps show that color
1096
- ctx.fillStyle = baseColor;
1097
- ctx.fillRect(0, 0, texSize, texSize);
1703
+ if (this._transparentTextureVoid) {
1704
+ ctx.clearRect(0, 0, texSize, texSize);
1705
+ } else {
1706
+ ctx.fillStyle = baseColor;
1707
+ ctx.fillRect(0, 0, texSize, texSize);
1708
+ }
1098
1709
 
1099
1710
  // Determine layout segments (matter vs void)
1100
1711
  let layoutHead = 0;
@@ -1154,54 +1765,129 @@ export class NeatGradient implements NeatController {
1154
1765
  return tex;
1155
1766
  }
1156
1767
 
1768
+ get silhouetteFade(): number {
1769
+ return this._silhouetteFade;
1770
+ }
1771
+ set silhouetteFade(value: number) {
1772
+ if (this._silhouetteFade !== value) {
1773
+ this._silhouetteFade = value;
1774
+ this._uniformsDirty = true;
1775
+ }
1776
+ }
1777
+
1778
+ get cylinderFade(): number {
1779
+ return this._cylinderFade;
1780
+ }
1781
+ set cylinderFade(value: number) {
1782
+ if (this._cylinderFade !== value) {
1783
+ this._cylinderFade = value;
1784
+ this._uniformsDirty = true;
1785
+ }
1786
+ }
1787
+
1788
+ get ribbonFade(): number {
1789
+ return this._ribbonFade;
1790
+ }
1791
+ set ribbonFade(value: number) {
1792
+ if (this._ribbonFade !== value) {
1793
+ this._ribbonFade = value;
1794
+ this._uniformsDirty = true;
1795
+ }
1796
+ }
1797
+
1798
+ get flatShading(): boolean {
1799
+ return this._flatShading;
1800
+ }
1801
+ set flatShading(value: boolean) {
1802
+ if (this._flatShading !== value) {
1803
+ this._flatShading = value;
1804
+ this._uniformsDirty = true;
1805
+ }
1806
+ }
1807
+
1808
+ get domainWarpEnabled(): boolean {
1809
+ return this._domainWarpEnabled;
1810
+ }
1157
1811
  set domainWarpEnabled(enabled: boolean) {
1158
1812
  if (this._domainWarpEnabled !== enabled) {
1159
1813
  this._domainWarpEnabled = enabled;
1160
1814
  this._uniformsDirty = true;
1161
1815
  }
1162
1816
  }
1817
+
1818
+ get domainWarpIntensity(): number {
1819
+ return this._domainWarpIntensity;
1820
+ }
1163
1821
  set domainWarpIntensity(intensity: number) {
1164
1822
  if (this._domainWarpIntensity !== intensity) {
1165
1823
  this._domainWarpIntensity = intensity;
1166
1824
  this._uniformsDirty = true;
1167
1825
  }
1168
1826
  }
1827
+
1828
+ get domainWarpScale(): number {
1829
+ return this._domainWarpScale;
1830
+ }
1169
1831
  set domainWarpScale(scale: number) {
1170
1832
  if (this._domainWarpScale !== scale) {
1171
1833
  this._domainWarpScale = scale;
1172
1834
  this._uniformsDirty = true;
1173
1835
  }
1174
1836
  }
1837
+
1838
+ get vignetteIntensity(): number {
1839
+ return this._vignetteIntensity;
1840
+ }
1175
1841
  set vignetteIntensity(intensity: number) {
1176
1842
  if (this._vignetteIntensity !== intensity) {
1177
1843
  this._vignetteIntensity = intensity;
1178
1844
  this._uniformsDirty = true;
1179
1845
  }
1180
1846
  }
1847
+
1848
+ get vignetteRadius(): number {
1849
+ return this._vignetteRadius;
1850
+ }
1181
1851
  set vignetteRadius(radius: number) {
1182
1852
  if (this._vignetteRadius !== radius) {
1183
1853
  this._vignetteRadius = radius;
1184
1854
  this._uniformsDirty = true;
1185
1855
  }
1186
1856
  }
1857
+
1858
+ get fresnelEnabled(): boolean {
1859
+ return this._fresnelEnabled;
1860
+ }
1187
1861
  set fresnelEnabled(enabled: boolean) {
1188
1862
  if (this._fresnelEnabled !== enabled) {
1189
1863
  this._fresnelEnabled = enabled;
1190
1864
  this._uniformsDirty = true;
1191
1865
  }
1192
1866
  }
1867
+
1868
+ get fresnelPower(): number {
1869
+ return this._fresnelPower;
1870
+ }
1193
1871
  set fresnelPower(power: number) {
1194
1872
  if (this._fresnelPower !== power) {
1195
1873
  this._fresnelPower = power;
1196
1874
  this._uniformsDirty = true;
1197
1875
  }
1198
1876
  }
1877
+
1878
+ get fresnelIntensity(): number {
1879
+ return this._fresnelIntensity;
1880
+ }
1199
1881
  set fresnelIntensity(intensity: number) {
1200
1882
  if (this._fresnelIntensity !== intensity) {
1201
1883
  this._fresnelIntensity = intensity;
1202
1884
  this._uniformsDirty = true;
1203
1885
  }
1204
1886
  }
1887
+
1888
+ get fresnelColor(): string {
1889
+ return this._fresnelColor;
1890
+ }
1205
1891
  set fresnelColor(fresnelColor: string) {
1206
1892
  if (this._fresnelColor !== fresnelColor) {
1207
1893
  this._fresnelColor = fresnelColor;
@@ -1209,42 +1895,226 @@ export class NeatGradient implements NeatController {
1209
1895
  this._uniformsDirty = true;
1210
1896
  }
1211
1897
  }
1898
+
1899
+ get iridescenceEnabled(): boolean {
1900
+ return this._iridescenceEnabled;
1901
+ }
1212
1902
  set iridescenceEnabled(enabled: boolean) {
1213
1903
  if (this._iridescenceEnabled !== enabled) {
1214
1904
  this._iridescenceEnabled = enabled;
1215
1905
  this._uniformsDirty = true;
1216
1906
  }
1217
1907
  }
1908
+
1909
+ get iridescenceIntensity(): number {
1910
+ return this._iridescenceIntensity;
1911
+ }
1218
1912
  set iridescenceIntensity(intensity: number) {
1219
1913
  if (this._iridescenceIntensity !== intensity) {
1220
1914
  this._iridescenceIntensity = intensity;
1221
1915
  this._uniformsDirty = true;
1222
1916
  }
1223
1917
  }
1918
+
1919
+ get iridescenceSpeed(): number {
1920
+ return this._iridescenceSpeed;
1921
+ }
1224
1922
  set iridescenceSpeed(speed: number) {
1225
1923
  if (this._iridescenceSpeed !== speed) {
1226
1924
  this._iridescenceSpeed = speed;
1227
1925
  this._uniformsDirty = true;
1228
1926
  }
1229
1927
  }
1928
+
1929
+ get bloomIntensity(): number {
1930
+ return this._bloomIntensity;
1931
+ }
1230
1932
  set bloomIntensity(intensity: number) {
1231
1933
  if (this._bloomIntensity !== intensity) {
1232
1934
  this._bloomIntensity = intensity;
1233
1935
  this._uniformsDirty = true;
1234
1936
  }
1235
1937
  }
1938
+
1939
+ get bloomThreshold(): number {
1940
+ return this._bloomThreshold;
1941
+ }
1236
1942
  set bloomThreshold(threshold: number) {
1237
1943
  if (this._bloomThreshold !== threshold) {
1238
1944
  this._bloomThreshold = threshold;
1239
1945
  this._uniformsDirty = true;
1240
1946
  }
1241
1947
  }
1948
+
1949
+ get chromaticAberration(): number {
1950
+ return this._chromaticAberration;
1951
+ }
1242
1952
  set chromaticAberration(aberration: number) {
1243
1953
  if (this._chromaticAberration !== aberration) {
1244
1954
  this._chromaticAberration = aberration;
1245
1955
  this._uniformsDirty = true;
1246
1956
  }
1247
1957
  }
1958
+
1959
+ // Getters and Setters for 3D Shapes
1960
+ get shapeType(): 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon' {
1961
+ return this._shapeType;
1962
+ }
1963
+ set shapeType(val: 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon') {
1964
+ if (this._shapeType !== val) {
1965
+ this._shapeType = val;
1966
+ this._updateGeometry();
1967
+ }
1968
+ }
1969
+
1970
+ get shapeRotationX(): number { return this._shapeRotationX; }
1971
+ set shapeRotationX(val: number) {
1972
+ this._shapeRotationX = val;
1973
+ this._uniformsDirty = true;
1974
+ }
1975
+
1976
+ get shapeRotationY(): number { return this._shapeRotationY; }
1977
+ set shapeRotationY(val: number) {
1978
+ this._shapeRotationY = val;
1979
+ this._uniformsDirty = true;
1980
+ }
1981
+
1982
+ get shapeRotationZ(): number { return this._shapeRotationZ; }
1983
+ set shapeRotationZ(val: number) {
1984
+ this._shapeRotationZ = val;
1985
+ this._uniformsDirty = true;
1986
+ }
1987
+
1988
+ get shapeAutoRotateSpeedX(): number { return this._shapeAutoRotateSpeedX; }
1989
+ set shapeAutoRotateSpeedX(val: number) {
1990
+ this._shapeAutoRotateSpeedX = val;
1991
+ this._uniformsDirty = true;
1992
+ }
1993
+
1994
+ get shapeAutoRotateSpeedY(): number { return this._shapeAutoRotateSpeedY; }
1995
+ set shapeAutoRotateSpeedY(val: number) {
1996
+ this._shapeAutoRotateSpeedY = val;
1997
+ this._uniformsDirty = true;
1998
+ }
1999
+
2000
+ get sphereRadius(): number { return this._sphereRadius; }
2001
+ set sphereRadius(val: number) {
2002
+ if (this._sphereRadius !== val) {
2003
+ this._sphereRadius = val;
2004
+ this._updateGeometry();
2005
+ }
2006
+ }
2007
+
2008
+ get torusRadius(): number { return this._torusRadius; }
2009
+ set torusRadius(val: number) {
2010
+ if (this._torusRadius !== val) {
2011
+ this._torusRadius = val;
2012
+ this._updateGeometry();
2013
+ }
2014
+ }
2015
+
2016
+ get torusTube(): number { return this._torusTube; }
2017
+ set torusTube(val: number) {
2018
+ if (this._torusTube !== val) {
2019
+ this._torusTube = val;
2020
+ this._updateGeometry();
2021
+ }
2022
+ }
2023
+
2024
+ get cylinderRadius(): number { return this._cylinderRadius; }
2025
+ set cylinderRadius(val: number) {
2026
+ if (this._cylinderRadius !== val) {
2027
+ this._cylinderRadius = val;
2028
+ this._updateGeometry();
2029
+ }
2030
+ }
2031
+
2032
+ get cylinderHeight(): number { return this._cylinderHeight; }
2033
+ set cylinderHeight(val: number) {
2034
+ if (this._cylinderHeight !== val) {
2035
+ this._cylinderHeight = val;
2036
+ this._updateGeometry();
2037
+ }
2038
+ }
2039
+
2040
+ get planeBend(): number { return this._planeBend; }
2041
+ set planeBend(val: number) {
2042
+ if (this._planeBend !== val) {
2043
+ this._planeBend = val;
2044
+ this._updateGeometry();
2045
+ }
2046
+ }
2047
+
2048
+ get planeTwist(): number { return this._planeTwist; }
2049
+ set planeTwist(val: number) {
2050
+ if (this._planeTwist !== val) {
2051
+ this._planeTwist = val;
2052
+ this._updateGeometry();
2053
+ }
2054
+ }
2055
+
2056
+ // Camera Getters and Setters
2057
+ get cameraLock(): boolean { return this._cameraLock; }
2058
+ set cameraLock(val: boolean) {
2059
+ this._cameraLock = val;
2060
+ }
2061
+
2062
+ get cameraX(): number { return this._cameraX; }
2063
+ set cameraX(val: number) {
2064
+ this._cameraX = val;
2065
+ this._uniformsDirty = true;
2066
+ }
2067
+
2068
+ get cameraY(): number { return this._cameraY; }
2069
+ set cameraY(val: number) {
2070
+ this._cameraY = val;
2071
+ this._uniformsDirty = true;
2072
+ }
2073
+
2074
+ get cameraZ(): number { return this._cameraZ; }
2075
+ set cameraZ(val: number) {
2076
+ this._cameraZ = val;
2077
+ this._uniformsDirty = true;
2078
+ }
2079
+
2080
+ get cameraRotationX(): number { return this._cameraRotationX; }
2081
+ set cameraRotationX(val: number) {
2082
+ this._cameraRotationX = val;
2083
+ this._uniformsDirty = true;
2084
+ }
2085
+
2086
+ get cameraRotationY(): number { return this._cameraRotationY; }
2087
+ set cameraRotationY(val: number) {
2088
+ this._cameraRotationY = val;
2089
+ this._uniformsDirty = true;
2090
+ }
2091
+
2092
+ get cameraRotationZ(): number { return this._cameraRotationZ; }
2093
+ set cameraRotationZ(val: number) {
2094
+ this._cameraRotationZ = val;
2095
+ this._uniformsDirty = true;
2096
+ }
2097
+
2098
+ get cameraZoom(): number { return this._cameraZoom; }
2099
+ set cameraZoom(val: number) {
2100
+ if (this._cameraZoom !== val) {
2101
+ this._cameraZoom = val;
2102
+ this._updateCameraFrustum();
2103
+ }
2104
+ }
2105
+
2106
+ _updateCameraFrustum() {
2107
+ if (!this.glState) return;
2108
+ const gl = this.glState.gl;
2109
+ const width = this._ref.clientWidth;
2110
+ const height = this._ref.clientHeight;
2111
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
2112
+
2113
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
2114
+ gl.useProgram(this.glState.program);
2115
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
2116
+ this._uniformsDirty = true;
2117
+ }
1248
2118
  }
1249
2119
 
1250
2120