@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.
@@ -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, otherwise search
311
- if (Math.floor(tick * 10) % 5 === 0) {
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
- // Optimized Color Update: Update the existing array objects instead of recreating array
357
- const shaderColors = u.u_colors.value;
358
- for(let i = 0; i < COLORS_COUNT; i++) {
359
- if (i < this._colors.length) {
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.u_colors_count.value = COLORS_COUNT;
370
- // Wireframe is a material property, not a uniform
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
- // FIX 2: Handle FBO Clearing correctly
397
- // Store current clear color (likely the main background color)
398
- renderer.getClearColor(this._tempClearColor);
399
- const oldClearAlpha = renderer.getClearAlpha();
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
- renderer.setRenderTarget(this._mouseFBO);
407
- renderer.clear();
439
+ // Set clear color to Black/Transparent for the FBO.
440
+ renderer.setClearColor(0x000000, 0.0);
408
441
 
409
- if (hasActiveBrushes) {
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
- // Restore main background color for the actual scene render
415
- renderer.setClearColor(this._tempClearColor, oldClearAlpha);
447
+ // Restore main background color for the actual scene render
448
+ renderer.setClearColor(this._tempClearColor, oldClearAlpha);
416
449
 
417
- // Update mouse texture uniform
418
- if (this._cachedUniforms) {
419
- this._cachedUniforms.u_mouse_texture.value = this._mouseFBO.texture;
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
- this.sizeObserver = new ResizeObserver(entries => {
454
- setSize();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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._proceduralTexture = this._createProceduralTexture();
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
- this._mouse.x = e.clientX - rect.left - width / 2;
918
- this._mouse.y = -(e.clientY - rect.top - height / 2);
919
-
920
- const brush = this._mouseObjects[this._currentBrush];
921
- brush.mesh.scale.set(this._mouseBrushBaseScale, this._mouseBrushBaseScale, 1.0);
922
- brush.active = true;
923
- brush.mesh.visible = true;
924
- brush.mesh.position.set(this._mouse.x, this._mouse.y, 0);
925
- brush.mesh.rotation.z = Math.random() * Math.PI * 2;
926
- if (brush.mesh.material instanceof THREE.MeshBasicMaterial) {
927
- brush.mesh.material.opacity = 1.0;
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
- // Can be increased to 2048 for even better quality if needed
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
- const left = -PLANE_WIDTH / 2;
1138
- const right = Math.min((left + targetWidth) / 1.5, PLANE_WIDTH / 2);
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
- const top = PLANE_HEIGHT / 4;
1141
- const bottom = Math.max((top - targetHeight) / 2, -PLANE_HEIGHT / 4);
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
- const buildUniforms = () => `
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
+ }