@firecms/neat 0.8.0 → 0.9.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,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,34 @@ 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
+
118
+ // 3D Shapes config
119
+ private _shapeType: 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon' = 'plane';
120
+ private _shapeRotationX: number = 0;
121
+ private _shapeRotationY: number = 0;
122
+ private _shapeRotationZ: number = 0;
123
+ private _shapeAutoRotateSpeedX: number = 0;
124
+ private _shapeAutoRotateSpeedY: number = 0;
125
+ private _sphereRadius: number = 15;
126
+ private _torusRadius: number = 15;
127
+ private _torusTube: number = 5;
128
+ private _cylinderRadius: number = 10;
129
+ private _cylinderHeight: number = 40;
130
+ private _planeBend: number = 0;
131
+ private _planeTwist: number = 0;
132
+
133
+ // Camera settings
134
+ private _cameraLock: boolean = false;
135
+ private _cameraX: number = 0;
136
+ private _cameraY: number = 0;
137
+ private _cameraZ: number = 0;
138
+ private _cameraRotationX: number = 0;
139
+ private _cameraRotationY: number = 0;
140
+ private _cameraRotationZ: number = 0;
141
+ private _cameraZoom: number = 1.0;
112
142
 
113
143
  private _proceduralTexture: WebGLTexture | null = null;
114
144
  private _proceduralBackgroundColor: string = "#000000";
@@ -130,6 +160,12 @@ export class NeatGradient implements NeatController {
130
160
  private _yOffsetColorMultiplier: number = 0.004;
131
161
  private _yOffsetFlowMultiplier: number = 0.004;
132
162
 
163
+ // Cached offscreen canvases for procedural texture generation
164
+ private _sourceCanvas: HTMLCanvasElement | null = null;
165
+ private _sourceCtx: CanvasRenderingContext2D | null = null;
166
+ private _maskedCanvas: HTMLCanvasElement | null = null;
167
+ private _maskedCtx: CanvasRenderingContext2D | null = null;
168
+
133
169
  // Performance optimizations
134
170
  private _resizeTimeoutId: number | null = null;
135
171
  private _textureNeedsUpdate: boolean = false;
@@ -184,6 +220,7 @@ export class NeatGradient implements NeatController {
184
220
  textureSeed = 333,
185
221
  textureEase = 0.5,
186
222
  proceduralBackgroundColor = "#000000",
223
+ transparentTextureVoid = false,
187
224
  textureShapeTriangles = 20,
188
225
  textureShapeCircles = 15,
189
226
  textureShapeBars = 15,
@@ -192,7 +229,7 @@ export class NeatGradient implements NeatController {
192
229
  domainWarpEnabled = false,
193
230
  domainWarpIntensity = 0.5,
194
231
  domainWarpScale = 1.0,
195
- vignetteIntensity = 0.5,
232
+ vignetteIntensity = 0.0,
196
233
  vignetteRadius = 0.8,
197
234
  fresnelEnabled = false,
198
235
  fresnelPower = 2.0,
@@ -204,6 +241,34 @@ export class NeatGradient implements NeatController {
204
241
  bloomIntensity = 0.0,
205
242
  bloomThreshold = 0.7,
206
243
  chromaticAberration = 0.0,
244
+ silhouetteFade = 0.25,
245
+ cylinderFade = 0.08,
246
+ ribbonFade = 0.05,
247
+
248
+ // Camera configuration
249
+ cameraLock = false,
250
+ cameraX = 0,
251
+ cameraY = 0,
252
+ cameraZ = 0,
253
+ cameraRotationX = 0,
254
+ cameraRotationY = 0,
255
+ cameraRotationZ = 0,
256
+ cameraZoom = 1.0,
257
+
258
+ // 3D shapes default
259
+ shapeType = 'plane',
260
+ shapeRotationX = 0,
261
+ shapeRotationY = 0,
262
+ shapeRotationZ = 0,
263
+ shapeAutoRotateSpeedX = 0,
264
+ shapeAutoRotateSpeedY = 0,
265
+ sphereRadius = 15,
266
+ torusRadius = 15,
267
+ torusTube = 5,
268
+ cylinderRadius = 10,
269
+ cylinderHeight = 40,
270
+ planeBend = 0,
271
+ planeTwist = 0,
207
272
  } = config;
208
273
 
209
274
 
@@ -219,6 +284,7 @@ export class NeatGradient implements NeatController {
219
284
  this.waveFrequencyY = waveFrequencyY;
220
285
  this.waveAmplitude = waveAmplitude;
221
286
  this.colorBlending = colorBlending;
287
+ this._resolution = resolution;
222
288
  this.grainScale = grainScale;
223
289
  this.grainIntensity = grainIntensity;
224
290
  this.grainSparsity = grainSparsity;
@@ -255,6 +321,7 @@ export class NeatGradient implements NeatController {
255
321
  this.textureSeed = textureSeed;
256
322
  this.textureEase = textureEase;
257
323
  this._proceduralBackgroundColor = proceduralBackgroundColor;
324
+ this.transparentTextureVoid = transparentTextureVoid;
258
325
 
259
326
  this._textureShapeTriangles = textureShapeTriangles;
260
327
  this._textureShapeCircles = textureShapeCircles;
@@ -276,6 +343,32 @@ export class NeatGradient implements NeatController {
276
343
  this.bloomIntensity = bloomIntensity;
277
344
  this.bloomThreshold = bloomThreshold;
278
345
  this.chromaticAberration = chromaticAberration;
346
+ this.silhouetteFade = silhouetteFade;
347
+ this.cylinderFade = cylinderFade;
348
+ this.ribbonFade = ribbonFade;
349
+
350
+ this._cameraLock = cameraLock;
351
+ this._cameraX = cameraX;
352
+ this._cameraY = cameraY;
353
+ this._cameraZ = cameraZ;
354
+ this._cameraRotationX = cameraRotationX;
355
+ this._cameraRotationY = cameraRotationY;
356
+ this._cameraRotationZ = cameraRotationZ;
357
+ this._cameraZoom = cameraZoom;
358
+
359
+ this._shapeType = shapeType;
360
+ this._shapeRotationX = shapeRotationX;
361
+ this._shapeRotationY = shapeRotationY;
362
+ this._shapeRotationZ = shapeRotationZ;
363
+ this._shapeAutoRotateSpeedX = shapeAutoRotateSpeedX;
364
+ this._shapeAutoRotateSpeedY = shapeAutoRotateSpeedY;
365
+ this._sphereRadius = sphereRadius;
366
+ this._torusRadius = torusRadius;
367
+ this._torusTube = torusTube;
368
+ this._cylinderRadius = cylinderRadius;
369
+ this._cylinderHeight = cylinderHeight;
370
+ this._planeBend = planeBend;
371
+ this._planeTwist = planeTwist;
279
372
 
280
373
  this.glState = this._initScene(resolution);
281
374
 
@@ -306,6 +399,45 @@ export class NeatGradient implements NeatController {
306
399
 
307
400
  gl.uniform1f(locations.uniforms['u_time'], tick);
308
401
 
402
+ // Update modelViewMatrix in every frame to support dynamic rotation and auto-rotation
403
+ const camera = this.glState.camera;
404
+ const modelViewMatrix = new Matrix4();
405
+
406
+ // 1. Camera translation (default camera distance + displacement)
407
+ modelViewMatrix.translate(
408
+ -camera.position[0] - this._cameraX,
409
+ -camera.position[1] - this._cameraY,
410
+ -camera.position[2] - this._cameraZ
411
+ );
412
+ modelViewMatrix.translate(0, 0, -1);
413
+
414
+ // 2. Camera rotation (revolving around target)
415
+ modelViewMatrix.rotateX(-this._cameraRotationX);
416
+ modelViewMatrix.rotateY(-this._cameraRotationY);
417
+ modelViewMatrix.rotateZ(-this._cameraRotationZ);
418
+
419
+ let rx = this._shapeRotationX;
420
+ let ry = this._shapeRotationY;
421
+ let rz = this._shapeRotationZ;
422
+
423
+ if (this._shapeAutoRotateSpeedX !== 0) {
424
+ rx += tick * this._shapeAutoRotateSpeedX * 0.1;
425
+ }
426
+ if (this._shapeAutoRotateSpeedY !== 0) {
427
+ ry += tick * this._shapeAutoRotateSpeedY * 0.1;
428
+ }
429
+
430
+ if (this._shapeType === 'plane' || this._shapeType === 'ribbon') {
431
+ modelViewMatrix.rotateX(rx - Math.PI / 3.5);
432
+ } else {
433
+ modelViewMatrix.rotateX(rx);
434
+ }
435
+ modelViewMatrix.rotateY(ry);
436
+ modelViewMatrix.rotateZ(rz);
437
+
438
+ const mvLoc = locations.uniforms["modelViewMatrix"];
439
+ if (mvLoc) gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
440
+
309
441
  // Only upload static uniforms when they've been modified
310
442
  if (this._uniformsDirty) {
311
443
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -333,8 +465,16 @@ export class NeatGradient implements NeatController {
333
465
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
334
466
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
335
467
 
468
+ let shapeTypeVal = 0.0;
469
+ if (this._shapeType === 'sphere') shapeTypeVal = 1.0;
470
+ else if (this._shapeType === 'torus') shapeTypeVal = 2.0;
471
+ else if (this._shapeType === 'cylinder') shapeTypeVal = 3.0;
472
+ else if (this._shapeType === 'ribbon') shapeTypeVal = 4.0;
473
+ gl.uniform1f(locations.uniforms['u_shape_type'], shapeTypeVal);
474
+
336
475
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
337
476
  gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
477
+ gl.uniform1f(locations.uniforms['u_transparent_texture_void'], this._transparentTextureVoid ? 1.0 : 0.0);
338
478
 
339
479
  gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
340
480
  gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
@@ -355,6 +495,9 @@ export class NeatGradient implements NeatController {
355
495
  gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
356
496
  gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
357
497
  gl.uniform1f(locations.uniforms['u_chromatic_aberration'], this._chromaticAberration);
498
+ gl.uniform1f(locations.uniforms['u_silhouette_fade'], this._silhouetteFade);
499
+ gl.uniform1f(locations.uniforms['u_cylinder_fade'], this._cylinderFade);
500
+ gl.uniform1f(locations.uniforms['u_ribbon_fade'], this._ribbonFade);
358
501
 
359
502
  this._uniformsDirty = false;
360
503
  }
@@ -430,14 +573,14 @@ export class NeatGradient implements NeatController {
430
573
 
431
574
  gl.viewport(0, 0, width, height);
432
575
 
433
- updateCamera(camera, width, height);
576
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
434
577
 
435
578
 
436
579
 
437
580
  // Recompute projection matrix on resize
438
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
581
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
439
582
  gl.useProgram(this.glState.program);
440
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
583
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
441
584
  };
442
585
 
443
586
  // Debounce resize to prevent excessive operations
@@ -493,36 +636,226 @@ export class NeatGradient implements NeatController {
493
636
  downloadURI(dataURL, filename);
494
637
  }
495
638
 
639
+ /**
640
+ * Records the canvas animation as a video with a NEAT watermark overlay.
641
+ * @param options.durationMs Recording duration in milliseconds (default 5000).
642
+ * @param options.filename Output file name (default "neat.firecms.co").
643
+ * @param options.width Output video width in pixels (default: current canvas width).
644
+ * @param options.height Output video height in pixels (default: current canvas height).
645
+ * @param options.format Preferred format: 'mp4' or 'webm' (default: best available).
646
+ * @param options.onProgress Callback with progress 0-1.
647
+ * @param options.onComplete Callback when recording finishes.
648
+ * @returns A stop function to end recording early.
649
+ */
650
+ recordVideo(options: {
651
+ durationMs?: number;
652
+ filename?: string;
653
+ width?: number;
654
+ height?: number;
655
+ format?: 'mp4' | 'webm';
656
+ onProgress?: (progress: number) => void;
657
+ onComplete?: () => void;
658
+ } = {}): () => void {
659
+ const {
660
+ durationMs = 5000,
661
+ filename = "neat.firecms.co",
662
+ format,
663
+ onProgress,
664
+ onComplete,
665
+ } = options;
666
+
667
+ const source = this._ref;
668
+ const width = options.width || source.width || source.clientWidth;
669
+ const height = options.height || source.height || source.clientHeight;
670
+
671
+ // Offscreen canvas that composites gradient + watermark each frame
672
+ const offscreen = document.createElement("canvas");
673
+ offscreen.width = width;
674
+ offscreen.height = height;
675
+ const ctx = offscreen.getContext("2d")!;
676
+
677
+ // Use captureStream(0) — only captures a frame when we explicitly
678
+ // call requestFrame() on the video track, so every composited frame
679
+ // is guaranteed to be captured.
680
+ const stream = offscreen.captureStream(0);
681
+ const videoTrack = stream.getVideoTracks()[0];
682
+
683
+ // Codec candidates ordered by preference
684
+ const mp4Candidates = [
685
+ "video/mp4;codecs=avc1",
686
+ "video/mp4;codecs=avc1,opus",
687
+ "video/mp4",
688
+ ];
689
+ const webmCandidates = [
690
+ "video/webm;codecs=vp9,opus",
691
+ "video/webm;codecs=vp9",
692
+ "video/webm;codecs=vp8,opus",
693
+ "video/webm",
694
+ ];
695
+
696
+ // Build candidate list based on preferred format
697
+ let candidates: string[];
698
+ if (format === 'mp4') candidates = [...mp4Candidates, ...webmCandidates];
699
+ else if (format === 'webm') candidates = [...webmCandidates, ...mp4Candidates];
700
+ else candidates = [...mp4Candidates, ...webmCandidates];
701
+
702
+ let mimeType = "video/webm";
703
+ for (const candidate of candidates) {
704
+ if (MediaRecorder.isTypeSupported(candidate)) {
705
+ mimeType = candidate;
706
+ break;
707
+ }
708
+ }
709
+
710
+ // Scale bitrate with pixel count: 8 Mbps baseline at 720p
711
+ const pixels = width * height;
712
+ const baseBitrate = 8_000_000;
713
+ const basePixels = 1280 * 720;
714
+ const videoBitsPerSecond = Math.round(baseBitrate * Math.max(1, pixels / basePixels));
715
+
716
+ const recorder = new MediaRecorder(stream, {
717
+ mimeType,
718
+ videoBitsPerSecond,
719
+ });
720
+
721
+ const chunks: Blob[] = [];
722
+ recorder.ondataavailable = (e) => {
723
+ if (e.data.size > 0) chunks.push(e.data);
724
+ };
725
+
726
+ let stopped = false;
727
+ let rafId: number;
728
+ const startTime = performance.now();
729
+ let lastProgressTime = 0;
730
+
731
+ // Composite loop: draw source canvas + watermark overlay on each frame
732
+ const drawFrame = () => {
733
+ if (stopped) return;
734
+
735
+ ctx.clearRect(0, 0, width, height);
736
+ ctx.drawImage(source, 0, 0, width, height);
737
+
738
+ // Watermark: "NEAT" in bottom-right corner
739
+ const fontSize = Math.max(14, Math.round(height * 0.025));
740
+ ctx.font = `bold ${fontSize}px "Sofia Sans", sans-serif`;
741
+ ctx.textAlign = "right";
742
+ ctx.textBaseline = "bottom";
743
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
744
+ ctx.shadowBlur = 4;
745
+ ctx.shadowOffsetX = 1;
746
+ ctx.shadowOffsetY = 1;
747
+ ctx.fillStyle = "rgba(255,255,255,0.7)";
748
+ ctx.fillText("NEAT", width - fontSize * 0.8, height - fontSize * 0.5);
749
+ ctx.shadowColor = "transparent";
750
+ ctx.shadowBlur = 0;
751
+ ctx.shadowOffsetX = 0;
752
+ ctx.shadowOffsetY = 0;
753
+
754
+ // Signal the stream to capture this frame
755
+ // @ts-ignore – requestFrame exists on CanvasCaptureMediaStreamTrack
756
+ if (videoTrack.requestFrame) videoTrack.requestFrame();
757
+
758
+ // Throttle progress to ~4 updates/sec to avoid flooding React state
759
+ if (onProgress) {
760
+ const now = performance.now();
761
+ if (now - lastProgressTime > 250) {
762
+ lastProgressTime = now;
763
+ onProgress(Math.min(0.99, (now - startTime) / durationMs));
764
+ }
765
+ }
766
+
767
+ rafId = requestAnimationFrame(drawFrame);
768
+ };
769
+
770
+ recorder.onstop = () => {
771
+ stopped = true;
772
+ cancelAnimationFrame(rafId);
773
+
774
+ // Use the correct file extension for the actual format
775
+ const isMP4 = mimeType.startsWith("video/mp4");
776
+ const ext = isMP4 ? ".mp4" : ".webm";
777
+ const blobType = isMP4 ? "video/mp4" : "video/webm";
778
+ const finalFilename = filename + ext;
779
+
780
+ const blob = new Blob(chunks, { type: blobType });
781
+ const url = URL.createObjectURL(blob);
782
+ downloadURI(url, finalFilename);
783
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
784
+
785
+ onProgress?.(1);
786
+ onComplete?.();
787
+ };
788
+
789
+ // Start drawing frames, then start recording
790
+ drawFrame();
791
+ recorder.start(100); // collect data every 100ms
792
+
793
+ // Auto-stop after the requested duration
794
+ const timeoutId = window.setTimeout(() => {
795
+ if (recorder.state === "recording") {
796
+ recorder.stop();
797
+ }
798
+ }, durationMs);
799
+
800
+ // Return a stop function for early termination
801
+ return () => {
802
+ clearTimeout(timeoutId);
803
+ if (recorder.state === "recording") {
804
+ recorder.stop();
805
+ }
806
+ };
807
+ }
808
+ get speed(): number {
809
+ return this._speed * 20;
810
+ }
496
811
  set speed(speed: number) {
497
812
  this._uniformsDirty = true;
498
813
  this._speed = speed / 20;
499
814
  }
500
815
 
816
+ get horizontalPressure(): number {
817
+ return this._horizontalPressure * 4;
818
+ }
501
819
  set horizontalPressure(horizontalPressure: number) {
502
820
  this._uniformsDirty = true;
503
821
  this._horizontalPressure = horizontalPressure / 4;
504
822
  }
505
823
 
824
+ get verticalPressure(): number {
825
+ return this._verticalPressure * 4;
826
+ }
506
827
  set verticalPressure(verticalPressure: number) {
507
828
  this._uniformsDirty = true;
508
829
  this._verticalPressure = verticalPressure / 4;
509
830
  }
510
831
 
832
+ get waveFrequencyX(): number {
833
+ return this._waveFrequencyX / 0.04;
834
+ }
511
835
  set waveFrequencyX(waveFrequencyX: number) {
512
836
  this._uniformsDirty = true;
513
837
  this._waveFrequencyX = waveFrequencyX * 0.04;
514
838
  }
515
839
 
840
+ get waveFrequencyY(): number {
841
+ return this._waveFrequencyY / 0.04;
842
+ }
516
843
  set waveFrequencyY(waveFrequencyY: number) {
517
844
  this._uniformsDirty = true;
518
845
  this._waveFrequencyY = waveFrequencyY * 0.04;
519
846
  }
520
847
 
848
+ get waveAmplitude(): number {
849
+ return this._waveAmplitude / 0.75;
850
+ }
521
851
  set waveAmplitude(waveAmplitude: number) {
522
852
  this._uniformsDirty = true;
523
853
  this._waveAmplitude = waveAmplitude * .75;
524
854
  }
525
855
 
856
+ get colors(): NeatColor[] {
857
+ return this._colors;
858
+ }
526
859
  set colors(colors: NeatColor[]) {
527
860
  this._uniformsDirty = true;
528
861
  this._colors = colors;
@@ -530,81 +863,115 @@ export class NeatGradient implements NeatController {
530
863
  this._colorsChanged = true;
531
864
  }
532
865
 
866
+ get highlights(): number {
867
+ return this._highlights * 100;
868
+ }
533
869
  set highlights(highlights: number) {
534
870
  this._uniformsDirty = true;
535
871
  this._highlights = highlights / 100;
536
872
  }
537
873
 
874
+ get shadows(): number {
875
+ return this._shadows * 100;
876
+ }
538
877
  set shadows(shadows: number) {
539
878
  this._uniformsDirty = true;
540
879
  this._shadows = shadows / 100;
541
880
  }
542
881
 
882
+ get colorSaturation(): number {
883
+ return this._saturation * 10;
884
+ }
543
885
  set colorSaturation(colorSaturation: number) {
544
886
  this._uniformsDirty = true;
545
887
  this._saturation = colorSaturation / 10;
546
888
  }
547
889
 
890
+ get colorBrightness(): number {
891
+ return this._brightness;
892
+ }
548
893
  set colorBrightness(colorBrightness: number) {
549
894
  this._uniformsDirty = true;
550
895
  this._brightness = colorBrightness;
551
896
  }
552
897
 
898
+ get colorBlending(): number {
899
+ return this._colorBlending * 10;
900
+ }
553
901
  set colorBlending(colorBlending: number) {
554
902
  this._uniformsDirty = true;
555
903
  this._colorBlending = colorBlending / 10;
556
904
  }
557
905
 
906
+ get grainScale(): number {
907
+ return this._grainScale;
908
+ }
558
909
  set grainScale(grainScale: number) {
559
910
  this._uniformsDirty = true;
560
911
  this._grainScale = grainScale == 0 ? 1 : grainScale;
561
912
  }
562
913
 
914
+ get grainIntensity(): number {
915
+ return this._grainIntensity;
916
+ }
563
917
  set grainIntensity(grainIntensity: number) {
564
918
  this._uniformsDirty = true;
565
919
  this._grainIntensity = grainIntensity;
566
920
  }
567
921
 
922
+ get grainSparsity(): number {
923
+ return this._grainSparsity;
924
+ }
568
925
  set grainSparsity(grainSparsity: number) {
569
926
  this._uniformsDirty = true;
570
927
  this._grainSparsity = grainSparsity;
571
928
  }
572
929
 
930
+ get grainSpeed(): number {
931
+ return this._grainSpeed;
932
+ }
573
933
  set grainSpeed(grainSpeed: number) {
574
934
  this._uniformsDirty = true;
575
935
  this._grainSpeed = grainSpeed;
576
936
  }
577
937
 
938
+ get wireframe(): boolean {
939
+ return this._wireframe;
940
+ }
578
941
  set wireframe(wireframe: boolean) {
579
942
  this._uniformsDirty = true;
580
943
  this._wireframe = wireframe;
581
944
  }
582
945
 
946
+ get resolution(): number {
947
+ return this._resolution;
948
+ }
583
949
  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);
950
+ if (this._resolution === resolution) return;
951
+ this._resolution = resolution;
952
+ this._updateGeometry();
595
953
  }
596
954
 
955
+ get backgroundColor(): string {
956
+ return this._backgroundColor;
957
+ }
597
958
  set backgroundColor(backgroundColor: string) {
598
959
  this._uniformsDirty = true;
599
960
  this._backgroundColor = backgroundColor;
600
961
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
601
962
  }
602
963
 
964
+ get backgroundAlpha(): number {
965
+ return this._backgroundAlpha;
966
+ }
603
967
  set backgroundAlpha(backgroundAlpha: number) {
604
968
  this._uniformsDirty = true;
605
969
  this._backgroundAlpha = backgroundAlpha;
606
970
  }
607
971
 
972
+ get yOffset(): number {
973
+ return this._yOffset;
974
+ }
608
975
  set yOffset(yOffset: number) {
609
976
  this._uniformsDirty = true;
610
977
  this._yOffset = yOffset;
@@ -637,21 +1004,33 @@ export class NeatGradient implements NeatController {
637
1004
  this._yOffsetFlowMultiplier = value / 1000;
638
1005
  }
639
1006
 
1007
+ get flowDistortionA(): number {
1008
+ return this._flowDistortionA;
1009
+ }
640
1010
  set flowDistortionA(value: number) {
641
1011
  this._uniformsDirty = true;
642
1012
  this._flowDistortionA = value;
643
1013
  }
644
1014
 
1015
+ get flowDistortionB(): number {
1016
+ return this._flowDistortionB;
1017
+ }
645
1018
  set flowDistortionB(value: number) {
646
1019
  this._uniformsDirty = true;
647
1020
  this._flowDistortionB = value;
648
1021
  }
649
1022
 
1023
+ get flowScale(): number {
1024
+ return this._flowScale;
1025
+ }
650
1026
  set flowScale(value: number) {
651
1027
  this._uniformsDirty = true;
652
1028
  this._flowScale = value;
653
1029
  }
654
1030
 
1031
+ get flowEase(): number {
1032
+ return this._flowEase;
1033
+ }
655
1034
  set flowEase(value: number) {
656
1035
  this._uniformsDirty = true;
657
1036
  this._flowEase = value;
@@ -668,6 +1047,9 @@ export class NeatGradient implements NeatController {
668
1047
 
669
1048
 
670
1049
 
1050
+ get enableProceduralTexture(): boolean {
1051
+ return this._enableProceduralTexture;
1052
+ }
671
1053
  set enableProceduralTexture(value: boolean) {
672
1054
  this._uniformsDirty = true;
673
1055
  this._enableProceduralTexture = value;
@@ -676,6 +1058,9 @@ export class NeatGradient implements NeatController {
676
1058
  }
677
1059
  }
678
1060
 
1061
+ get textureVoidLikelihood(): number {
1062
+ return this._textureVoidLikelihood;
1063
+ }
679
1064
  set textureVoidLikelihood(value: number) {
680
1065
  this._uniformsDirty = true;
681
1066
  this._textureVoidLikelihood = value;
@@ -684,6 +1069,9 @@ export class NeatGradient implements NeatController {
684
1069
  }
685
1070
  }
686
1071
 
1072
+ get textureVoidWidthMin(): number {
1073
+ return this._textureVoidWidthMin;
1074
+ }
687
1075
  set textureVoidWidthMin(value: number) {
688
1076
  this._uniformsDirty = true;
689
1077
  this._textureVoidWidthMin = value;
@@ -692,6 +1080,9 @@ export class NeatGradient implements NeatController {
692
1080
  }
693
1081
  }
694
1082
 
1083
+ get textureVoidWidthMax(): number {
1084
+ return this._textureVoidWidthMax;
1085
+ }
695
1086
  set textureVoidWidthMax(value: number) {
696
1087
  this._uniformsDirty = true;
697
1088
  this._textureVoidWidthMax = value;
@@ -700,6 +1091,9 @@ export class NeatGradient implements NeatController {
700
1091
  }
701
1092
  }
702
1093
 
1094
+ get textureBandDensity(): number {
1095
+ return this._textureBandDensity;
1096
+ }
703
1097
  set textureBandDensity(value: number) {
704
1098
  this._uniformsDirty = true;
705
1099
  this._textureBandDensity = value;
@@ -708,6 +1102,9 @@ export class NeatGradient implements NeatController {
708
1102
  }
709
1103
  }
710
1104
 
1105
+ get textureColorBlending(): number {
1106
+ return this._textureColorBlending;
1107
+ }
711
1108
  set textureColorBlending(value: number) {
712
1109
  this._uniformsDirty = true;
713
1110
  this._textureColorBlending = value;
@@ -716,6 +1113,9 @@ export class NeatGradient implements NeatController {
716
1113
  }
717
1114
  }
718
1115
 
1116
+ get textureSeed(): number {
1117
+ return this._textureSeed;
1118
+ }
719
1119
  set textureSeed(value: number) {
720
1120
  this._uniformsDirty = true;
721
1121
  this._textureSeed = value;
@@ -733,6 +1133,21 @@ export class NeatGradient implements NeatController {
733
1133
  this._textureEase = value;
734
1134
  }
735
1135
 
1136
+ get transparentTextureVoid(): boolean {
1137
+ return this._transparentTextureVoid;
1138
+ }
1139
+
1140
+ set transparentTextureVoid(value: boolean) {
1141
+ this._uniformsDirty = true;
1142
+ this._transparentTextureVoid = value;
1143
+ if (this._enableProceduralTexture) {
1144
+ this._textureNeedsUpdate = true;
1145
+ }
1146
+ }
1147
+
1148
+ get proceduralBackgroundColor(): string {
1149
+ return this._proceduralBackgroundColor;
1150
+ }
736
1151
  set proceduralBackgroundColor(value: string) {
737
1152
  this._uniformsDirty = true;
738
1153
  this._proceduralBackgroundColor = value;
@@ -741,27 +1156,93 @@ export class NeatGradient implements NeatController {
741
1156
  }
742
1157
  }
743
1158
 
1159
+ get textureShapeTriangles(): number {
1160
+ return this._textureShapeTriangles;
1161
+ }
744
1162
  set textureShapeTriangles(value: number) {
745
1163
  this._uniformsDirty = true;
746
1164
  this._textureShapeTriangles = value;
747
1165
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
748
1166
  }
1167
+ get textureShapeCircles(): number {
1168
+ return this._textureShapeCircles;
1169
+ }
749
1170
  set textureShapeCircles(value: number) {
750
1171
  this._uniformsDirty = true;
751
1172
  this._textureShapeCircles = value;
752
1173
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
753
1174
  }
1175
+ get textureShapeBars(): number {
1176
+ return this._textureShapeBars;
1177
+ }
754
1178
  set textureShapeBars(value: number) {
755
1179
  this._uniformsDirty = true;
756
1180
  this._textureShapeBars = value;
757
1181
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
758
1182
  }
1183
+ get textureShapeSquiggles(): number {
1184
+ return this._textureShapeSquiggles;
1185
+ }
759
1186
  set textureShapeSquiggles(value: number) {
760
1187
  this._uniformsDirty = true;
761
1188
  this._textureShapeSquiggles = value;
762
1189
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
763
1190
  }
764
1191
 
1192
+ _updateGeometry() {
1193
+ if (!this.glState) return;
1194
+ const gl = this.glState.gl;
1195
+ const resolution = this._resolution || 1;
1196
+
1197
+ let geometry;
1198
+ if (this._shapeType === 'sphere') {
1199
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
1200
+ } else if (this._shapeType === 'torus') {
1201
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
1202
+ } else if (this._shapeType === 'cylinder') {
1203
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
1204
+ } else if (this._shapeType === 'ribbon') {
1205
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
1206
+ } else {
1207
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1208
+ }
1209
+ const { position, normal, uv, index, wireframeIndex } = geometry;
1210
+
1211
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.position);
1212
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
1213
+
1214
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.normal);
1215
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
1216
+
1217
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.uv);
1218
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
1219
+
1220
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
1221
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
1222
+
1223
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
1224
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
1225
+
1226
+ // Restore default bound element buffer
1227
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
1228
+
1229
+ this.glState.indexCount = index.length;
1230
+ this.glState.wireframeIndexCount = wireframeIndex.length;
1231
+ this.glState.indexType = (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
1232
+
1233
+ // Keep camera updated with the new shapeType and dimensions
1234
+ const width = this._ref.clientWidth;
1235
+ const height = this._ref.clientHeight;
1236
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1237
+
1238
+ // Recompute projection matrix
1239
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1240
+ gl.useProgram(this.glState.program);
1241
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1242
+
1243
+ this._uniformsDirty = true;
1244
+ }
1245
+
765
1246
  _hexToRgb(hex: string): [number, number, number] {
766
1247
  const bigint = parseInt(hex.replace('#', ''), 16);
767
1248
  return [
@@ -788,8 +1269,20 @@ export class NeatGradient implements NeatController {
788
1269
 
789
1270
  gl.viewport(0, 0, width, height);
790
1271
 
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);
1272
+ // Generate parametric geometry based on shapeType
1273
+ let geometry;
1274
+ if (this._shapeType === 'sphere') {
1275
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
1276
+ } else if (this._shapeType === 'torus') {
1277
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
1278
+ } else if (this._shapeType === 'cylinder') {
1279
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
1280
+ } else if (this._shapeType === 'ribbon') {
1281
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
1282
+ } else {
1283
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1284
+ }
1285
+ const { position, normal, uv, index, wireframeIndex } = geometry;
793
1286
 
794
1287
  const positionBuffer = gl.createBuffer()!;
795
1288
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
@@ -855,7 +1348,7 @@ export class NeatGradient implements NeatController {
855
1348
 
856
1349
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
857
1350
  camera.position = [0, 0, 5];
858
- updateCamera(camera, width, height);
1351
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
859
1352
 
860
1353
  // Define attributes
861
1354
  const aPosition = gl.getAttribLocation(program, "position");
@@ -876,17 +1369,7 @@ export class NeatGradient implements NeatController {
876
1369
 
877
1370
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
878
1371
 
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);
1372
+ // modelViewMatrix is set dynamically in the render loop
890
1373
 
891
1374
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
892
1375
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
@@ -901,18 +1384,20 @@ export class NeatGradient implements NeatController {
901
1384
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
902
1385
 
903
1386
  const uniformsList = [
1387
+ "projectionMatrix", "modelViewMatrix",
904
1388
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
905
1389
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
906
1390
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
907
1391
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
908
1392
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
909
1393
 
910
- "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending",
1394
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_transparent_texture_void", "u_saturation", "u_brightness", "u_color_blending",
911
1395
  "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
912
1396
  "u_vignette_intensity", "u_vignette_radius",
913
1397
  "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
914
1398
  "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
915
- "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration"
1399
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1400
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade"
916
1401
  ];
917
1402
 
918
1403
  const locations: WebGLState["locations"] = {
@@ -966,10 +1451,15 @@ export class NeatGradient implements NeatController {
966
1451
  // Texture size - 1024 provides good balance between quality and performance
967
1452
  // Reduced from 2048 for better performance
968
1453
  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 });
1454
+
1455
+ if (!this._sourceCanvas) {
1456
+ this._sourceCanvas = document.createElement('canvas');
1457
+ this._sourceCanvas.width = texSize;
1458
+ this._sourceCanvas.height = texSize;
1459
+ this._sourceCtx = this._sourceCanvas.getContext('2d');
1460
+ }
1461
+ const sourceCanvas = this._sourceCanvas;
1462
+ const sCtx = this._sourceCtx;
973
1463
  if (!sCtx) return null;
974
1464
 
975
1465
  let seed = this._textureSeed;
@@ -988,6 +1478,10 @@ export class NeatGradient implements NeatController {
988
1478
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
989
1479
  if (colors.length === 0) return null;
990
1480
 
1481
+ const shouldTile = this._shapeType !== 'plane';
1482
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1483
+ const dys = shouldTile ? [-1, 0, 1] : [0];
1484
+
991
1485
  // Helper functions
992
1486
  function hexToRgb(hex: string) {
993
1487
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -1029,72 +1523,133 @@ export class NeatGradient implements NeatController {
1029
1523
 
1030
1524
  // Triangles: use configurable count
1031
1525
  for (let i = 0; i < this._textureShapeTriangles; i++) {
1032
- sCtx.fillStyle = getInterColor();
1033
- sCtx.beginPath();
1526
+ const fillStyle = getInterColor();
1034
1527
  const x = random() * texSize;
1035
1528
  const y = random() * texSize;
1036
1529
  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();
1530
+ const x1 = (random() - 0.5) * s;
1531
+ const y1 = (random() - 0.5) * s;
1532
+ const x2 = (random() - 0.5) * s;
1533
+ const y2 = (random() - 0.5) * s;
1534
+
1535
+ for (const dx of dxs) {
1536
+ for (const dy of dys) {
1537
+ sCtx.fillStyle = fillStyle;
1538
+ sCtx.beginPath();
1539
+ const tx = x + dx * texSize;
1540
+ const ty = y + dy * texSize;
1541
+ sCtx.moveTo(tx, ty);
1542
+ sCtx.lineTo(tx + x1, ty + y1);
1543
+ sCtx.lineTo(tx + x2, ty + y2);
1544
+ sCtx.fill();
1545
+ }
1546
+ }
1041
1547
  }
1042
1548
 
1043
1549
  // Circles / rings: use configurable count
1044
1550
  for (let i = 0; i < this._textureShapeCircles; i++) {
1045
- sCtx.strokeStyle = getInterColor();
1046
- sCtx.lineWidth = 10 + random() * 50;
1047
- sCtx.beginPath();
1551
+ const strokeStyle = getInterColor();
1552
+ const lineWidth = 10 + random() * 50;
1048
1553
  const x = random() * texSize;
1049
1554
  const y = random() * texSize;
1050
1555
  const r = 50 + random() * 150;
1051
- sCtx.arc(x, y, r, 0, Math.PI * 2);
1052
- sCtx.stroke();
1556
+
1557
+ for (const dx of dxs) {
1558
+ for (const dy of dys) {
1559
+ sCtx.strokeStyle = strokeStyle;
1560
+ sCtx.lineWidth = lineWidth;
1561
+ sCtx.beginPath();
1562
+ sCtx.arc(x + dx * texSize, y + dy * texSize, r, 0, Math.PI * 2);
1563
+ sCtx.stroke();
1564
+ }
1565
+ }
1053
1566
  }
1054
1567
 
1055
1568
  // Bars: use configurable count
1056
1569
  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();
1570
+ const fillStyle = getInterColor();
1571
+ const x = random() * texSize;
1572
+ const y = random() * texSize;
1573
+ const rot = random() * Math.PI;
1574
+
1575
+ for (const dx of dxs) {
1576
+ for (const dy of dys) {
1577
+ sCtx.fillStyle = fillStyle;
1578
+ sCtx.save();
1579
+ sCtx.translate(x + dx * texSize, y + dy * texSize);
1580
+ sCtx.rotate(rot);
1581
+ sCtx.fillRect(-150, -25, 300, 50);
1582
+ sCtx.restore();
1583
+ }
1584
+ }
1063
1585
  }
1064
1586
 
1065
1587
  // Squiggles: use configurable count
1066
1588
  sCtx.lineWidth = 15;
1067
1589
  sCtx.lineCap = 'round';
1068
1590
  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);
1591
+ const strokeStyle = getInterColor();
1592
+ const x = random() * texSize;
1593
+ const y = random() * texSize;
1594
+
1595
+ const curves: Array<{ cx1: number, cy1: number, cx2: number, cy2: number, ex: number, ey: number }> = [];
1596
+ let cx = 0;
1597
+ let cy = 0;
1074
1598
  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;
1599
+ const ex = cx + (random() - 0.5) * 300;
1600
+ const ey = cy + (random() - 0.5) * 300;
1601
+ curves.push({
1602
+ cx1: cx + (random() - 0.5) * 300,
1603
+ cy1: cy + (random() - 0.5) * 300,
1604
+ cx2: cx + (random() - 0.5) * 300,
1605
+ cy2: cy + (random() - 0.5) * 300,
1606
+ ex: ex,
1607
+ ey: ey
1608
+ });
1609
+ cx = ex;
1610
+ cy = ey;
1611
+ }
1612
+
1613
+ for (const dx of dxs) {
1614
+ for (const dy of dys) {
1615
+ sCtx.strokeStyle = strokeStyle;
1616
+ sCtx.beginPath();
1617
+ const tx = x + dx * texSize;
1618
+ const ty = y + dy * texSize;
1619
+ sCtx.moveTo(tx, ty);
1620
+
1621
+ for (const curve of curves) {
1622
+ sCtx.bezierCurveTo(
1623
+ tx + curve.cx1, ty + curve.cy1,
1624
+ tx + curve.cx2, ty + curve.cy2,
1625
+ tx + curve.ex, ty + curve.ey
1626
+ );
1627
+ }
1628
+ sCtx.stroke();
1629
+ }
1082
1630
  }
1083
- sCtx.stroke();
1084
1631
  }
1085
1632
 
1086
1633
  // === MASKED CANVAS ===
1087
1634
  // Masking: Seed isolation
1088
1635
  setSeed(50000);
1089
- const canvas = document.createElement('canvas');
1090
- canvas.width = texSize;
1091
- canvas.height = texSize;
1092
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1636
+ if (!this._maskedCanvas) {
1637
+ this._maskedCanvas = document.createElement('canvas');
1638
+ this._maskedCanvas.width = texSize;
1639
+ this._maskedCanvas.height = texSize;
1640
+ this._maskedCtx = this._maskedCanvas.getContext('2d');
1641
+ }
1642
+ const canvas = this._maskedCanvas;
1643
+ const ctx = this._maskedCtx;
1093
1644
  if (!ctx) return null;
1094
1645
 
1095
1646
  // Start filled with the chosen void color so gaps show that color
1096
- ctx.fillStyle = baseColor;
1097
- ctx.fillRect(0, 0, texSize, texSize);
1647
+ if (this._transparentTextureVoid) {
1648
+ ctx.clearRect(0, 0, texSize, texSize);
1649
+ } else {
1650
+ ctx.fillStyle = baseColor;
1651
+ ctx.fillRect(0, 0, texSize, texSize);
1652
+ }
1098
1653
 
1099
1654
  // Determine layout segments (matter vs void)
1100
1655
  let layoutHead = 0;
@@ -1154,54 +1709,119 @@ export class NeatGradient implements NeatController {
1154
1709
  return tex;
1155
1710
  }
1156
1711
 
1712
+ get silhouetteFade(): number {
1713
+ return this._silhouetteFade;
1714
+ }
1715
+ set silhouetteFade(value: number) {
1716
+ if (this._silhouetteFade !== value) {
1717
+ this._silhouetteFade = value;
1718
+ this._uniformsDirty = true;
1719
+ }
1720
+ }
1721
+
1722
+ get cylinderFade(): number {
1723
+ return this._cylinderFade;
1724
+ }
1725
+ set cylinderFade(value: number) {
1726
+ if (this._cylinderFade !== value) {
1727
+ this._cylinderFade = value;
1728
+ this._uniformsDirty = true;
1729
+ }
1730
+ }
1731
+
1732
+ get ribbonFade(): number {
1733
+ return this._ribbonFade;
1734
+ }
1735
+ set ribbonFade(value: number) {
1736
+ if (this._ribbonFade !== value) {
1737
+ this._ribbonFade = value;
1738
+ this._uniformsDirty = true;
1739
+ }
1740
+ }
1741
+
1742
+ get domainWarpEnabled(): boolean {
1743
+ return this._domainWarpEnabled;
1744
+ }
1157
1745
  set domainWarpEnabled(enabled: boolean) {
1158
1746
  if (this._domainWarpEnabled !== enabled) {
1159
1747
  this._domainWarpEnabled = enabled;
1160
1748
  this._uniformsDirty = true;
1161
1749
  }
1162
1750
  }
1751
+
1752
+ get domainWarpIntensity(): number {
1753
+ return this._domainWarpIntensity;
1754
+ }
1163
1755
  set domainWarpIntensity(intensity: number) {
1164
1756
  if (this._domainWarpIntensity !== intensity) {
1165
1757
  this._domainWarpIntensity = intensity;
1166
1758
  this._uniformsDirty = true;
1167
1759
  }
1168
1760
  }
1761
+
1762
+ get domainWarpScale(): number {
1763
+ return this._domainWarpScale;
1764
+ }
1169
1765
  set domainWarpScale(scale: number) {
1170
1766
  if (this._domainWarpScale !== scale) {
1171
1767
  this._domainWarpScale = scale;
1172
1768
  this._uniformsDirty = true;
1173
1769
  }
1174
1770
  }
1771
+
1772
+ get vignetteIntensity(): number {
1773
+ return this._vignetteIntensity;
1774
+ }
1175
1775
  set vignetteIntensity(intensity: number) {
1176
1776
  if (this._vignetteIntensity !== intensity) {
1177
1777
  this._vignetteIntensity = intensity;
1178
1778
  this._uniformsDirty = true;
1179
1779
  }
1180
1780
  }
1781
+
1782
+ get vignetteRadius(): number {
1783
+ return this._vignetteRadius;
1784
+ }
1181
1785
  set vignetteRadius(radius: number) {
1182
1786
  if (this._vignetteRadius !== radius) {
1183
1787
  this._vignetteRadius = radius;
1184
1788
  this._uniformsDirty = true;
1185
1789
  }
1186
1790
  }
1791
+
1792
+ get fresnelEnabled(): boolean {
1793
+ return this._fresnelEnabled;
1794
+ }
1187
1795
  set fresnelEnabled(enabled: boolean) {
1188
1796
  if (this._fresnelEnabled !== enabled) {
1189
1797
  this._fresnelEnabled = enabled;
1190
1798
  this._uniformsDirty = true;
1191
1799
  }
1192
1800
  }
1801
+
1802
+ get fresnelPower(): number {
1803
+ return this._fresnelPower;
1804
+ }
1193
1805
  set fresnelPower(power: number) {
1194
1806
  if (this._fresnelPower !== power) {
1195
1807
  this._fresnelPower = power;
1196
1808
  this._uniformsDirty = true;
1197
1809
  }
1198
1810
  }
1811
+
1812
+ get fresnelIntensity(): number {
1813
+ return this._fresnelIntensity;
1814
+ }
1199
1815
  set fresnelIntensity(intensity: number) {
1200
1816
  if (this._fresnelIntensity !== intensity) {
1201
1817
  this._fresnelIntensity = intensity;
1202
1818
  this._uniformsDirty = true;
1203
1819
  }
1204
1820
  }
1821
+
1822
+ get fresnelColor(): string {
1823
+ return this._fresnelColor;
1824
+ }
1205
1825
  set fresnelColor(fresnelColor: string) {
1206
1826
  if (this._fresnelColor !== fresnelColor) {
1207
1827
  this._fresnelColor = fresnelColor;
@@ -1209,42 +1829,226 @@ export class NeatGradient implements NeatController {
1209
1829
  this._uniformsDirty = true;
1210
1830
  }
1211
1831
  }
1832
+
1833
+ get iridescenceEnabled(): boolean {
1834
+ return this._iridescenceEnabled;
1835
+ }
1212
1836
  set iridescenceEnabled(enabled: boolean) {
1213
1837
  if (this._iridescenceEnabled !== enabled) {
1214
1838
  this._iridescenceEnabled = enabled;
1215
1839
  this._uniformsDirty = true;
1216
1840
  }
1217
1841
  }
1842
+
1843
+ get iridescenceIntensity(): number {
1844
+ return this._iridescenceIntensity;
1845
+ }
1218
1846
  set iridescenceIntensity(intensity: number) {
1219
1847
  if (this._iridescenceIntensity !== intensity) {
1220
1848
  this._iridescenceIntensity = intensity;
1221
1849
  this._uniformsDirty = true;
1222
1850
  }
1223
1851
  }
1852
+
1853
+ get iridescenceSpeed(): number {
1854
+ return this._iridescenceSpeed;
1855
+ }
1224
1856
  set iridescenceSpeed(speed: number) {
1225
1857
  if (this._iridescenceSpeed !== speed) {
1226
1858
  this._iridescenceSpeed = speed;
1227
1859
  this._uniformsDirty = true;
1228
1860
  }
1229
1861
  }
1862
+
1863
+ get bloomIntensity(): number {
1864
+ return this._bloomIntensity;
1865
+ }
1230
1866
  set bloomIntensity(intensity: number) {
1231
1867
  if (this._bloomIntensity !== intensity) {
1232
1868
  this._bloomIntensity = intensity;
1233
1869
  this._uniformsDirty = true;
1234
1870
  }
1235
1871
  }
1872
+
1873
+ get bloomThreshold(): number {
1874
+ return this._bloomThreshold;
1875
+ }
1236
1876
  set bloomThreshold(threshold: number) {
1237
1877
  if (this._bloomThreshold !== threshold) {
1238
1878
  this._bloomThreshold = threshold;
1239
1879
  this._uniformsDirty = true;
1240
1880
  }
1241
1881
  }
1882
+
1883
+ get chromaticAberration(): number {
1884
+ return this._chromaticAberration;
1885
+ }
1242
1886
  set chromaticAberration(aberration: number) {
1243
1887
  if (this._chromaticAberration !== aberration) {
1244
1888
  this._chromaticAberration = aberration;
1245
1889
  this._uniformsDirty = true;
1246
1890
  }
1247
1891
  }
1892
+
1893
+ // Getters and Setters for 3D Shapes
1894
+ get shapeType(): 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon' {
1895
+ return this._shapeType;
1896
+ }
1897
+ set shapeType(val: 'plane' | 'sphere' | 'torus' | 'cylinder' | 'ribbon') {
1898
+ if (this._shapeType !== val) {
1899
+ this._shapeType = val;
1900
+ this._updateGeometry();
1901
+ }
1902
+ }
1903
+
1904
+ get shapeRotationX(): number { return this._shapeRotationX; }
1905
+ set shapeRotationX(val: number) {
1906
+ this._shapeRotationX = val;
1907
+ this._uniformsDirty = true;
1908
+ }
1909
+
1910
+ get shapeRotationY(): number { return this._shapeRotationY; }
1911
+ set shapeRotationY(val: number) {
1912
+ this._shapeRotationY = val;
1913
+ this._uniformsDirty = true;
1914
+ }
1915
+
1916
+ get shapeRotationZ(): number { return this._shapeRotationZ; }
1917
+ set shapeRotationZ(val: number) {
1918
+ this._shapeRotationZ = val;
1919
+ this._uniformsDirty = true;
1920
+ }
1921
+
1922
+ get shapeAutoRotateSpeedX(): number { return this._shapeAutoRotateSpeedX; }
1923
+ set shapeAutoRotateSpeedX(val: number) {
1924
+ this._shapeAutoRotateSpeedX = val;
1925
+ this._uniformsDirty = true;
1926
+ }
1927
+
1928
+ get shapeAutoRotateSpeedY(): number { return this._shapeAutoRotateSpeedY; }
1929
+ set shapeAutoRotateSpeedY(val: number) {
1930
+ this._shapeAutoRotateSpeedY = val;
1931
+ this._uniformsDirty = true;
1932
+ }
1933
+
1934
+ get sphereRadius(): number { return this._sphereRadius; }
1935
+ set sphereRadius(val: number) {
1936
+ if (this._sphereRadius !== val) {
1937
+ this._sphereRadius = val;
1938
+ this._updateGeometry();
1939
+ }
1940
+ }
1941
+
1942
+ get torusRadius(): number { return this._torusRadius; }
1943
+ set torusRadius(val: number) {
1944
+ if (this._torusRadius !== val) {
1945
+ this._torusRadius = val;
1946
+ this._updateGeometry();
1947
+ }
1948
+ }
1949
+
1950
+ get torusTube(): number { return this._torusTube; }
1951
+ set torusTube(val: number) {
1952
+ if (this._torusTube !== val) {
1953
+ this._torusTube = val;
1954
+ this._updateGeometry();
1955
+ }
1956
+ }
1957
+
1958
+ get cylinderRadius(): number { return this._cylinderRadius; }
1959
+ set cylinderRadius(val: number) {
1960
+ if (this._cylinderRadius !== val) {
1961
+ this._cylinderRadius = val;
1962
+ this._updateGeometry();
1963
+ }
1964
+ }
1965
+
1966
+ get cylinderHeight(): number { return this._cylinderHeight; }
1967
+ set cylinderHeight(val: number) {
1968
+ if (this._cylinderHeight !== val) {
1969
+ this._cylinderHeight = val;
1970
+ this._updateGeometry();
1971
+ }
1972
+ }
1973
+
1974
+ get planeBend(): number { return this._planeBend; }
1975
+ set planeBend(val: number) {
1976
+ if (this._planeBend !== val) {
1977
+ this._planeBend = val;
1978
+ this._updateGeometry();
1979
+ }
1980
+ }
1981
+
1982
+ get planeTwist(): number { return this._planeTwist; }
1983
+ set planeTwist(val: number) {
1984
+ if (this._planeTwist !== val) {
1985
+ this._planeTwist = val;
1986
+ this._updateGeometry();
1987
+ }
1988
+ }
1989
+
1990
+ // Camera Getters and Setters
1991
+ get cameraLock(): boolean { return this._cameraLock; }
1992
+ set cameraLock(val: boolean) {
1993
+ this._cameraLock = val;
1994
+ }
1995
+
1996
+ get cameraX(): number { return this._cameraX; }
1997
+ set cameraX(val: number) {
1998
+ this._cameraX = val;
1999
+ this._uniformsDirty = true;
2000
+ }
2001
+
2002
+ get cameraY(): number { return this._cameraY; }
2003
+ set cameraY(val: number) {
2004
+ this._cameraY = val;
2005
+ this._uniformsDirty = true;
2006
+ }
2007
+
2008
+ get cameraZ(): number { return this._cameraZ; }
2009
+ set cameraZ(val: number) {
2010
+ this._cameraZ = val;
2011
+ this._uniformsDirty = true;
2012
+ }
2013
+
2014
+ get cameraRotationX(): number { return this._cameraRotationX; }
2015
+ set cameraRotationX(val: number) {
2016
+ this._cameraRotationX = val;
2017
+ this._uniformsDirty = true;
2018
+ }
2019
+
2020
+ get cameraRotationY(): number { return this._cameraRotationY; }
2021
+ set cameraRotationY(val: number) {
2022
+ this._cameraRotationY = val;
2023
+ this._uniformsDirty = true;
2024
+ }
2025
+
2026
+ get cameraRotationZ(): number { return this._cameraRotationZ; }
2027
+ set cameraRotationZ(val: number) {
2028
+ this._cameraRotationZ = val;
2029
+ this._uniformsDirty = true;
2030
+ }
2031
+
2032
+ get cameraZoom(): number { return this._cameraZoom; }
2033
+ set cameraZoom(val: number) {
2034
+ if (this._cameraZoom !== val) {
2035
+ this._cameraZoom = val;
2036
+ this._updateCameraFrustum();
2037
+ }
2038
+ }
2039
+
2040
+ _updateCameraFrustum() {
2041
+ if (!this.glState) return;
2042
+ const gl = this.glState.gl;
2043
+ const width = this._ref.clientWidth;
2044
+ const height = this._ref.clientHeight;
2045
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
2046
+
2047
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
2048
+ gl.useProgram(this.glState.program);
2049
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
2050
+ this._uniformsDirty = true;
2051
+ }
1248
2052
  }
1249
2053
 
1250
2054