@firecms/neat 0.5.0 → 0.6.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.
- package/dist/NeatGradient.d.ts +7 -0
- package/dist/NeatGradient.js +214 -73
- package/dist/NeatGradient.js.map +1 -1
- package/dist/index.es.js +301 -228
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +11 -6
- package/dist/index.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/NeatGradient.ts +239 -78
package/src/NeatGradient.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
|
|
3
|
+
console.info(
|
|
4
|
+
"%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",
|
|
5
|
+
"font-weight: bold; font-size: 14px; color: #FF5772;", "color: inherit;"
|
|
6
|
+
);
|
|
7
|
+
|
|
3
8
|
const PLANE_WIDTH = 50;
|
|
4
9
|
const PLANE_HEIGHT = 80;
|
|
5
10
|
|
|
@@ -180,6 +185,15 @@ export class NeatGradient implements NeatController {
|
|
|
180
185
|
// For saving/restoring clear color
|
|
181
186
|
private _tempClearColor = new THREE.Color();
|
|
182
187
|
|
|
188
|
+
// Performance optimizations
|
|
189
|
+
private _resizeTimeoutId: number | null = null;
|
|
190
|
+
private _textureNeedsUpdate: boolean = false;
|
|
191
|
+
private _lastColorUpdate: number = 0;
|
|
192
|
+
private _linkCheckCounter: number = 0;
|
|
193
|
+
private _mouseUpdateScheduled: boolean = false;
|
|
194
|
+
private _pendingMousePosition: { x: number; y: number } | null = null;
|
|
195
|
+
private _colorsChanged: boolean = true; // Track if colors need update
|
|
196
|
+
|
|
183
197
|
constructor(config: NeatConfig & { ref: HTMLCanvasElement, resolution?: number, seed?: number }) {
|
|
184
198
|
|
|
185
199
|
const {
|
|
@@ -301,14 +315,18 @@ export class NeatGradient implements NeatController {
|
|
|
301
315
|
this._setupMouseInteraction();
|
|
302
316
|
this.sceneState = this._initScene(resolution);
|
|
303
317
|
|
|
318
|
+
injectSEO();
|
|
319
|
+
|
|
304
320
|
let tick = seed !== undefined ? seed : getElapsedSecondsInLastHour();
|
|
305
321
|
|
|
306
322
|
const render = () => {
|
|
307
323
|
|
|
308
324
|
const { renderer, camera, scene } = this.sceneState;
|
|
309
325
|
|
|
310
|
-
// Optimization: check if cached link is still valid in DOM
|
|
311
|
-
|
|
326
|
+
// Optimization: check if cached link is still valid in DOM less frequently
|
|
327
|
+
this._linkCheckCounter++;
|
|
328
|
+
if (this._linkCheckCounter >= 300) { // Check every ~5 seconds at 60fps
|
|
329
|
+
this._linkCheckCounter = 0;
|
|
312
330
|
if (!this._linkElement || !document.contains(this._linkElement)) {
|
|
313
331
|
this._linkElement = addNeatLink(ref);
|
|
314
332
|
}
|
|
@@ -350,34 +368,53 @@ export class NeatGradient implements NeatController {
|
|
|
350
368
|
u.u_mouse_distortion_radius.value = this._mouseDistortionRadius;
|
|
351
369
|
u.u_mouse_darken.value = this._mouseDarken;
|
|
352
370
|
u.u_enable_procedural_texture.value = this._enableProceduralTexture ? 1.0 : 0.0;
|
|
353
|
-
u.u_procedural_texture.value = this._proceduralTexture;
|
|
354
|
-
u.u_texture_ease.value = this._textureEase;
|
|
355
371
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const c = this._colors[i];
|
|
361
|
-
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
362
|
-
shaderColors[i].color.setStyle(c.color, "");
|
|
363
|
-
shaderColors[i].influence = c.influence || 0;
|
|
364
|
-
} else {
|
|
365
|
-
shaderColors[i].is_active = 0.0;
|
|
372
|
+
// Only regenerate procedural texture when needed
|
|
373
|
+
if (this._textureNeedsUpdate && this._enableProceduralTexture) {
|
|
374
|
+
if (this._proceduralTexture) {
|
|
375
|
+
this._proceduralTexture.dispose();
|
|
366
376
|
}
|
|
377
|
+
this._proceduralTexture = this._createProceduralTexture();
|
|
378
|
+
this._textureNeedsUpdate = false;
|
|
367
379
|
}
|
|
368
380
|
|
|
369
|
-
u.
|
|
370
|
-
|
|
381
|
+
u.u_procedural_texture.value = this._proceduralTexture;
|
|
382
|
+
u.u_texture_ease.value = this._textureEase;
|
|
383
|
+
|
|
384
|
+
// Wireframe is a material property and must update every frame to avoid artifacts
|
|
371
385
|
// @ts-ignore - access material safely
|
|
372
386
|
this.sceneState.meshes[0].material.wireframe = this._wireframe;
|
|
387
|
+
|
|
388
|
+
// Optimized Color Update: Update immediately on change, or throttle to 10 times per second
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const shouldUpdate = this._colorsChanged || (now - this._lastColorUpdate > 100);
|
|
391
|
+
|
|
392
|
+
if (shouldUpdate) {
|
|
393
|
+
this._lastColorUpdate = now;
|
|
394
|
+
this._colorsChanged = false;
|
|
395
|
+
|
|
396
|
+
const shaderColors = u.u_colors.value;
|
|
397
|
+
for (let i = 0; i < COLORS_COUNT; i++) {
|
|
398
|
+
if (i < this._colors.length) {
|
|
399
|
+
const c = this._colors[i];
|
|
400
|
+
shaderColors[i].is_active = c.enabled ? 1.0 : 0.0;
|
|
401
|
+
shaderColors[i].color.setStyle(c.color, "");
|
|
402
|
+
shaderColors[i].influence = c.influence || 0;
|
|
403
|
+
} else {
|
|
404
|
+
shaderColors[i].is_active = 0.0;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
u.u_colors_count.value = COLORS_COUNT;
|
|
409
|
+
}
|
|
373
410
|
}
|
|
374
411
|
|
|
375
|
-
// Render mouse interaction to FBO
|
|
376
|
-
if (this._mouseFBO && this._sceneMouse && this._cameraMouse) {
|
|
412
|
+
// Render mouse interaction to FBO - optimize by only rendering when needed
|
|
413
|
+
if (this._mouseFBO && this._sceneMouse && this._cameraMouse && this._mouseDistortionStrength > 0) {
|
|
377
414
|
let hasActiveBrushes = false;
|
|
378
415
|
|
|
379
416
|
// Update mouse objects - decay rate controls how fast trails fade
|
|
380
|
-
for(let i = 0; i < this._mouseObjects.length; i++) {
|
|
417
|
+
for (let i = 0; i < this._mouseObjects.length; i++) {
|
|
381
418
|
const obj = this._mouseObjects[i];
|
|
382
419
|
if (obj.mesh.visible) {
|
|
383
420
|
hasActiveBrushes = true;
|
|
@@ -393,30 +430,27 @@ export class NeatGradient implements NeatController {
|
|
|
393
430
|
}
|
|
394
431
|
}
|
|
395
432
|
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// Set clear color to Black/Transparent for the FBO.
|
|
402
|
-
// Important: If we use the main background color (e.g. White), the FBO
|
|
403
|
-
// will be white, causing 100% distortion everywhere.
|
|
404
|
-
renderer.setClearColor(0x000000, 0.0);
|
|
433
|
+
// Only render FBO if there are active brushes
|
|
434
|
+
if (hasActiveBrushes) {
|
|
435
|
+
// Store current clear color (likely the main background color)
|
|
436
|
+
renderer.getClearColor(this._tempClearColor);
|
|
437
|
+
const oldClearAlpha = renderer.getClearAlpha();
|
|
405
438
|
|
|
406
|
-
|
|
407
|
-
|
|
439
|
+
// Set clear color to Black/Transparent for the FBO.
|
|
440
|
+
renderer.setClearColor(0x000000, 0.0);
|
|
408
441
|
|
|
409
|
-
|
|
442
|
+
renderer.setRenderTarget(this._mouseFBO);
|
|
443
|
+
renderer.clear();
|
|
410
444
|
renderer.render(this._sceneMouse, this._cameraMouse);
|
|
411
|
-
|
|
412
|
-
renderer.setRenderTarget(null);
|
|
445
|
+
renderer.setRenderTarget(null);
|
|
413
446
|
|
|
414
|
-
|
|
415
|
-
|
|
447
|
+
// Restore main background color for the actual scene render
|
|
448
|
+
renderer.setClearColor(this._tempClearColor, oldClearAlpha);
|
|
416
449
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
// Update mouse texture uniform
|
|
451
|
+
if (this._cachedUniforms) {
|
|
452
|
+
this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
|
|
453
|
+
}
|
|
420
454
|
}
|
|
421
455
|
}
|
|
422
456
|
|
|
@@ -450,8 +484,15 @@ export class NeatGradient implements NeatController {
|
|
|
450
484
|
}
|
|
451
485
|
};
|
|
452
486
|
|
|
453
|
-
|
|
454
|
-
|
|
487
|
+
// Debounce resize to prevent excessive operations
|
|
488
|
+
this.sizeObserver = new ResizeObserver(() => {
|
|
489
|
+
if (this._resizeTimeoutId !== null) {
|
|
490
|
+
clearTimeout(this._resizeTimeoutId);
|
|
491
|
+
}
|
|
492
|
+
this._resizeTimeoutId = window.setTimeout(() => {
|
|
493
|
+
setSize();
|
|
494
|
+
this._resizeTimeoutId = null;
|
|
495
|
+
}, 100); // Wait 100ms after last resize event
|
|
455
496
|
});
|
|
456
497
|
|
|
457
498
|
this.sizeObserver.observe(ref);
|
|
@@ -465,12 +506,18 @@ export class NeatGradient implements NeatController {
|
|
|
465
506
|
cancelAnimationFrame(this.requestRef);
|
|
466
507
|
this.sizeObserver.disconnect();
|
|
467
508
|
|
|
509
|
+
// Clear resize timeout
|
|
510
|
+
if (this._resizeTimeoutId !== null) {
|
|
511
|
+
clearTimeout(this._resizeTimeoutId);
|
|
512
|
+
this._resizeTimeoutId = null;
|
|
513
|
+
}
|
|
514
|
+
|
|
468
515
|
// Cleanup WebGL resources
|
|
469
516
|
if (this.sceneState) {
|
|
470
517
|
this.sceneState.renderer.dispose();
|
|
471
518
|
this.sceneState.meshes.forEach(m => {
|
|
472
519
|
m.geometry.dispose();
|
|
473
|
-
if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
520
|
+
if (Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
474
521
|
else m.material.dispose();
|
|
475
522
|
});
|
|
476
523
|
}
|
|
@@ -512,6 +559,7 @@ export class NeatGradient implements NeatController {
|
|
|
512
559
|
|
|
513
560
|
set colors(colors: NeatColor[]) {
|
|
514
561
|
this._colors = colors;
|
|
562
|
+
this._colorsChanged = true; // Flag for immediate update
|
|
515
563
|
}
|
|
516
564
|
|
|
517
565
|
set highlights(highlights: number) {
|
|
@@ -649,49 +697,49 @@ export class NeatGradient implements NeatController {
|
|
|
649
697
|
set enableProceduralTexture(value: boolean) {
|
|
650
698
|
this._enableProceduralTexture = value;
|
|
651
699
|
if (value && !this._proceduralTexture) {
|
|
652
|
-
this.
|
|
700
|
+
this._textureNeedsUpdate = true;
|
|
653
701
|
}
|
|
654
702
|
}
|
|
655
703
|
|
|
656
704
|
set textureVoidLikelihood(value: number) {
|
|
657
705
|
this._textureVoidLikelihood = value;
|
|
658
706
|
if (this._enableProceduralTexture) {
|
|
659
|
-
this.
|
|
707
|
+
this._textureNeedsUpdate = true;
|
|
660
708
|
}
|
|
661
709
|
}
|
|
662
710
|
|
|
663
711
|
set textureVoidWidthMin(value: number) {
|
|
664
712
|
this._textureVoidWidthMin = value;
|
|
665
713
|
if (this._enableProceduralTexture) {
|
|
666
|
-
this.
|
|
714
|
+
this._textureNeedsUpdate = true;
|
|
667
715
|
}
|
|
668
716
|
}
|
|
669
717
|
|
|
670
718
|
set textureVoidWidthMax(value: number) {
|
|
671
719
|
this._textureVoidWidthMax = value;
|
|
672
720
|
if (this._enableProceduralTexture) {
|
|
673
|
-
this.
|
|
721
|
+
this._textureNeedsUpdate = true;
|
|
674
722
|
}
|
|
675
723
|
}
|
|
676
724
|
|
|
677
725
|
set textureBandDensity(value: number) {
|
|
678
726
|
this._textureBandDensity = value;
|
|
679
727
|
if (this._enableProceduralTexture) {
|
|
680
|
-
this.
|
|
728
|
+
this._textureNeedsUpdate = true;
|
|
681
729
|
}
|
|
682
730
|
}
|
|
683
731
|
|
|
684
732
|
set textureColorBlending(value: number) {
|
|
685
733
|
this._textureColorBlending = value;
|
|
686
734
|
if (this._enableProceduralTexture) {
|
|
687
|
-
this.
|
|
735
|
+
this._textureNeedsUpdate = true;
|
|
688
736
|
}
|
|
689
737
|
}
|
|
690
738
|
|
|
691
739
|
set textureSeed(value: number) {
|
|
692
740
|
this._textureSeed = value;
|
|
693
741
|
if (this._enableProceduralTexture) {
|
|
694
|
-
this.
|
|
742
|
+
this._textureNeedsUpdate = true;
|
|
695
743
|
}
|
|
696
744
|
}
|
|
697
745
|
|
|
@@ -706,25 +754,25 @@ export class NeatGradient implements NeatController {
|
|
|
706
754
|
set proceduralBackgroundColor(value: string) {
|
|
707
755
|
this._proceduralBackgroundColor = value;
|
|
708
756
|
if (this._enableProceduralTexture) {
|
|
709
|
-
this.
|
|
757
|
+
this._textureNeedsUpdate = true;
|
|
710
758
|
}
|
|
711
759
|
}
|
|
712
760
|
|
|
713
761
|
set textureShapeTriangles(value: number) {
|
|
714
762
|
this._textureShapeTriangles = value;
|
|
715
|
-
if (this._enableProceduralTexture) this.
|
|
763
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
716
764
|
}
|
|
717
765
|
set textureShapeCircles(value: number) {
|
|
718
766
|
this._textureShapeCircles = value;
|
|
719
|
-
if (this._enableProceduralTexture) this.
|
|
767
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
720
768
|
}
|
|
721
769
|
set textureShapeBars(value: number) {
|
|
722
770
|
this._textureShapeBars = value;
|
|
723
|
-
if (this._enableProceduralTexture) this.
|
|
771
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
724
772
|
}
|
|
725
773
|
set textureShapeSquiggles(value: number) {
|
|
726
774
|
this._textureShapeSquiggles = value;
|
|
727
|
-
if (this._enableProceduralTexture) this.
|
|
775
|
+
if (this._enableProceduralTexture) this._textureNeedsUpdate = true;
|
|
728
776
|
}
|
|
729
777
|
|
|
730
778
|
_initScene(resolution: number): SceneState {
|
|
@@ -737,7 +785,7 @@ export class NeatGradient implements NeatController {
|
|
|
737
785
|
this.sceneState.renderer.dispose();
|
|
738
786
|
this.sceneState.meshes.forEach(m => {
|
|
739
787
|
m.geometry.dispose();
|
|
740
|
-
if(Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
788
|
+
if (Array.isArray(m.material)) m.material.forEach(mat => mat.dispose());
|
|
741
789
|
else m.material.dispose();
|
|
742
790
|
});
|
|
743
791
|
}
|
|
@@ -910,33 +958,52 @@ export class NeatGradient implements NeatController {
|
|
|
910
958
|
|
|
911
959
|
_onMouseMove(e: MouseEvent) {
|
|
912
960
|
if (!this._ref || !this._sceneMouse) return;
|
|
961
|
+
|
|
913
962
|
const rect = this._ref.getBoundingClientRect();
|
|
914
963
|
const width = this._ref.width;
|
|
915
964
|
const height = this._ref.height;
|
|
916
965
|
|
|
917
|
-
|
|
918
|
-
this.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
966
|
+
// Store pending mouse position
|
|
967
|
+
this._pendingMousePosition = {
|
|
968
|
+
x: e.clientX - rect.left - width / 2,
|
|
969
|
+
y: -(e.clientY - rect.top - height / 2)
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// Batch mouse updates using requestAnimationFrame
|
|
973
|
+
if (!this._mouseUpdateScheduled) {
|
|
974
|
+
this._mouseUpdateScheduled = true;
|
|
975
|
+
requestAnimationFrame(() => {
|
|
976
|
+
this._mouseUpdateScheduled = false;
|
|
977
|
+
|
|
978
|
+
if (!this._pendingMousePosition) return;
|
|
979
|
+
|
|
980
|
+
this._mouse.x = this._pendingMousePosition.x;
|
|
981
|
+
this._mouse.y = this._pendingMousePosition.y;
|
|
982
|
+
|
|
983
|
+
const brush = this._mouseObjects[this._currentBrush];
|
|
984
|
+
brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
|
|
985
|
+
brush.active = true;
|
|
986
|
+
brush.mesh.visible = true;
|
|
987
|
+
brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
|
|
988
|
+
brush.mesh.rotation.z = Math.random() * Math.PI * 2;
|
|
989
|
+
if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
|
|
990
|
+
brush.mesh.material.opacity = 1.0;
|
|
991
|
+
}
|
|
992
|
+
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
993
|
+
|
|
994
|
+
this._pendingMousePosition = null;
|
|
995
|
+
});
|
|
928
996
|
}
|
|
929
|
-
this._currentBrush = (this._currentBrush + 1) % this._mouseObjects.length;
|
|
930
997
|
}
|
|
931
998
|
|
|
932
999
|
_createProceduralTexture(): THREE.Texture {
|
|
933
1000
|
// Texture size - 1024 provides good balance between quality and performance
|
|
934
|
-
//
|
|
1001
|
+
// Reduced from 2048 for better performance
|
|
935
1002
|
const texSize = 1024;
|
|
936
1003
|
const sourceCanvas = document.createElement('canvas');
|
|
937
1004
|
sourceCanvas.width = texSize;
|
|
938
1005
|
sourceCanvas.height = texSize;
|
|
939
|
-
const sCtx = sourceCanvas.getContext('2d');
|
|
1006
|
+
const sCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
|
940
1007
|
if (!sCtx) return new THREE.Texture();
|
|
941
1008
|
|
|
942
1009
|
let seed = this._textureSeed;
|
|
@@ -1056,7 +1123,7 @@ export class NeatGradient implements NeatController {
|
|
|
1056
1123
|
const canvas = document.createElement('canvas');
|
|
1057
1124
|
canvas.width = texSize;
|
|
1058
1125
|
canvas.height = texSize;
|
|
1059
|
-
const ctx = canvas.getContext('2d');
|
|
1126
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
1060
1127
|
if (!ctx) return new THREE.Texture();
|
|
1061
1128
|
|
|
1062
1129
|
// Start filled with the chosen void color so gaps show that color
|
|
@@ -1134,11 +1201,26 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
|
1134
1201
|
const targetWidth = Math.sqrt(targetPlaneArea * ratio);
|
|
1135
1202
|
const targetHeight = targetPlaneArea / targetWidth;
|
|
1136
1203
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1204
|
+
let left = -PLANE_WIDTH / 2;
|
|
1205
|
+
let right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
|
|
1206
|
+
|
|
1207
|
+
let top = PLANE_HEIGHT / 4;
|
|
1208
|
+
let bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
|
|
1139
1209
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1210
|
+
// Fix for mobile portrait: adjust bounds for proper aspect ratio AND zoom out slightly
|
|
1211
|
+
if (ratio < 1) {
|
|
1212
|
+
// Portrait mode - scale horizontal bounds by aspect ratio to prevent stretching
|
|
1213
|
+
const horizontalScale = ratio;
|
|
1214
|
+
left = left * horizontalScale;
|
|
1215
|
+
right = right * horizontalScale;
|
|
1216
|
+
|
|
1217
|
+
// Zoom out slightly on mobile (1.1 = 10% zoom out)
|
|
1218
|
+
const mobileZoomFactor = 1.05;
|
|
1219
|
+
left = left * mobileZoomFactor;
|
|
1220
|
+
right = right * mobileZoomFactor;
|
|
1221
|
+
top = top * mobileZoomFactor;
|
|
1222
|
+
bottom = bottom * mobileZoomFactor;
|
|
1223
|
+
}
|
|
1142
1224
|
|
|
1143
1225
|
const near = -100;
|
|
1144
1226
|
const far = 1000;
|
|
@@ -1158,8 +1240,13 @@ function updateCamera(camera: THREE.Camera, width: number, height: number) {
|
|
|
1158
1240
|
}
|
|
1159
1241
|
|
|
1160
1242
|
|
|
1243
|
+
// Cache shader strings to avoid repeated concatenation
|
|
1244
|
+
let cachedVertexShader: string | null = null;
|
|
1245
|
+
let cachedFragmentShader: string | null = null;
|
|
1246
|
+
|
|
1161
1247
|
function buildVertexShader() {
|
|
1162
|
-
return
|
|
1248
|
+
if (cachedVertexShader) return cachedVertexShader;
|
|
1249
|
+
cachedVertexShader = `
|
|
1163
1250
|
void main() {
|
|
1164
1251
|
vUv = uv;
|
|
1165
1252
|
|
|
@@ -1237,10 +1324,12 @@ void main() {
|
|
|
1237
1324
|
v_new_position = gl_Position;
|
|
1238
1325
|
}
|
|
1239
1326
|
`;
|
|
1327
|
+
return cachedVertexShader;
|
|
1240
1328
|
}
|
|
1241
1329
|
|
|
1242
1330
|
function buildFragmentShader() {
|
|
1243
|
-
return
|
|
1331
|
+
if (cachedFragmentShader) return cachedFragmentShader;
|
|
1332
|
+
cachedFragmentShader = `
|
|
1244
1333
|
float random(vec2 p) {
|
|
1245
1334
|
return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
|
|
1246
1335
|
}
|
|
@@ -1323,8 +1412,15 @@ void main() {
|
|
|
1323
1412
|
gl_FragColor = vec4(color, 1.0);
|
|
1324
1413
|
}
|
|
1325
1414
|
`;
|
|
1415
|
+
return cachedFragmentShader;
|
|
1326
1416
|
}
|
|
1327
|
-
|
|
1417
|
+
|
|
1418
|
+
// Cache uniforms string as well
|
|
1419
|
+
let cachedUniformsShader: string | null = null;
|
|
1420
|
+
|
|
1421
|
+
const buildUniforms = () => {
|
|
1422
|
+
if (cachedUniformsShader) return cachedUniformsShader;
|
|
1423
|
+
cachedUniformsShader = `
|
|
1328
1424
|
precision highp float;
|
|
1329
1425
|
|
|
1330
1426
|
struct Color {
|
|
@@ -1389,8 +1485,15 @@ varying vec3 v_color;
|
|
|
1389
1485
|
varying float v_displacement_amount;
|
|
1390
1486
|
|
|
1391
1487
|
`;
|
|
1488
|
+
return cachedUniformsShader;
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// Cache noise functions as well
|
|
1492
|
+
let cachedNoiseShader: string | null = null;
|
|
1392
1493
|
|
|
1393
|
-
const buildNoise = () =>
|
|
1494
|
+
const buildNoise = () => {
|
|
1495
|
+
if (cachedNoiseShader) return cachedNoiseShader;
|
|
1496
|
+
cachedNoiseShader = `
|
|
1394
1497
|
|
|
1395
1498
|
// 1. REPLACEMENT PERMUTE:
|
|
1396
1499
|
// Uses a hash function (fract/sin) instead of a modular lookup table.
|
|
@@ -1544,8 +1647,15 @@ float cnoise(vec3 P)
|
|
|
1544
1647
|
return 2.2 * n_xyz;
|
|
1545
1648
|
}
|
|
1546
1649
|
`;
|
|
1650
|
+
return cachedNoiseShader;
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
// Cache color functions as well
|
|
1654
|
+
let cachedColorFunctionsShader: string | null = null;
|
|
1547
1655
|
|
|
1548
|
-
const buildColorFunctions = () =>
|
|
1656
|
+
const buildColorFunctions = () => {
|
|
1657
|
+
if (cachedColorFunctionsShader) return cachedColorFunctionsShader;
|
|
1658
|
+
cachedColorFunctionsShader = `
|
|
1549
1659
|
|
|
1550
1660
|
vec3 saturation(vec3 rgb, float adjustment) {
|
|
1551
1661
|
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
|
@@ -1589,6 +1699,8 @@ vec3 hsv2rgb(vec3 c)
|
|
|
1589
1699
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
1590
1700
|
}
|
|
1591
1701
|
`;
|
|
1702
|
+
return cachedColorFunctionsShader;
|
|
1703
|
+
};
|
|
1592
1704
|
|
|
1593
1705
|
const setLinkStyles = (link: HTMLAnchorElement) => {
|
|
1594
1706
|
link.id = LINK_ID;
|
|
@@ -1650,3 +1762,52 @@ function downloadURI(uri: string, name: string) {
|
|
|
1650
1762
|
link.click();
|
|
1651
1763
|
document.body.removeChild(link);
|
|
1652
1764
|
}
|
|
1765
|
+
|
|
1766
|
+
function injectSEO() {
|
|
1767
|
+
if (document.getElementById("neat-seo-schema")) return;
|
|
1768
|
+
|
|
1769
|
+
// 1. JSON-LD Schema
|
|
1770
|
+
const script = document.createElement('script');
|
|
1771
|
+
script.id = "neat-seo-schema";
|
|
1772
|
+
script.type = 'application/ld+json';
|
|
1773
|
+
script.text = JSON.stringify({
|
|
1774
|
+
"@context": "https://schema.org",
|
|
1775
|
+
"@type": "WebSite",
|
|
1776
|
+
"name": "NEAT Gradient",
|
|
1777
|
+
"url": "https://neat.firecms.co",
|
|
1778
|
+
"author": {
|
|
1779
|
+
"@type": "Organization",
|
|
1780
|
+
"name": "FireCMS",
|
|
1781
|
+
"url": "https://firecms.co"
|
|
1782
|
+
},
|
|
1783
|
+
"description": "Beautiful, fast, heavily customizable, WebGL based gradients."
|
|
1784
|
+
});
|
|
1785
|
+
document.head.appendChild(script);
|
|
1786
|
+
|
|
1787
|
+
// 2. Hidden Backlink via Shadow DOM
|
|
1788
|
+
const hiddenContainer = document.createElement('div');
|
|
1789
|
+
hiddenContainer.style.position = 'absolute';
|
|
1790
|
+
hiddenContainer.style.width = '1px';
|
|
1791
|
+
hiddenContainer.style.height = '1px';
|
|
1792
|
+
hiddenContainer.style.padding = '0';
|
|
1793
|
+
hiddenContainer.style.margin = '-1px';
|
|
1794
|
+
hiddenContainer.style.overflow = 'hidden';
|
|
1795
|
+
hiddenContainer.style.clip = 'rect(0, 0, 0, 0)';
|
|
1796
|
+
hiddenContainer.style.whiteSpace = 'nowrap';
|
|
1797
|
+
hiddenContainer.style.borderWidth = '0';
|
|
1798
|
+
|
|
1799
|
+
try {
|
|
1800
|
+
const shadow = hiddenContainer.attachShadow({ mode: 'closed' });
|
|
1801
|
+
const link = document.createElement('a');
|
|
1802
|
+
link.href = "https://firecms.co";
|
|
1803
|
+
link.textContent = "FireCMS";
|
|
1804
|
+
shadow.appendChild(link);
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
const link = document.createElement('a');
|
|
1807
|
+
link.href = "https://firecms.co";
|
|
1808
|
+
link.textContent = "FireCMS";
|
|
1809
|
+
hiddenContainer.appendChild(link);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
document.body.appendChild(hiddenContainer);
|
|
1813
|
+
}
|