@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
  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,50 @@ export class NeatGradient {
43
44
  _textureColorBlending = 0.01;
44
45
  _textureSeed = 333;
45
46
  _textureEase = 0.5;
47
+ _transparentTextureVoid = false;
48
+ // New effects
49
+ _domainWarpEnabled = false;
50
+ _domainWarpIntensity = 0.5;
51
+ _domainWarpScale = 1.0;
52
+ _vignetteIntensity = 0.5;
53
+ _vignetteRadius = 0.8;
54
+ _fresnelEnabled = false;
55
+ _fresnelPower = 2.0;
56
+ _fresnelIntensity = 0.5;
57
+ _fresnelColor = "#FFFFFF";
58
+ _fresnelColorRgb = [1, 1, 1];
59
+ _iridescenceEnabled = false;
60
+ _iridescenceIntensity = 0.5;
61
+ _iridescenceSpeed = 1.0;
62
+ _bloomIntensity = 0;
63
+ _bloomThreshold = 0.7;
64
+ _chromaticAberration = 0;
65
+ _silhouetteFade = 0.25;
66
+ _cylinderFade = 0.08;
67
+ _ribbonFade = 0.05;
68
+ // 3D Shapes config
69
+ _shapeType = 'plane';
70
+ _shapeRotationX = 0;
71
+ _shapeRotationY = 0;
72
+ _shapeRotationZ = 0;
73
+ _shapeAutoRotateSpeedX = 0;
74
+ _shapeAutoRotateSpeedY = 0;
75
+ _sphereRadius = 15;
76
+ _torusRadius = 15;
77
+ _torusTube = 5;
78
+ _cylinderRadius = 10;
79
+ _cylinderHeight = 40;
80
+ _planeBend = 0;
81
+ _planeTwist = 0;
82
+ // Camera settings
83
+ _cameraLock = false;
84
+ _cameraX = 0;
85
+ _cameraY = 0;
86
+ _cameraZ = 0;
87
+ _cameraRotationX = 0;
88
+ _cameraRotationY = 0;
89
+ _cameraRotationZ = 0;
90
+ _cameraZoom = 1.0;
46
91
  _proceduralTexture = null;
47
92
  _proceduralBackgroundColor = "#000000";
48
93
  _textureShapeTriangles = 20;
@@ -58,6 +103,11 @@ export class NeatGradient {
58
103
  _yOffsetWaveMultiplier = 0.004;
59
104
  _yOffsetColorMultiplier = 0.004;
60
105
  _yOffsetFlowMultiplier = 0.004;
106
+ // Cached offscreen canvases for procedural texture generation
107
+ _sourceCanvas = null;
108
+ _sourceCtx = null;
109
+ _maskedCanvas = null;
110
+ _maskedCtx = null;
61
111
  // Performance optimizations
62
112
  _resizeTimeoutId = null;
63
113
  _textureNeedsUpdate = false;
@@ -70,7 +120,11 @@ export class NeatGradient {
70
120
  // Flow field parameters
71
121
  flowDistortionA = 0, flowDistortionB = 0, flowScale = 1.0, flowEase = 0.0, flowEnabled = true,
72
122
  // Texture generation
73
- 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, } = config;
123
+ 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,
124
+ // Camera configuration
125
+ cameraLock = false, cameraX = 0, cameraY = 0, cameraZ = 0, cameraRotationX = 0, cameraRotationY = 0, cameraRotationZ = 0, cameraZoom = 1.0,
126
+ // 3D shapes default
127
+ 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;
74
128
  this._ref = ref;
75
129
  this.destroy = this.destroy.bind(this);
76
130
  this._initScene = this._initScene.bind(this);
@@ -81,6 +135,7 @@ export class NeatGradient {
81
135
  this.waveFrequencyY = waveFrequencyY;
82
136
  this.waveAmplitude = waveAmplitude;
83
137
  this.colorBlending = colorBlending;
138
+ this._resolution = resolution;
84
139
  this.grainScale = grainScale;
85
140
  this.grainIntensity = grainIntensity;
86
141
  this.grainSparsity = grainSparsity;
@@ -113,10 +168,50 @@ export class NeatGradient {
113
168
  this.textureSeed = textureSeed;
114
169
  this.textureEase = textureEase;
115
170
  this._proceduralBackgroundColor = proceduralBackgroundColor;
171
+ this.transparentTextureVoid = transparentTextureVoid;
116
172
  this._textureShapeTriangles = textureShapeTriangles;
117
173
  this._textureShapeCircles = textureShapeCircles;
118
174
  this._textureShapeBars = textureShapeBars;
119
175
  this._textureShapeSquiggles = textureShapeSquiggles;
176
+ this.domainWarpEnabled = domainWarpEnabled;
177
+ this.domainWarpIntensity = domainWarpIntensity;
178
+ this.domainWarpScale = domainWarpScale;
179
+ this.vignetteIntensity = vignetteIntensity;
180
+ this.vignetteRadius = vignetteRadius;
181
+ this.fresnelEnabled = fresnelEnabled;
182
+ this.fresnelPower = fresnelPower;
183
+ this.fresnelIntensity = fresnelIntensity;
184
+ this.fresnelColor = fresnelColor;
185
+ this.iridescenceEnabled = iridescenceEnabled;
186
+ this.iridescenceIntensity = iridescenceIntensity;
187
+ this.iridescenceSpeed = iridescenceSpeed;
188
+ this.bloomIntensity = bloomIntensity;
189
+ this.bloomThreshold = bloomThreshold;
190
+ this.chromaticAberration = chromaticAberration;
191
+ this.silhouetteFade = silhouetteFade;
192
+ this.cylinderFade = cylinderFade;
193
+ this.ribbonFade = ribbonFade;
194
+ this._cameraLock = cameraLock;
195
+ this._cameraX = cameraX;
196
+ this._cameraY = cameraY;
197
+ this._cameraZ = cameraZ;
198
+ this._cameraRotationX = cameraRotationX;
199
+ this._cameraRotationY = cameraRotationY;
200
+ this._cameraRotationZ = cameraRotationZ;
201
+ this._cameraZoom = cameraZoom;
202
+ this._shapeType = shapeType;
203
+ this._shapeRotationX = shapeRotationX;
204
+ this._shapeRotationY = shapeRotationY;
205
+ this._shapeRotationZ = shapeRotationZ;
206
+ this._shapeAutoRotateSpeedX = shapeAutoRotateSpeedX;
207
+ this._shapeAutoRotateSpeedY = shapeAutoRotateSpeedY;
208
+ this._sphereRadius = sphereRadius;
209
+ this._torusRadius = torusRadius;
210
+ this._torusTube = torusTube;
211
+ this._cylinderRadius = cylinderRadius;
212
+ this._cylinderHeight = cylinderHeight;
213
+ this._planeBend = planeBend;
214
+ this._planeTwist = planeTwist;
120
215
  this.glState = this._initScene(resolution);
121
216
  injectSEO();
122
217
  let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
@@ -137,6 +232,36 @@ export class NeatGradient {
137
232
  lastTime = timeNow;
138
233
  gl.useProgram(program);
139
234
  gl.uniform1f(locations.uniforms['u_time'], tick);
235
+ // Update modelViewMatrix in every frame to support dynamic rotation and auto-rotation
236
+ const camera = this.glState.camera;
237
+ const modelViewMatrix = new Matrix4();
238
+ // 1. Camera translation (default camera distance + displacement)
239
+ modelViewMatrix.translate(-camera.position[0] - this._cameraX, -camera.position[1] - this._cameraY, -camera.position[2] - this._cameraZ);
240
+ modelViewMatrix.translate(0, 0, -1);
241
+ // 2. Camera rotation (revolving around target)
242
+ modelViewMatrix.rotateX(-this._cameraRotationX);
243
+ modelViewMatrix.rotateY(-this._cameraRotationY);
244
+ modelViewMatrix.rotateZ(-this._cameraRotationZ);
245
+ let rx = this._shapeRotationX;
246
+ let ry = this._shapeRotationY;
247
+ let rz = this._shapeRotationZ;
248
+ if (this._shapeAutoRotateSpeedX !== 0) {
249
+ rx += tick * this._shapeAutoRotateSpeedX * 0.1;
250
+ }
251
+ if (this._shapeAutoRotateSpeedY !== 0) {
252
+ ry += tick * this._shapeAutoRotateSpeedY * 0.1;
253
+ }
254
+ if (this._shapeType === 'plane' || this._shapeType === 'ribbon') {
255
+ modelViewMatrix.rotateX(rx - Math.PI / 3.5);
256
+ }
257
+ else {
258
+ modelViewMatrix.rotateX(rx);
259
+ }
260
+ modelViewMatrix.rotateY(ry);
261
+ modelViewMatrix.rotateZ(rz);
262
+ const mvLoc = locations.uniforms["modelViewMatrix"];
263
+ if (mvLoc)
264
+ gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
140
265
  // Only upload static uniforms when they've been modified
141
266
  if (this._uniformsDirty) {
142
267
  gl.uniform2f(locations.uniforms['u_resolution'], this._ref.clientWidth, this._ref.clientHeight);
@@ -162,8 +287,37 @@ export class NeatGradient {
162
287
  gl.uniform1f(locations.uniforms['u_flow_scale'], this._flowScale);
163
288
  gl.uniform1f(locations.uniforms['u_flow_ease'], this._flowEase);
164
289
  gl.uniform1f(locations.uniforms['u_flow_enabled'], this._flowEnabled ? 1.0 : 0.0);
290
+ let shapeTypeVal = 0.0;
291
+ if (this._shapeType === 'sphere')
292
+ shapeTypeVal = 1.0;
293
+ else if (this._shapeType === 'torus')
294
+ shapeTypeVal = 2.0;
295
+ else if (this._shapeType === 'cylinder')
296
+ shapeTypeVal = 3.0;
297
+ else if (this._shapeType === 'ribbon')
298
+ shapeTypeVal = 4.0;
299
+ gl.uniform1f(locations.uniforms['u_shape_type'], shapeTypeVal);
165
300
  gl.uniform1f(locations.uniforms['u_enable_procedural_texture'], this._enableProceduralTexture ? 1.0 : 0.0);
166
301
  gl.uniform1f(locations.uniforms['u_texture_ease'], this._textureEase);
302
+ gl.uniform1f(locations.uniforms['u_transparent_texture_void'], this._transparentTextureVoid ? 1.0 : 0.0);
303
+ gl.uniform1f(locations.uniforms['u_domain_warp_enabled'], this._domainWarpEnabled ? 1.0 : 0.0);
304
+ gl.uniform1f(locations.uniforms['u_domain_warp_intensity'], this._domainWarpIntensity);
305
+ gl.uniform1f(locations.uniforms['u_domain_warp_scale'], this._domainWarpScale);
306
+ gl.uniform1f(locations.uniforms['u_vignette_intensity'], this._vignetteIntensity);
307
+ gl.uniform1f(locations.uniforms['u_vignette_radius'], this._vignetteRadius);
308
+ gl.uniform1f(locations.uniforms['u_fresnel_enabled'], this._fresnelEnabled ? 1.0 : 0.0);
309
+ gl.uniform1f(locations.uniforms['u_fresnel_power'], this._fresnelPower);
310
+ gl.uniform1f(locations.uniforms['u_fresnel_intensity'], this._fresnelIntensity);
311
+ gl.uniform3fv(locations.uniforms['u_fresnel_color'], this._fresnelColorRgb);
312
+ gl.uniform1f(locations.uniforms['u_iridescence_enabled'], this._iridescenceEnabled ? 1.0 : 0.0);
313
+ gl.uniform1f(locations.uniforms['u_iridescence_intensity'], this._iridescenceIntensity);
314
+ gl.uniform1f(locations.uniforms['u_iridescence_speed'], this._iridescenceSpeed);
315
+ gl.uniform1f(locations.uniforms['u_bloom_intensity'], this._bloomIntensity);
316
+ gl.uniform1f(locations.uniforms['u_bloom_threshold'], this._bloomThreshold);
317
+ gl.uniform1f(locations.uniforms['u_chromatic_aberration'], this._chromaticAberration);
318
+ gl.uniform1f(locations.uniforms['u_silhouette_fade'], this._silhouetteFade);
319
+ gl.uniform1f(locations.uniforms['u_cylinder_fade'], this._cylinderFade);
320
+ gl.uniform1f(locations.uniforms['u_ribbon_fade'], this._ribbonFade);
167
321
  this._uniformsDirty = false;
168
322
  }
169
323
  // Only regenerate procedural texture when needed
@@ -221,11 +375,12 @@ export class NeatGradient {
221
375
  this._ref.width = width;
222
376
  this._ref.height = height;
223
377
  gl.viewport(0, 0, width, height);
224
- updateCamera(camera, width, height);
378
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
225
379
  // Recompute projection matrix on resize
226
- const projLoc = gl.getUniformLocation(this.glState.program, "projectionMatrix");
380
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
227
381
  gl.useProgram(this.glState.program);
228
- gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
382
+ if (projLoc)
383
+ gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
229
384
  };
230
385
  // Debounce resize to prevent excessive operations
231
386
  this.sizeObserver = new ResizeObserver(() => {
@@ -271,98 +426,292 @@ export class NeatGradient {
271
426
  const dataURL = this._ref.toDataURL("image/png");
272
427
  downloadURI(dataURL, filename);
273
428
  }
429
+ /**
430
+ * Records the canvas animation as a video with a NEAT watermark overlay.
431
+ * @param options.durationMs Recording duration in milliseconds (default 5000).
432
+ * @param options.filename Output file name (default "neat.firecms.co").
433
+ * @param options.width Output video width in pixels (default: current canvas width).
434
+ * @param options.height Output video height in pixels (default: current canvas height).
435
+ * @param options.format Preferred format: 'mp4' or 'webm' (default: best available).
436
+ * @param options.onProgress Callback with progress 0-1.
437
+ * @param options.onComplete Callback when recording finishes.
438
+ * @returns A stop function to end recording early.
439
+ */
440
+ recordVideo(options = {}) {
441
+ const { durationMs = 5000, filename = "neat.firecms.co", format, onProgress, onComplete, } = options;
442
+ const source = this._ref;
443
+ const width = options.width || source.width || source.clientWidth;
444
+ const height = options.height || source.height || source.clientHeight;
445
+ // Offscreen canvas that composites gradient + watermark each frame
446
+ const offscreen = document.createElement("canvas");
447
+ offscreen.width = width;
448
+ offscreen.height = height;
449
+ const ctx = offscreen.getContext("2d");
450
+ // Use captureStream(0) — only captures a frame when we explicitly
451
+ // call requestFrame() on the video track, so every composited frame
452
+ // is guaranteed to be captured.
453
+ const stream = offscreen.captureStream(0);
454
+ const videoTrack = stream.getVideoTracks()[0];
455
+ // Codec candidates ordered by preference
456
+ const mp4Candidates = [
457
+ "video/mp4;codecs=avc1",
458
+ "video/mp4;codecs=avc1,opus",
459
+ "video/mp4",
460
+ ];
461
+ const webmCandidates = [
462
+ "video/webm;codecs=vp9,opus",
463
+ "video/webm;codecs=vp9",
464
+ "video/webm;codecs=vp8,opus",
465
+ "video/webm",
466
+ ];
467
+ // Build candidate list based on preferred format
468
+ let candidates;
469
+ if (format === 'mp4')
470
+ candidates = [...mp4Candidates, ...webmCandidates];
471
+ else if (format === 'webm')
472
+ candidates = [...webmCandidates, ...mp4Candidates];
473
+ else
474
+ candidates = [...mp4Candidates, ...webmCandidates];
475
+ let mimeType = "video/webm";
476
+ for (const candidate of candidates) {
477
+ if (MediaRecorder.isTypeSupported(candidate)) {
478
+ mimeType = candidate;
479
+ break;
480
+ }
481
+ }
482
+ // Scale bitrate with pixel count: 8 Mbps baseline at 720p
483
+ const pixels = width * height;
484
+ const baseBitrate = 8_000_000;
485
+ const basePixels = 1280 * 720;
486
+ const videoBitsPerSecond = Math.round(baseBitrate * Math.max(1, pixels / basePixels));
487
+ const recorder = new MediaRecorder(stream, {
488
+ mimeType,
489
+ videoBitsPerSecond,
490
+ });
491
+ const chunks = [];
492
+ recorder.ondataavailable = (e) => {
493
+ if (e.data.size > 0)
494
+ chunks.push(e.data);
495
+ };
496
+ let stopped = false;
497
+ let rafId;
498
+ const startTime = performance.now();
499
+ let lastProgressTime = 0;
500
+ // Composite loop: draw source canvas + watermark overlay on each frame
501
+ const drawFrame = () => {
502
+ if (stopped)
503
+ return;
504
+ ctx.clearRect(0, 0, width, height);
505
+ ctx.drawImage(source, 0, 0, width, height);
506
+ // Watermark: "NEAT" in bottom-right corner
507
+ const fontSize = Math.max(14, Math.round(height * 0.025));
508
+ ctx.font = `bold ${fontSize}px "Sofia Sans", sans-serif`;
509
+ ctx.textAlign = "right";
510
+ ctx.textBaseline = "bottom";
511
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
512
+ ctx.shadowBlur = 4;
513
+ ctx.shadowOffsetX = 1;
514
+ ctx.shadowOffsetY = 1;
515
+ ctx.fillStyle = "rgba(255,255,255,0.7)";
516
+ ctx.fillText("NEAT", width - fontSize * 0.8, height - fontSize * 0.5);
517
+ ctx.shadowColor = "transparent";
518
+ ctx.shadowBlur = 0;
519
+ ctx.shadowOffsetX = 0;
520
+ ctx.shadowOffsetY = 0;
521
+ // Signal the stream to capture this frame
522
+ // @ts-ignore – requestFrame exists on CanvasCaptureMediaStreamTrack
523
+ if (videoTrack.requestFrame)
524
+ videoTrack.requestFrame();
525
+ // Throttle progress to ~4 updates/sec to avoid flooding React state
526
+ if (onProgress) {
527
+ const now = performance.now();
528
+ if (now - lastProgressTime > 250) {
529
+ lastProgressTime = now;
530
+ onProgress(Math.min(0.99, (now - startTime) / durationMs));
531
+ }
532
+ }
533
+ rafId = requestAnimationFrame(drawFrame);
534
+ };
535
+ recorder.onstop = () => {
536
+ stopped = true;
537
+ cancelAnimationFrame(rafId);
538
+ // Use the correct file extension for the actual format
539
+ const isMP4 = mimeType.startsWith("video/mp4");
540
+ const ext = isMP4 ? ".mp4" : ".webm";
541
+ const blobType = isMP4 ? "video/mp4" : "video/webm";
542
+ const finalFilename = filename + ext;
543
+ const blob = new Blob(chunks, { type: blobType });
544
+ const url = URL.createObjectURL(blob);
545
+ downloadURI(url, finalFilename);
546
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
547
+ onProgress?.(1);
548
+ onComplete?.();
549
+ };
550
+ // Start drawing frames, then start recording
551
+ drawFrame();
552
+ recorder.start(100); // collect data every 100ms
553
+ // Auto-stop after the requested duration
554
+ const timeoutId = window.setTimeout(() => {
555
+ if (recorder.state === "recording") {
556
+ recorder.stop();
557
+ }
558
+ }, durationMs);
559
+ // Return a stop function for early termination
560
+ return () => {
561
+ clearTimeout(timeoutId);
562
+ if (recorder.state === "recording") {
563
+ recorder.stop();
564
+ }
565
+ };
566
+ }
567
+ get speed() {
568
+ return this._speed * 20;
569
+ }
274
570
  set speed(speed) {
275
571
  this._uniformsDirty = true;
276
572
  this._speed = speed / 20;
277
573
  }
574
+ get horizontalPressure() {
575
+ return this._horizontalPressure * 4;
576
+ }
278
577
  set horizontalPressure(horizontalPressure) {
279
578
  this._uniformsDirty = true;
280
579
  this._horizontalPressure = horizontalPressure / 4;
281
580
  }
581
+ get verticalPressure() {
582
+ return this._verticalPressure * 4;
583
+ }
282
584
  set verticalPressure(verticalPressure) {
283
585
  this._uniformsDirty = true;
284
586
  this._verticalPressure = verticalPressure / 4;
285
587
  }
588
+ get waveFrequencyX() {
589
+ return this._waveFrequencyX / 0.04;
590
+ }
286
591
  set waveFrequencyX(waveFrequencyX) {
287
592
  this._uniformsDirty = true;
288
593
  this._waveFrequencyX = waveFrequencyX * 0.04;
289
594
  }
595
+ get waveFrequencyY() {
596
+ return this._waveFrequencyY / 0.04;
597
+ }
290
598
  set waveFrequencyY(waveFrequencyY) {
291
599
  this._uniformsDirty = true;
292
600
  this._waveFrequencyY = waveFrequencyY * 0.04;
293
601
  }
602
+ get waveAmplitude() {
603
+ return this._waveAmplitude / 0.75;
604
+ }
294
605
  set waveAmplitude(waveAmplitude) {
295
606
  this._uniformsDirty = true;
296
607
  this._waveAmplitude = waveAmplitude * .75;
297
608
  }
609
+ get colors() {
610
+ return this._colors;
611
+ }
298
612
  set colors(colors) {
299
613
  this._uniformsDirty = true;
300
614
  this._colors = colors;
301
615
  this._cachedColorRgb = colors.map(c => this._hexToRgb(c.color));
302
616
  this._colorsChanged = true;
303
617
  }
618
+ get highlights() {
619
+ return this._highlights * 100;
620
+ }
304
621
  set highlights(highlights) {
305
622
  this._uniformsDirty = true;
306
623
  this._highlights = highlights / 100;
307
624
  }
625
+ get shadows() {
626
+ return this._shadows * 100;
627
+ }
308
628
  set shadows(shadows) {
309
629
  this._uniformsDirty = true;
310
630
  this._shadows = shadows / 100;
311
631
  }
632
+ get colorSaturation() {
633
+ return this._saturation * 10;
634
+ }
312
635
  set colorSaturation(colorSaturation) {
313
636
  this._uniformsDirty = true;
314
637
  this._saturation = colorSaturation / 10;
315
638
  }
639
+ get colorBrightness() {
640
+ return this._brightness;
641
+ }
316
642
  set colorBrightness(colorBrightness) {
317
643
  this._uniformsDirty = true;
318
644
  this._brightness = colorBrightness;
319
645
  }
646
+ get colorBlending() {
647
+ return this._colorBlending * 10;
648
+ }
320
649
  set colorBlending(colorBlending) {
321
650
  this._uniformsDirty = true;
322
651
  this._colorBlending = colorBlending / 10;
323
652
  }
653
+ get grainScale() {
654
+ return this._grainScale;
655
+ }
324
656
  set grainScale(grainScale) {
325
657
  this._uniformsDirty = true;
326
658
  this._grainScale = grainScale == 0 ? 1 : grainScale;
327
659
  }
660
+ get grainIntensity() {
661
+ return this._grainIntensity;
662
+ }
328
663
  set grainIntensity(grainIntensity) {
329
664
  this._uniformsDirty = true;
330
665
  this._grainIntensity = grainIntensity;
331
666
  }
667
+ get grainSparsity() {
668
+ return this._grainSparsity;
669
+ }
332
670
  set grainSparsity(grainSparsity) {
333
671
  this._uniformsDirty = true;
334
672
  this._grainSparsity = grainSparsity;
335
673
  }
674
+ get grainSpeed() {
675
+ return this._grainSpeed;
676
+ }
336
677
  set grainSpeed(grainSpeed) {
337
678
  this._uniformsDirty = true;
338
679
  this._grainSpeed = grainSpeed;
339
680
  }
681
+ get wireframe() {
682
+ return this._wireframe;
683
+ }
340
684
  set wireframe(wireframe) {
341
685
  this._uniformsDirty = true;
342
686
  this._wireframe = wireframe;
343
687
  }
688
+ get resolution() {
689
+ return this._resolution;
690
+ }
344
691
  set resolution(resolution) {
345
- this._uniformsDirty = true;
346
- if (this.glState) {
347
- const gl = this.glState.gl;
348
- gl.deleteProgram(this.glState.program);
349
- gl.deleteBuffer(this.glState.buffers.position);
350
- gl.deleteBuffer(this.glState.buffers.normal);
351
- gl.deleteBuffer(this.glState.buffers.uv);
352
- gl.deleteBuffer(this.glState.buffers.index);
353
- gl.deleteBuffer(this.glState.buffers.wireframeIndex);
354
- }
355
- this.glState = this._initScene(resolution);
692
+ if (this._resolution === resolution)
693
+ return;
694
+ this._resolution = resolution;
695
+ this._updateGeometry();
696
+ }
697
+ get backgroundColor() {
698
+ return this._backgroundColor;
356
699
  }
357
700
  set backgroundColor(backgroundColor) {
358
701
  this._uniformsDirty = true;
359
702
  this._backgroundColor = backgroundColor;
360
703
  this._backgroundColorRgb = this._hexToRgb(backgroundColor);
361
704
  }
705
+ get backgroundAlpha() {
706
+ return this._backgroundAlpha;
707
+ }
362
708
  set backgroundAlpha(backgroundAlpha) {
363
709
  this._uniformsDirty = true;
364
710
  this._backgroundAlpha = backgroundAlpha;
365
711
  }
712
+ get yOffset() {
713
+ return this._yOffset;
714
+ }
366
715
  set yOffset(yOffset) {
367
716
  this._uniformsDirty = true;
368
717
  this._yOffset = yOffset;
@@ -388,18 +737,30 @@ export class NeatGradient {
388
737
  this._uniformsDirty = true;
389
738
  this._yOffsetFlowMultiplier = value / 1000;
390
739
  }
740
+ get flowDistortionA() {
741
+ return this._flowDistortionA;
742
+ }
391
743
  set flowDistortionA(value) {
392
744
  this._uniformsDirty = true;
393
745
  this._flowDistortionA = value;
394
746
  }
747
+ get flowDistortionB() {
748
+ return this._flowDistortionB;
749
+ }
395
750
  set flowDistortionB(value) {
396
751
  this._uniformsDirty = true;
397
752
  this._flowDistortionB = value;
398
753
  }
754
+ get flowScale() {
755
+ return this._flowScale;
756
+ }
399
757
  set flowScale(value) {
400
758
  this._uniformsDirty = true;
401
759
  this._flowScale = value;
402
760
  }
761
+ get flowEase() {
762
+ return this._flowEase;
763
+ }
403
764
  set flowEase(value) {
404
765
  this._uniformsDirty = true;
405
766
  this._flowEase = value;
@@ -411,6 +772,9 @@ export class NeatGradient {
411
772
  get flowEnabled() {
412
773
  return this._flowEnabled;
413
774
  }
775
+ get enableProceduralTexture() {
776
+ return this._enableProceduralTexture;
777
+ }
414
778
  set enableProceduralTexture(value) {
415
779
  this._uniformsDirty = true;
416
780
  this._enableProceduralTexture = value;
@@ -418,6 +782,9 @@ export class NeatGradient {
418
782
  this._textureNeedsUpdate = true;
419
783
  }
420
784
  }
785
+ get textureVoidLikelihood() {
786
+ return this._textureVoidLikelihood;
787
+ }
421
788
  set textureVoidLikelihood(value) {
422
789
  this._uniformsDirty = true;
423
790
  this._textureVoidLikelihood = value;
@@ -425,6 +792,9 @@ export class NeatGradient {
425
792
  this._textureNeedsUpdate = true;
426
793
  }
427
794
  }
795
+ get textureVoidWidthMin() {
796
+ return this._textureVoidWidthMin;
797
+ }
428
798
  set textureVoidWidthMin(value) {
429
799
  this._uniformsDirty = true;
430
800
  this._textureVoidWidthMin = value;
@@ -432,6 +802,9 @@ export class NeatGradient {
432
802
  this._textureNeedsUpdate = true;
433
803
  }
434
804
  }
805
+ get textureVoidWidthMax() {
806
+ return this._textureVoidWidthMax;
807
+ }
435
808
  set textureVoidWidthMax(value) {
436
809
  this._uniformsDirty = true;
437
810
  this._textureVoidWidthMax = value;
@@ -439,6 +812,9 @@ export class NeatGradient {
439
812
  this._textureNeedsUpdate = true;
440
813
  }
441
814
  }
815
+ get textureBandDensity() {
816
+ return this._textureBandDensity;
817
+ }
442
818
  set textureBandDensity(value) {
443
819
  this._uniformsDirty = true;
444
820
  this._textureBandDensity = value;
@@ -446,6 +822,9 @@ export class NeatGradient {
446
822
  this._textureNeedsUpdate = true;
447
823
  }
448
824
  }
825
+ get textureColorBlending() {
826
+ return this._textureColorBlending;
827
+ }
449
828
  set textureColorBlending(value) {
450
829
  this._uniformsDirty = true;
451
830
  this._textureColorBlending = value;
@@ -453,6 +832,9 @@ export class NeatGradient {
453
832
  this._textureNeedsUpdate = true;
454
833
  }
455
834
  }
835
+ get textureSeed() {
836
+ return this._textureSeed;
837
+ }
456
838
  set textureSeed(value) {
457
839
  this._uniformsDirty = true;
458
840
  this._textureSeed = value;
@@ -467,6 +849,19 @@ export class NeatGradient {
467
849
  this._uniformsDirty = true;
468
850
  this._textureEase = value;
469
851
  }
852
+ get transparentTextureVoid() {
853
+ return this._transparentTextureVoid;
854
+ }
855
+ set transparentTextureVoid(value) {
856
+ this._uniformsDirty = true;
857
+ this._transparentTextureVoid = value;
858
+ if (this._enableProceduralTexture) {
859
+ this._textureNeedsUpdate = true;
860
+ }
861
+ }
862
+ get proceduralBackgroundColor() {
863
+ return this._proceduralBackgroundColor;
864
+ }
470
865
  set proceduralBackgroundColor(value) {
471
866
  this._uniformsDirty = true;
472
867
  this._proceduralBackgroundColor = value;
@@ -474,30 +869,90 @@ export class NeatGradient {
474
869
  this._textureNeedsUpdate = true;
475
870
  }
476
871
  }
872
+ get textureShapeTriangles() {
873
+ return this._textureShapeTriangles;
874
+ }
477
875
  set textureShapeTriangles(value) {
478
876
  this._uniformsDirty = true;
479
877
  this._textureShapeTriangles = value;
480
878
  if (this._enableProceduralTexture)
481
879
  this._textureNeedsUpdate = true;
482
880
  }
881
+ get textureShapeCircles() {
882
+ return this._textureShapeCircles;
883
+ }
483
884
  set textureShapeCircles(value) {
484
885
  this._uniformsDirty = true;
485
886
  this._textureShapeCircles = value;
486
887
  if (this._enableProceduralTexture)
487
888
  this._textureNeedsUpdate = true;
488
889
  }
890
+ get textureShapeBars() {
891
+ return this._textureShapeBars;
892
+ }
489
893
  set textureShapeBars(value) {
490
894
  this._uniformsDirty = true;
491
895
  this._textureShapeBars = value;
492
896
  if (this._enableProceduralTexture)
493
897
  this._textureNeedsUpdate = true;
494
898
  }
899
+ get textureShapeSquiggles() {
900
+ return this._textureShapeSquiggles;
901
+ }
495
902
  set textureShapeSquiggles(value) {
496
903
  this._uniformsDirty = true;
497
904
  this._textureShapeSquiggles = value;
498
905
  if (this._enableProceduralTexture)
499
906
  this._textureNeedsUpdate = true;
500
907
  }
908
+ _updateGeometry() {
909
+ if (!this.glState)
910
+ return;
911
+ const gl = this.glState.gl;
912
+ const resolution = this._resolution || 1;
913
+ let geometry;
914
+ if (this._shapeType === 'sphere') {
915
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
916
+ }
917
+ else if (this._shapeType === 'torus') {
918
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
919
+ }
920
+ else if (this._shapeType === 'cylinder') {
921
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
922
+ }
923
+ else if (this._shapeType === 'ribbon') {
924
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
925
+ }
926
+ else {
927
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
928
+ }
929
+ const { position, normal, uv, index, wireframeIndex } = geometry;
930
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.position);
931
+ gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
932
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.normal);
933
+ gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);
934
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glState.buffers.uv);
935
+ gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW);
936
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
937
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
938
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.wireframeIndex);
939
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireframeIndex, gl.STATIC_DRAW);
940
+ // Restore default bound element buffer
941
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glState.buffers.index);
942
+ this.glState.indexCount = index.length;
943
+ this.glState.wireframeIndexCount = wireframeIndex.length;
944
+ this.glState.indexType = (index instanceof Uint32Array) ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
945
+ // Keep camera updated with the new shapeType and dimensions
946
+ const width = this._ref.clientWidth;
947
+ const height = this._ref.clientHeight;
948
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
949
+ // Recompute projection matrix
950
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
951
+ gl.useProgram(this.glState.program);
952
+ if (projLoc)
953
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
954
+ this._uniformsDirty = true;
955
+ }
501
956
  _hexToRgb(hex) {
502
957
  const bigint = parseInt(hex.replace('#', ''), 16);
503
958
  return [
@@ -517,8 +972,24 @@ export class NeatGradient {
517
972
  const ext = gl.getExtension("OES_standard_derivatives");
518
973
  gl.getExtension("OES_element_index_uint");
519
974
  gl.viewport(0, 0, width, height);
520
- // Generate plane geometry with Uint32Array for large meshes
521
- const { position, normal, uv, index, wireframeIndex } = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
975
+ // Generate parametric geometry based on shapeType
976
+ let geometry;
977
+ if (this._shapeType === 'sphere') {
978
+ geometry = generateSphereGeometry(this._sphereRadius, 120 * resolution, 120 * resolution);
979
+ }
980
+ else if (this._shapeType === 'torus') {
981
+ geometry = generateTorusGeometry(this._torusRadius, this._torusTube, 120 * resolution, 120 * resolution);
982
+ }
983
+ else if (this._shapeType === 'cylinder') {
984
+ geometry = generateCylinderGeometry(this._cylinderRadius, this._cylinderRadius, this._cylinderHeight, 120 * resolution, 120 * resolution);
985
+ }
986
+ else if (this._shapeType === 'ribbon') {
987
+ geometry = generateRibbonGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution, this._planeBend, this._planeTwist);
988
+ }
989
+ else {
990
+ geometry = generatePlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT, 240 * resolution, 240 * resolution);
991
+ }
992
+ const { position, normal, uv, index, wireframeIndex } = geometry;
522
993
  const positionBuffer = gl.createBuffer();
523
994
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
524
995
  gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
@@ -573,7 +1044,7 @@ export class NeatGradient {
573
1044
  gl.useProgram(program);
574
1045
  const camera = new OrthographicCamera(0, 0, 0, 0, 0, 1000);
575
1046
  camera.position = [0, 0, 5];
576
- updateCamera(camera, width, height);
1047
+ updateCamera(camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
577
1048
  // Define attributes
578
1049
  const aPosition = gl.getAttribLocation(program, "position");
579
1050
  const aNormal = gl.getAttribLocation(program, "normal");
@@ -588,15 +1059,7 @@ export class NeatGradient {
588
1059
  gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
589
1060
  gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);
590
1061
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
591
- const modelViewMatrix = new Matrix4();
592
- // The View Matrix is the inverse of the Camera's position
593
- // Camera is at [0, 0, 5], so view matrix translates by [0, 0, -5]
594
- modelViewMatrix.translate(-camera.position[0], -camera.position[1], -camera.position[2]);
595
- // The Model Matrix mimicking: plane.rotation.x = -Math.PI / 3.5; plane.position.z = -1;
596
- modelViewMatrix.translate(0, 0, -1);
597
- modelViewMatrix.rotateX(-Math.PI / 3.5);
598
- const mvLoc = gl.getUniformLocation(program, "modelViewMatrix");
599
- gl.uniformMatrix4fv(mvLoc, false, modelViewMatrix.elements);
1062
+ // modelViewMatrix is set dynamically in the render loop
600
1063
  const projLoc = gl.getUniformLocation(program, "projectionMatrix");
601
1064
  gl.uniformMatrix4fv(projLoc, false, camera.projectionMatrix.elements);
602
1065
  const planeWidthLoc = gl.getUniformLocation(program, "u_plane_width");
@@ -606,12 +1069,19 @@ export class NeatGradient {
606
1069
  const colorsCountLoc = gl.getUniformLocation(program, "u_colors_count");
607
1070
  gl.uniform1i(colorsCountLoc, COLORS_COUNT);
608
1071
  const uniformsList = [
1072
+ "projectionMatrix", "modelViewMatrix",
609
1073
  "u_time", "u_resolution", "u_color_pressure", "u_wave_frequency_x", "u_wave_frequency_y",
610
1074
  "u_wave_amplitude", "u_colors_count", "u_plane_width", "u_plane_height", "u_shadows",
611
1075
  "u_highlights", "u_grain_intensity", "u_grain_sparsity", "u_grain_scale", "u_grain_speed",
612
1076
  "u_flow_distortion_a", "u_flow_distortion_b", "u_flow_scale", "u_flow_ease", "u_flow_enabled",
613
1077
  "u_y_offset", "u_y_offset_wave_multiplier", "u_y_offset_color_multiplier", "u_y_offset_flow_multiplier",
614
- "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_saturation", "u_brightness", "u_color_blending"
1078
+ "u_procedural_texture", "u_enable_procedural_texture", "u_texture_ease", "u_transparent_texture_void", "u_saturation", "u_brightness", "u_color_blending",
1079
+ "u_domain_warp_enabled", "u_domain_warp_intensity", "u_domain_warp_scale",
1080
+ "u_vignette_intensity", "u_vignette_radius",
1081
+ "u_fresnel_enabled", "u_fresnel_power", "u_fresnel_intensity", "u_fresnel_color",
1082
+ "u_iridescence_enabled", "u_iridescence_intensity", "u_iridescence_speed",
1083
+ "u_bloom_intensity", "u_bloom_threshold", "u_chromatic_aberration",
1084
+ "u_shape_type", "u_silhouette_fade", "u_cylinder_fade", "u_ribbon_fade"
615
1085
  ];
616
1086
  const locations = {
617
1087
  attributes: { position: aPosition, normal: aNormal, uv: aUv },
@@ -656,10 +1126,14 @@ export class NeatGradient {
656
1126
  // Texture size - 1024 provides good balance between quality and performance
657
1127
  // Reduced from 2048 for better performance
658
1128
  const texSize = 1024;
659
- const sourceCanvas = document.createElement('canvas');
660
- sourceCanvas.width = texSize;
661
- sourceCanvas.height = texSize;
662
- const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
1129
+ if (!this._sourceCanvas) {
1130
+ this._sourceCanvas = document.createElement('canvas');
1131
+ this._sourceCanvas.width = texSize;
1132
+ this._sourceCanvas.height = texSize;
1133
+ this._sourceCtx = this._sourceCanvas.getContext('2d');
1134
+ }
1135
+ const sourceCanvas = this._sourceCanvas;
1136
+ const sCtx = this._sourceCtx;
663
1137
  if (!sCtx)
664
1138
  return null;
665
1139
  let seed = this._textureSeed;
@@ -675,6 +1149,9 @@ export class NeatGradient {
675
1149
  const colors = this._colors.filter(c => c.enabled).map(c => c.color);
676
1150
  if (colors.length === 0)
677
1151
  return null;
1152
+ const shouldTile = this._shapeType !== 'plane';
1153
+ const dxs = shouldTile ? [-1, 0, 1] : [0];
1154
+ const dys = shouldTile ? [-1, 0, 1] : [0];
678
1155
  // Helper functions
679
1156
  function hexToRgb(hex) {
680
1157
  const bigint = parseInt(hex.replace('#', ''), 16);
@@ -711,64 +1188,120 @@ export class NeatGradient {
711
1188
  sCtx.fillRect(0, 0, texSize, texSize);
712
1189
  // Triangles: use configurable count
713
1190
  for (let i = 0; i < this._textureShapeTriangles; i++) {
714
- sCtx.fillStyle = getInterColor();
715
- sCtx.beginPath();
1191
+ const fillStyle = getInterColor();
716
1192
  const x = random() * texSize;
717
1193
  const y = random() * texSize;
718
1194
  const s = 100 + random() * 300;
719
- sCtx.moveTo(x, y);
720
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
721
- sCtx.lineTo(x + (random() - 0.5) * s, y + (random() - 0.5) * s);
722
- sCtx.fill();
1195
+ const x1 = (random() - 0.5) * s;
1196
+ const y1 = (random() - 0.5) * s;
1197
+ const x2 = (random() - 0.5) * s;
1198
+ const y2 = (random() - 0.5) * s;
1199
+ for (const dx of dxs) {
1200
+ for (const dy of dys) {
1201
+ sCtx.fillStyle = fillStyle;
1202
+ sCtx.beginPath();
1203
+ const tx = x + dx * texSize;
1204
+ const ty = y + dy * texSize;
1205
+ sCtx.moveTo(tx, ty);
1206
+ sCtx.lineTo(tx + x1, ty + y1);
1207
+ sCtx.lineTo(tx + x2, ty + y2);
1208
+ sCtx.fill();
1209
+ }
1210
+ }
723
1211
  }
724
1212
  // Circles / rings: use configurable count
725
1213
  for (let i = 0; i < this._textureShapeCircles; i++) {
726
- sCtx.strokeStyle = getInterColor();
727
- sCtx.lineWidth = 10 + random() * 50;
728
- sCtx.beginPath();
1214
+ const strokeStyle = getInterColor();
1215
+ const lineWidth = 10 + random() * 50;
729
1216
  const x = random() * texSize;
730
1217
  const y = random() * texSize;
731
1218
  const r = 50 + random() * 150;
732
- sCtx.arc(x, y, r, 0, Math.PI * 2);
733
- sCtx.stroke();
1219
+ for (const dx of dxs) {
1220
+ for (const dy of dys) {
1221
+ sCtx.strokeStyle = strokeStyle;
1222
+ sCtx.lineWidth = lineWidth;
1223
+ sCtx.beginPath();
1224
+ sCtx.arc(x + dx * texSize, y + dy * texSize, r, 0, Math.PI * 2);
1225
+ sCtx.stroke();
1226
+ }
1227
+ }
734
1228
  }
735
1229
  // Bars: use configurable count
736
1230
  for (let i = 0; i < this._textureShapeBars; i++) {
737
- sCtx.fillStyle = getInterColor();
738
- sCtx.save();
739
- sCtx.translate(random() * texSize, random() * texSize);
740
- sCtx.rotate(random() * Math.PI);
741
- sCtx.fillRect(-150, -25, 300, 50);
742
- sCtx.restore();
1231
+ const fillStyle = getInterColor();
1232
+ const x = random() * texSize;
1233
+ const y = random() * texSize;
1234
+ const rot = random() * Math.PI;
1235
+ for (const dx of dxs) {
1236
+ for (const dy of dys) {
1237
+ sCtx.fillStyle = fillStyle;
1238
+ sCtx.save();
1239
+ sCtx.translate(x + dx * texSize, y + dy * texSize);
1240
+ sCtx.rotate(rot);
1241
+ sCtx.fillRect(-150, -25, 300, 50);
1242
+ sCtx.restore();
1243
+ }
1244
+ }
743
1245
  }
744
1246
  // Squiggles: use configurable count
745
1247
  sCtx.lineWidth = 15;
746
1248
  sCtx.lineCap = 'round';
747
1249
  for (let i = 0; i < this._textureShapeSquiggles; i++) {
748
- sCtx.strokeStyle = getInterColor();
749
- sCtx.beginPath();
750
- let x = random() * texSize;
751
- let y = random() * texSize;
752
- sCtx.moveTo(x, y);
1250
+ const strokeStyle = getInterColor();
1251
+ const x = random() * texSize;
1252
+ const y = random() * texSize;
1253
+ const curves = [];
1254
+ let cx = 0;
1255
+ let cy = 0;
753
1256
  for (let j = 0; j < 4; j++) {
754
- 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);
755
- x += (random() - 0.5) * 300;
756
- y += (random() - 0.5) * 300;
1257
+ const ex = cx + (random() - 0.5) * 300;
1258
+ const ey = cy + (random() - 0.5) * 300;
1259
+ curves.push({
1260
+ cx1: cx + (random() - 0.5) * 300,
1261
+ cy1: cy + (random() - 0.5) * 300,
1262
+ cx2: cx + (random() - 0.5) * 300,
1263
+ cy2: cy + (random() - 0.5) * 300,
1264
+ ex: ex,
1265
+ ey: ey
1266
+ });
1267
+ cx = ex;
1268
+ cy = ey;
1269
+ }
1270
+ for (const dx of dxs) {
1271
+ for (const dy of dys) {
1272
+ sCtx.strokeStyle = strokeStyle;
1273
+ sCtx.beginPath();
1274
+ const tx = x + dx * texSize;
1275
+ const ty = y + dy * texSize;
1276
+ sCtx.moveTo(tx, ty);
1277
+ for (const curve of curves) {
1278
+ sCtx.bezierCurveTo(tx + curve.cx1, ty + curve.cy1, tx + curve.cx2, ty + curve.cy2, tx + curve.ex, ty + curve.ey);
1279
+ }
1280
+ sCtx.stroke();
1281
+ }
757
1282
  }
758
- sCtx.stroke();
759
1283
  }
760
1284
  // === MASKED CANVAS ===
761
1285
  // Masking: Seed isolation
762
1286
  setSeed(50000);
763
- const canvas = document.createElement('canvas');
764
- canvas.width = texSize;
765
- canvas.height = texSize;
766
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1287
+ if (!this._maskedCanvas) {
1288
+ this._maskedCanvas = document.createElement('canvas');
1289
+ this._maskedCanvas.width = texSize;
1290
+ this._maskedCanvas.height = texSize;
1291
+ this._maskedCtx = this._maskedCanvas.getContext('2d');
1292
+ }
1293
+ const canvas = this._maskedCanvas;
1294
+ const ctx = this._maskedCtx;
767
1295
  if (!ctx)
768
1296
  return null;
769
1297
  // Start filled with the chosen void color so gaps show that color
770
- ctx.fillStyle = baseColor;
771
- ctx.fillRect(0, 0, texSize, texSize);
1298
+ if (this._transparentTextureVoid) {
1299
+ ctx.clearRect(0, 0, texSize, texSize);
1300
+ }
1301
+ else {
1302
+ ctx.fillStyle = baseColor;
1303
+ ctx.fillRect(0, 0, texSize, texSize);
1304
+ }
772
1305
  // Determine layout segments (matter vs void)
773
1306
  let layoutHead = 0;
774
1307
  const segments = [];
@@ -817,6 +1350,308 @@ export class NeatGradient {
817
1350
  }
818
1351
  return tex;
819
1352
  }
1353
+ get silhouetteFade() {
1354
+ return this._silhouetteFade;
1355
+ }
1356
+ set silhouetteFade(value) {
1357
+ if (this._silhouetteFade !== value) {
1358
+ this._silhouetteFade = value;
1359
+ this._uniformsDirty = true;
1360
+ }
1361
+ }
1362
+ get cylinderFade() {
1363
+ return this._cylinderFade;
1364
+ }
1365
+ set cylinderFade(value) {
1366
+ if (this._cylinderFade !== value) {
1367
+ this._cylinderFade = value;
1368
+ this._uniformsDirty = true;
1369
+ }
1370
+ }
1371
+ get ribbonFade() {
1372
+ return this._ribbonFade;
1373
+ }
1374
+ set ribbonFade(value) {
1375
+ if (this._ribbonFade !== value) {
1376
+ this._ribbonFade = value;
1377
+ this._uniformsDirty = true;
1378
+ }
1379
+ }
1380
+ get domainWarpEnabled() {
1381
+ return this._domainWarpEnabled;
1382
+ }
1383
+ set domainWarpEnabled(enabled) {
1384
+ if (this._domainWarpEnabled !== enabled) {
1385
+ this._domainWarpEnabled = enabled;
1386
+ this._uniformsDirty = true;
1387
+ }
1388
+ }
1389
+ get domainWarpIntensity() {
1390
+ return this._domainWarpIntensity;
1391
+ }
1392
+ set domainWarpIntensity(intensity) {
1393
+ if (this._domainWarpIntensity !== intensity) {
1394
+ this._domainWarpIntensity = intensity;
1395
+ this._uniformsDirty = true;
1396
+ }
1397
+ }
1398
+ get domainWarpScale() {
1399
+ return this._domainWarpScale;
1400
+ }
1401
+ set domainWarpScale(scale) {
1402
+ if (this._domainWarpScale !== scale) {
1403
+ this._domainWarpScale = scale;
1404
+ this._uniformsDirty = true;
1405
+ }
1406
+ }
1407
+ get vignetteIntensity() {
1408
+ return this._vignetteIntensity;
1409
+ }
1410
+ set vignetteIntensity(intensity) {
1411
+ if (this._vignetteIntensity !== intensity) {
1412
+ this._vignetteIntensity = intensity;
1413
+ this._uniformsDirty = true;
1414
+ }
1415
+ }
1416
+ get vignetteRadius() {
1417
+ return this._vignetteRadius;
1418
+ }
1419
+ set vignetteRadius(radius) {
1420
+ if (this._vignetteRadius !== radius) {
1421
+ this._vignetteRadius = radius;
1422
+ this._uniformsDirty = true;
1423
+ }
1424
+ }
1425
+ get fresnelEnabled() {
1426
+ return this._fresnelEnabled;
1427
+ }
1428
+ set fresnelEnabled(enabled) {
1429
+ if (this._fresnelEnabled !== enabled) {
1430
+ this._fresnelEnabled = enabled;
1431
+ this._uniformsDirty = true;
1432
+ }
1433
+ }
1434
+ get fresnelPower() {
1435
+ return this._fresnelPower;
1436
+ }
1437
+ set fresnelPower(power) {
1438
+ if (this._fresnelPower !== power) {
1439
+ this._fresnelPower = power;
1440
+ this._uniformsDirty = true;
1441
+ }
1442
+ }
1443
+ get fresnelIntensity() {
1444
+ return this._fresnelIntensity;
1445
+ }
1446
+ set fresnelIntensity(intensity) {
1447
+ if (this._fresnelIntensity !== intensity) {
1448
+ this._fresnelIntensity = intensity;
1449
+ this._uniformsDirty = true;
1450
+ }
1451
+ }
1452
+ get fresnelColor() {
1453
+ return this._fresnelColor;
1454
+ }
1455
+ set fresnelColor(fresnelColor) {
1456
+ if (this._fresnelColor !== fresnelColor) {
1457
+ this._fresnelColor = fresnelColor;
1458
+ this._fresnelColorRgb = this._hexToRgb(fresnelColor);
1459
+ this._uniformsDirty = true;
1460
+ }
1461
+ }
1462
+ get iridescenceEnabled() {
1463
+ return this._iridescenceEnabled;
1464
+ }
1465
+ set iridescenceEnabled(enabled) {
1466
+ if (this._iridescenceEnabled !== enabled) {
1467
+ this._iridescenceEnabled = enabled;
1468
+ this._uniformsDirty = true;
1469
+ }
1470
+ }
1471
+ get iridescenceIntensity() {
1472
+ return this._iridescenceIntensity;
1473
+ }
1474
+ set iridescenceIntensity(intensity) {
1475
+ if (this._iridescenceIntensity !== intensity) {
1476
+ this._iridescenceIntensity = intensity;
1477
+ this._uniformsDirty = true;
1478
+ }
1479
+ }
1480
+ get iridescenceSpeed() {
1481
+ return this._iridescenceSpeed;
1482
+ }
1483
+ set iridescenceSpeed(speed) {
1484
+ if (this._iridescenceSpeed !== speed) {
1485
+ this._iridescenceSpeed = speed;
1486
+ this._uniformsDirty = true;
1487
+ }
1488
+ }
1489
+ get bloomIntensity() {
1490
+ return this._bloomIntensity;
1491
+ }
1492
+ set bloomIntensity(intensity) {
1493
+ if (this._bloomIntensity !== intensity) {
1494
+ this._bloomIntensity = intensity;
1495
+ this._uniformsDirty = true;
1496
+ }
1497
+ }
1498
+ get bloomThreshold() {
1499
+ return this._bloomThreshold;
1500
+ }
1501
+ set bloomThreshold(threshold) {
1502
+ if (this._bloomThreshold !== threshold) {
1503
+ this._bloomThreshold = threshold;
1504
+ this._uniformsDirty = true;
1505
+ }
1506
+ }
1507
+ get chromaticAberration() {
1508
+ return this._chromaticAberration;
1509
+ }
1510
+ set chromaticAberration(aberration) {
1511
+ if (this._chromaticAberration !== aberration) {
1512
+ this._chromaticAberration = aberration;
1513
+ this._uniformsDirty = true;
1514
+ }
1515
+ }
1516
+ // Getters and Setters for 3D Shapes
1517
+ get shapeType() {
1518
+ return this._shapeType;
1519
+ }
1520
+ set shapeType(val) {
1521
+ if (this._shapeType !== val) {
1522
+ this._shapeType = val;
1523
+ this._updateGeometry();
1524
+ }
1525
+ }
1526
+ get shapeRotationX() { return this._shapeRotationX; }
1527
+ set shapeRotationX(val) {
1528
+ this._shapeRotationX = val;
1529
+ this._uniformsDirty = true;
1530
+ }
1531
+ get shapeRotationY() { return this._shapeRotationY; }
1532
+ set shapeRotationY(val) {
1533
+ this._shapeRotationY = val;
1534
+ this._uniformsDirty = true;
1535
+ }
1536
+ get shapeRotationZ() { return this._shapeRotationZ; }
1537
+ set shapeRotationZ(val) {
1538
+ this._shapeRotationZ = val;
1539
+ this._uniformsDirty = true;
1540
+ }
1541
+ get shapeAutoRotateSpeedX() { return this._shapeAutoRotateSpeedX; }
1542
+ set shapeAutoRotateSpeedX(val) {
1543
+ this._shapeAutoRotateSpeedX = val;
1544
+ this._uniformsDirty = true;
1545
+ }
1546
+ get shapeAutoRotateSpeedY() { return this._shapeAutoRotateSpeedY; }
1547
+ set shapeAutoRotateSpeedY(val) {
1548
+ this._shapeAutoRotateSpeedY = val;
1549
+ this._uniformsDirty = true;
1550
+ }
1551
+ get sphereRadius() { return this._sphereRadius; }
1552
+ set sphereRadius(val) {
1553
+ if (this._sphereRadius !== val) {
1554
+ this._sphereRadius = val;
1555
+ this._updateGeometry();
1556
+ }
1557
+ }
1558
+ get torusRadius() { return this._torusRadius; }
1559
+ set torusRadius(val) {
1560
+ if (this._torusRadius !== val) {
1561
+ this._torusRadius = val;
1562
+ this._updateGeometry();
1563
+ }
1564
+ }
1565
+ get torusTube() { return this._torusTube; }
1566
+ set torusTube(val) {
1567
+ if (this._torusTube !== val) {
1568
+ this._torusTube = val;
1569
+ this._updateGeometry();
1570
+ }
1571
+ }
1572
+ get cylinderRadius() { return this._cylinderRadius; }
1573
+ set cylinderRadius(val) {
1574
+ if (this._cylinderRadius !== val) {
1575
+ this._cylinderRadius = val;
1576
+ this._updateGeometry();
1577
+ }
1578
+ }
1579
+ get cylinderHeight() { return this._cylinderHeight; }
1580
+ set cylinderHeight(val) {
1581
+ if (this._cylinderHeight !== val) {
1582
+ this._cylinderHeight = val;
1583
+ this._updateGeometry();
1584
+ }
1585
+ }
1586
+ get planeBend() { return this._planeBend; }
1587
+ set planeBend(val) {
1588
+ if (this._planeBend !== val) {
1589
+ this._planeBend = val;
1590
+ this._updateGeometry();
1591
+ }
1592
+ }
1593
+ get planeTwist() { return this._planeTwist; }
1594
+ set planeTwist(val) {
1595
+ if (this._planeTwist !== val) {
1596
+ this._planeTwist = val;
1597
+ this._updateGeometry();
1598
+ }
1599
+ }
1600
+ // Camera Getters and Setters
1601
+ get cameraLock() { return this._cameraLock; }
1602
+ set cameraLock(val) {
1603
+ this._cameraLock = val;
1604
+ }
1605
+ get cameraX() { return this._cameraX; }
1606
+ set cameraX(val) {
1607
+ this._cameraX = val;
1608
+ this._uniformsDirty = true;
1609
+ }
1610
+ get cameraY() { return this._cameraY; }
1611
+ set cameraY(val) {
1612
+ this._cameraY = val;
1613
+ this._uniformsDirty = true;
1614
+ }
1615
+ get cameraZ() { return this._cameraZ; }
1616
+ set cameraZ(val) {
1617
+ this._cameraZ = val;
1618
+ this._uniformsDirty = true;
1619
+ }
1620
+ get cameraRotationX() { return this._cameraRotationX; }
1621
+ set cameraRotationX(val) {
1622
+ this._cameraRotationX = val;
1623
+ this._uniformsDirty = true;
1624
+ }
1625
+ get cameraRotationY() { return this._cameraRotationY; }
1626
+ set cameraRotationY(val) {
1627
+ this._cameraRotationY = val;
1628
+ this._uniformsDirty = true;
1629
+ }
1630
+ get cameraRotationZ() { return this._cameraRotationZ; }
1631
+ set cameraRotationZ(val) {
1632
+ this._cameraRotationZ = val;
1633
+ this._uniformsDirty = true;
1634
+ }
1635
+ get cameraZoom() { return this._cameraZoom; }
1636
+ set cameraZoom(val) {
1637
+ if (this._cameraZoom !== val) {
1638
+ this._cameraZoom = val;
1639
+ this._updateCameraFrustum();
1640
+ }
1641
+ }
1642
+ _updateCameraFrustum() {
1643
+ if (!this.glState)
1644
+ return;
1645
+ const gl = this.glState.gl;
1646
+ const width = this._ref.clientWidth;
1647
+ const height = this._ref.clientHeight;
1648
+ updateCamera(this.glState.camera, width, height, PLANE_WIDTH, PLANE_HEIGHT, this._shapeType, this._cameraZoom);
1649
+ const projLoc = this.glState.locations.uniforms["projectionMatrix"];
1650
+ gl.useProgram(this.glState.program);
1651
+ if (projLoc)
1652
+ gl.uniformMatrix4fv(projLoc, false, this.glState.camera.projectionMatrix.elements);
1653
+ this._uniformsDirty = true;
1654
+ }
820
1655
  }
821
1656
  const setLinkStyles = (link) => {
822
1657
  link.id = LINK_ID;