@firecms/neat 0.7.1 → 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",
@@ -35,67 +35,7 @@ export interface WebGLState {
35
35
  }
36
36
 
37
37
 
38
-
39
- export type NeatConfig = {
40
- resolution?: number;
41
- speed?: number;
42
- horizontalPressure?: number;
43
- verticalPressure?: number;
44
- waveFrequencyX?: number;
45
- waveFrequencyY?: number;
46
- waveAmplitude?: number;
47
- highlights?: number;
48
- shadows?: number;
49
- colorSaturation?: number;
50
- colorBrightness?: number;
51
- colors: NeatColor[];
52
- colorBlending?: number;
53
- grainScale?: number;
54
- grainIntensity?: number;
55
- grainSparsity?: number;
56
- grainSpeed?: number;
57
- wireframe?: boolean;
58
- backgroundColor?: string;
59
- backgroundAlpha?: number;
60
- yOffset?: number;
61
- yOffsetWaveMultiplier?: number;
62
- yOffsetColorMultiplier?: number;
63
- yOffsetFlowMultiplier?: number;
64
- // Flow field parameters
65
- flowDistortionA?: number;
66
- flowDistortionB?: number;
67
- flowScale?: number;
68
- flowEase?: number;
69
- flowEnabled?: boolean;
70
-
71
- // Texture generation
72
- enableProceduralTexture?: boolean;
73
- textureVoidLikelihood?: number;
74
- textureVoidWidthMin?: number;
75
- textureVoidWidthMax?: number;
76
- textureBandDensity?: number;
77
- textureColorBlending?: number;
78
- textureSeed?: number;
79
- textureEase?: number;
80
- proceduralBackgroundColor?: string;
81
- textureShapeTriangles?: number;
82
- textureShapeCircles?: number;
83
- textureShapeBars?: number;
84
- textureShapeSquiggles?: number;
85
- };
86
-
87
- export type NeatColor = {
88
- color: string;
89
- enabled: boolean;
90
- /**
91
- * Value from 0 to 1
92
- */
93
- influence?: number;
94
- }
95
-
96
- export type NeatController = {
97
- destroy: () => void;
98
- }
38
+ import { NeatConfig, NeatColor, NeatController } from "./types";
99
39
 
100
40
  export class NeatGradient implements NeatController {
101
41
 
@@ -121,6 +61,7 @@ export class NeatGradient implements NeatController {
121
61
  private _grainSpeed: number = -1;
122
62
 
123
63
  private _colorBlending: number = -1;
64
+ private _resolution: number = 1;
124
65
 
125
66
  private _colors: NeatColor[] = [];
126
67
  private _wireframe: boolean = false;
@@ -147,6 +88,58 @@ export class NeatGradient implements NeatController {
147
88
  private _textureColorBlending: number = 0.01;
148
89
  private _textureSeed: number = 333;
149
90
  private _textureEase: number = 0.5;
91
+ private _transparentTextureVoid: boolean = false;
92
+
93
+ // New effects
94
+ private _domainWarpEnabled: boolean = false;
95
+ private _domainWarpIntensity: number = 0.5;
96
+ private _domainWarpScale: number = 1.0;
97
+
98
+ private _vignetteIntensity: number = 0.5;
99
+ private _vignetteRadius: number = 0.8;
100
+
101
+ private _fresnelEnabled: boolean = false;
102
+ private _fresnelPower: number = 2.0;
103
+ private _fresnelIntensity: number = 0.5;
104
+ private _fresnelColor: string = "#FFFFFF";
105
+ private _fresnelColorRgb: [number, number, number] = [1, 1, 1];
106
+
107
+ private _iridescenceEnabled: boolean = false;
108
+ private _iridescenceIntensity: number = 0.5;
109
+ private _iridescenceSpeed: number = 1.0;
110
+
111
+ private _bloomIntensity: number = 0;
112
+ private _bloomThreshold: number = 0.7;
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;
142
+
150
143
  private _proceduralTexture: WebGLTexture | null = null;
151
144
  private _proceduralBackgroundColor: string = "#000000";
152
145
 
@@ -167,6 +160,12 @@ export class NeatGradient implements NeatController {
167
160
  private _yOffsetColorMultiplier: number = 0.004;
168
161
  private _yOffsetFlowMultiplier: number = 0.004;
169
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
+
170
169
  // Performance optimizations
171
170
  private _resizeTimeoutId: number | null = null;
172
171
  private _textureNeedsUpdate: boolean = false;
@@ -221,10 +220,55 @@ export class NeatGradient implements NeatController {
221
220
  textureSeed = 333,
222
221
  textureEase = 0.5,
223
222
  proceduralBackgroundColor = "#000000",
223
+ transparentTextureVoid = false,
224
224
  textureShapeTriangles = 20,
225
225
  textureShapeCircles = 15,
226
226
  textureShapeBars = 15,
227
227
  textureShapeSquiggles = 10,
228
+
229
+ domainWarpEnabled = false,
230
+ domainWarpIntensity = 0.5,
231
+ domainWarpScale = 1.0,
232
+ vignetteIntensity = 0.0,
233
+ vignetteRadius = 0.8,
234
+ fresnelEnabled = false,
235
+ fresnelPower = 2.0,
236
+ fresnelIntensity = 0.5,
237
+ fresnelColor = "#FFFFFF",
238
+ iridescenceEnabled = false,
239
+ iridescenceIntensity = 0.5,
240
+ iridescenceSpeed = 1.0,
241
+ bloomIntensity = 0.0,
242
+ bloomThreshold = 0.7,
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,
228
272
  } = config;
229
273
 
230
274
 
@@ -240,6 +284,7 @@ export class NeatGradient implements NeatController {
240
284
  this.waveFrequencyY = waveFrequencyY;
241
285
  this.waveAmplitude = waveAmplitude;
242
286
  this.colorBlending = colorBlending;
287
+ this._resolution = resolution;
243
288
  this.grainScale = grainScale;
244
289
  this.grainIntensity = grainIntensity;
245
290
  this.grainSparsity = grainSparsity;
@@ -276,12 +321,54 @@ export class NeatGradient implements NeatController {
276
321
  this.textureSeed = textureSeed;
277
322
  this.textureEase = textureEase;
278
323
  this._proceduralBackgroundColor = proceduralBackgroundColor;
324
+ this.transparentTextureVoid = transparentTextureVoid;
279
325
 
280
326
  this._textureShapeTriangles = textureShapeTriangles;
281
327
  this._textureShapeCircles = textureShapeCircles;
282
328
  this._textureShapeBars = textureShapeBars;
283
329
  this._textureShapeSquiggles = textureShapeSquiggles;
284
330
 
331
+ this.domainWarpEnabled = domainWarpEnabled;
332
+ this.domainWarpIntensity = domainWarpIntensity;
333
+ this.domainWarpScale = domainWarpScale;
334
+ this.vignetteIntensity = vignetteIntensity;
335
+ this.vignetteRadius = vignetteRadius;
336
+ this.fresnelEnabled = fresnelEnabled;
337
+ this.fresnelPower = fresnelPower;
338
+ this.fresnelIntensity = fresnelIntensity;
339
+ this.fresnelColor = fresnelColor;
340
+ this.iridescenceEnabled = iridescenceEnabled;
341
+ this.iridescenceIntensity = iridescenceIntensity;
342
+ this.iridescenceSpeed = iridescenceSpeed;
343
+ this.bloomIntensity = bloomIntensity;
344
+ this.bloomThreshold = bloomThreshold;
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;
285
372
 
286
373
  this.glState = this._initScene(resolution);
287
374
 
@@ -312,6 +399,45 @@ export class NeatGradient implements NeatController {
312
399
 
313
400
  gl.uniform1f(locations.uniforms['u_time'], tick);
314
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
+
315
441
  // Only upload static uniforms when they've been modified
316
442
  if (this._uniformsDirty) {
317
443
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -339,8 +465,39 @@ export class NeatGradient implements NeatController {
339
465
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
340
466
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
341
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
+
342
475
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
343
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);
478
+
479
+ gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
480
+ gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
481
+ gl.uniform1f(locations.uniforms['u_domain_warp_scale'], this._domainWarpScale);
482
+
483
+ gl.uniform1f(locations.uniforms['u_vignette_intensity'], this._vignetteIntensity);
484
+ gl.uniform1f(locations.uniforms['u_vignette_radius'], this._vignetteRadius);
485
+
486
+ gl.uniform1f(locations.uniforms['u_fresnel_enabled'], this._fresnelEnabled ? 1.0 : 0.0);
487
+ gl.uniform1f(locations.uniforms['u_fresnel_power'], this._fresnelPower);
488
+ gl.uniform1f(locations.uniforms['u_fresnel_intensity'], this._fresnelIntensity);
489
+ gl.uniform3fv(locations.uniforms['u_fresnel_color'], this._fresnelColorRgb);
490
+
491
+ gl.uniform1f(locations.uniforms['u_iridescence_enabled'], this._iridescenceEnabled ? 1.0 : 0.0);
492
+ gl.uniform1f(locations.uniforms['u_iridescence_intensity'], this._iridescenceIntensity);
493
+ gl.uniform1f(locations.uniforms['u_iridescence_speed'], this._iridescenceSpeed);
494
+
495
+ gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
496
+ gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
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);
344
501
 
345
502
  this._uniformsDirty = false;
346
503
  }
@@ -416,14 +573,14 @@ export class NeatGradient implements NeatController {
416
573
 
417
574
  gl.viewport(0, 0, width, height);
418
575
 
419
- updateCamera(camera, width, height);
576
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
420
577
 
421
578
 
422
579
 
423
580
  // Recompute projection matrix on resize
424
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
581
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
425
582
  gl.useProgram(this.glState.program);
426
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
583
+ if (projLoc) gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
427
584
  };
428
585
 
429
586
  // Debounce resize to prevent excessive operations
@@ -479,36 +636,226 @@ export class NeatGradient implements NeatController {
479
636
  downloadURI(dataURL, filename);
480
637
  }
481
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
+ }
482
811
  set speed(speed: number) {
483
812
  this._uniformsDirty = true;
484
813
  this._speed = speed / 20;
485
814
  }
486
815
 
816
+ get horizontalPressure(): number {
817
+ return this._horizontalPressure * 4;
818
+ }
487
819
  set horizontalPressure(horizontalPressure: number) {
488
820
  this._uniformsDirty = true;
489
821
  this._horizontalPressure = horizontalPressure / 4;
490
822
  }
491
823
 
824
+ get verticalPressure(): number {
825
+ return this._verticalPressure * 4;
826
+ }
492
827
  set verticalPressure(verticalPressure: number) {
493
828
  this._uniformsDirty = true;
494
829
  this._verticalPressure = verticalPressure / 4;
495
830
  }
496
831
 
832
+ get waveFrequencyX(): number {
833
+ return this._waveFrequencyX / 0.04;
834
+ }
497
835
  set waveFrequencyX(waveFrequencyX: number) {
498
836
  this._uniformsDirty = true;
499
837
  this._waveFrequencyX = waveFrequencyX * 0.04;
500
838
  }
501
839
 
840
+ get waveFrequencyY(): number {
841
+ return this._waveFrequencyY / 0.04;
842
+ }
502
843
  set waveFrequencyY(waveFrequencyY: number) {
503
844
  this._uniformsDirty = true;
504
845
  this._waveFrequencyY = waveFrequencyY * 0.04;
505
846
  }
506
847
 
848
+ get waveAmplitude(): number {
849
+ return this._waveAmplitude / 0.75;
850
+ }
507
851
  set waveAmplitude(waveAmplitude: number) {
508
852
  this._uniformsDirty = true;
509
853
  this._waveAmplitude = waveAmplitude * .75;
510
854
  }
511
855
 
856
+ get colors(): NeatColor[] {
857
+ return this._colors;
858
+ }
512
859
  set colors(colors: NeatColor[]) {
513
860
  this._uniformsDirty = true;
514
861
  this._colors = colors;
@@ -516,81 +863,115 @@ export class NeatGradient implements NeatController {
516
863
  this._colorsChanged = true;
517
864
  }
518
865
 
866
+ get highlights(): number {
867
+ return this._highlights * 100;
868
+ }
519
869
  set highlights(highlights: number) {
520
870
  this._uniformsDirty = true;
521
871
  this._highlights = highlights / 100;
522
872
  }
523
873
 
874
+ get shadows(): number {
875
+ return this._shadows * 100;
876
+ }
524
877
  set shadows(shadows: number) {
525
878
  this._uniformsDirty = true;
526
879
  this._shadows = shadows / 100;
527
880
  }
528
881
 
882
+ get colorSaturation(): number {
883
+ return this._saturation * 10;
884
+ }
529
885
  set colorSaturation(colorSaturation: number) {
530
886
  this._uniformsDirty = true;
531
887
  this._saturation = colorSaturation / 10;
532
888
  }
533
889
 
890
+ get colorBrightness(): number {
891
+ return this._brightness;
892
+ }
534
893
  set colorBrightness(colorBrightness: number) {
535
894
  this._uniformsDirty = true;
536
895
  this._brightness = colorBrightness;
537
896
  }
538
897
 
898
+ get colorBlending(): number {
899
+ return this._colorBlending * 10;
900
+ }
539
901
  set colorBlending(colorBlending: number) {
540
902
  this._uniformsDirty = true;
541
903
  this._colorBlending = colorBlending / 10;
542
904
  }
543
905
 
906
+ get grainScale(): number {
907
+ return this._grainScale;
908
+ }
544
909
  set grainScale(grainScale: number) {
545
910
  this._uniformsDirty = true;
546
911
  this._grainScale = grainScale == 0 ? 1 : grainScale;
547
912
  }
548
913
 
914
+ get grainIntensity(): number {
915
+ return this._grainIntensity;
916
+ }
549
917
  set grainIntensity(grainIntensity: number) {
550
918
  this._uniformsDirty = true;
551
919
  this._grainIntensity = grainIntensity;
552
920
  }
553
921
 
922
+ get grainSparsity(): number {
923
+ return this._grainSparsity;
924
+ }
554
925
  set grainSparsity(grainSparsity: number) {
555
926
  this._uniformsDirty = true;
556
927
  this._grainSparsity = grainSparsity;
557
928
  }
558
929
 
930
+ get grainSpeed(): number {
931
+ return this._grainSpeed;
932
+ }
559
933
  set grainSpeed(grainSpeed: number) {
560
934
  this._uniformsDirty = true;
561
935
  this._grainSpeed = grainSpeed;
562
936
  }
563
937
 
938
+ get wireframe(): boolean {
939
+ return this._wireframe;
940
+ }
564
941
  set wireframe(wireframe: boolean) {
565
942
  this._uniformsDirty = true;
566
943
  this._wireframe = wireframe;
567
944
  }
568
945
 
946
+ get resolution(): number {
947
+ return this._resolution;
948
+ }
569
949
  set resolution(resolution: number) {
570
- this._uniformsDirty = true;
571
- if (this.glState) {
572
- const gl = this.glState.gl;
573
- gl.deleteProgram(this.glState.program);
574
- gl.deleteBuffer(this.glState.buffers.position);
575
- gl.deleteBuffer(this.glState.buffers.normal);
576
- gl.deleteBuffer(this.glState.buffers.uv);
577
- gl.deleteBuffer(this.glState.buffers.index);
578
- gl.deleteBuffer(this.glState.buffers.wireframeIndex);
579
- }
580
- this.glState = this._initScene(resolution);
950
+ if (this._resolution === resolution) return;
951
+ this._resolution = resolution;
952
+ this._updateGeometry();
581
953
  }
582
954
 
955
+ get backgroundColor(): string {
956
+ return this._backgroundColor;
957
+ }
583
958
  set backgroundColor(backgroundColor: string) {
584
959
  this._uniformsDirty = true;
585
960
  this._backgroundColor = backgroundColor;
586
961
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
587
962
  }
588
963
 
964
+ get backgroundAlpha(): number {
965
+ return this._backgroundAlpha;
966
+ }
589
967
  set backgroundAlpha(backgroundAlpha: number) {
590
968
  this._uniformsDirty = true;
591
969
  this._backgroundAlpha = backgroundAlpha;
592
970
  }
593
971
 
972
+ get yOffset(): number {
973
+ return this._yOffset;
974
+ }
594
975
  set yOffset(yOffset: number) {
595
976
  this._uniformsDirty = true;
596
977
  this._yOffset = yOffset;
@@ -623,21 +1004,33 @@ export class NeatGradient implements NeatController {
623
1004
  this._yOffsetFlowMultiplier = value / 1000;
624
1005
  }
625
1006
 
1007
+ get flowDistortionA(): number {
1008
+ return this._flowDistortionA;
1009
+ }
626
1010
  set flowDistortionA(value: number) {
627
1011
  this._uniformsDirty = true;
628
1012
  this._flowDistortionA = value;
629
1013
  }
630
1014
 
1015
+ get flowDistortionB(): number {
1016
+ return this._flowDistortionB;
1017
+ }
631
1018
  set flowDistortionB(value: number) {
632
1019
  this._uniformsDirty = true;
633
1020
  this._flowDistortionB = value;
634
1021
  }
635
1022
 
1023
+ get flowScale(): number {
1024
+ return this._flowScale;
1025
+ }
636
1026
  set flowScale(value: number) {
637
1027
  this._uniformsDirty = true;
638
1028
  this._flowScale = value;
639
1029
  }
640
1030
 
1031
+ get flowEase(): number {
1032
+ return this._flowEase;
1033
+ }
641
1034
  set flowEase(value: number) {
642
1035
  this._uniformsDirty = true;
643
1036
  this._flowEase = value;
@@ -654,6 +1047,9 @@ export class NeatGradient implements NeatController {
654
1047
 
655
1048
 
656
1049
 
1050
+ get enableProceduralTexture(): boolean {
1051
+ return this._enableProceduralTexture;
1052
+ }
657
1053
  set enableProceduralTexture(value: boolean) {
658
1054
  this._uniformsDirty = true;
659
1055
  this._enableProceduralTexture = value;
@@ -662,6 +1058,9 @@ export class NeatGradient implements NeatController {
662
1058
  }
663
1059
  }
664
1060
 
1061
+ get textureVoidLikelihood(): number {
1062
+ return this._textureVoidLikelihood;
1063
+ }
665
1064
  set textureVoidLikelihood(value: number) {
666
1065
  this._uniformsDirty = true;
667
1066
  this._textureVoidLikelihood = value;
@@ -670,6 +1069,9 @@ export class NeatGradient implements NeatController {
670
1069
  }
671
1070
  }
672
1071
 
1072
+ get textureVoidWidthMin(): number {
1073
+ return this._textureVoidWidthMin;
1074
+ }
673
1075
  set textureVoidWidthMin(value: number) {
674
1076
  this._uniformsDirty = true;
675
1077
  this._textureVoidWidthMin = value;
@@ -678,6 +1080,9 @@ export class NeatGradient implements NeatController {
678
1080
  }
679
1081
  }
680
1082
 
1083
+ get textureVoidWidthMax(): number {
1084
+ return this._textureVoidWidthMax;
1085
+ }
681
1086
  set textureVoidWidthMax(value: number) {
682
1087
  this._uniformsDirty = true;
683
1088
  this._textureVoidWidthMax = value;
@@ -686,6 +1091,9 @@ export class NeatGradient implements NeatController {
686
1091
  }
687
1092
  }
688
1093
 
1094
+ get textureBandDensity(): number {
1095
+ return this._textureBandDensity;
1096
+ }
689
1097
  set textureBandDensity(value: number) {
690
1098
  this._uniformsDirty = true;
691
1099
  this._textureBandDensity = value;
@@ -694,6 +1102,9 @@ export class NeatGradient implements NeatController {
694
1102
  }
695
1103
  }
696
1104
 
1105
+ get textureColorBlending(): number {
1106
+ return this._textureColorBlending;
1107
+ }
697
1108
  set textureColorBlending(value: number) {
698
1109
  this._uniformsDirty = true;
699
1110
  this._textureColorBlending = value;
@@ -702,6 +1113,9 @@ export class NeatGradient implements NeatController {
702
1113
  }
703
1114
  }
704
1115
 
1116
+ get textureSeed(): number {
1117
+ return this._textureSeed;
1118
+ }
705
1119
  set textureSeed(value: number) {
706
1120
  this._uniformsDirty = true;
707
1121
  this._textureSeed = value;
@@ -719,6 +1133,21 @@ export class NeatGradient implements NeatController {
719
1133
  this._textureEase = value;
720
1134
  }
721
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
+ }
722
1151
  set proceduralBackgroundColor(value: string) {
723
1152
  this._uniformsDirty = true;
724
1153
  this._proceduralBackgroundColor = value;
@@ -727,27 +1156,93 @@ export class NeatGradient implements NeatController {
727
1156
  }
728
1157
  }
729
1158
 
1159
+ get textureShapeTriangles(): number {
1160
+ return this._textureShapeTriangles;
1161
+ }
730
1162
  set textureShapeTriangles(value: number) {
731
1163
  this._uniformsDirty = true;
732
1164
  this._textureShapeTriangles = value;
733
1165
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
734
1166
  }
1167
+ get textureShapeCircles(): number {
1168
+ return this._textureShapeCircles;
1169
+ }
735
1170
  set textureShapeCircles(value: number) {
736
1171
  this._uniformsDirty = true;
737
1172
  this._textureShapeCircles = value;
738
1173
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
739
1174
  }
1175
+ get textureShapeBars(): number {
1176
+ return this._textureShapeBars;
1177
+ }
740
1178
  set textureShapeBars(value: number) {
741
1179
  this._uniformsDirty = true;
742
1180
  this._textureShapeBars = value;
743
1181
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
744
1182
  }
1183
+ get textureShapeSquiggles(): number {
1184
+ return this._textureShapeSquiggles;
1185
+ }
745
1186
  set textureShapeSquiggles(value: number) {
746
1187
  this._uniformsDirty = true;
747
1188
  this._textureShapeSquiggles = value;
748
1189
  if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
749
1190
  }
750
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
+
751
1246
  _hexToRgb(hex: string): [number, number, number] {
752
1247
  const bigint = parseInt(hex.replace('#', ''), 16);
753
1248
  return [
@@ -774,8 +1269,20 @@ export class NeatGradient implements NeatController {
774
1269
 
775
1270
  gl.viewport(0, 0, width, height);
776
1271
 
777
- // Generate plane geometry with Uint32Array for large meshes
778
- const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
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;
779
1286
 
780
1287
  const positionBuffer = gl.createBuffer()!;
781
1288
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
@@ -841,7 +1348,7 @@ export class NeatGradient implements NeatController {
841
1348
 
842
1349
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
843
1350
  camera.position = [0, 0, 5];
844
- updateCamera(camera, width, height);
1351
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
845
1352
 
846
1353
  // Define attributes
847
1354
  const aPosition = gl.getAttribLocation(program, "position");
@@ -862,17 +1369,7 @@ export class NeatGradient implements NeatController {
862
1369
 
863
1370
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
864
1371
 
865
- const modelViewMatrix = new Matrix4();
866
- // The View Matrix is the inverse of the Camera's position
867
- // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
868
- modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
869
-
870
- // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
871
- modelViewMatrix.translate(0, 0, -1);
872
- modelViewMatrix.rotateX(-Math.PI / 3.5);
873
-
874
- const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
875
- gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
1372
+ // modelViewMatrix is set dynamically in the render loop
876
1373
 
877
1374
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
878
1375
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
@@ -887,13 +1384,20 @@ export class NeatGradient implements NeatController {
887
1384
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
888
1385
 
889
1386
  const uniformsList = [
1387
+ "projectionMatrix", "modelViewMatrix",
890
1388
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
891
1389
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
892
1390
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
893
1391
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
894
1392
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
895
1393
 
896
- "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",
1395
+ "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
1396
+ "u_vignette_intensity", "u_vignette_radius",
1397
+ "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
1398
+ "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
1399
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1400
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade"
897
1401
  ];
898
1402
 
899
1403
  const locations: WebGLState["locations"] = {
@@ -947,10 +1451,15 @@ export class NeatGradient implements NeatController {
947
1451
  // Texture size - 1024 provides good balance between quality and performance
948
1452
  // Reduced from 2048 for better performance
949
1453
  const texSize = 1024;
950
- const sourceCanvas = document.createElement('canvas');
951
- sourceCanvas.width = texSize;
952
- sourceCanvas.height = texSize;
953
- 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;
954
1463
  if (!sCtx) return null;
955
1464
 
956
1465
  let seed = this._textureSeed;
@@ -969,6 +1478,10 @@ export class NeatGradient implements NeatController {
969
1478
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
970
1479
  if (colors.length === 0) return null;
971
1480
 
1481
+ const shouldTile = this._shapeType !== 'plane';
1482
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1483
+ const dys = shouldTile ? [-1, 0, 1] : [0];
1484
+
972
1485
  // Helper functions
973
1486
  function hexToRgb(hex: string) {
974
1487
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -1010,72 +1523,133 @@ export class NeatGradient implements NeatController {
1010
1523
 
1011
1524
  // Triangles: use configurable count
1012
1525
  for (let i = 0; i < this._textureShapeTriangles; i++) {
1013
- sCtx.fillStyle = getInterColor();
1014
- sCtx.beginPath();
1526
+ const fillStyle = getInterColor();
1015
1527
  const x = random() * texSize;
1016
1528
  const y = random() * texSize;
1017
1529
  const s = 100 + random() * 300;
1018
- sCtx.moveTo(x, y);
1019
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1020
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
1021
- 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
+ }
1022
1547
  }
1023
1548
 
1024
1549
  // Circles / rings: use configurable count
1025
1550
  for (let i = 0; i < this._textureShapeCircles; i++) {
1026
- sCtx.strokeStyle = getInterColor();
1027
- sCtx.lineWidth = 10 + random() * 50;
1028
- sCtx.beginPath();
1551
+ const strokeStyle = getInterColor();
1552
+ const lineWidth = 10 + random() * 50;
1029
1553
  const x = random() * texSize;
1030
1554
  const y = random() * texSize;
1031
1555
  const r = 50 + random() * 150;
1032
- sCtx.arc(x, y, r, 0, Math.PI * 2);
1033
- 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
+ }
1034
1566
  }
1035
1567
 
1036
1568
  // Bars: use configurable count
1037
1569
  for (let i = 0; i < this._textureShapeBars; i++) {
1038
- sCtx.fillStyle = getInterColor();
1039
- sCtx.save();
1040
- sCtx.translate(random() * texSize, random() * texSize);
1041
- sCtx.rotate(random() * Math.PI);
1042
- sCtx.fillRect(-150, -25, 300, 50);
1043
- 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
+ }
1044
1585
  }
1045
1586
 
1046
1587
  // Squiggles: use configurable count
1047
1588
  sCtx.lineWidth = 15;
1048
1589
  sCtx.lineCap = 'round';
1049
1590
  for (let i = 0; i < this._textureShapeSquiggles; i++) {
1050
- sCtx.strokeStyle = getInterColor();
1051
- sCtx.beginPath();
1052
- let x = random() * texSize;
1053
- let y = random() * texSize;
1054
- 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;
1055
1598
  for (let j = 0; j < 4; j++) {
1056
- sCtx.bezierCurveTo(
1057
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1058
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300,
1059
- x + (random() - 0.5) * 300, y + (random() - 0.5) * 300
1060
- );
1061
- x += (random() - 0.5) * 300;
1062
- 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
+ }
1063
1630
  }
1064
- sCtx.stroke();
1065
1631
  }
1066
1632
 
1067
1633
  // === MASKED CANVAS ===
1068
1634
  // Masking: Seed isolation
1069
1635
  setSeed(50000);
1070
- const canvas = document.createElement('canvas');
1071
- canvas.width = texSize;
1072
- canvas.height = texSize;
1073
- 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;
1074
1644
  if (!ctx) return null;
1075
1645
 
1076
1646
  // Start filled with the chosen void color so gaps show that color
1077
- ctx.fillStyle = baseColor;
1078
- 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
+ }
1079
1653
 
1080
1654
  // Determine layout segments (matter vs void)
1081
1655
  let layoutHead = 0;
@@ -1135,7 +1709,346 @@ export class NeatGradient implements NeatController {
1135
1709
  return tex;
1136
1710
  }
1137
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
+ }
1745
+ set domainWarpEnabled(enabled: boolean) {
1746
+ if (this._domainWarpEnabled !== enabled) {
1747
+ this._domainWarpEnabled = enabled;
1748
+ this._uniformsDirty = true;
1749
+ }
1750
+ }
1751
+
1752
+ get domainWarpIntensity(): number {
1753
+ return this._domainWarpIntensity;
1754
+ }
1755
+ set domainWarpIntensity(intensity: number) {
1756
+ if (this._domainWarpIntensity !== intensity) {
1757
+ this._domainWarpIntensity = intensity;
1758
+ this._uniformsDirty = true;
1759
+ }
1760
+ }
1761
+
1762
+ get domainWarpScale(): number {
1763
+ return this._domainWarpScale;
1764
+ }
1765
+ set domainWarpScale(scale: number) {
1766
+ if (this._domainWarpScale !== scale) {
1767
+ this._domainWarpScale = scale;
1768
+ this._uniformsDirty = true;
1769
+ }
1770
+ }
1771
+
1772
+ get vignetteIntensity(): number {
1773
+ return this._vignetteIntensity;
1774
+ }
1775
+ set vignetteIntensity(intensity: number) {
1776
+ if (this._vignetteIntensity !== intensity) {
1777
+ this._vignetteIntensity = intensity;
1778
+ this._uniformsDirty = true;
1779
+ }
1780
+ }
1781
+
1782
+ get vignetteRadius(): number {
1783
+ return this._vignetteRadius;
1784
+ }
1785
+ set vignetteRadius(radius: number) {
1786
+ if (this._vignetteRadius !== radius) {
1787
+ this._vignetteRadius = radius;
1788
+ this._uniformsDirty = true;
1789
+ }
1790
+ }
1791
+
1792
+ get fresnelEnabled(): boolean {
1793
+ return this._fresnelEnabled;
1794
+ }
1795
+ set fresnelEnabled(enabled: boolean) {
1796
+ if (this._fresnelEnabled !== enabled) {
1797
+ this._fresnelEnabled = enabled;
1798
+ this._uniformsDirty = true;
1799
+ }
1800
+ }
1801
+
1802
+ get fresnelPower(): number {
1803
+ return this._fresnelPower;
1804
+ }
1805
+ set fresnelPower(power: number) {
1806
+ if (this._fresnelPower !== power) {
1807
+ this._fresnelPower = power;
1808
+ this._uniformsDirty = true;
1809
+ }
1810
+ }
1811
+
1812
+ get fresnelIntensity(): number {
1813
+ return this._fresnelIntensity;
1814
+ }
1815
+ set fresnelIntensity(intensity: number) {
1816
+ if (this._fresnelIntensity !== intensity) {
1817
+ this._fresnelIntensity = intensity;
1818
+ this._uniformsDirty = true;
1819
+ }
1820
+ }
1821
+
1822
+ get fresnelColor(): string {
1823
+ return this._fresnelColor;
1824
+ }
1825
+ set fresnelColor(fresnelColor: string) {
1826
+ if (this._fresnelColor !== fresnelColor) {
1827
+ this._fresnelColor = fresnelColor;
1828
+ this._fresnelColorRgb = this._hexToRgb(fresnelColor);
1829
+ this._uniformsDirty = true;
1830
+ }
1831
+ }
1832
+
1833
+ get iridescenceEnabled(): boolean {
1834
+ return this._iridescenceEnabled;
1835
+ }
1836
+ set iridescenceEnabled(enabled: boolean) {
1837
+ if (this._iridescenceEnabled !== enabled) {
1838
+ this._iridescenceEnabled = enabled;
1839
+ this._uniformsDirty = true;
1840
+ }
1841
+ }
1842
+
1843
+ get iridescenceIntensity(): number {
1844
+ return this._iridescenceIntensity;
1845
+ }
1846
+ set iridescenceIntensity(intensity: number) {
1847
+ if (this._iridescenceIntensity !== intensity) {
1848
+ this._iridescenceIntensity = intensity;
1849
+ this._uniformsDirty = true;
1850
+ }
1851
+ }
1852
+
1853
+ get iridescenceSpeed(): number {
1854
+ return this._iridescenceSpeed;
1855
+ }
1856
+ set iridescenceSpeed(speed: number) {
1857
+ if (this._iridescenceSpeed !== speed) {
1858
+ this._iridescenceSpeed = speed;
1859
+ this._uniformsDirty = true;
1860
+ }
1861
+ }
1862
+
1863
+ get bloomIntensity(): number {
1864
+ return this._bloomIntensity;
1865
+ }
1866
+ set bloomIntensity(intensity: number) {
1867
+ if (this._bloomIntensity !== intensity) {
1868
+ this._bloomIntensity = intensity;
1869
+ this._uniformsDirty = true;
1870
+ }
1871
+ }
1872
+
1873
+ get bloomThreshold(): number {
1874
+ return this._bloomThreshold;
1875
+ }
1876
+ set bloomThreshold(threshold: number) {
1877
+ if (this._bloomThreshold !== threshold) {
1878
+ this._bloomThreshold = threshold;
1879
+ this._uniformsDirty = true;
1880
+ }
1881
+ }
1882
+
1883
+ get chromaticAberration(): number {
1884
+ return this._chromaticAberration;
1885
+ }
1886
+ set chromaticAberration(aberration: number) {
1887
+ if (this._chromaticAberration !== aberration) {
1888
+ this._chromaticAberration = aberration;
1889
+ this._uniformsDirty = true;
1890
+ }
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
+ }
1138
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
+ }
1139
2052
  }
1140
2053
 
1141
2054