@firecms/neat 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { buildColorFunctions, buildNoise, buildVertUniforms, buildFragUniforms, fragmentShaderSource, vertexShaderSource } from "./shaders";
2
- import { generatePlaneGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
2
+ import { generatePlaneGeometry, generateSphereGeometry, generateTorusGeometry, generateCylinderGeometry, generateRibbonGeometry, OrthographicCamera, updateCamera, Matrix4 } from "./math";
3
3
  console.info("%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", "font-weight: bold; font-size: 14px; color: #FF5772;", "color: inherit;");
4
4
  const PLANE_WIDTH = 50;
5
5
  const PLANE_HEIGHT = 80;
@@ -22,6 +22,7 @@ export class NeatGradient {
22
22
  _grainSparsity = -1;
23
23
  _grainSpeed = -1;
24
24
  _colorBlending = -1;
25
+ _resolution = 1;
25
26
  _colors = [];
26
27
  _wireframe = false;
27
28
  _backgroundColor = "#FFFFFF";
@@ -43,6 +44,7 @@ export class NeatGradient {
43
44
  _textureColorBlending = 0.01;
44
45
  _textureSeed = 333;
45
46
  _textureEase = 0.5;
47
+ _transparentTextureVoid = false;
46
48
  // New effects
47
49
  _domainWarpEnabled = false;
48
50
  _domainWarpIntensity = 0.5;
@@ -60,6 +62,33 @@ export class NeatGradient {
60
62
  _bloomIntensity = 0;
61
63
  _bloomThreshold = 0.7;
62
64
  _chromaticAberration = 0;
65
+ _silhouetteFade = 0.25;
66
+ _cylinderFade = 0.08;
67
+ _ribbonFade = 0.05;
68
+ _flatShading = true;
69
+ // 3D Shapes config
70
+ _shapeType = 'plane';
71
+ _shapeRotationX = 0;
72
+ _shapeRotationY = 0;
73
+ _shapeRotationZ = 0;
74
+ _shapeAutoRotateSpeedX = 0;
75
+ _shapeAutoRotateSpeedY = 0;
76
+ _sphereRadius = 15;
77
+ _torusRadius = 15;
78
+ _torusTube = 5;
79
+ _cylinderRadius = 10;
80
+ _cylinderHeight = 40;
81
+ _planeBend = 0;
82
+ _planeTwist = 0;
83
+ // Camera settings
84
+ _cameraLock = false;
85
+ _cameraX = 0;
86
+ _cameraY = 0;
87
+ _cameraZ = 0;
88
+ _cameraRotationX = 0;
89
+ _cameraRotationY = 0;
90
+ _cameraRotationZ = 0;
91
+ _cameraZoom = 1.0;
63
92
  _proceduralTexture = null;
64
93
  _proceduralBackgroundColor = "#000000";
65
94
  _textureShapeTriangles = 20;
@@ -75,6 +104,11 @@ export class NeatGradient {
75
104
  _yOffsetWaveMultiplier = 0.004;
76
105
  _yOffsetColorMultiplier = 0.004;
77
106
  _yOffsetFlowMultiplier = 0.004;
107
+ // Cached offscreen canvases for procedural texture generation
108
+ _sourceCanvas = null;
109
+ _sourceCtx = null;
110
+ _maskedCanvas = null;
111
+ _maskedCtx = null;
78
112
  // Performance optimizations
79
113
  _resizeTimeoutId = null;
80
114
  _textureNeedsUpdate = false;
@@ -82,12 +116,21 @@ export class NeatGradient {
82
116
  _colorsChanged = true;
83
117
  _uniformsDirty = true;
84
118
  _textureDirty = true;
119
+ _yOffsetDirty = false;
120
+ _modelViewMatrix = new Matrix4();
121
+ _isVisible = true;
122
+ _visibilityObserver = null;
123
+ _visibilityHandler = null;
85
124
  constructor(config) {
86
125
  const { ref, speed = 4, horizontalPressure = 3, verticalPressure = 3, waveFrequencyX = 5, waveFrequencyY = 5, waveAmplitude = 3, colors, highlights = 4, shadows = 4, colorSaturation = 0, colorBrightness = 1, colorBlending = 5, grainScale = 2, grainIntensity = 0.55, grainSparsity = 0.0, grainSpeed = 0.1, wireframe = false, backgroundColor = "#FFFFFF", backgroundAlpha = 1.0, resolution = 1, seed, yOffset = 0, yOffsetWaveMultiplier = 4, yOffsetColorMultiplier = 4, yOffsetFlowMultiplier = 4,
87
126
  // Flow field parameters
88
127
  flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
89
128
  // Texture generation
90
- enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, domainWarpEnabled = false, domainWarpIntensity = 0.5, domainWarpScale = 1.0, vignetteIntensity = 0.5, vignetteRadius = 0.8, fresnelEnabled = false, fresnelPower = 2.0, fresnelIntensity = 0.5, fresnelColor = "#FFFFFF", iridescenceEnabled = false, iridescenceIntensity = 0.5, iridescenceSpeed = 1.0, bloomIntensity = 0.0, bloomThreshold = 0.7, chromaticAberration = 0.0, } = config;
129
+ enableProceduralTexture = false, textureVoidLikelihood = 0.45, textureVoidWidthMin = 200, textureVoidWidthMax = 486, textureBandDensity = 2.15, textureColorBlending = 0.01, textureSeed = 333, textureEase = 0.5, proceduralBackgroundColor = "#000000", transparentTextureVoid = false, textureShapeTriangles = 20, textureShapeCircles = 15, textureShapeBars = 15, textureShapeSquiggles = 10, domainWarpEnabled = false, domainWarpIntensity = 0.5, domainWarpScale = 1.0, vignetteIntensity = 0.0, vignetteRadius = 0.8, fresnelEnabled = false, fresnelPower = 2.0, fresnelIntensity = 0.5, fresnelColor = "#FFFFFF", iridescenceEnabled = false, iridescenceIntensity = 0.5, iridescenceSpeed = 1.0, bloomIntensity = 0.0, bloomThreshold = 0.7, chromaticAberration = 0.0, silhouetteFade = 0.25, cylinderFade = 0.08, ribbonFade = 0.05, flatShading = true,
130
+ // Camera configuration
131
+ cameraLock = false, cameraX = 0, cameraY = 0, cameraZ = 0, cameraRotationX = 0, cameraRotationY = 0, cameraRotationZ = 0, cameraZoom = 1.0,
132
+ // 3D shapes default
133
+ shapeType = 'plane', shapeRotationX = 0, shapeRotationY = 0, shapeRotationZ = 0, shapeAutoRotateSpeedX = 0, shapeAutoRotateSpeedY = 0, sphereRadius = 15, torusRadius = 15, torusTube = 5, cylinderRadius = 10, cylinderHeight = 40, planeBend = 0, planeTwist = 0, } = config;
91
134
  this._ref = ref;
92
135
  this.destroy = this.destroy.bind(this);
93
136
  this._initScene = this._initScene.bind(this);
@@ -98,6 +141,7 @@ export class NeatGradient {
98
141
  this.waveFrequencyY = waveFrequencyY;
99
142
  this.waveAmplitude = waveAmplitude;
100
143
  this.colorBlending = colorBlending;
144
+ this._resolution = resolution;
101
145
  this.grainScale = grainScale;
102
146
  this.grainIntensity = grainIntensity;
103
147
  this.grainSparsity = grainSparsity;
@@ -130,6 +174,7 @@ export class NeatGradient {
130
174
  this.textureSeed = textureSeed;
131
175
  this.textureEase = textureEase;
132
176
  this._proceduralBackgroundColor = proceduralBackgroundColor;
177
+ this.transparentTextureVoid = transparentTextureVoid;
133
178
  this._textureShapeTriangles = textureShapeTriangles;
134
179
  this._textureShapeCircles = textureShapeCircles;
135
180
  this._textureShapeBars = textureShapeBars;
@@ -149,6 +194,31 @@ export class NeatGradient {
149
194
  this.bloomIntensity = bloomIntensity;
150
195
  this.bloomThreshold = bloomThreshold;
151
196
  this.chromaticAberration = chromaticAberration;
197
+ this.silhouetteFade = silhouetteFade;
198
+ this.cylinderFade = cylinderFade;
199
+ this.ribbonFade = ribbonFade;
200
+ this._flatShading = flatShading;
201
+ this._cameraLock = cameraLock;
202
+ this._cameraX = cameraX;
203
+ this._cameraY = cameraY;
204
+ this._cameraZ = cameraZ;
205
+ this._cameraRotationX = cameraRotationX;
206
+ this._cameraRotationY = cameraRotationY;
207
+ this._cameraRotationZ = cameraRotationZ;
208
+ this._cameraZoom = cameraZoom;
209
+ this._shapeType = shapeType;
210
+ this._shapeRotationX = shapeRotationX;
211
+ this._shapeRotationY = shapeRotationY;
212
+ this._shapeRotationZ = shapeRotationZ;
213
+ this._shapeAutoRotateSpeedX = shapeAutoRotateSpeedX;
214
+ this._shapeAutoRotateSpeedY = shapeAutoRotateSpeedY;
215
+ this._sphereRadius = sphereRadius;
216
+ this._torusRadius = torusRadius;
217
+ this._torusTube = torusTube;
218
+ this._cylinderRadius = cylinderRadius;
219
+ this._cylinderHeight = cylinderHeight;
220
+ this._planeBend = planeBend;
221
+ this._planeTwist = planeTwist;
152
222
  this.glState = this._initScene(resolution);
153
223
  injectSEO();
154
224
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
@@ -169,6 +239,42 @@ export class NeatGradient {
169
239
  lastTime = timeNow;
170
240
  gl.useProgram(program);
171
241
  gl.uniform1f(locations.uniforms['u_time'], tick);
242
+ // Update modelViewMatrix in every frame to support dynamic rotation and auto-rotation
243
+ const camera = this.glState.camera;
244
+ const modelViewMatrix = this._modelViewMatrix;
245
+ modelViewMatrix.identity();
246
+ // 1. Camera translation (default camera distance + displacement)
247
+ modelViewMatrix.translate(-camera.position[0] - this._cameraX, -camera.position[1] - this._cameraY, -camera.position[2] - this._cameraZ);
248
+ modelViewMatrix.translate(0, 0, -1);
249
+ // 2. Camera rotation (revolving around target)
250
+ modelViewMatrix.rotateX(-this._cameraRotationX);
251
+ modelViewMatrix.rotateY(-this._cameraRotationY);
252
+ modelViewMatrix.rotateZ(-this._cameraRotationZ);
253
+ let rx = this._shapeRotationX;
254
+ let ry = this._shapeRotationY;
255
+ let rz = this._shapeRotationZ;
256
+ if (this._shapeAutoRotateSpeedX !== 0) {
257
+ rx += tick * this._shapeAutoRotateSpeedX * 0.1;
258
+ }
259
+ if (this._shapeAutoRotateSpeedY !== 0) {
260
+ ry += tick * this._shapeAutoRotateSpeedY * 0.1;
261
+ }
262
+ if (this._shapeType === 'plane' || this._shapeType === 'ribbon') {
263
+ modelViewMatrix.rotateX(rx - Math.PI / 3.5);
264
+ }
265
+ else {
266
+ modelViewMatrix.rotateX(rx);
267
+ }
268
+ modelViewMatrix.rotateY(ry);
269
+ modelViewMatrix.rotateZ(rz);
270
+ const mvLoc = locations.uniforms["modelViewMatrix"];
271
+ if (mvLoc)
272
+ gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
273
+ // Fast path: only upload yOffset when it changed (scroll)
274
+ if (this._yOffsetDirty && !this._uniformsDirty) {
275
+ gl.uniform1f(locations.uniforms['u_y_offset'], this._yOffset);
276
+ this._yOffsetDirty = false;
277
+ }
172
278
  // Only upload static uniforms when they've been modified
173
279
  if (this._uniformsDirty) {
174
280
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -194,8 +300,19 @@ export class NeatGradient {
194
300
  gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
195
301
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
196
302
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
303
+ let shapeTypeVal = 0.0;
304
+ if (this._shapeType === 'sphere')
305
+ shapeTypeVal = 1.0;
306
+ else if (this._shapeType === 'torus')
307
+ shapeTypeVal = 2.0;
308
+ else if (this._shapeType === 'cylinder')
309
+ shapeTypeVal = 3.0;
310
+ else if (this._shapeType === 'ribbon')
311
+ shapeTypeVal = 4.0;
312
+ gl.uniform1f(locations.uniforms['u_shape_type'], shapeTypeVal);
197
313
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
198
314
  gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
315
+ gl.uniform1f(locations.uniforms['u_transparent_texture_void'], this._transparentTextureVoid ? 1.0 : 0.0);
199
316
  gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
200
317
  gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
201
318
  gl.uniform1f(locations.uniforms['u_domain_warp_scale'], this._domainWarpScale);
@@ -211,7 +328,12 @@ export class NeatGradient {
211
328
  gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
212
329
  gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
213
330
  gl.uniform1f(locations.uniforms['u_chromatic_aberration'], this._chromaticAberration);
331
+ gl.uniform1f(locations.uniforms['u_silhouette_fade'], this._silhouetteFade);
332
+ gl.uniform1f(locations.uniforms['u_cylinder_fade'], this._cylinderFade);
333
+ gl.uniform1f(locations.uniforms['u_ribbon_fade'], this._ribbonFade);
334
+ gl.uniform1f(locations.uniforms['u_flat_shading'], this._flatShading ? 1.0 : 0.0);
214
335
  this._uniformsDirty = false;
336
+ this._yOffsetDirty = false;
215
337
  }
216
338
  // Only regenerate procedural texture when needed
217
339
  if (this._textureNeedsUpdate && this._enableProceduralTexture) {
@@ -258,8 +380,34 @@ export class NeatGradient {
258
380
  else {
259
381
  gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
260
382
  }
261
- this.requestRef = requestAnimationFrame(render);
383
+ if (this._isVisible) {
384
+ this.requestRef = requestAnimationFrame(render);
385
+ }
386
+ };
387
+ // Visibility optimization: pause rendering when off-screen or tab hidden
388
+ this._visibilityObserver = new IntersectionObserver((entries) => {
389
+ const wasVisible = this._isVisible;
390
+ this._isVisible = entries[0].isIntersecting && document.visibilityState !== 'hidden';
391
+ if (this._isVisible && !wasVisible) {
392
+ lastTime = performance.now(); // Avoid time jump after resume
393
+ this.requestRef = requestAnimationFrame(render);
394
+ }
395
+ }, { threshold: 0 });
396
+ this._visibilityObserver.observe(ref);
397
+ this._visibilityHandler = () => {
398
+ const wasVisible = this._isVisible;
399
+ if (document.visibilityState === 'hidden') {
400
+ this._isVisible = false;
401
+ }
402
+ else {
403
+ this._isVisible = true;
404
+ if (!wasVisible) {
405
+ lastTime = performance.now();
406
+ this.requestRef = requestAnimationFrame(render);
407
+ }
408
+ }
262
409
  };
410
+ document.addEventListener('visibilitychange', this._visibilityHandler);
263
411
  const setSize = () => {
264
412
  const { gl, camera } = this.glState;
265
413
  const width = this._ref.clientWidth;
@@ -268,11 +416,12 @@ export class NeatGradient {
268
416
  this._ref.width = width;
269
417
  this._ref.height = height;
270
418
  gl.viewport(0, 0, width, height);
271
- updateCamera(camera, width, height);
419
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
272
420
  // Recompute projection matrix on resize
273
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
421
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
274
422
  gl.useProgram(this.glState.program);
275
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
423
+ if (projLoc)
424
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
276
425
  };
277
426
  // Debounce resize to prevent excessive operations
278
427
  this.sizeObserver = new ResizeObserver(() => {
@@ -290,6 +439,15 @@ export class NeatGradient {
290
439
  destroy() {
291
440
  cancelAnimationFrame(this.requestRef);
292
441
  this.sizeObserver.disconnect();
442
+ // Cleanup visibility observers
443
+ if (this._visibilityObserver) {
444
+ this._visibilityObserver.disconnect();
445
+ this._visibilityObserver = null;
446
+ }
447
+ if (this._visibilityHandler) {
448
+ document.removeEventListener('visibilitychange', this._visibilityHandler);
449
+ this._visibilityHandler = null;
450
+ }
293
451
  // Clear resize timeout
294
452
  if (this._resizeTimeoutId !== null) {
295
453
  clearTimeout(this._resizeTimeoutId);
@@ -318,101 +476,297 @@ export class NeatGradient {
318
476
  const dataURL = this._ref.toDataURL("image/png");
319
477
  downloadURI(dataURL, filename);
320
478
  }
479
+ /**
480
+ * Records the canvas animation as a video with a NEAT watermark overlay.
481
+ * @param options.durationMs Recording duration in milliseconds (default 5000).
482
+ * @param options.filename Output file name (default "neat.firecms.co").
483
+ * @param options.width Output video width in pixels (default: current canvas width).
484
+ * @param options.height Output video height in pixels (default: current canvas height).
485
+ * @param options.format Preferred format: 'mp4' or 'webm' (default: best available).
486
+ * @param options.onProgress Callback with progress 0-1.
487
+ * @param options.onComplete Callback when recording finishes.
488
+ * @returns A stop function to end recording early.
489
+ */
490
+ recordVideo(options = {}) {
491
+ const { durationMs = 5000, filename = "neat.firecms.co", format, onProgress, onComplete, } = options;
492
+ const source = this._ref;
493
+ const width = options.width || source.width || source.clientWidth;
494
+ const height = options.height || source.height || source.clientHeight;
495
+ // Offscreen canvas that composites gradient + watermark each frame
496
+ const offscreen = document.createElement("canvas");
497
+ offscreen.width = width;
498
+ offscreen.height = height;
499
+ const ctx = offscreen.getContext("2d");
500
+ // Use captureStream(0) — only captures a frame when we explicitly
501
+ // call requestFrame() on the video track, so every composited frame
502
+ // is guaranteed to be captured.
503
+ const stream = offscreen.captureStream(0);
504
+ const videoTrack = stream.getVideoTracks()[0];
505
+ // Codec candidates ordered by preference
506
+ const mp4Candidates = [
507
+ "video/mp4;codecs=avc1",
508
+ "video/mp4;codecs=avc1,opus",
509
+ "video/mp4",
510
+ ];
511
+ const webmCandidates = [
512
+ "video/webm;codecs=vp9,opus",
513
+ "video/webm;codecs=vp9",
514
+ "video/webm;codecs=vp8,opus",
515
+ "video/webm",
516
+ ];
517
+ // Build candidate list based on preferred format
518
+ let candidates;
519
+ if (format === 'mp4')
520
+ candidates = [...mp4Candidates, ...webmCandidates];
521
+ else if (format === 'webm')
522
+ candidates = [...webmCandidates, ...mp4Candidates];
523
+ else
524
+ candidates = [...mp4Candidates, ...webmCandidates];
525
+ let mimeType = "video/webm";
526
+ for (const candidate of candidates) {
527
+ if (MediaRecorder.isTypeSupported(candidate)) {
528
+ mimeType = candidate;
529
+ break;
530
+ }
531
+ }
532
+ // Scale bitrate with pixel count: 8 Mbps baseline at 720p
533
+ const pixels = width * height;
534
+ const baseBitrate = 8_000_000;
535
+ const basePixels = 1280 * 720;
536
+ const videoBitsPerSecond = Math.round(baseBitrate * Math.max(1, pixels / basePixels));
537
+ const recorder = new MediaRecorder(stream, {
538
+ mimeType,
539
+ videoBitsPerSecond,
540
+ });
541
+ const chunks = [];
542
+ recorder.ondataavailable = (e) => {
543
+ if (e.data.size > 0)
544
+ chunks.push(e.data);
545
+ };
546
+ let stopped = false;
547
+ let rafId;
548
+ const startTime = performance.now();
549
+ let lastProgressTime = 0;
550
+ // Composite loop: draw source canvas + watermark overlay on each frame
551
+ const drawFrame = () => {
552
+ if (stopped)
553
+ return;
554
+ ctx.clearRect(0, 0, width, height);
555
+ ctx.drawImage(source, 0, 0, width, height);
556
+ // Watermark: "NEAT" in bottom-right corner
557
+ const fontSize = Math.max(14, Math.round(height * 0.025));
558
+ ctx.font = `bold ${fontSize}px "Sofia Sans", sans-serif`;
559
+ ctx.textAlign = "right";
560
+ ctx.textBaseline = "bottom";
561
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
562
+ ctx.shadowBlur = 4;
563
+ ctx.shadowOffsetX = 1;
564
+ ctx.shadowOffsetY = 1;
565
+ ctx.fillStyle = "rgba(255,255,255,0.7)";
566
+ ctx.fillText("NEAT", width - fontSize * 0.8, height - fontSize * 0.5);
567
+ ctx.shadowColor = "transparent";
568
+ ctx.shadowBlur = 0;
569
+ ctx.shadowOffsetX = 0;
570
+ ctx.shadowOffsetY = 0;
571
+ // Signal the stream to capture this frame
572
+ // @ts-ignore – requestFrame exists on CanvasCaptureMediaStreamTrack
573
+ if (videoTrack.requestFrame)
574
+ videoTrack.requestFrame();
575
+ // Throttle progress to ~4 updates/sec to avoid flooding React state
576
+ if (onProgress) {
577
+ const now = performance.now();
578
+ if (now - lastProgressTime > 250) {
579
+ lastProgressTime = now;
580
+ onProgress(Math.min(0.99, (now - startTime) / durationMs));
581
+ }
582
+ }
583
+ rafId = requestAnimationFrame(drawFrame);
584
+ };
585
+ recorder.onstop = () => {
586
+ stopped = true;
587
+ cancelAnimationFrame(rafId);
588
+ // Use the correct file extension for the actual format
589
+ const isMP4 = mimeType.startsWith("video/mp4");
590
+ const ext = isMP4 ? ".mp4" : ".webm";
591
+ const blobType = isMP4 ? "video/mp4" : "video/webm";
592
+ const finalFilename = filename + ext;
593
+ const blob = new Blob(chunks, { type: blobType });
594
+ const url = URL.createObjectURL(blob);
595
+ downloadURI(url, finalFilename);
596
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
597
+ onProgress?.(1);
598
+ onComplete?.();
599
+ };
600
+ // Start drawing frames, then start recording
601
+ drawFrame();
602
+ recorder.start(100); // collect data every 100ms
603
+ // Auto-stop after the requested duration
604
+ const timeoutId = window.setTimeout(() => {
605
+ if (recorder.state === "recording") {
606
+ recorder.stop();
607
+ }
608
+ }, durationMs);
609
+ // Return a stop function for early termination
610
+ return () => {
611
+ clearTimeout(timeoutId);
612
+ if (recorder.state === "recording") {
613
+ recorder.stop();
614
+ }
615
+ };
616
+ }
617
+ get speed() {
618
+ return this._speed * 20;
619
+ }
321
620
  set speed(speed) {
322
621
  this._uniformsDirty = true;
323
622
  this._speed = speed / 20;
324
623
  }
624
+ get horizontalPressure() {
625
+ return this._horizontalPressure * 4;
626
+ }
325
627
  set horizontalPressure(horizontalPressure) {
326
628
  this._uniformsDirty = true;
327
629
  this._horizontalPressure = horizontalPressure / 4;
328
630
  }
631
+ get verticalPressure() {
632
+ return this._verticalPressure * 4;
633
+ }
329
634
  set verticalPressure(verticalPressure) {
330
635
  this._uniformsDirty = true;
331
636
  this._verticalPressure = verticalPressure / 4;
332
637
  }
638
+ get waveFrequencyX() {
639
+ return this._waveFrequencyX / 0.04;
640
+ }
333
641
  set waveFrequencyX(waveFrequencyX) {
334
642
  this._uniformsDirty = true;
335
643
  this._waveFrequencyX = waveFrequencyX * 0.04;
336
644
  }
645
+ get waveFrequencyY() {
646
+ return this._waveFrequencyY / 0.04;
647
+ }
337
648
  set waveFrequencyY(waveFrequencyY) {
338
649
  this._uniformsDirty = true;
339
650
  this._waveFrequencyY = waveFrequencyY * 0.04;
340
651
  }
652
+ get waveAmplitude() {
653
+ return this._waveAmplitude / 0.75;
654
+ }
341
655
  set waveAmplitude(waveAmplitude) {
342
656
  this._uniformsDirty = true;
343
657
  this._waveAmplitude = waveAmplitude * .75;
344
658
  }
659
+ get colors() {
660
+ return this._colors;
661
+ }
345
662
  set colors(colors) {
346
663
  this._uniformsDirty = true;
347
664
  this._colors = colors;
348
665
  this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
349
666
  this._colorsChanged = true;
350
667
  }
668
+ get highlights() {
669
+ return this._highlights * 100;
670
+ }
351
671
  set highlights(highlights) {
352
672
  this._uniformsDirty = true;
353
673
  this._highlights = highlights / 100;
354
674
  }
675
+ get shadows() {
676
+ return this._shadows * 100;
677
+ }
355
678
  set shadows(shadows) {
356
679
  this._uniformsDirty = true;
357
680
  this._shadows = shadows / 100;
358
681
  }
682
+ get colorSaturation() {
683
+ return this._saturation * 10;
684
+ }
359
685
  set colorSaturation(colorSaturation) {
360
686
  this._uniformsDirty = true;
361
687
  this._saturation = colorSaturation / 10;
362
688
  }
689
+ get colorBrightness() {
690
+ return this._brightness;
691
+ }
363
692
  set colorBrightness(colorBrightness) {
364
693
  this._uniformsDirty = true;
365
694
  this._brightness = colorBrightness;
366
695
  }
696
+ get colorBlending() {
697
+ return this._colorBlending * 10;
698
+ }
367
699
  set colorBlending(colorBlending) {
368
700
  this._uniformsDirty = true;
369
701
  this._colorBlending = colorBlending / 10;
370
702
  }
703
+ get grainScale() {
704
+ return this._grainScale;
705
+ }
371
706
  set grainScale(grainScale) {
372
707
  this._uniformsDirty = true;
373
708
  this._grainScale = grainScale == 0 ? 1 : grainScale;
374
709
  }
710
+ get grainIntensity() {
711
+ return this._grainIntensity;
712
+ }
375
713
  set grainIntensity(grainIntensity) {
376
714
  this._uniformsDirty = true;
377
715
  this._grainIntensity = grainIntensity;
378
716
  }
717
+ get grainSparsity() {
718
+ return this._grainSparsity;
719
+ }
379
720
  set grainSparsity(grainSparsity) {
380
721
  this._uniformsDirty = true;
381
722
  this._grainSparsity = grainSparsity;
382
723
  }
724
+ get grainSpeed() {
725
+ return this._grainSpeed;
726
+ }
383
727
  set grainSpeed(grainSpeed) {
384
728
  this._uniformsDirty = true;
385
729
  this._grainSpeed = grainSpeed;
386
730
  }
731
+ get wireframe() {
732
+ return this._wireframe;
733
+ }
387
734
  set wireframe(wireframe) {
388
735
  this._uniformsDirty = true;
389
736
  this._wireframe = wireframe;
390
737
  }
738
+ get resolution() {
739
+ return this._resolution;
740
+ }
391
741
  set resolution(resolution) {
392
- this._uniformsDirty = true;
393
- if (this.glState) {
394
- const gl = this.glState.gl;
395
- gl.deleteProgram(this.glState.program);
396
- gl.deleteBuffer(this.glState.buffers.position);
397
- gl.deleteBuffer(this.glState.buffers.normal);
398
- gl.deleteBuffer(this.glState.buffers.uv);
399
- gl.deleteBuffer(this.glState.buffers.index);
400
- gl.deleteBuffer(this.glState.buffers.wireframeIndex);
401
- }
402
- this.glState = this._initScene(resolution);
742
+ if (this._resolution === resolution)
743
+ return;
744
+ this._resolution = resolution;
745
+ this._updateGeometry();
746
+ }
747
+ get backgroundColor() {
748
+ return this._backgroundColor;
403
749
  }
404
750
  set backgroundColor(backgroundColor) {
405
751
  this._uniformsDirty = true;
406
752
  this._backgroundColor = backgroundColor;
407
753
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
408
754
  }
755
+ get backgroundAlpha() {
756
+ return this._backgroundAlpha;
757
+ }
409
758
  set backgroundAlpha(backgroundAlpha) {
410
759
  this._uniformsDirty = true;
411
760
  this._backgroundAlpha = backgroundAlpha;
412
761
  }
762
+ get yOffset() {
763
+ return this._yOffset;
764
+ }
413
765
  set yOffset(yOffset) {
414
- this._uniformsDirty = true;
415
- this._yOffset = yOffset;
766
+ if (this._yOffset !== yOffset) {
767
+ this._yOffsetDirty = true;
768
+ this._yOffset = yOffset;
769
+ }
416
770
  }
417
771
  get yOffsetWaveMultiplier() {
418
772
  return this._yOffsetWaveMultiplier * 1000;
@@ -435,18 +789,30 @@ export class NeatGradient {
435
789
  this._uniformsDirty = true;
436
790
  this._yOffsetFlowMultiplier = value / 1000;
437
791
  }
792
+ get flowDistortionA() {
793
+ return this._flowDistortionA;
794
+ }
438
795
  set flowDistortionA(value) {
439
796
  this._uniformsDirty = true;
440
797
  this._flowDistortionA = value;
441
798
  }
799
+ get flowDistortionB() {
800
+ return this._flowDistortionB;
801
+ }
442
802
  set flowDistortionB(value) {
443
803
  this._uniformsDirty = true;
444
804
  this._flowDistortionB = value;
445
805
  }
806
+ get flowScale() {
807
+ return this._flowScale;
808
+ }
446
809
  set flowScale(value) {
447
810
  this._uniformsDirty = true;
448
811
  this._flowScale = value;
449
812
  }
813
+ get flowEase() {
814
+ return this._flowEase;
815
+ }
450
816
  set flowEase(value) {
451
817
  this._uniformsDirty = true;
452
818
  this._flowEase = value;
@@ -458,6 +824,9 @@ export class NeatGradient {
458
824
  get flowEnabled() {
459
825
  return this._flowEnabled;
460
826
  }
827
+ get enableProceduralTexture() {
828
+ return this._enableProceduralTexture;
829
+ }
461
830
  set enableProceduralTexture(value) {
462
831
  this._uniformsDirty = true;
463
832
  this._enableProceduralTexture = value;
@@ -465,6 +834,9 @@ export class NeatGradient {
465
834
  this._textureNeedsUpdate = true;
466
835
  }
467
836
  }
837
+ get textureVoidLikelihood() {
838
+ return this._textureVoidLikelihood;
839
+ }
468
840
  set textureVoidLikelihood(value) {
469
841
  this._uniformsDirty = true;
470
842
  this._textureVoidLikelihood = value;
@@ -472,6 +844,9 @@ export class NeatGradient {
472
844
  this._textureNeedsUpdate = true;
473
845
  }
474
846
  }
847
+ get textureVoidWidthMin() {
848
+ return this._textureVoidWidthMin;
849
+ }
475
850
  set textureVoidWidthMin(value) {
476
851
  this._uniformsDirty = true;
477
852
  this._textureVoidWidthMin = value;
@@ -479,6 +854,9 @@ export class NeatGradient {
479
854
  this._textureNeedsUpdate = true;
480
855
  }
481
856
  }
857
+ get textureVoidWidthMax() {
858
+ return this._textureVoidWidthMax;
859
+ }
482
860
  set textureVoidWidthMax(value) {
483
861
  this._uniformsDirty = true;
484
862
  this._textureVoidWidthMax = value;
@@ -486,6 +864,9 @@ export class NeatGradient {
486
864
  this._textureNeedsUpdate = true;
487
865
  }
488
866
  }
867
+ get textureBandDensity() {
868
+ return this._textureBandDensity;
869
+ }
489
870
  set textureBandDensity(value) {
490
871
  this._uniformsDirty = true;
491
872
  this._textureBandDensity = value;
@@ -493,6 +874,9 @@ export class NeatGradient {
493
874
  this._textureNeedsUpdate = true;
494
875
  }
495
876
  }
877
+ get textureColorBlending() {
878
+ return this._textureColorBlending;
879
+ }
496
880
  set textureColorBlending(value) {
497
881
  this._uniformsDirty = true;
498
882
  this._textureColorBlending = value;
@@ -500,6 +884,9 @@ export class NeatGradient {
500
884
  this._textureNeedsUpdate = true;
501
885
  }
502
886
  }
887
+ get textureSeed() {
888
+ return this._textureSeed;
889
+ }
503
890
  set textureSeed(value) {
504
891
  this._uniformsDirty = true;
505
892
  this._textureSeed = value;
@@ -514,6 +901,19 @@ export class NeatGradient {
514
901
  this._uniformsDirty = true;
515
902
  this._textureEase = value;
516
903
  }
904
+ get transparentTextureVoid() {
905
+ return this._transparentTextureVoid;
906
+ }
907
+ set transparentTextureVoid(value) {
908
+ this._uniformsDirty = true;
909
+ this._transparentTextureVoid = value;
910
+ if (this._enableProceduralTexture) {
911
+ this._textureNeedsUpdate = true;
912
+ }
913
+ }
914
+ get proceduralBackgroundColor() {
915
+ return this._proceduralBackgroundColor;
916
+ }
517
917
  set proceduralBackgroundColor(value) {
518
918
  this._uniformsDirty = true;
519
919
  this._proceduralBackgroundColor = value;
@@ -521,30 +921,90 @@ export class NeatGradient {
521
921
  this._textureNeedsUpdate = true;
522
922
  }
523
923
  }
924
+ get textureShapeTriangles() {
925
+ return this._textureShapeTriangles;
926
+ }
524
927
  set textureShapeTriangles(value) {
525
928
  this._uniformsDirty = true;
526
929
  this._textureShapeTriangles = value;
527
930
  if (this._enableProceduralTexture)
528
931
  this._textureNeedsUpdate = true;
529
932
  }
933
+ get textureShapeCircles() {
934
+ return this._textureShapeCircles;
935
+ }
530
936
  set textureShapeCircles(value) {
531
937
  this._uniformsDirty = true;
532
938
  this._textureShapeCircles = value;
533
939
  if (this._enableProceduralTexture)
534
940
  this._textureNeedsUpdate = true;
535
941
  }
942
+ get textureShapeBars() {
943
+ return this._textureShapeBars;
944
+ }
536
945
  set textureShapeBars(value) {
537
946
  this._uniformsDirty = true;
538
947
  this._textureShapeBars = value;
539
948
  if (this._enableProceduralTexture)
540
949
  this._textureNeedsUpdate = true;
541
950
  }
951
+ get textureShapeSquiggles() {
952
+ return this._textureShapeSquiggles;
953
+ }
542
954
  set textureShapeSquiggles(value) {
543
955
  this._uniformsDirty = true;
544
956
  this._textureShapeSquiggles = value;
545
957
  if (this._enableProceduralTexture)
546
958
  this._textureNeedsUpdate = true;
547
959
  }
960
+ _updateGeometry() {
961
+ if (!this.glState)
962
+ return;
963
+ const gl = this.glState.gl;
964
+ const resolution = this._resolution || 1;
965
+ let geometry;
966
+ if (this._shapeType === 'sphere') {
967
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
968
+ }
969
+ else if (this._shapeType === 'torus') {
970
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
971
+ }
972
+ else if (this._shapeType === 'cylinder') {
973
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
974
+ }
975
+ else if (this._shapeType === 'ribbon') {
976
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
977
+ }
978
+ else {
979
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
980
+ }
981
+ const { position, normal, uv, index, wireframeIndex } = geometry;
982
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.position);
983
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
984
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.normal);
985
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
986
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.uv);
987
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
988
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
989
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
990
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
991
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
992
+ // Restore default bound element buffer
993
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
994
+ this.glState.indexCount = index.length;
995
+ this.glState.wireframeIndexCount = wireframeIndex.length;
996
+ this.glState.indexType = (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
997
+ // Keep camera updated with the new shapeType and dimensions
998
+ const width = this._ref.clientWidth;
999
+ const height = this._ref.clientHeight;
1000
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1001
+ // Recompute projection matrix
1002
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1003
+ gl.useProgram(this.glState.program);
1004
+ if (projLoc)
1005
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1006
+ this._uniformsDirty = true;
1007
+ }
548
1008
  _hexToRgb(hex) {
549
1009
  const bigint = parseInt(hex.replace('#', ''), 16);
550
1010
  return [
@@ -564,8 +1024,24 @@ export class NeatGradient {
564
1024
  const ext = gl.getExtension("OES_standard_derivatives");
565
1025
  gl.getExtension("OES_element_index_uint");
566
1026
  gl.viewport(0, 0, width, height);
567
- // Generate plane geometry with Uint32Array for large meshes
568
- const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1027
+ // Generate parametric geometry based on shapeType
1028
+ let geometry;
1029
+ if (this._shapeType === 'sphere') {
1030
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
1031
+ }
1032
+ else if (this._shapeType === 'torus') {
1033
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
1034
+ }
1035
+ else if (this._shapeType === 'cylinder') {
1036
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
1037
+ }
1038
+ else if (this._shapeType === 'ribbon') {
1039
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
1040
+ }
1041
+ else {
1042
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
1043
+ }
1044
+ const { position, normal, uv, index, wireframeIndex } = geometry;
569
1045
  const positionBuffer = gl.createBuffer();
570
1046
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
571
1047
  gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
@@ -620,7 +1096,7 @@ export class NeatGradient {
620
1096
  gl.useProgram(program);
621
1097
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
622
1098
  camera.position = [0, 0, 5];
623
- updateCamera(camera, width, height);
1099
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
624
1100
  // Define attributes
625
1101
  const aPosition = gl.getAttribLocation(program, "position");
626
1102
  const aNormal = gl.getAttribLocation(program, "normal");
@@ -635,15 +1111,7 @@ export class NeatGradient {
635
1111
  gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
636
1112
  gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
637
1113
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
638
- const modelViewMatrix = new Matrix4();
639
- // The View Matrix is the inverse of the Camera's position
640
- // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
641
- modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
642
- // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
643
- modelViewMatrix.translate(0, 0, -1);
644
- modelViewMatrix.rotateX(-Math.PI / 3.5);
645
- const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
646
- gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
1114
+ // modelViewMatrix is set dynamically in the render loop
647
1115
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
648
1116
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
649
1117
  const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
@@ -653,17 +1121,19 @@ export class NeatGradient {
653
1121
  const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
654
1122
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
655
1123
  const uniformsList = [
1124
+ "projectionMatrix", "modelViewMatrix",
656
1125
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
657
1126
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
658
1127
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
659
1128
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
660
1129
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
661
- "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending",
1130
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_transparent_texture_void", "u_saturation", "u_brightness", "u_color_blending",
662
1131
  "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
663
1132
  "u_vignette_intensity", "u_vignette_radius",
664
1133
  "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
665
1134
  "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
666
- "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration"
1135
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1136
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade", "u_flat_shading"
667
1137
  ];
668
1138
  const locations = {
669
1139
  attributes: { position: aPosition, normal: aNormal, uv: aUv },
@@ -708,10 +1178,14 @@ export class NeatGradient {
708
1178
  // Texture size - 1024 provides good balance between quality and performance
709
1179
  // Reduced from 2048 for better performance
710
1180
  const texSize = 1024;
711
- const sourceCanvas = document.createElement('canvas');
712
- sourceCanvas.width = texSize;
713
- sourceCanvas.height = texSize;
714
- const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
1181
+ if (!this._sourceCanvas) {
1182
+ this._sourceCanvas = document.createElement('canvas');
1183
+ this._sourceCanvas.width = texSize;
1184
+ this._sourceCanvas.height = texSize;
1185
+ this._sourceCtx = this._sourceCanvas.getContext('2d');
1186
+ }
1187
+ const sourceCanvas = this._sourceCanvas;
1188
+ const sCtx = this._sourceCtx;
715
1189
  if (!sCtx)
716
1190
  return null;
717
1191
  let seed = this._textureSeed;
@@ -727,6 +1201,9 @@ export class NeatGradient {
727
1201
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
728
1202
  if (colors.length === 0)
729
1203
  return null;
1204
+ const shouldTile = this._shapeType !== 'plane';
1205
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1206
+ const dys = shouldTile ? [-1, 0, 1] : [0];
730
1207
  // Helper functions
731
1208
  function hexToRgb(hex) {
732
1209
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -763,64 +1240,120 @@ export class NeatGradient {
763
1240
  sCtx.fillRect(0, 0, texSize, texSize);
764
1241
  // Triangles: use configurable count
765
1242
  for (let i = 0; i < this._textureShapeTriangles; i++) {
766
- sCtx.fillStyle = getInterColor();
767
- sCtx.beginPath();
1243
+ const fillStyle = getInterColor();
768
1244
  const x = random() * texSize;
769
1245
  const y = random() * texSize;
770
1246
  const s = 100 + random() * 300;
771
- sCtx.moveTo(x, y);
772
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
773
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
774
- sCtx.fill();
1247
+ const x1 = (random() - 0.5) * s;
1248
+ const y1 = (random() - 0.5) * s;
1249
+ const x2 = (random() - 0.5) * s;
1250
+ const y2 = (random() - 0.5) * s;
1251
+ for (const dx of dxs) {
1252
+ for (const dy of dys) {
1253
+ sCtx.fillStyle = fillStyle;
1254
+ sCtx.beginPath();
1255
+ const tx = x + dx * texSize;
1256
+ const ty = y + dy * texSize;
1257
+ sCtx.moveTo(tx, ty);
1258
+ sCtx.lineTo(tx + x1, ty + y1);
1259
+ sCtx.lineTo(tx + x2, ty + y2);
1260
+ sCtx.fill();
1261
+ }
1262
+ }
775
1263
  }
776
1264
  // Circles / rings: use configurable count
777
1265
  for (let i = 0; i < this._textureShapeCircles; i++) {
778
- sCtx.strokeStyle = getInterColor();
779
- sCtx.lineWidth = 10 + random() * 50;
780
- sCtx.beginPath();
1266
+ const strokeStyle = getInterColor();
1267
+ const lineWidth = 10 + random() * 50;
781
1268
  const x = random() * texSize;
782
1269
  const y = random() * texSize;
783
1270
  const r = 50 + random() * 150;
784
- sCtx.arc(x, y, r, 0, Math.PI * 2);
785
- sCtx.stroke();
1271
+ for (const dx of dxs) {
1272
+ for (const dy of dys) {
1273
+ sCtx.strokeStyle = strokeStyle;
1274
+ sCtx.lineWidth = lineWidth;
1275
+ sCtx.beginPath();
1276
+ sCtx.arc(x + dx * texSize, y + dy * texSize, r, 0, Math.PI * 2);
1277
+ sCtx.stroke();
1278
+ }
1279
+ }
786
1280
  }
787
1281
  // Bars: use configurable count
788
1282
  for (let i = 0; i < this._textureShapeBars; i++) {
789
- sCtx.fillStyle = getInterColor();
790
- sCtx.save();
791
- sCtx.translate(random() * texSize, random() * texSize);
792
- sCtx.rotate(random() * Math.PI);
793
- sCtx.fillRect(-150, -25, 300, 50);
794
- sCtx.restore();
1283
+ const fillStyle = getInterColor();
1284
+ const x = random() * texSize;
1285
+ const y = random() * texSize;
1286
+ const rot = random() * Math.PI;
1287
+ for (const dx of dxs) {
1288
+ for (const dy of dys) {
1289
+ sCtx.fillStyle = fillStyle;
1290
+ sCtx.save();
1291
+ sCtx.translate(x + dx * texSize, y + dy * texSize);
1292
+ sCtx.rotate(rot);
1293
+ sCtx.fillRect(-150, -25, 300, 50);
1294
+ sCtx.restore();
1295
+ }
1296
+ }
795
1297
  }
796
1298
  // Squiggles: use configurable count
797
1299
  sCtx.lineWidth = 15;
798
1300
  sCtx.lineCap = 'round';
799
1301
  for (let i = 0; i < this._textureShapeSquiggles; i++) {
800
- sCtx.strokeStyle = getInterColor();
801
- sCtx.beginPath();
802
- let x = random() * texSize;
803
- let y = random() * texSize;
804
- sCtx.moveTo(x, y);
1302
+ const strokeStyle = getInterColor();
1303
+ const x = random() * texSize;
1304
+ const y = random() * texSize;
1305
+ const curves = [];
1306
+ let cx = 0;
1307
+ let cy = 0;
805
1308
  for (let j = 0; j < 4; j++) {
806
- sCtx.bezierCurveTo(x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300, x + (random() - 0.5) * 300, y + (random() - 0.5) * 300);
807
- x += (random() - 0.5) * 300;
808
- y += (random() - 0.5) * 300;
1309
+ const ex = cx + (random() - 0.5) * 300;
1310
+ const ey = cy + (random() - 0.5) * 300;
1311
+ curves.push({
1312
+ cx1: cx + (random() - 0.5) * 300,
1313
+ cy1: cy + (random() - 0.5) * 300,
1314
+ cx2: cx + (random() - 0.5) * 300,
1315
+ cy2: cy + (random() - 0.5) * 300,
1316
+ ex: ex,
1317
+ ey: ey
1318
+ });
1319
+ cx = ex;
1320
+ cy = ey;
1321
+ }
1322
+ for (const dx of dxs) {
1323
+ for (const dy of dys) {
1324
+ sCtx.strokeStyle = strokeStyle;
1325
+ sCtx.beginPath();
1326
+ const tx = x + dx * texSize;
1327
+ const ty = y + dy * texSize;
1328
+ sCtx.moveTo(tx, ty);
1329
+ for (const curve of curves) {
1330
+ sCtx.bezierCurveTo(tx + curve.cx1, ty + curve.cy1, tx + curve.cx2, ty + curve.cy2, tx + curve.ex, ty + curve.ey);
1331
+ }
1332
+ sCtx.stroke();
1333
+ }
809
1334
  }
810
- sCtx.stroke();
811
1335
  }
812
1336
  // === MASKED CANVAS ===
813
1337
  // Masking: Seed isolation
814
1338
  setSeed(50000);
815
- const canvas = document.createElement('canvas');
816
- canvas.width = texSize;
817
- canvas.height = texSize;
818
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1339
+ if (!this._maskedCanvas) {
1340
+ this._maskedCanvas = document.createElement('canvas');
1341
+ this._maskedCanvas.width = texSize;
1342
+ this._maskedCanvas.height = texSize;
1343
+ this._maskedCtx = this._maskedCanvas.getContext('2d');
1344
+ }
1345
+ const canvas = this._maskedCanvas;
1346
+ const ctx = this._maskedCtx;
819
1347
  if (!ctx)
820
1348
  return null;
821
1349
  // Start filled with the chosen void color so gaps show that color
822
- ctx.fillStyle = baseColor;
823
- ctx.fillRect(0, 0, texSize, texSize);
1350
+ if (this._transparentTextureVoid) {
1351
+ ctx.clearRect(0, 0, texSize, texSize);
1352
+ }
1353
+ else {
1354
+ ctx.fillStyle = baseColor;
1355
+ ctx.fillRect(0, 0, texSize, texSize);
1356
+ }
824
1357
  // Determine layout segments (matter vs void)
825
1358
  let layoutHead = 0;
826
1359
  const segments = [];
@@ -869,54 +1402,117 @@ export class NeatGradient {
869
1402
  }
870
1403
  return tex;
871
1404
  }
1405
+ get silhouetteFade() {
1406
+ return this._silhouetteFade;
1407
+ }
1408
+ set silhouetteFade(value) {
1409
+ if (this._silhouetteFade !== value) {
1410
+ this._silhouetteFade = value;
1411
+ this._uniformsDirty = true;
1412
+ }
1413
+ }
1414
+ get cylinderFade() {
1415
+ return this._cylinderFade;
1416
+ }
1417
+ set cylinderFade(value) {
1418
+ if (this._cylinderFade !== value) {
1419
+ this._cylinderFade = value;
1420
+ this._uniformsDirty = true;
1421
+ }
1422
+ }
1423
+ get ribbonFade() {
1424
+ return this._ribbonFade;
1425
+ }
1426
+ set ribbonFade(value) {
1427
+ if (this._ribbonFade !== value) {
1428
+ this._ribbonFade = value;
1429
+ this._uniformsDirty = true;
1430
+ }
1431
+ }
1432
+ get flatShading() {
1433
+ return this._flatShading;
1434
+ }
1435
+ set flatShading(value) {
1436
+ if (this._flatShading !== value) {
1437
+ this._flatShading = value;
1438
+ this._uniformsDirty = true;
1439
+ }
1440
+ }
1441
+ get domainWarpEnabled() {
1442
+ return this._domainWarpEnabled;
1443
+ }
872
1444
  set domainWarpEnabled(enabled) {
873
1445
  if (this._domainWarpEnabled !== enabled) {
874
1446
  this._domainWarpEnabled = enabled;
875
1447
  this._uniformsDirty = true;
876
1448
  }
877
1449
  }
1450
+ get domainWarpIntensity() {
1451
+ return this._domainWarpIntensity;
1452
+ }
878
1453
  set domainWarpIntensity(intensity) {
879
1454
  if (this._domainWarpIntensity !== intensity) {
880
1455
  this._domainWarpIntensity = intensity;
881
1456
  this._uniformsDirty = true;
882
1457
  }
883
1458
  }
1459
+ get domainWarpScale() {
1460
+ return this._domainWarpScale;
1461
+ }
884
1462
  set domainWarpScale(scale) {
885
1463
  if (this._domainWarpScale !== scale) {
886
1464
  this._domainWarpScale = scale;
887
1465
  this._uniformsDirty = true;
888
1466
  }
889
1467
  }
1468
+ get vignetteIntensity() {
1469
+ return this._vignetteIntensity;
1470
+ }
890
1471
  set vignetteIntensity(intensity) {
891
1472
  if (this._vignetteIntensity !== intensity) {
892
1473
  this._vignetteIntensity = intensity;
893
1474
  this._uniformsDirty = true;
894
1475
  }
895
1476
  }
1477
+ get vignetteRadius() {
1478
+ return this._vignetteRadius;
1479
+ }
896
1480
  set vignetteRadius(radius) {
897
1481
  if (this._vignetteRadius !== radius) {
898
1482
  this._vignetteRadius = radius;
899
1483
  this._uniformsDirty = true;
900
1484
  }
901
1485
  }
1486
+ get fresnelEnabled() {
1487
+ return this._fresnelEnabled;
1488
+ }
902
1489
  set fresnelEnabled(enabled) {
903
1490
  if (this._fresnelEnabled !== enabled) {
904
1491
  this._fresnelEnabled = enabled;
905
1492
  this._uniformsDirty = true;
906
1493
  }
907
1494
  }
1495
+ get fresnelPower() {
1496
+ return this._fresnelPower;
1497
+ }
908
1498
  set fresnelPower(power) {
909
1499
  if (this._fresnelPower !== power) {
910
1500
  this._fresnelPower = power;
911
1501
  this._uniformsDirty = true;
912
1502
  }
913
1503
  }
1504
+ get fresnelIntensity() {
1505
+ return this._fresnelIntensity;
1506
+ }
914
1507
  set fresnelIntensity(intensity) {
915
1508
  if (this._fresnelIntensity !== intensity) {
916
1509
  this._fresnelIntensity = intensity;
917
1510
  this._uniformsDirty = true;
918
1511
  }
919
1512
  }
1513
+ get fresnelColor() {
1514
+ return this._fresnelColor;
1515
+ }
920
1516
  set fresnelColor(fresnelColor) {
921
1517
  if (this._fresnelColor !== fresnelColor) {
922
1518
  this._fresnelColor = fresnelColor;
@@ -924,42 +1520,199 @@ export class NeatGradient {
924
1520
  this._uniformsDirty = true;
925
1521
  }
926
1522
  }
1523
+ get iridescenceEnabled() {
1524
+ return this._iridescenceEnabled;
1525
+ }
927
1526
  set iridescenceEnabled(enabled) {
928
1527
  if (this._iridescenceEnabled !== enabled) {
929
1528
  this._iridescenceEnabled = enabled;
930
1529
  this._uniformsDirty = true;
931
1530
  }
932
1531
  }
1532
+ get iridescenceIntensity() {
1533
+ return this._iridescenceIntensity;
1534
+ }
933
1535
  set iridescenceIntensity(intensity) {
934
1536
  if (this._iridescenceIntensity !== intensity) {
935
1537
  this._iridescenceIntensity = intensity;
936
1538
  this._uniformsDirty = true;
937
1539
  }
938
1540
  }
1541
+ get iridescenceSpeed() {
1542
+ return this._iridescenceSpeed;
1543
+ }
939
1544
  set iridescenceSpeed(speed) {
940
1545
  if (this._iridescenceSpeed !== speed) {
941
1546
  this._iridescenceSpeed = speed;
942
1547
  this._uniformsDirty = true;
943
1548
  }
944
1549
  }
1550
+ get bloomIntensity() {
1551
+ return this._bloomIntensity;
1552
+ }
945
1553
  set bloomIntensity(intensity) {
946
1554
  if (this._bloomIntensity !== intensity) {
947
1555
  this._bloomIntensity = intensity;
948
1556
  this._uniformsDirty = true;
949
1557
  }
950
1558
  }
1559
+ get bloomThreshold() {
1560
+ return this._bloomThreshold;
1561
+ }
951
1562
  set bloomThreshold(threshold) {
952
1563
  if (this._bloomThreshold !== threshold) {
953
1564
  this._bloomThreshold = threshold;
954
1565
  this._uniformsDirty = true;
955
1566
  }
956
1567
  }
1568
+ get chromaticAberration() {
1569
+ return this._chromaticAberration;
1570
+ }
957
1571
  set chromaticAberration(aberration) {
958
1572
  if (this._chromaticAberration !== aberration) {
959
1573
  this._chromaticAberration = aberration;
960
1574
  this._uniformsDirty = true;
961
1575
  }
962
1576
  }
1577
+ // Getters and Setters for 3D Shapes
1578
+ get shapeType() {
1579
+ return this._shapeType;
1580
+ }
1581
+ set shapeType(val) {
1582
+ if (this._shapeType !== val) {
1583
+ this._shapeType = val;
1584
+ this._updateGeometry();
1585
+ }
1586
+ }
1587
+ get shapeRotationX() { return this._shapeRotationX; }
1588
+ set shapeRotationX(val) {
1589
+ this._shapeRotationX = val;
1590
+ this._uniformsDirty = true;
1591
+ }
1592
+ get shapeRotationY() { return this._shapeRotationY; }
1593
+ set shapeRotationY(val) {
1594
+ this._shapeRotationY = val;
1595
+ this._uniformsDirty = true;
1596
+ }
1597
+ get shapeRotationZ() { return this._shapeRotationZ; }
1598
+ set shapeRotationZ(val) {
1599
+ this._shapeRotationZ = val;
1600
+ this._uniformsDirty = true;
1601
+ }
1602
+ get shapeAutoRotateSpeedX() { return this._shapeAutoRotateSpeedX; }
1603
+ set shapeAutoRotateSpeedX(val) {
1604
+ this._shapeAutoRotateSpeedX = val;
1605
+ this._uniformsDirty = true;
1606
+ }
1607
+ get shapeAutoRotateSpeedY() { return this._shapeAutoRotateSpeedY; }
1608
+ set shapeAutoRotateSpeedY(val) {
1609
+ this._shapeAutoRotateSpeedY = val;
1610
+ this._uniformsDirty = true;
1611
+ }
1612
+ get sphereRadius() { return this._sphereRadius; }
1613
+ set sphereRadius(val) {
1614
+ if (this._sphereRadius !== val) {
1615
+ this._sphereRadius = val;
1616
+ this._updateGeometry();
1617
+ }
1618
+ }
1619
+ get torusRadius() { return this._torusRadius; }
1620
+ set torusRadius(val) {
1621
+ if (this._torusRadius !== val) {
1622
+ this._torusRadius = val;
1623
+ this._updateGeometry();
1624
+ }
1625
+ }
1626
+ get torusTube() { return this._torusTube; }
1627
+ set torusTube(val) {
1628
+ if (this._torusTube !== val) {
1629
+ this._torusTube = val;
1630
+ this._updateGeometry();
1631
+ }
1632
+ }
1633
+ get cylinderRadius() { return this._cylinderRadius; }
1634
+ set cylinderRadius(val) {
1635
+ if (this._cylinderRadius !== val) {
1636
+ this._cylinderRadius = val;
1637
+ this._updateGeometry();
1638
+ }
1639
+ }
1640
+ get cylinderHeight() { return this._cylinderHeight; }
1641
+ set cylinderHeight(val) {
1642
+ if (this._cylinderHeight !== val) {
1643
+ this._cylinderHeight = val;
1644
+ this._updateGeometry();
1645
+ }
1646
+ }
1647
+ get planeBend() { return this._planeBend; }
1648
+ set planeBend(val) {
1649
+ if (this._planeBend !== val) {
1650
+ this._planeBend = val;
1651
+ this._updateGeometry();
1652
+ }
1653
+ }
1654
+ get planeTwist() { return this._planeTwist; }
1655
+ set planeTwist(val) {
1656
+ if (this._planeTwist !== val) {
1657
+ this._planeTwist = val;
1658
+ this._updateGeometry();
1659
+ }
1660
+ }
1661
+ // Camera Getters and Setters
1662
+ get cameraLock() { return this._cameraLock; }
1663
+ set cameraLock(val) {
1664
+ this._cameraLock = val;
1665
+ }
1666
+ get cameraX() { return this._cameraX; }
1667
+ set cameraX(val) {
1668
+ this._cameraX = val;
1669
+ this._uniformsDirty = true;
1670
+ }
1671
+ get cameraY() { return this._cameraY; }
1672
+ set cameraY(val) {
1673
+ this._cameraY = val;
1674
+ this._uniformsDirty = true;
1675
+ }
1676
+ get cameraZ() { return this._cameraZ; }
1677
+ set cameraZ(val) {
1678
+ this._cameraZ = val;
1679
+ this._uniformsDirty = true;
1680
+ }
1681
+ get cameraRotationX() { return this._cameraRotationX; }
1682
+ set cameraRotationX(val) {
1683
+ this._cameraRotationX = val;
1684
+ this._uniformsDirty = true;
1685
+ }
1686
+ get cameraRotationY() { return this._cameraRotationY; }
1687
+ set cameraRotationY(val) {
1688
+ this._cameraRotationY = val;
1689
+ this._uniformsDirty = true;
1690
+ }
1691
+ get cameraRotationZ() { return this._cameraRotationZ; }
1692
+ set cameraRotationZ(val) {
1693
+ this._cameraRotationZ = val;
1694
+ this._uniformsDirty = true;
1695
+ }
1696
+ get cameraZoom() { return this._cameraZoom; }
1697
+ set cameraZoom(val) {
1698
+ if (this._cameraZoom !== val) {
1699
+ this._cameraZoom = val;
1700
+ this._updateCameraFrustum();
1701
+ }
1702
+ }
1703
+ _updateCameraFrustum() {
1704
+ if (!this.glState)
1705
+ return;
1706
+ const gl = this.glState.gl;
1707
+ const width = this._ref.clientWidth;
1708
+ const height = this._ref.clientHeight;
1709
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1710
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1711
+ gl.useProgram(this.glState.program);
1712
+ if (projLoc)
1713
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1714
+ this._uniformsDirty = true;
1715
+ }
963
1716
  }
964
1717
  const setLinkStyles = (link) => {
965
1718
  link.id = LINK_ID;