@shopify/klint 0.0.98 → 0.3.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/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
1
5
  // src/Klint.tsx
2
6
  import React, { useRef, useEffect, useState, useCallback } from "react";
3
7
  var DEFAULT_FPS = 60;
@@ -37,11 +41,18 @@ var CONFIG_PROPS = [
37
41
  "__textWeight",
38
42
  "__textStyle",
39
43
  "__textSize",
44
+ "__textLeading",
40
45
  "__textAlignment",
46
+ "__fillRule",
41
47
  "__isPlaying"
42
48
  ];
43
- function useAnimate(contextRef, draw, isVisible) {
49
+ function useAnimate(contextRef, draw, isVisible, enablePerformanceTracking = false) {
44
50
  const animationFrameId = useRef(0);
51
+ const frameTimeHistoryRef = useRef([]);
52
+ const frameStartTimeRef = useRef(0);
53
+ const frameCountRef = useRef(0);
54
+ const lastFpsUpdateRef = useRef(0);
55
+ const droppedFramesRef = useRef(0);
45
56
  const animate = useCallback(
46
57
  (timestamp = 0) => {
47
58
  if (!contextRef.current || !isVisible) return;
@@ -55,22 +66,55 @@ function useAnimate(contextRef, draw, isVisible) {
55
66
  if (!context.__lastTargetTime) {
56
67
  context.__lastTargetTime = now;
57
68
  context.__lastRealTime = now;
69
+ frameStartTimeRef.current = now;
70
+ lastFpsUpdateRef.current = now;
58
71
  }
59
72
  const sinceLast = now - context.__lastTargetTime;
60
73
  const epsilon = 5;
61
74
  if (sinceLast >= target - epsilon) {
75
+ const frameStart = enablePerformanceTracking && context.__performance ? performance.now() : 0;
62
76
  context.deltaTime = now - context.__lastRealTime;
63
77
  draw(context);
64
78
  if (context.time > 1e7) context.time = 0;
65
79
  if (context.frame > 1e7) context.frame = 0;
66
- context.time += context.deltaTime / DEFAULT_FPS;
80
+ context.time += context.deltaTime / 1e3;
67
81
  context.frame++;
68
82
  context.__lastTargetTime = now;
69
83
  context.__lastRealTime = now;
84
+ if (enablePerformanceTracking && context.__performance) {
85
+ const frameTime = performance.now() - frameStart;
86
+ const targetFrameTime = 1e3 / context.fps;
87
+ frameTimeHistoryRef.current.push(frameTime);
88
+ if (frameTimeHistoryRef.current.length > 60) {
89
+ frameTimeHistoryRef.current.shift();
90
+ }
91
+ const avgFrameTime = frameTimeHistoryRef.current.reduce((a, b) => a + b, 0) / frameTimeHistoryRef.current.length;
92
+ context.__performance.frameTime = frameTime;
93
+ context.__performance.averageFrameTime = avgFrameTime;
94
+ context.__performance.minFrameTime = Math.min(
95
+ ...frameTimeHistoryRef.current
96
+ );
97
+ context.__performance.maxFrameTime = Math.max(
98
+ ...frameTimeHistoryRef.current
99
+ );
100
+ if (frameTime > targetFrameTime * 1.1) {
101
+ droppedFramesRef.current++;
102
+ }
103
+ context.__performance.droppedFrames = droppedFramesRef.current;
104
+ frameCountRef.current++;
105
+ if (now - lastFpsUpdateRef.current >= 1e3) {
106
+ context.__performance.fps = frameCountRef.current;
107
+ frameCountRef.current = 0;
108
+ lastFpsUpdateRef.current = now;
109
+ if (typeof performance !== "undefined" && performance.memory) {
110
+ context.__performance.memoryUsage = performance.memory.usedJSHeapSize / 1048576;
111
+ }
112
+ }
113
+ }
70
114
  }
71
115
  animationFrameId.current = requestAnimationFrame(animate);
72
116
  },
73
- [draw, isVisible, contextRef]
117
+ [draw, isVisible, contextRef, enablePerformanceTracking]
74
118
  );
75
119
  return {
76
120
  animate,
@@ -83,7 +127,8 @@ function Klint({
83
127
  draw,
84
128
  options = {},
85
129
  preload,
86
- onVisible
130
+ onVisible,
131
+ enablePerformanceTracking = false
87
132
  }) {
88
133
  const canvasRef = useRef(null);
89
134
  const containerRef = useRef(null);
@@ -93,13 +138,36 @@ function Klint({
93
138
  null
94
139
  );
95
140
  const [isVisible, setIsVisible] = useState(true);
141
+ useEffect(() => {
142
+ if (typeof import.meta !== "undefined" && import.meta.hot) {
143
+ import.meta.hot.dispose(() => {
144
+ console.log("[Klint] Component unmounting due to HMR");
145
+ if (contextRef.current) {
146
+ contextRef.current.__isPlaying = false;
147
+ }
148
+ });
149
+ }
150
+ if (typeof module !== "undefined" && module.hot) {
151
+ module.hot.dispose(() => {
152
+ console.log("[Klint] Component unmounting due to Webpack HMR");
153
+ if (contextRef.current) {
154
+ contextRef.current.__isPlaying = false;
155
+ }
156
+ });
157
+ }
158
+ }, []);
96
159
  const __options = {
97
160
  ...DEFAULT_OPTIONS,
98
161
  ...options
99
162
  };
100
163
  const [toStaticImage, setStaticImage] = useState(null);
101
164
  const initContext = context?.initCoreContext;
102
- const { animate, animationFrameId } = useAnimate(contextRef, draw, isVisible);
165
+ const { animate, animationFrameId } = useAnimate(
166
+ contextRef,
167
+ draw,
168
+ isVisible,
169
+ enablePerformanceTracking
170
+ );
103
171
  const updateCanvasSize = (shouldRedraw = false) => {
104
172
  if (!containerRef.current || !contextRef.current || !canvasRef.current)
105
173
  return;
@@ -129,6 +197,16 @@ function Klint({
129
197
  const context2 = contextRef.current;
130
198
  if (!context2) return;
131
199
  context2.__dpr = dpr;
200
+ if (enablePerformanceTracking) {
201
+ context2.__performance = {
202
+ fps: 0,
203
+ frameTime: 0,
204
+ averageFrameTime: 0,
205
+ minFrameTime: Infinity,
206
+ maxFrameTime: 0,
207
+ droppedFrames: 0
208
+ };
209
+ }
132
210
  if (__options.fps && __options.fps !== context2.fps) {
133
211
  context2.fps = __options.fps;
134
212
  }
@@ -591,7 +669,8 @@ var Color_default = Color;
591
669
 
592
670
  // src/elements/Easing.tsx
593
671
  var Easing = class {
594
- constructor(ctx) {
672
+ constructor() {
673
+ this.zeroOut = (val) => Object.is(val, -0) || Math.abs(val) < 1e-12 ? 0 : val;
595
674
  this.normalize = (val) => {
596
675
  return val * 0.5 + 0.5;
597
676
  };
@@ -615,19 +694,19 @@ var Easing = class {
615
694
  };
616
695
  this.overshootIn = (val) => {
617
696
  const k = 1.70158;
618
- return val * val * (val * (k + 1) - k);
697
+ return this.zeroOut(val * val * (val * (k + 1) - k));
619
698
  };
620
699
  this.overshootOut = (val) => {
621
700
  const m = val - 1;
622
701
  const k = 1.70158;
623
- return 1 + m * m * (m * (k + 1) + k);
702
+ return this.zeroOut(1 + m * m * (m * (k + 1) + k));
624
703
  };
625
704
  this.overshootInOut = (val) => {
626
705
  const m = val - 1;
627
706
  const t = val * 2;
628
707
  const k = 1.70158 * 1.525;
629
- if (val < 0.5) return val * t * (t * (k + 1) - k);
630
- return 1 + 2 * m * m * (2 * m * (k + 1) + k);
708
+ if (val < 0.5) return this.zeroOut(val * t * (t * (k + 1) - k));
709
+ return this.zeroOut(1 + 2 * m * m * (2 * m * (k + 1) + k));
631
710
  };
632
711
  this.bounceOut = (val) => {
633
712
  const r = 1 / 2.75;
@@ -680,48 +759,22 @@ var Easing = class {
680
759
  this.log = () => {
681
760
  console.log(this);
682
761
  };
683
- this.context = ctx;
684
762
  }
685
763
  };
686
764
  var Easing_default = Easing;
687
765
 
688
- // src/elements/State.tsx
689
- var State = class {
690
- constructor() {
691
- this.store = /* @__PURE__ */ new Map();
692
- }
693
- set(key, value, callback) {
694
- this.store.set(key, value);
695
- callback?.(key, value);
696
- }
697
- get(key, callback) {
698
- const value = this.store.get(key);
699
- callback?.(key, value);
700
- return value;
701
- }
702
- has(key) {
703
- return this.store.has(key);
704
- }
705
- delete(key, callback) {
706
- this.store.delete(key);
707
- callback?.(key);
708
- }
709
- log() {
710
- return this.store;
711
- }
712
- };
713
- var State_default = State;
714
-
715
766
  // src/elements/Vector.tsx
716
767
  var Vector = class _Vector {
717
768
  /**
718
769
  * Creates a new Vector
719
770
  * @param x - X-coordinate (default: 0)
720
771
  * @param y - Y-coordinate (default: 0)
772
+ * @param z - Z-coordinate (default: 0)
721
773
  */
722
- constructor(x = 0, y = 0) {
774
+ constructor(x = 0, y = 0, z = 0) {
723
775
  this.x = x;
724
776
  this.y = y;
777
+ this.z = z;
725
778
  }
726
779
  /**
727
780
  * Adds another vector to this vector
@@ -731,6 +784,7 @@ var Vector = class _Vector {
731
784
  add(v) {
732
785
  this.x += v.x;
733
786
  this.y += v.y;
787
+ this.z += v.z;
734
788
  return this;
735
789
  }
736
790
  /**
@@ -741,6 +795,7 @@ var Vector = class _Vector {
741
795
  sub(v) {
742
796
  this.x -= v.x;
743
797
  this.y -= v.y;
798
+ this.z -= v.z;
744
799
  return this;
745
800
  }
746
801
  /**
@@ -751,6 +806,7 @@ var Vector = class _Vector {
751
806
  mult(n) {
752
807
  this.x *= n;
753
808
  this.y *= n;
809
+ this.z *= n;
754
810
  return this;
755
811
  }
756
812
  /**
@@ -761,11 +817,11 @@ var Vector = class _Vector {
761
817
  div(n) {
762
818
  this.x /= n;
763
819
  this.y /= n;
820
+ this.z /= n;
764
821
  return this;
765
822
  }
766
- // to do : project, perp, slerp
767
823
  /**
768
- * Rotates this vector by an angle
824
+ * Rotates this vector by an angle (around z-axis for 2D rotation)
769
825
  * @param angle - Angle in radians
770
826
  * @returns This vector after rotation
771
827
  */
@@ -783,7 +839,7 @@ var Vector = class _Vector {
783
839
  * @returns The magnitude of the vector
784
840
  */
785
841
  mag() {
786
- return Math.sqrt(this.x * this.x + this.y * this.y);
842
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
787
843
  }
788
844
  /**
789
845
  * Alias for mag() - calculates the length of this vector
@@ -798,7 +854,7 @@ var Vector = class _Vector {
798
854
  * @returns The dot product
799
855
  */
800
856
  dot(v) {
801
- return this.x * v.x + this.y * v.y;
857
+ return this.x * v.x + this.y * v.y + this.z * v.z;
802
858
  }
803
859
  /**
804
860
  * Calculates the distance between this vector and another vector
@@ -806,21 +862,21 @@ var Vector = class _Vector {
806
862
  * @returns The distance between the vectors
807
863
  */
808
864
  dist(v) {
809
- return Math.hypot(this.x - v.x, this.y - v.y);
865
+ return Math.hypot(this.x - v.x, this.y - v.y, this.z - v.z);
810
866
  }
811
867
  /**
812
- * Calculates the angle of this vector
868
+ * Calculates the angle of this vector (in 2D, ignoring z)
813
869
  * @returns The angle in radians
814
870
  */
815
871
  angle() {
816
- return Math.atan2(-this.x, -this.y) + Math.PI;
872
+ return Math.atan2(this.y, this.x);
817
873
  }
818
874
  /**
819
875
  * Creates a copy of this vector
820
876
  * @returns A new Vector with the same coordinates
821
877
  */
822
878
  copy() {
823
- return new _Vector(this.x, this.y);
879
+ return new _Vector(this.x, this.y, this.z);
824
880
  }
825
881
  /**
826
882
  * Normalizes this vector (sets its magnitude to 1)
@@ -834,107 +890,128 @@ var Vector = class _Vector {
834
890
  * Sets the coordinates of this vector
835
891
  * @param x - New X-coordinate
836
892
  * @param y - New Y-coordinate
893
+ * @param z - New Z-coordinate (default: 0)
837
894
  * @returns This vector after setting coordinates
838
895
  */
839
- set(x, y) {
896
+ set(x, y, z = 0) {
840
897
  this.x = x;
841
898
  this.y = y;
899
+ this.z = z;
842
900
  return this;
843
901
  }
844
902
  /**
845
- * Creates a new vector at a specified angle and distance from a center point
846
- * @param center - The center point vector
847
- * @param a - The angle in radians
848
- * @param r - The radius (distance from center)
849
- * @returns A new Vector at the calculated position
903
+ * Calculates the cross product of this vector with another vector
904
+ * @param v - The other vector
905
+ * @returns A new Vector representing the cross product
850
906
  */
851
- static fromAngle(center, a, r) {
852
- const x = Math.cos(a) * r + center.x;
853
- const y = Math.sin(a) * r + center.y;
854
- return new _Vector(x, y);
855
- }
856
- };
857
- var Vector_default = Vector;
858
-
859
- // src/elements/Time.tsx
860
- var Time = class {
861
- constructor(ctx) {
862
- this.timelines = /* @__PURE__ */ new Map();
863
- this.currentTimeline = "default";
864
- this.DEFAULT_DURATION = 8 * 60;
865
- this.staggers = [];
866
- this.context = ctx;
867
- this.timelines.set("default", {
868
- progress: 0,
869
- duration: this.DEFAULT_DURATION
870
- });
907
+ cross(v) {
908
+ return new _Vector(
909
+ this.y * v.z - this.z * v.y,
910
+ this.z * v.x - this.x * v.z,
911
+ this.x * v.y - this.y * v.x
912
+ );
871
913
  }
872
- timeline(key) {
873
- if (!this.timelines.has(key)) {
874
- this.timelines.set(key, { progress: 0, duration: this.DEFAULT_DURATION });
875
- }
876
- this.currentTimeline = key;
914
+ /**
915
+ * Calculates this vector's position relative to another vector
916
+ * @param v - The reference vector
917
+ * @returns This vector after making it relative to v
918
+ */
919
+ relativeTo(v) {
920
+ this.x -= v.x;
921
+ this.y -= v.y;
922
+ this.z -= v.z;
877
923
  return this;
878
924
  }
879
- use(progress) {
880
- const timeline = this.timelines.get(this.currentTimeline);
881
- if (timeline.duration <= 0) {
882
- timeline.progress = 0;
883
- return this;
884
- }
885
- timeline.progress = timeline.duration === 1 ? Math.min(progress, 1) : progress / timeline.duration % 1;
925
+ /**
926
+ * Makes this vector point towards a target vector
927
+ * @param target - The target vector to look at
928
+ * @returns This vector after pointing towards target
929
+ */
930
+ lookAt(target) {
931
+ const direction = new _Vector(
932
+ target.x - this.x,
933
+ target.y - this.y,
934
+ target.z - this.z
935
+ );
936
+ direction.normalize();
937
+ this.x = direction.x;
938
+ this.y = direction.y;
939
+ this.z = direction.z;
886
940
  return this;
887
941
  }
888
- for(duration) {
889
- const timeline = this.timelines.get(this.currentTimeline);
890
- timeline.duration = duration;
942
+ /**
943
+ * Converts this vector from world space to screen space
944
+ * @param width - Screen width
945
+ * @param height - Screen height
946
+ * @returns This vector after screen space conversion
947
+ */
948
+ toScreen(width, height) {
949
+ this.x = (this.x + 1) * width / 2;
950
+ this.y = (1 - this.y) * height / 2;
891
951
  return this;
892
952
  }
893
- stagger(num, offset = 0, callback) {
894
- const timeline = this.timelines.get(this.currentTimeline);
895
- const totalduration = this.context.remap(
896
- timeline.progress,
897
- 0,
898
- 1,
899
- 0,
900
- 1 + offset
901
- );
902
- for (let i = 0; i < num; i++) {
903
- const id = 1 - i / (num - 1);
904
- const progress = this.context.constrain(
905
- totalduration - id * offset,
906
- 0,
907
- 1
908
- );
909
- if (!callback) {
910
- if (this.staggers[i]) {
911
- this.staggers[i].progress = progress;
912
- } else {
913
- this.staggers[i] = { progress, id };
914
- }
953
+ /**
954
+ * Spherical linear interpolation between this vector and another vector
955
+ * @param v - The target vector
956
+ * @param amt - Interpolation amount (0-1)
957
+ * @returns This vector after interpolation
958
+ */
959
+ slerp(v, amt) {
960
+ if (amt === 0) {
961
+ return this;
962
+ }
963
+ if (amt === 1) {
964
+ return this.set(v.x, v.y, v.z);
965
+ }
966
+ const selfMag = this.mag();
967
+ const vMag = v.mag();
968
+ const magmag = selfMag * vMag;
969
+ if (magmag === 0) {
970
+ this.mult(1 - amt).add(new _Vector(v.x * amt, v.y * amt, v.z * amt));
971
+ return this;
972
+ }
973
+ const axis = this.cross(v);
974
+ const axisMag = axis.mag();
975
+ const theta = Math.atan2(axisMag, this.dot(v));
976
+ if (axisMag > 0) {
977
+ axis.x /= axisMag;
978
+ axis.y /= axisMag;
979
+ axis.z /= axisMag;
980
+ } else if (theta < Math.PI * 0.5) {
981
+ this.mult(1 - amt).add(new _Vector(v.x * amt, v.y * amt, v.z * amt));
982
+ return this;
983
+ } else {
984
+ if (this.z === 0 && v.z === 0) {
985
+ axis.set(0, 0, 1);
986
+ } else if (this.x !== 0) {
987
+ axis.set(this.y, -this.x, 0).normalize();
915
988
  } else {
916
- callback?.(progress, id, num);
989
+ axis.set(1, 0, 0);
917
990
  }
918
991
  }
919
- return callback ? this : this.staggers;
920
- }
921
- between(from = 0, to = 1, callback) {
922
- const timeline = this.timelines.get(this.currentTimeline);
923
- const localProgress = this.context.remap(
924
- timeline.progress,
925
- Math.max(0, from),
926
- Math.min(1, to),
927
- 0,
928
- 1
929
- );
930
- callback(localProgress);
992
+ const ey = axis.cross(this);
993
+ const lerpedMagFactor = 1 - amt + amt * vMag / selfMag;
994
+ const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta);
995
+ const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta);
996
+ this.x = this.x * cosMultiplier + ey.x * sinMultiplier;
997
+ this.y = this.y * cosMultiplier + ey.y * sinMultiplier;
998
+ this.z = this.z * cosMultiplier + ey.z * sinMultiplier;
931
999
  return this;
932
1000
  }
933
- progress() {
934
- return this.timelines.get(this.currentTimeline)?.progress || 0;
1001
+ /**
1002
+ * Creates a new vector at a specified angle and distance from a center point
1003
+ * @param center - The center point vector
1004
+ * @param a - The angle in radians
1005
+ * @param r - The radius (distance from center)
1006
+ * @returns A new Vector at the calculated position
1007
+ */
1008
+ static fromAngle(center, a, r) {
1009
+ const x = Math.cos(a) * r + center.x;
1010
+ const y = Math.sin(a) * r + center.y;
1011
+ return new _Vector(x, y, center.z);
935
1012
  }
936
1013
  };
937
- var Time_default = Time;
1014
+ var Vector_default = Vector;
938
1015
 
939
1016
  // src/elements/Text.tsx
940
1017
  var Text = class {
@@ -1143,138 +1220,1512 @@ var Thing = class {
1143
1220
  };
1144
1221
  var Thing_default = Thing;
1145
1222
 
1146
- // src/KlintFunctions.tsx
1147
- var KlintCoreFunctions = {
1148
- saveCanvas: (ctx) => () => {
1149
- const link = document.createElement("a");
1150
- link.download = "canvas.png";
1151
- link.href = ctx.canvas.toDataURL();
1152
- link.click();
1153
- },
1154
- fullscreen: (ctx) => () => {
1155
- ctx.canvas.requestFullscreen?.();
1156
- },
1157
- play: (ctx) => () => {
1158
- if (!ctx.__isPlaying) ctx.__isPlaying = true;
1159
- },
1160
- pause: (ctx) => () => {
1161
- if (ctx.__isPlaying) ctx.__isPlaying = false;
1162
- },
1163
- // to do
1164
- redraw: () => () => {
1165
- },
1166
- extend: (ctx) => (name, data, enforceReplace = false) => {
1167
- if (name in ctx && !enforceReplace) return;
1168
- ctx[name] = data;
1169
- },
1170
- passImage: () => (element) => {
1171
- if (!element.complete) {
1172
- console.warn("Image passed to passImage() is not fully loaded");
1173
- return null;
1223
+ // src/elements/Grid.tsx
1224
+ var Grid = class {
1225
+ /**
1226
+ * Creates a new Grid instance
1227
+ * @param ctx - The Klint context
1228
+ */
1229
+ constructor(ctx) {
1230
+ this.context = ctx;
1231
+ }
1232
+ /**
1233
+ * Create a rectangular grid of points
1234
+ * @param x - X position of the grid
1235
+ * @param y - Y position of the grid
1236
+ * @param width - Width of the grid
1237
+ * @param height - Height of the grid
1238
+ * @param countX - Number of points horizontally
1239
+ * @param countY - Number of points vertically
1240
+ * @param options - Grid options
1241
+ * @returns Array of grid points
1242
+ */
1243
+ rect(x, y, width, height, countX, countY, options) {
1244
+ const origin = options?.origin || "corner";
1245
+ const points = [];
1246
+ let startX = x;
1247
+ let startY = y;
1248
+ if (origin === "center") {
1249
+ startX = x - width / 2;
1250
+ startY = y - height / 2;
1174
1251
  }
1175
- return element;
1176
- },
1177
- passImages: () => (elements) => {
1178
- return elements.map((element) => {
1179
- if (!element.complete) {
1180
- console.warn("Image passed to passImages() is not fully loaded");
1181
- return null;
1252
+ const spacingX = countX > 1 ? width / (countX - 1) : 0;
1253
+ const spacingY = countY > 1 ? height / (countY - 1) : 0;
1254
+ for (let j = 0; j < countY; j++) {
1255
+ for (let i = 0; i < countX; i++) {
1256
+ const pointX = startX + i * spacingX;
1257
+ const pointY = startY + j * spacingY;
1258
+ const id = j * countX + i;
1259
+ points.push({
1260
+ x: pointX,
1261
+ y: pointY,
1262
+ i,
1263
+ j,
1264
+ id
1265
+ });
1182
1266
  }
1183
- return element;
1184
- });
1185
- },
1186
- saveConfig: (ctx) => (from) => {
1187
- return Object.fromEntries(
1188
- CONFIG_PROPS.map((key) => [
1189
- key,
1190
- from?.[key] ?? ctx[key]
1191
- ])
1192
- );
1193
- },
1194
- restoreConfig: (ctx) => (config) => {
1195
- Object.assign(ctx, config);
1196
- },
1197
- describe: (ctx) => (description) => {
1198
- ctx.__description = description;
1199
- },
1200
- createOffscreen: (ctx) => (id, width, height, options, callback) => {
1201
- const offscreen = document.createElement("canvas");
1202
- offscreen.width = width * ctx.__dpr;
1203
- offscreen.height = height * ctx.__dpr;
1204
- const context = offscreen.getContext("2d", {
1205
- alpha: options?.alpha ?? true,
1206
- willReadFrequently: options?.willreadfrequently ?? false
1207
- });
1208
- if (!context) throw new Error("Failed to create offscreen context");
1209
- context.__dpr = ctx.__dpr;
1210
- context.width = width * ctx.__dpr;
1211
- context.height = height * ctx.__dpr;
1212
- context.__isMainContext = false;
1213
- context.__imageOrigin = "corner";
1214
- context.__rectangleOrigin = "corner";
1215
- context.__canvasOrigin = "corner";
1216
- context.__textFont = "sans-serif";
1217
- context.__textWeight = "normal";
1218
- context.__textStyle = "normal";
1219
- context.__textSize = 120;
1220
- context.__textAlignment = {
1221
- horizontal: "left",
1222
- vertical: "top"
1223
- };
1224
- if (!options?.ignoreFunctions) {
1225
- context.Color = ctx.Color;
1226
- context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
1227
- context.Easing = ctx.Easing;
1228
- context.Text = ctx.Text;
1229
- Object.entries(KlintFunctions).forEach(([name, fn]) => {
1230
- context[name] = fn(context);
1267
+ }
1268
+ return points;
1269
+ }
1270
+ /**
1271
+ * Create a radial grid of points
1272
+ * @param x - Center X position
1273
+ * @param y - Center Y position
1274
+ * @param radius - Maximum radius
1275
+ * @param count - Number of points per ring
1276
+ * @param ringCount - Number of rings
1277
+ * @param ringSpace - Space between rings
1278
+ * @param options - Grid options
1279
+ * @returns Array of grid points
1280
+ */
1281
+ radial(x, y, radius, count, ringCount, ringSpace, options) {
1282
+ const perStepCount = options?.perStepCount || 0;
1283
+ const points = [];
1284
+ let id = 0;
1285
+ for (let ring = 0; ring < ringCount; ring++) {
1286
+ const ringRadius = ring * ringSpace;
1287
+ if (ringRadius > radius) break;
1288
+ const ringPointCount = count + perStepCount * ring;
1289
+ for (let i = 0; i < ringPointCount; i++) {
1290
+ const angle = Math.PI * 2 * i / ringPointCount;
1291
+ const pointX = x + Math.cos(angle) * ringRadius;
1292
+ const pointY = y + Math.sin(angle) * ringRadius;
1293
+ points.push({
1294
+ x: pointX,
1295
+ y: pointY,
1296
+ i,
1297
+ j: ring,
1298
+ id: id++
1299
+ });
1300
+ }
1301
+ }
1302
+ if (ringCount > 0 && points.length === 0) {
1303
+ points.push({
1304
+ x,
1305
+ y,
1306
+ i: 0,
1307
+ j: 0,
1308
+ id: 0
1231
1309
  });
1232
1310
  }
1233
- if (options?.origin) {
1234
- context.__canvasOrigin = options.origin;
1235
- if (options.origin === "center") {
1236
- context.translate(context.width * 0.5, context.height * 0.5);
1311
+ return points;
1312
+ }
1313
+ /**
1314
+ * Create a hexagonal grid of points
1315
+ * @param x - X position of the grid
1316
+ * @param y - Y position of the grid
1317
+ * @param width - Width of the grid area
1318
+ * @param height - Height of the grid area
1319
+ * @param size - Size of each hexagon
1320
+ * @param options - Grid options
1321
+ * @returns Array of grid points
1322
+ */
1323
+ hex(x, y, width, height, size, options) {
1324
+ const origin = options?.origin || "corner";
1325
+ const pointy = options?.pointy !== false;
1326
+ const points = [];
1327
+ let startX = x;
1328
+ let startY = y;
1329
+ if (origin === "center") {
1330
+ startX = x - width / 2;
1331
+ startY = y - height / 2;
1332
+ }
1333
+ const hexWidth = pointy ? Math.sqrt(3) * size : 2 * size;
1334
+ const hexHeight = pointy ? 2 * size : Math.sqrt(3) * size;
1335
+ const cols = Math.ceil(width / hexWidth) + 1;
1336
+ const rows = Math.ceil(height / (hexHeight * 0.75)) + 1;
1337
+ let id = 0;
1338
+ for (let j = 0; j < rows; j++) {
1339
+ for (let i = 0; i < cols; i++) {
1340
+ let pointX;
1341
+ let pointY;
1342
+ if (pointy) {
1343
+ pointX = startX + i * hexWidth;
1344
+ pointY = startY + j * hexHeight * 0.75;
1345
+ if (j % 2 === 1) {
1346
+ pointX += hexWidth / 2;
1347
+ }
1348
+ } else {
1349
+ pointX = startX + i * hexWidth * 0.75;
1350
+ pointY = startY + j * hexHeight;
1351
+ if (i % 2 === 1) {
1352
+ pointY += hexHeight / 2;
1353
+ }
1354
+ }
1355
+ if (pointX <= startX + width && pointY <= startY + height) {
1356
+ points.push({
1357
+ x: pointX,
1358
+ y: pointY,
1359
+ i,
1360
+ j,
1361
+ id: id++
1362
+ });
1363
+ }
1237
1364
  }
1238
1365
  }
1239
- if (callback) {
1240
- callback(context);
1366
+ return points;
1367
+ }
1368
+ /**
1369
+ * Create a triangular grid of points
1370
+ * @param x - X position of the grid
1371
+ * @param y - Y position of the grid
1372
+ * @param width - Width of the grid
1373
+ * @param height - Height of the grid
1374
+ * @param size - Size of each triangle
1375
+ * @param options - Grid options
1376
+ * @returns Array of grid points
1377
+ */
1378
+ triangle(x, y, width, height, size, options) {
1379
+ const origin = options?.origin || "corner";
1380
+ const points = [];
1381
+ let startX = x;
1382
+ let startY = y;
1383
+ if (origin === "center") {
1384
+ startX = x - width / 2;
1385
+ startY = y - height / 2;
1241
1386
  }
1242
- if (options?.static === "true") {
1243
- const base64 = offscreen.toDataURL();
1244
- const img = new Image();
1245
- img.src = base64;
1246
- ctx.__offscreens.set(id, img);
1247
- return img;
1387
+ const triHeight = Math.sqrt(3) / 2 * size;
1388
+ const cols = Math.ceil(width / (size / 2)) + 1;
1389
+ const rows = Math.ceil(height / triHeight) + 1;
1390
+ let id = 0;
1391
+ for (let j = 0; j < rows; j++) {
1392
+ for (let i = 0; i < cols; i++) {
1393
+ const pointX = startX + i * (size / 2);
1394
+ const pointY = startY + j * triHeight;
1395
+ if (pointX <= startX + width && pointY <= startY + height) {
1396
+ points.push({
1397
+ x: pointX,
1398
+ y: pointY,
1399
+ i,
1400
+ j,
1401
+ id: id++
1402
+ });
1403
+ }
1404
+ }
1248
1405
  }
1249
- ctx.__offscreens.set(id, context);
1250
- return context;
1251
- },
1252
- getOffscreen: (ctx) => (id) => {
1253
- const offscreen = ctx.__offscreens.get(id);
1254
- if (!offscreen)
1255
- throw new Error(`No offscreen context found with id: ${id}`);
1256
- return offscreen;
1406
+ return points;
1257
1407
  }
1258
1408
  };
1259
- var KlintFunctions = {
1260
- extend: (ctx) => (name, data, enforceReplace = false) => {
1261
- if (name in ctx && !enforceReplace) return;
1262
- ctx[name] = data;
1263
- },
1264
- background: (ctx) => (color) => {
1265
- ctx.resetTransform();
1266
- ctx.push();
1267
- if (color && color !== "transparent") {
1268
- ctx.fillStyle = color;
1269
- ctx.fillRect(0, 0, ctx.width, ctx.height);
1270
- } else {
1271
- ctx.clearRect(0, 0, ctx.width, ctx.height);
1409
+ var Grid_default = Grid;
1410
+
1411
+ // src/elements/Strip.tsx
1412
+ var Strip = class {
1413
+ /**
1414
+ * Creates a new Strip instance
1415
+ * @param ctx - The Klint context
1416
+ */
1417
+ constructor(ctx) {
1418
+ this.context = ctx;
1419
+ }
1420
+ /**
1421
+ * Create a strip of triangles from points
1422
+ * Points are connected in a zigzag pattern:
1423
+ * 0 - 2 - 4 ...
1424
+ * | / | / |
1425
+ * 1 - 3 - 5 ...
1426
+ *
1427
+ * @param points - Array of points (must be even number for complete triangles)
1428
+ * @param draw - Optional callback to customize each triangle's appearance
1429
+ */
1430
+ triangles(points, draw) {
1431
+ if (points.length < 3) return;
1432
+ const numTriangles = Math.floor((points.length - 2) * 2);
1433
+ for (let i = 0; i < numTriangles; i++) {
1434
+ const baseIndex = Math.floor(i / 2) * 2;
1435
+ const isEven = i % 2 === 0;
1436
+ let p1;
1437
+ let p2;
1438
+ let p3;
1439
+ if (isEven) {
1440
+ p1 = points[baseIndex];
1441
+ p2 = points[baseIndex + 1];
1442
+ p3 = points[baseIndex + 2];
1443
+ } else {
1444
+ p1 = points[baseIndex + 1];
1445
+ p2 = points[baseIndex + 3];
1446
+ p3 = points[baseIndex + 2];
1447
+ }
1448
+ if (!p1 || !p2 || !p3) continue;
1449
+ const center = {
1450
+ x: (p1.x + p2.x + p3.x) / 3,
1451
+ y: (p1.y + p2.y + p3.y) / 3
1452
+ };
1453
+ const triangle = {
1454
+ id: i,
1455
+ center,
1456
+ points: [p1, p2, p3]
1457
+ };
1458
+ let fillColor;
1459
+ if (draw) {
1460
+ const result = draw(triangle);
1461
+ if (typeof result === "string") {
1462
+ fillColor = result;
1463
+ }
1464
+ }
1465
+ this.context.beginPath();
1466
+ this.context.moveTo(p1.x, p1.y);
1467
+ this.context.lineTo(p2.x, p2.y);
1468
+ this.context.lineTo(p3.x, p3.y);
1469
+ this.context.closePath();
1470
+ if (fillColor) {
1471
+ const prevFill = this.context.fillStyle;
1472
+ this.context.fillStyle = fillColor;
1473
+ this.context.fill();
1474
+ this.context.fillStyle = prevFill;
1475
+ } else if (this.context.checkTransparency("fill")) {
1476
+ this.context.fill();
1477
+ }
1478
+ if (this.context.checkTransparency("stroke")) {
1479
+ this.context.stroke();
1480
+ }
1272
1481
  }
1273
- ctx.pop();
1274
- if (ctx.__canvasOrigin === "center")
1275
- ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
1276
- },
1277
- reset: (ctx) => () => {
1482
+ }
1483
+ /**
1484
+ * Create a strip of quads from points
1485
+ * Points are connected in a grid pattern:
1486
+ * 0 - 2 - 4 ...
1487
+ * | | |
1488
+ * 1 - 3 - 5 ...
1489
+ *
1490
+ * @param points - Array of points (must be even number for complete quads)
1491
+ * @param draw - Optional callback to customize each quad's appearance
1492
+ */
1493
+ quads(points, draw) {
1494
+ if (points.length < 4) return;
1495
+ const numQuads = Math.floor((points.length - 2) / 2);
1496
+ for (let i = 0; i < numQuads; i++) {
1497
+ const baseIndex = i * 2;
1498
+ const p1 = points[baseIndex];
1499
+ const p2 = points[baseIndex + 1];
1500
+ const p3 = points[baseIndex + 3];
1501
+ const p4 = points[baseIndex + 2];
1502
+ if (!p1 || !p2 || !p3 || !p4) continue;
1503
+ const center = {
1504
+ x: (p1.x + p2.x + p3.x + p4.x) / 4,
1505
+ y: (p1.y + p2.y + p3.y + p4.y) / 4
1506
+ };
1507
+ const quad = {
1508
+ id: i,
1509
+ center,
1510
+ points: [p1, p2, p3, p4]
1511
+ };
1512
+ let fillColor;
1513
+ if (draw) {
1514
+ const result = draw(quad);
1515
+ if (typeof result === "string") {
1516
+ fillColor = result;
1517
+ }
1518
+ }
1519
+ this.context.beginPath();
1520
+ this.context.moveTo(p1.x, p1.y);
1521
+ this.context.lineTo(p2.x, p2.y);
1522
+ this.context.lineTo(p3.x, p3.y);
1523
+ this.context.lineTo(p4.x, p4.y);
1524
+ this.context.closePath();
1525
+ if (fillColor) {
1526
+ const prevFill = this.context.fillStyle;
1527
+ this.context.fillStyle = fillColor;
1528
+ this.context.fill();
1529
+ this.context.fillStyle = prevFill;
1530
+ } else if (this.context.checkTransparency("fill")) {
1531
+ this.context.fill();
1532
+ }
1533
+ if (this.context.checkTransparency("stroke")) {
1534
+ this.context.stroke();
1535
+ }
1536
+ }
1537
+ }
1538
+ /**
1539
+ * Create a single hull shape from points
1540
+ * Points are connected following the winding order:
1541
+ * 0 - 2 - 4 - ... n-1
1542
+ * | |
1543
+ * 1 - 3 - 5 - ... n
1544
+ *
1545
+ * Final order: 0 - 2 - 4 ... n-1, n, ... 5 - 3 - 1
1546
+ *
1547
+ * @param points - Array of points
1548
+ * @param draw - Optional callback to add elements along the hull
1549
+ */
1550
+ hull(points, draw) {
1551
+ if (points.length < 2) return;
1552
+ const hullPath = [];
1553
+ for (let i = 0; i < points.length; i += 2) {
1554
+ hullPath.push(points[i]);
1555
+ }
1556
+ for (let i = points.length - 1 - points.length % 2; i >= 1; i -= 2) {
1557
+ hullPath.push(points[i]);
1558
+ }
1559
+ this.context.beginPath();
1560
+ this.context.moveTo(hullPath[0].x, hullPath[0].y);
1561
+ for (let i = 1; i < hullPath.length; i++) {
1562
+ this.context.lineTo(hullPath[i].x, hullPath[i].y);
1563
+ }
1564
+ this.context.closePath();
1565
+ if (this.context.checkTransparency("fill")) {
1566
+ this.context.fill();
1567
+ }
1568
+ if (this.context.checkTransparency("stroke")) {
1569
+ this.context.stroke();
1570
+ }
1571
+ if (draw) {
1572
+ const numPairs = Math.floor(points.length / 2);
1573
+ for (let i = 0; i < numPairs; i++) {
1574
+ const topPoint = points[i * 2];
1575
+ const bottomPoint = points[i * 2 + 1];
1576
+ if (topPoint && bottomPoint) {
1577
+ const center = {
1578
+ x: (topPoint.x + bottomPoint.x) / 2,
1579
+ y: (topPoint.y + bottomPoint.y) / 2
1580
+ };
1581
+ const hull = {
1582
+ id: i,
1583
+ center
1584
+ };
1585
+ draw(hull);
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ /**
1591
+ * Create a ribbon/tape effect from points
1592
+ * Similar to hull but with configurable width
1593
+ *
1594
+ * @param points - Array of center points
1595
+ * @param width - Width of the ribbon
1596
+ * @param draw - Optional callback
1597
+ */
1598
+ ribbon(points, width, draw) {
1599
+ if (points.length < 2) return;
1600
+ const offsetPoints = [];
1601
+ for (let i = 0; i < points.length; i++) {
1602
+ const curr = points[i];
1603
+ const prev = points[i - 1] || curr;
1604
+ const next = points[i + 1] || curr;
1605
+ const dx = next.x - prev.x;
1606
+ const dy = next.y - prev.y;
1607
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
1608
+ const perpX = -dy / len * (width / 2);
1609
+ const perpY = dx / len * (width / 2);
1610
+ offsetPoints.push({
1611
+ top: { x: curr.x + perpX, y: curr.y + perpY },
1612
+ bottom: { x: curr.x - perpX, y: curr.y - perpY }
1613
+ });
1614
+ }
1615
+ this.context.beginPath();
1616
+ this.context.moveTo(offsetPoints[0].top.x, offsetPoints[0].top.y);
1617
+ for (let i = 1; i < offsetPoints.length; i++) {
1618
+ this.context.lineTo(offsetPoints[i].top.x, offsetPoints[i].top.y);
1619
+ }
1620
+ for (let i = offsetPoints.length - 1; i >= 0; i--) {
1621
+ this.context.lineTo(offsetPoints[i].bottom.x, offsetPoints[i].bottom.y);
1622
+ }
1623
+ this.context.closePath();
1624
+ if (this.context.checkTransparency("fill")) {
1625
+ this.context.fill();
1626
+ }
1627
+ if (this.context.checkTransparency("stroke")) {
1628
+ this.context.stroke();
1629
+ }
1630
+ if (draw) {
1631
+ for (let i = 0; i < points.length - 1; i++) {
1632
+ const center = {
1633
+ x: (points[i].x + points[i + 1].x) / 2,
1634
+ y: (points[i].y + points[i + 1].y) / 2
1635
+ };
1636
+ const result = draw({ id: i, center });
1637
+ if (typeof result === "string") {
1638
+ this.context.beginPath();
1639
+ this.context.moveTo(offsetPoints[i].top.x, offsetPoints[i].top.y);
1640
+ this.context.lineTo(offsetPoints[i + 1].top.x, offsetPoints[i + 1].top.y);
1641
+ this.context.lineTo(offsetPoints[i + 1].bottom.x, offsetPoints[i + 1].bottom.y);
1642
+ this.context.lineTo(offsetPoints[i].bottom.x, offsetPoints[i].bottom.y);
1643
+ this.context.closePath();
1644
+ const prevFill = this.context.fillStyle;
1645
+ this.context.fillStyle = result;
1646
+ this.context.fill();
1647
+ this.context.fillStyle = prevFill;
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ };
1653
+ var Strip_default = Strip;
1654
+
1655
+ // src/elements/Noise.tsx
1656
+ var Noise = class {
1657
+ /**
1658
+ * Creates a new Noise instance
1659
+ * @param ctx - The Klint context
1660
+ */
1661
+ constructor(ctx) {
1662
+ this.perm = [];
1663
+ this.permMod12 = [];
1664
+ this.grad3 = [
1665
+ [1, 1, 0],
1666
+ [-1, 1, 0],
1667
+ [1, -1, 0],
1668
+ [-1, -1, 0],
1669
+ [1, 0, 1],
1670
+ [-1, 0, 1],
1671
+ [1, 0, -1],
1672
+ [-1, 0, -1],
1673
+ [0, 1, 1],
1674
+ [0, -1, 1],
1675
+ [0, 1, -1],
1676
+ [0, -1, -1]
1677
+ ];
1678
+ this.grad4 = [
1679
+ [0, 1, 1, 1],
1680
+ [0, 1, 1, -1],
1681
+ [0, 1, -1, 1],
1682
+ [0, 1, -1, -1],
1683
+ [0, -1, 1, 1],
1684
+ [0, -1, 1, -1],
1685
+ [0, -1, -1, 1],
1686
+ [0, -1, -1, -1],
1687
+ [1, 0, 1, 1],
1688
+ [1, 0, 1, -1],
1689
+ [1, 0, -1, 1],
1690
+ [1, 0, -1, -1],
1691
+ [-1, 0, 1, 1],
1692
+ [-1, 0, 1, -1],
1693
+ [-1, 0, -1, 1],
1694
+ [-1, 0, -1, -1],
1695
+ [1, 1, 0, 1],
1696
+ [1, 1, 0, -1],
1697
+ [1, -1, 0, 1],
1698
+ [1, -1, 0, -1],
1699
+ [-1, 1, 0, 1],
1700
+ [-1, 1, 0, -1],
1701
+ [-1, -1, 0, 1],
1702
+ [-1, -1, 0, -1],
1703
+ [1, 1, 1, 0],
1704
+ [1, 1, -1, 0],
1705
+ [1, -1, 1, 0],
1706
+ [1, -1, -1, 0],
1707
+ [-1, 1, 1, 0],
1708
+ [-1, 1, -1, 0],
1709
+ [-1, -1, 1, 0],
1710
+ [-1, -1, -1, 0]
1711
+ ];
1712
+ this.currentSeed = Math.random();
1713
+ this.F2 = 0.5 * (Math.sqrt(3) - 1);
1714
+ this.G2 = (3 - Math.sqrt(3)) / 6;
1715
+ this.F3 = 1 / 3;
1716
+ this.G3 = 1 / 6;
1717
+ this.F4 = (Math.sqrt(5) - 1) / 4;
1718
+ this.G4 = (5 - Math.sqrt(5)) / 20;
1719
+ this.context = ctx;
1720
+ this.buildPermutationTable();
1721
+ }
1722
+ /**
1723
+ * Build permutation table for noise generation
1724
+ */
1725
+ buildPermutationTable() {
1726
+ const p = [];
1727
+ for (let i = 0; i < 256; i++) {
1728
+ p[i] = i;
1729
+ }
1730
+ let n = 256;
1731
+ while (n > 0) {
1732
+ const index = Math.floor(this.random() * n--);
1733
+ const temp = p[n];
1734
+ p[n] = p[index];
1735
+ p[index] = temp;
1736
+ }
1737
+ this.perm = [];
1738
+ this.permMod12 = [];
1739
+ for (let i = 0; i < 512; i++) {
1740
+ this.perm[i] = p[i & 255];
1741
+ this.permMod12[i] = this.perm[i] % 12;
1742
+ }
1743
+ }
1744
+ /**
1745
+ * Seeded random number generator
1746
+ */
1747
+ random() {
1748
+ const x = Math.sin(this.currentSeed++) * 1e4;
1749
+ return x - Math.floor(x);
1750
+ }
1751
+ /**
1752
+ * Set seed for noise generation
1753
+ * @param seed - Seed value for reproducible noise
1754
+ */
1755
+ seed(seed) {
1756
+ this.currentSeed = seed !== void 0 ? seed : Math.random() * 1e4;
1757
+ this.buildPermutationTable();
1758
+ }
1759
+ /**
1760
+ * Fade function for Perlin noise
1761
+ */
1762
+ fade(t) {
1763
+ return t * t * t * (t * (t * 6 - 15) + 10);
1764
+ }
1765
+ /**
1766
+ * Linear interpolation
1767
+ */
1768
+ lerp(t, a, b) {
1769
+ return a + t * (b - a);
1770
+ }
1771
+ perlin(x, y, z, w) {
1772
+ if (y === void 0) {
1773
+ const xi = Math.floor(x) & 255;
1774
+ const xf = x - Math.floor(x);
1775
+ const u = this.fade(xf);
1776
+ const a = this.perm[xi];
1777
+ const b = this.perm[xi + 1];
1778
+ const grad1 = (hash, x2) => (hash & 1) === 0 ? x2 : -x2;
1779
+ return this.lerp(u, grad1(a, xf), grad1(b, xf - 1));
1780
+ } else if (z === void 0) {
1781
+ const xi = Math.floor(x) & 255;
1782
+ const yi = Math.floor(y) & 255;
1783
+ const xf = x - Math.floor(x);
1784
+ const yf = y - Math.floor(y);
1785
+ const u = this.fade(xf);
1786
+ const v = this.fade(yf);
1787
+ const aa = this.perm[this.perm[xi] + yi];
1788
+ const ab = this.perm[this.perm[xi] + yi + 1];
1789
+ const ba = this.perm[this.perm[xi + 1] + yi];
1790
+ const bb = this.perm[this.perm[xi + 1] + yi + 1];
1791
+ const grad2 = (hash, x3, y2) => {
1792
+ const h = hash & 3;
1793
+ const u2 = h < 2 ? x3 : y2;
1794
+ const v2 = h < 2 ? y2 : x3;
1795
+ return ((h & 1) === 0 ? u2 : -u2) + ((h & 2) === 0 ? v2 : -v2);
1796
+ };
1797
+ const x1 = this.lerp(u, grad2(aa, xf, yf), grad2(ba, xf - 1, yf));
1798
+ const x2 = this.lerp(u, grad2(ab, xf, yf - 1), grad2(bb, xf - 1, yf - 1));
1799
+ return this.lerp(v, x1, x2);
1800
+ } else if (w === void 0) {
1801
+ const xi = Math.floor(x) & 255;
1802
+ const yi = Math.floor(y) & 255;
1803
+ const zi = Math.floor(z) & 255;
1804
+ const xf = x - Math.floor(x);
1805
+ const yf = y - Math.floor(y);
1806
+ const zf = z - Math.floor(z);
1807
+ const u = this.fade(xf);
1808
+ const v = this.fade(yf);
1809
+ const w2 = this.fade(zf);
1810
+ const aaa = this.perm[this.perm[this.perm[xi] + yi] + zi];
1811
+ const aba = this.perm[this.perm[this.perm[xi] + yi + 1] + zi];
1812
+ const aab = this.perm[this.perm[this.perm[xi] + yi] + zi + 1];
1813
+ const abb = this.perm[this.perm[this.perm[xi] + yi + 1] + zi + 1];
1814
+ const baa = this.perm[this.perm[this.perm[xi + 1] + yi] + zi];
1815
+ const bba = this.perm[this.perm[this.perm[xi + 1] + yi + 1] + zi];
1816
+ const bab = this.perm[this.perm[this.perm[xi + 1] + yi] + zi + 1];
1817
+ const bbb = this.perm[this.perm[this.perm[xi + 1] + yi + 1] + zi + 1];
1818
+ const grad3 = (hash, x2, y2, z2) => {
1819
+ const h = hash & 15;
1820
+ const u2 = h < 8 ? x2 : y2;
1821
+ const v2 = h < 4 ? y2 : h === 12 || h === 14 ? x2 : z2;
1822
+ return ((h & 1) === 0 ? u2 : -u2) + ((h & 2) === 0 ? v2 : -v2);
1823
+ };
1824
+ const x1 = this.lerp(
1825
+ w2,
1826
+ this.lerp(
1827
+ v,
1828
+ this.lerp(u, grad3(aaa, xf, yf, zf), grad3(baa, xf - 1, yf, zf)),
1829
+ this.lerp(u, grad3(aba, xf, yf - 1, zf), grad3(bba, xf - 1, yf - 1, zf))
1830
+ ),
1831
+ this.lerp(
1832
+ v,
1833
+ this.lerp(u, grad3(aab, xf, yf, zf - 1), grad3(bab, xf - 1, yf, zf - 1)),
1834
+ this.lerp(u, grad3(abb, xf, yf - 1, zf - 1), grad3(bbb, xf - 1, yf - 1, zf - 1))
1835
+ )
1836
+ );
1837
+ return x1;
1838
+ } else {
1839
+ return this.perlin(x, y, z) * 0.5 + this.perlin(x + w, y + w, z + w) * 0.5;
1840
+ }
1841
+ }
1842
+ simplex(x, y, z, w) {
1843
+ if (y === void 0) {
1844
+ return this.perlin(x);
1845
+ } else if (z === void 0) {
1846
+ let n0 = 0, n1 = 0, n2 = 0;
1847
+ const s = (x + y) * this.F2;
1848
+ const i = Math.floor(x + s);
1849
+ const j = Math.floor(y + s);
1850
+ const t = (i + j) * this.G2;
1851
+ const X0 = i - t;
1852
+ const Y0 = j - t;
1853
+ const x0 = x - X0;
1854
+ const y0 = y - Y0;
1855
+ let i1, j1;
1856
+ if (x0 > y0) {
1857
+ i1 = 1;
1858
+ j1 = 0;
1859
+ } else {
1860
+ i1 = 0;
1861
+ j1 = 1;
1862
+ }
1863
+ const x1 = x0 - i1 + this.G2;
1864
+ const y1 = y0 - j1 + this.G2;
1865
+ const x2 = x0 - 1 + 2 * this.G2;
1866
+ const y2 = y0 - 1 + 2 * this.G2;
1867
+ const ii = i & 255;
1868
+ const jj = j & 255;
1869
+ const gi0 = this.permMod12[ii + this.perm[jj]];
1870
+ const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]];
1871
+ const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]];
1872
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
1873
+ if (t0 < 0) {
1874
+ n0 = 0;
1875
+ } else {
1876
+ t0 *= t0;
1877
+ n0 = t0 * t0 * this.dot2(this.grad3[gi0], x0, y0);
1878
+ }
1879
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
1880
+ if (t1 < 0) {
1881
+ n1 = 0;
1882
+ } else {
1883
+ t1 *= t1;
1884
+ n1 = t1 * t1 * this.dot2(this.grad3[gi1], x1, y1);
1885
+ }
1886
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
1887
+ if (t2 < 0) {
1888
+ n2 = 0;
1889
+ } else {
1890
+ t2 *= t2;
1891
+ n2 = t2 * t2 * this.dot2(this.grad3[gi2], x2, y2);
1892
+ }
1893
+ return 70 * (n0 + n1 + n2);
1894
+ } else if (w === void 0) {
1895
+ let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
1896
+ const s = (x + y + z) * this.F3;
1897
+ const i = Math.floor(x + s);
1898
+ const j = Math.floor(y + s);
1899
+ const k = Math.floor(z + s);
1900
+ const t = (i + j + k) * this.G3;
1901
+ const X0 = i - t;
1902
+ const Y0 = j - t;
1903
+ const Z0 = k - t;
1904
+ const x0 = x - X0;
1905
+ const y0 = y - Y0;
1906
+ const z0 = z - Z0;
1907
+ let i1, j1, k1;
1908
+ let i2, j2, k2;
1909
+ if (x0 >= y0) {
1910
+ if (y0 >= z0) {
1911
+ i1 = 1;
1912
+ j1 = 0;
1913
+ k1 = 0;
1914
+ i2 = 1;
1915
+ j2 = 1;
1916
+ k2 = 0;
1917
+ } else if (x0 >= z0) {
1918
+ i1 = 1;
1919
+ j1 = 0;
1920
+ k1 = 0;
1921
+ i2 = 1;
1922
+ j2 = 0;
1923
+ k2 = 1;
1924
+ } else {
1925
+ i1 = 0;
1926
+ j1 = 0;
1927
+ k1 = 1;
1928
+ i2 = 1;
1929
+ j2 = 0;
1930
+ k2 = 1;
1931
+ }
1932
+ } else {
1933
+ if (y0 < z0) {
1934
+ i1 = 0;
1935
+ j1 = 0;
1936
+ k1 = 1;
1937
+ i2 = 0;
1938
+ j2 = 1;
1939
+ k2 = 1;
1940
+ } else if (x0 < z0) {
1941
+ i1 = 0;
1942
+ j1 = 1;
1943
+ k1 = 0;
1944
+ i2 = 0;
1945
+ j2 = 1;
1946
+ k2 = 1;
1947
+ } else {
1948
+ i1 = 0;
1949
+ j1 = 1;
1950
+ k1 = 0;
1951
+ i2 = 1;
1952
+ j2 = 1;
1953
+ k2 = 0;
1954
+ }
1955
+ }
1956
+ const x1 = x0 - i1 + this.G3;
1957
+ const y1 = y0 - j1 + this.G3;
1958
+ const z1 = z0 - k1 + this.G3;
1959
+ const x2 = x0 - i2 + 2 * this.G3;
1960
+ const y2 = y0 - j2 + 2 * this.G3;
1961
+ const z2 = z0 - k2 + 2 * this.G3;
1962
+ const x3 = x0 - 1 + 3 * this.G3;
1963
+ const y3 = y0 - 1 + 3 * this.G3;
1964
+ const z3 = z0 - 1 + 3 * this.G3;
1965
+ const ii = i & 255;
1966
+ const jj = j & 255;
1967
+ const kk = k & 255;
1968
+ const gi0 = this.permMod12[ii + this.perm[jj + this.perm[kk]]];
1969
+ const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1 + this.perm[kk + k1]]];
1970
+ const gi2 = this.permMod12[ii + i2 + this.perm[jj + j2 + this.perm[kk + k2]]];
1971
+ const gi3 = this.permMod12[ii + 1 + this.perm[jj + 1 + this.perm[kk + 1]]];
1972
+ let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
1973
+ if (t0 < 0) {
1974
+ n0 = 0;
1975
+ } else {
1976
+ t0 *= t0;
1977
+ n0 = t0 * t0 * this.dot3(this.grad3[gi0], x0, y0, z0);
1978
+ }
1979
+ let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
1980
+ if (t1 < 0) {
1981
+ n1 = 0;
1982
+ } else {
1983
+ t1 *= t1;
1984
+ n1 = t1 * t1 * this.dot3(this.grad3[gi1], x1, y1, z1);
1985
+ }
1986
+ let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
1987
+ if (t2 < 0) {
1988
+ n2 = 0;
1989
+ } else {
1990
+ t2 *= t2;
1991
+ n2 = t2 * t2 * this.dot3(this.grad3[gi2], x2, y2, z2);
1992
+ }
1993
+ let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
1994
+ if (t3 < 0) {
1995
+ n3 = 0;
1996
+ } else {
1997
+ t3 *= t3;
1998
+ n3 = t3 * t3 * this.dot3(this.grad3[gi3], x3, y3, z3);
1999
+ }
2000
+ return 32 * (n0 + n1 + n2 + n3);
2001
+ } else {
2002
+ return this.simplex(x, y, z) * 0.5 + this.simplex(x + w, y + w, z + w) * 0.5;
2003
+ }
2004
+ }
2005
+ /**
2006
+ * Dot product for 2D
2007
+ */
2008
+ dot2(g, x, y) {
2009
+ return g[0] * x + g[1] * y;
2010
+ }
2011
+ /**
2012
+ * Dot product for 3D
2013
+ */
2014
+ dot3(g, x, y, z) {
2015
+ return g[0] * x + g[1] * y + g[2] * z;
2016
+ }
2017
+ hash(x, y, z, w) {
2018
+ let n = 0;
2019
+ if (y === void 0) {
2020
+ n = Math.sin(x * 12.9898 + this.currentSeed) * 43758.5453;
2021
+ } else if (z === void 0) {
2022
+ n = Math.sin(x * 12.9898 + y * 78.233 + this.currentSeed) * 43758.5453;
2023
+ } else if (w === void 0) {
2024
+ n = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + this.currentSeed) * 43758.5453;
2025
+ } else {
2026
+ n = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + w * 59.1337 + this.currentSeed) * 43758.5453;
2027
+ }
2028
+ return n - Math.floor(n);
2029
+ }
2030
+ /**
2031
+ * Fractal Brownian Motion (fBm) noise
2032
+ * Supports options object for amplitude/frequency/lacunarity/gain/octaves.
2033
+ */
2034
+ fbm(x, y, z, options) {
2035
+ let yVal = void 0;
2036
+ let zVal = void 0;
2037
+ let opts = typeof y === "object" ? y : typeof z === "object" ? z : {};
2038
+ if (typeof y === "number") yVal = y;
2039
+ if (typeof z === "number") zVal = z;
2040
+ if (options) opts = { ...opts, ...options };
2041
+ const octaves = opts.octaves ?? 4;
2042
+ if (octaves <= 0) return 0;
2043
+ let amplitude = opts.amplitude ?? 1;
2044
+ let frequency = opts.frequency ?? 1;
2045
+ const lacunarity = opts.lacunarity ?? 2;
2046
+ const gain = opts.gain ?? 0.5;
2047
+ let value = 0;
2048
+ let maxValue = 0;
2049
+ for (let i = 0; i < octaves; i++) {
2050
+ if (yVal === void 0) {
2051
+ value += amplitude * this.perlin(x * frequency);
2052
+ } else if (zVal === void 0) {
2053
+ value += amplitude * this.perlin(x * frequency, yVal * frequency);
2054
+ } else {
2055
+ value += amplitude * this.perlin(x * frequency, yVal * frequency, zVal * frequency);
2056
+ }
2057
+ maxValue += amplitude;
2058
+ amplitude *= gain;
2059
+ frequency *= lacunarity;
2060
+ }
2061
+ return maxValue === 0 ? 0 : value / maxValue;
2062
+ }
2063
+ /**
2064
+ * Turbulence noise (absolute value of noise)
2065
+ * Supports options object for octaves.
2066
+ */
2067
+ turbulence(x, y, z, options) {
2068
+ let yVal = void 0;
2069
+ let zVal = void 0;
2070
+ let opts = typeof y === "object" ? y : typeof z === "object" ? z : {};
2071
+ if (typeof y === "number") yVal = y;
2072
+ if (typeof z === "number") zVal = z;
2073
+ if (options) opts = { ...opts, ...options };
2074
+ const octaves = opts.octaves ?? 4;
2075
+ if (octaves <= 0) return 0;
2076
+ let value = 0;
2077
+ let amplitude = 1;
2078
+ let frequency = 1;
2079
+ let maxValue = 0;
2080
+ for (let i = 0; i < octaves; i++) {
2081
+ let noise = 0;
2082
+ if (yVal === void 0) {
2083
+ noise = this.perlin(x * frequency);
2084
+ } else if (zVal === void 0) {
2085
+ noise = this.perlin(x * frequency, yVal * frequency);
2086
+ } else {
2087
+ noise = this.perlin(x * frequency, yVal * frequency, zVal * frequency);
2088
+ }
2089
+ value += amplitude * Math.abs(noise);
2090
+ maxValue += amplitude;
2091
+ amplitude *= 0.5;
2092
+ frequency *= 2;
2093
+ }
2094
+ return maxValue === 0 ? 0 : value / maxValue;
2095
+ }
2096
+ /**
2097
+ * Ridged multifractal noise (simple implementation)
2098
+ */
2099
+ ridge(x, y, options) {
2100
+ let yVal = void 0;
2101
+ let opts = typeof y === "object" ? y : {};
2102
+ if (typeof y === "number") yVal = y;
2103
+ if (options) opts = { ...opts, ...options };
2104
+ const octaves = opts.octaves ?? 4;
2105
+ if (octaves <= 0) return 0;
2106
+ let amplitude = opts.amplitude ?? 1;
2107
+ let frequency = opts.frequency ?? 1;
2108
+ const lacunarity = opts.lacunarity ?? 2;
2109
+ const gain = opts.gain ?? 0.5;
2110
+ let value = 0;
2111
+ let weight = 1;
2112
+ let maxValue = 0;
2113
+ for (let i = 0; i < octaves; i++) {
2114
+ const n = yVal === void 0 ? this.perlin(x * frequency) : this.perlin(x * frequency, yVal * frequency);
2115
+ let signal = 1 - Math.abs(n);
2116
+ signal *= signal;
2117
+ signal *= weight;
2118
+ weight = signal * 2;
2119
+ weight = Math.min(Math.max(weight, 0), 1);
2120
+ value += signal * amplitude;
2121
+ maxValue += amplitude;
2122
+ amplitude *= gain;
2123
+ frequency *= lacunarity;
2124
+ }
2125
+ return maxValue === 0 ? 0 : value / maxValue;
2126
+ }
2127
+ /**
2128
+ * Cellular / Worley noise (2D simple implementation)
2129
+ */
2130
+ cellular(x, y, options) {
2131
+ let yVal = void 0;
2132
+ let opts = typeof y === "object" ? y : {};
2133
+ if (typeof y === "number") yVal = y;
2134
+ if (options) opts = { ...opts, ...options };
2135
+ if (yVal === void 0) yVal = 0;
2136
+ const xi = Math.floor(x);
2137
+ const yi = Math.floor(yVal);
2138
+ const distance = opts.distance ?? "euclidean";
2139
+ let minDist = Infinity;
2140
+ for (let j = -1; j <= 1; j++) {
2141
+ for (let i = -1; i <= 1; i++) {
2142
+ const fx = i + this.hash(xi + i, yi + j);
2143
+ const fy = j + this.hash(yi + j, xi + i);
2144
+ const dx = fx + xi - x;
2145
+ const dy = fy + yi - yVal;
2146
+ const dist = distance === "manhattan" ? Math.abs(dx) + Math.abs(dy) : Math.sqrt(dx * dx + dy * dy);
2147
+ if (dist < minDist) minDist = dist;
2148
+ }
2149
+ }
2150
+ const maxDist = Math.SQRT2;
2151
+ const normalized = 1 - Math.min(minDist / maxDist, 1);
2152
+ return normalized;
2153
+ }
2154
+ };
2155
+ var Noise_default = Noise;
2156
+
2157
+ // src/elements/Hotspot.tsx
2158
+ var Hotspot = class {
2159
+ /**
2160
+ * Creates a new Hotspot instance
2161
+ * @param ctx - The Klint context
2162
+ */
2163
+ constructor(ctx) {
2164
+ this.context = ctx;
2165
+ }
2166
+ /**
2167
+ * Check if point is inside a circle
2168
+ * @param point - Point to check (usually mouse position)
2169
+ * @param x - Circle center X
2170
+ * @param y - Circle center Y
2171
+ * @param radius - Circle radius
2172
+ * @returns True if point is inside the circle
2173
+ */
2174
+ circle(point, x, y, radius) {
2175
+ const dx = point.x - x;
2176
+ const dy = point.y - y;
2177
+ const distance = Math.sqrt(dx * dx + dy * dy);
2178
+ return distance <= radius;
2179
+ }
2180
+ /**
2181
+ * Check if point is inside a rectangle
2182
+ * @param point - Point to check (usually mouse position)
2183
+ * @param x - Rectangle X position
2184
+ * @param y - Rectangle Y position
2185
+ * @param width - Rectangle width
2186
+ * @param height - Rectangle height
2187
+ * @returns True if point is inside the rectangle
2188
+ */
2189
+ rect(point, x, y, width, height) {
2190
+ const origin = this.context.__rectangleOrigin || "corner";
2191
+ let left, top;
2192
+ if (origin === "center") {
2193
+ left = x - width / 2;
2194
+ top = y - height / 2;
2195
+ } else {
2196
+ left = x;
2197
+ top = y;
2198
+ }
2199
+ const right = left + width;
2200
+ const bottom = top + height;
2201
+ return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
2202
+ }
2203
+ /**
2204
+ * Check if point is inside an ellipse
2205
+ * @param point - Point to check
2206
+ * @param x - Ellipse center X
2207
+ * @param y - Ellipse center Y
2208
+ * @param radiusX - Horizontal radius
2209
+ * @param radiusY - Vertical radius
2210
+ * @param rotation - Rotation angle in radians
2211
+ * @returns True if point is inside the ellipse
2212
+ */
2213
+ ellipse(point, x, y, radiusX, radiusY, rotation = 0) {
2214
+ const cos = Math.cos(-rotation);
2215
+ const sin = Math.sin(-rotation);
2216
+ const dx = point.x - x;
2217
+ const dy = point.y - y;
2218
+ const localX = dx * cos - dy * sin;
2219
+ const localY = dx * sin + dy * cos;
2220
+ return localX * localX / (radiusX * radiusX) + localY * localY / (radiusY * radiusY) <= 1;
2221
+ }
2222
+ /**
2223
+ * Check if point is inside a polygon
2224
+ * @param point - Point to check
2225
+ * @param vertices - Array of polygon vertices
2226
+ * @returns True if point is inside the polygon
2227
+ */
2228
+ polygon(point, vertices) {
2229
+ let inside = false;
2230
+ const n = vertices.length;
2231
+ for (let i = 0, j = n - 1; i < n; j = i++) {
2232
+ const xi = vertices[i].x, yi = vertices[i].y;
2233
+ const xj = vertices[j].x, yj = vertices[j].y;
2234
+ const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
2235
+ if (intersect) inside = !inside;
2236
+ }
2237
+ return inside;
2238
+ }
2239
+ /**
2240
+ * Check if point is inside a Path2D
2241
+ * @param point - Point to check
2242
+ * @param path - Path2D object
2243
+ * @returns True if point is inside the path
2244
+ */
2245
+ path(point, path) {
2246
+ return this.context.isPointInPath(path, point.x, point.y);
2247
+ }
2248
+ };
2249
+ var Hotspot_default = Hotspot;
2250
+
2251
+ // src/elements/Performance.tsx
2252
+ var Performance = class {
2253
+ constructor(ctx) {
2254
+ this.textMetricsCache = /* @__PURE__ */ new Map();
2255
+ this.frameTimeHistory = [];
2256
+ this.MAX_HISTORY = 60;
2257
+ this.context = ctx;
2258
+ }
2259
+ /**
2260
+ * Batch multiple canvas operations together for better performance
2261
+ */
2262
+ batchDraw(drawFn) {
2263
+ this.context.save();
2264
+ this.context.beginPath();
2265
+ try {
2266
+ drawFn();
2267
+ } finally {
2268
+ this.context.restore();
2269
+ }
2270
+ }
2271
+ /**
2272
+ * Optimize drawing by using offscreen canvas for static elements
2273
+ */
2274
+ useOffscreenCache(id, width, height, renderFn) {
2275
+ let offscreen = this.context.__offscreens.get(id);
2276
+ if (!offscreen || offscreen instanceof HTMLImageElement) {
2277
+ offscreen = this.context.createOffscreen(id, width, height);
2278
+ if (offscreen && !(offscreen instanceof HTMLImageElement)) {
2279
+ renderFn(offscreen);
2280
+ }
2281
+ }
2282
+ if (offscreen) {
2283
+ if (offscreen instanceof HTMLImageElement) {
2284
+ this.context.image(offscreen, 0, 0);
2285
+ } else {
2286
+ this.context.image(offscreen.canvas, 0, 0);
2287
+ }
2288
+ }
2289
+ }
2290
+ /**
2291
+ * Throttle expensive operations to run less frequently
2292
+ */
2293
+ throttleFrame(interval, fn) {
2294
+ const cacheKey = `__throttle_${fn.toString().slice(0, 50)}`;
2295
+ const cached = this.context[cacheKey];
2296
+ if (!cached || this.context.frame % interval === 0) {
2297
+ const result = fn();
2298
+ this.context[cacheKey] = { value: result, frame: this.context.frame };
2299
+ return result;
2300
+ }
2301
+ return cached.value;
2302
+ }
2303
+ /**
2304
+ * Detect potential memory leaks by tracking object creation
2305
+ */
2306
+ useLeakDetection() {
2307
+ const objectCounts = /* @__PURE__ */ new Map();
2308
+ let lastCheckFrame = 0;
2309
+ return {
2310
+ track: (type) => {
2311
+ const count = objectCounts.get(type) || 0;
2312
+ objectCounts.set(type, count + 1);
2313
+ },
2314
+ check: () => {
2315
+ if (this.context.frame - lastCheckFrame < 60) return;
2316
+ lastCheckFrame = this.context.frame;
2317
+ const warnings = [];
2318
+ objectCounts.forEach((count, type) => {
2319
+ if (count > 1e4) {
2320
+ warnings.push(
2321
+ `Potential memory leak: ${type} has ${count} instances`
2322
+ );
2323
+ }
2324
+ });
2325
+ if (warnings.length > 0) {
2326
+ console.warn("[Klint] Memory leak warnings:", warnings);
2327
+ }
2328
+ objectCounts.clear();
2329
+ }
2330
+ };
2331
+ }
2332
+ /**
2333
+ * Optimize text rendering by caching text measurements
2334
+ */
2335
+ getCachedTextMetrics(text, font) {
2336
+ const cacheKey = `${font}:${text}`;
2337
+ if (!this.textMetricsCache.has(cacheKey)) {
2338
+ this.context.save();
2339
+ this.context.font = font;
2340
+ const metrics = this.context.measureText(text);
2341
+ this.context.restore();
2342
+ this.textMetricsCache.set(cacheKey, metrics);
2343
+ if (this.textMetricsCache.size > 1e3) {
2344
+ const firstKey = this.textMetricsCache.keys().next().value;
2345
+ if (firstKey) this.textMetricsCache.delete(firstKey);
2346
+ }
2347
+ }
2348
+ return this.textMetricsCache.get(cacheKey);
2349
+ }
2350
+ /**
2351
+ * Clear all performance caches
2352
+ */
2353
+ clearCaches() {
2354
+ this.textMetricsCache.clear();
2355
+ }
2356
+ /**
2357
+ * Get current performance metrics
2358
+ */
2359
+ getMetrics() {
2360
+ return this.context.__performance || null;
2361
+ }
2362
+ /**
2363
+ * Render performance widget on canvas
2364
+ */
2365
+ render(options = {}) {
2366
+ const metrics = this.getMetrics();
2367
+ if (!metrics) return;
2368
+ const {
2369
+ x = 10,
2370
+ y = 10,
2371
+ width = 200,
2372
+ height = 120,
2373
+ backgroundColor = "rgba(0, 0, 0, 0.8)",
2374
+ textColor = "#ffffff",
2375
+ accentColor = "#4ecdc4",
2376
+ showGraph = true,
2377
+ graphHeight = 40,
2378
+ fontSize = 12,
2379
+ showMemory = true
2380
+ } = options;
2381
+ if (showGraph) {
2382
+ this.frameTimeHistory.push(metrics.frameTime);
2383
+ if (this.frameTimeHistory.length > this.MAX_HISTORY) {
2384
+ this.frameTimeHistory.shift();
2385
+ }
2386
+ }
2387
+ this.context.save();
2388
+ this.context.fillStyle = backgroundColor;
2389
+ this.context.fillRect(x, y, width, height);
2390
+ this.context.strokeStyle = accentColor;
2391
+ this.context.lineWidth = 1;
2392
+ this.context.strokeRect(x, y, width, height);
2393
+ this.context.fillStyle = textColor;
2394
+ this.context.font = `${fontSize}px monospace`;
2395
+ this.context.textAlign = "left";
2396
+ this.context.textBaseline = "top";
2397
+ let currentY = y + 8;
2398
+ const fpsColor = metrics.fps >= 55 ? "#4ecdc4" : metrics.fps >= 30 ? "#ffd93d" : "#ff6b6b";
2399
+ this.context.fillStyle = fpsColor;
2400
+ this.context.fillText(`FPS: ${metrics.fps}`, x + 8, currentY);
2401
+ currentY += fontSize + 4;
2402
+ this.context.fillStyle = textColor;
2403
+ this.context.fillText(`Frame: ${metrics.frameTime.toFixed(2)}ms`, x + 8, currentY);
2404
+ currentY += fontSize + 4;
2405
+ this.context.fillText(`Avg: ${metrics.averageFrameTime.toFixed(2)}ms`, x + 8, currentY);
2406
+ currentY += fontSize + 4;
2407
+ this.context.fillText(
2408
+ `Min: ${metrics.minFrameTime.toFixed(2)}ms / Max: ${metrics.maxFrameTime.toFixed(2)}ms`,
2409
+ x + 8,
2410
+ currentY
2411
+ );
2412
+ currentY += fontSize + 4;
2413
+ if (metrics.droppedFrames > 0) {
2414
+ this.context.fillStyle = "#ff6b6b";
2415
+ this.context.fillText(`Dropped: ${metrics.droppedFrames}`, x + 8, currentY);
2416
+ currentY += fontSize + 4;
2417
+ }
2418
+ if (showMemory && metrics.memoryUsage !== void 0) {
2419
+ this.context.fillStyle = textColor;
2420
+ this.context.fillText(`Memory: ${metrics.memoryUsage.toFixed(2)}MB`, x + 8, currentY);
2421
+ currentY += fontSize + 4;
2422
+ }
2423
+ if (showGraph && this.frameTimeHistory.length > 1) {
2424
+ const graphX = x + 8;
2425
+ const graphY = currentY + 4;
2426
+ const graphWidth = width - 16;
2427
+ const targetFrameTime = 1e3 / this.context.fps;
2428
+ this.context.fillStyle = "rgba(255, 255, 255, 0.1)";
2429
+ this.context.fillRect(graphX, graphY, graphWidth, graphHeight);
2430
+ const targetY = graphY + graphHeight - targetFrameTime / (targetFrameTime * 2) * graphHeight;
2431
+ this.context.strokeStyle = "rgba(255, 255, 255, 0.3)";
2432
+ this.context.lineWidth = 1;
2433
+ this.context.beginPath();
2434
+ this.context.moveTo(graphX, targetY);
2435
+ this.context.lineTo(graphX + graphWidth, targetY);
2436
+ this.context.stroke();
2437
+ this.context.strokeStyle = accentColor;
2438
+ this.context.lineWidth = 2;
2439
+ this.context.beginPath();
2440
+ const maxFrameTime = Math.max(...this.frameTimeHistory, targetFrameTime * 2);
2441
+ const stepX = graphWidth / (this.frameTimeHistory.length - 1);
2442
+ this.frameTimeHistory.forEach((frameTime, index) => {
2443
+ const normalizedTime = Math.min(frameTime / maxFrameTime, 1);
2444
+ const pointY = graphY + graphHeight - normalizedTime * graphHeight;
2445
+ const pointX = graphX + index * stepX;
2446
+ if (index === 0) {
2447
+ this.context.moveTo(pointX, pointY);
2448
+ } else {
2449
+ this.context.lineTo(pointX, pointY);
2450
+ }
2451
+ });
2452
+ this.context.stroke();
2453
+ }
2454
+ this.context.restore();
2455
+ }
2456
+ /**
2457
+ * Alias for render() - shows performance widget
2458
+ */
2459
+ show(options) {
2460
+ this.render(options);
2461
+ }
2462
+ };
2463
+ var Performance_default = Performance;
2464
+
2465
+ // src/elements/SSR.tsx
2466
+ var SSR = class {
2467
+ constructor(ctx) {
2468
+ this.context = ctx || null;
2469
+ }
2470
+ /**
2471
+ * Render a Klint sketch to a static image (base64 data URL)
2472
+ */
2473
+ async renderToImage(draw, options) {
2474
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
2475
+ return this.renderInBrowser(draw, options);
2476
+ }
2477
+ throw new Error(
2478
+ "Server-side rendering requires 'canvas' package. Install it with: npm install canvas\nAlternatively, use SSR.generateImageUrl() to generate images via an API endpoint."
2479
+ );
2480
+ }
2481
+ /**
2482
+ * Generate a static image URL for a Klint sketch
2483
+ */
2484
+ async generateImageUrl(draw, options) {
2485
+ if (typeof window === "undefined") {
2486
+ throw new Error(
2487
+ "generateImageUrl() requires a browser environment. For server-side rendering, use renderToImage() with the 'canvas' package."
2488
+ );
2489
+ }
2490
+ return this.renderInBrowser(draw, options);
2491
+ }
2492
+ /**
2493
+ * Check if Klint can run in the current environment
2494
+ */
2495
+ canRender() {
2496
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
2497
+ return true;
2498
+ }
2499
+ try {
2500
+ __require("canvas");
2501
+ return true;
2502
+ } catch {
2503
+ return false;
2504
+ }
2505
+ }
2506
+ /**
2507
+ * Render in browser environment (client-side)
2508
+ */
2509
+ async renderInBrowser(draw, options) {
2510
+ return new Promise((resolve, reject) => {
2511
+ try {
2512
+ const canvas = document.createElement("canvas");
2513
+ const dpr = options.dpr || (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
2514
+ canvas.width = options.width * dpr;
2515
+ canvas.height = options.height * dpr;
2516
+ const ctx = canvas.getContext("2d");
2517
+ if (!ctx) {
2518
+ reject(new Error("Failed to get canvas context"));
2519
+ return;
2520
+ }
2521
+ const klintContext = this.createMinimalContext(
2522
+ ctx,
2523
+ canvas,
2524
+ options.width,
2525
+ options.height,
2526
+ dpr
2527
+ );
2528
+ draw(klintContext);
2529
+ const mimeType = options.format === "jpeg" ? "image/jpeg" : options.format === "webp" ? "image/webp" : "image/png";
2530
+ const quality = options.quality !== void 0 ? options.quality : 0.85;
2531
+ const dataUrl = canvas.toDataURL(mimeType, quality);
2532
+ resolve(dataUrl);
2533
+ } catch (error) {
2534
+ reject(error);
2535
+ }
2536
+ });
2537
+ }
2538
+ /**
2539
+ * Create a minimal KlintContext for server rendering
2540
+ * This provides basic functionality without full Klint features
2541
+ */
2542
+ createMinimalContext(ctx, canvas, width, height, dpr) {
2543
+ return {
2544
+ canvas,
2545
+ width: width * dpr,
2546
+ height: height * dpr,
2547
+ // Basic canvas methods
2548
+ fillRect: ctx.fillRect.bind(ctx),
2549
+ strokeRect: ctx.strokeRect.bind(ctx),
2550
+ clearRect: ctx.clearRect.bind(ctx),
2551
+ beginPath: ctx.beginPath.bind(ctx),
2552
+ moveTo: ctx.moveTo.bind(ctx),
2553
+ lineTo: ctx.lineTo.bind(ctx),
2554
+ arc: ctx.arc.bind(ctx),
2555
+ fill: ctx.fill.bind(ctx),
2556
+ stroke: ctx.stroke.bind(ctx),
2557
+ save: ctx.save.bind(ctx),
2558
+ restore: ctx.restore.bind(ctx),
2559
+ translate: ctx.translate.bind(ctx),
2560
+ rotate: ctx.rotate.bind(ctx),
2561
+ scale: ctx.scale.bind(ctx),
2562
+ // Basic properties
2563
+ fillStyle: ctx.fillStyle,
2564
+ strokeStyle: ctx.strokeStyle,
2565
+ lineWidth: ctx.lineWidth,
2566
+ // Minimal Klint-like API
2567
+ background: (color) => {
2568
+ ctx.fillStyle = color || "#000";
2569
+ ctx.fillRect(0, 0, width * dpr, height * dpr);
2570
+ },
2571
+ fillColor: (color) => {
2572
+ ctx.fillStyle = color;
2573
+ },
2574
+ strokeColor: (color) => {
2575
+ ctx.strokeStyle = color;
2576
+ },
2577
+ circle: (x, y, radius) => {
2578
+ ctx.beginPath();
2579
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
2580
+ ctx.fill();
2581
+ },
2582
+ rectangle: (x, y, w, h) => {
2583
+ ctx.fillRect(x, y, w, h || w);
2584
+ },
2585
+ // Time properties (static for server rendering)
2586
+ frame: 0,
2587
+ time: 0,
2588
+ deltaTime: 0,
2589
+ fps: 60
2590
+ };
2591
+ }
2592
+ };
2593
+ var SSR_default = SSR;
2594
+
2595
+ // src/KlintFunctions.tsx
2596
+ var KlintCoreFunctions = {
2597
+ saveCanvas: (ctx) => () => {
2598
+ const link = document.createElement("a");
2599
+ link.download = "canvas.png";
2600
+ link.href = ctx.canvas.toDataURL();
2601
+ link.click();
2602
+ },
2603
+ fullscreen: (ctx) => () => {
2604
+ ctx.canvas.requestFullscreen?.();
2605
+ },
2606
+ play: (ctx) => () => {
2607
+ if (!ctx.__isPlaying) ctx.__isPlaying = true;
2608
+ },
2609
+ pause: (ctx) => () => {
2610
+ if (ctx.__isPlaying) ctx.__isPlaying = false;
2611
+ },
2612
+ // to do
2613
+ redraw: () => () => {
2614
+ },
2615
+ extend: (ctx) => (name, data, enforceReplace = false) => {
2616
+ if (name in ctx && !enforceReplace) return;
2617
+ ctx[name] = data;
2618
+ },
2619
+ passImage: () => (element) => {
2620
+ if (!element.complete) {
2621
+ console.warn("Image passed to passImage() is not fully loaded");
2622
+ return null;
2623
+ }
2624
+ return element;
2625
+ },
2626
+ passImages: () => (elements) => {
2627
+ return elements.map((element) => {
2628
+ if (!element.complete) {
2629
+ console.warn("Image passed to passImages() is not fully loaded");
2630
+ return null;
2631
+ }
2632
+ return element;
2633
+ });
2634
+ },
2635
+ saveConfig: (ctx) => (from) => {
2636
+ return Object.fromEntries(
2637
+ CONFIG_PROPS.map((key) => [
2638
+ key,
2639
+ from?.[key] ?? ctx[key]
2640
+ ])
2641
+ );
2642
+ },
2643
+ restoreConfig: (ctx) => (config) => {
2644
+ Object.assign(ctx, config);
2645
+ },
2646
+ describe: (ctx) => (description) => {
2647
+ ctx.__description = description;
2648
+ },
2649
+ createOffscreen: (ctx) => (id, width, height, options, callback) => {
2650
+ const offscreen = document.createElement("canvas");
2651
+ offscreen.width = width * ctx.__dpr;
2652
+ offscreen.height = height * ctx.__dpr;
2653
+ const context = offscreen.getContext("2d", {
2654
+ alpha: options?.alpha ?? true,
2655
+ willReadFrequently: options?.willreadfrequently ?? false
2656
+ });
2657
+ if (!context) throw new Error("Failed to create offscreen context");
2658
+ context.__dpr = ctx.__dpr;
2659
+ context.width = width * ctx.__dpr;
2660
+ context.height = height * ctx.__dpr;
2661
+ context.__isMainContext = false;
2662
+ context.__imageOrigin = "corner";
2663
+ context.__rectangleOrigin = "corner";
2664
+ context.__canvasOrigin = "corner";
2665
+ context.__textFont = "sans-serif";
2666
+ context.__textWeight = "normal";
2667
+ context.__textStyle = "normal";
2668
+ context.__textSize = 120;
2669
+ context.__textLeading = void 0;
2670
+ context.__textAlignment = {
2671
+ horizontal: "left",
2672
+ vertical: "top"
2673
+ };
2674
+ context.__fillRule = "nonzero";
2675
+ if (!options?.ignoreFunctions) {
2676
+ context.Color = ctx.Color;
2677
+ context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
2678
+ context.Easing = ctx.Easing;
2679
+ context.Text = ctx.Text;
2680
+ Object.entries(KlintFunctions).forEach(([name, fn]) => {
2681
+ context[name] = fn(context);
2682
+ });
2683
+ }
2684
+ if (options?.origin) {
2685
+ context.__canvasOrigin = options.origin;
2686
+ if (options.origin === "center") {
2687
+ context.translate(context.width * 0.5, context.height * 0.5);
2688
+ }
2689
+ }
2690
+ if (callback) {
2691
+ callback(context);
2692
+ }
2693
+ if (options?.static === "true") {
2694
+ const base64 = offscreen.toDataURL();
2695
+ const img = new Image();
2696
+ img.src = base64;
2697
+ ctx.__offscreens.set(id, img);
2698
+ return img;
2699
+ }
2700
+ ctx.__offscreens.set(id, context);
2701
+ return context;
2702
+ },
2703
+ getOffscreen: (ctx) => (id) => {
2704
+ const offscreen = ctx.__offscreens.get(id);
2705
+ if (!offscreen)
2706
+ throw new Error(`No offscreen context found with id: ${id}`);
2707
+ return offscreen;
2708
+ }
2709
+ };
2710
+ var KlintFunctions = {
2711
+ extend: (ctx) => (name, data, enforceReplace = false) => {
2712
+ if (name in ctx && !enforceReplace) return;
2713
+ ctx[name] = data;
2714
+ },
2715
+ background: (ctx) => (color) => {
2716
+ ctx.resetTransform();
2717
+ ctx.push();
2718
+ if (color && color !== "transparent") {
2719
+ ctx.fillStyle = color;
2720
+ ctx.fillRect(0, 0, ctx.width, ctx.height);
2721
+ } else {
2722
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
2723
+ }
2724
+ ctx.pop();
2725
+ if (ctx.__canvasOrigin === "center")
2726
+ ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
2727
+ },
2728
+ reset: (ctx) => () => {
1278
2729
  ctx.clearRect(0, 0, ctx.width, ctx.height);
1279
2730
  ctx.resetTransform();
1280
2731
  },
@@ -1322,9 +2773,12 @@ var KlintFunctions = {
1322
2773
  return true;
1323
2774
  },
1324
2775
  drawIfVisible: (ctx) => () => {
1325
- if (ctx.checkTransparency("fill")) ctx.fill();
2776
+ if (ctx.checkTransparency("fill")) ctx.fill(ctx.__fillRule || "nonzero");
1326
2777
  if (ctx.checkTransparency("stroke")) ctx.stroke();
1327
2778
  },
2779
+ fillRule: (ctx) => (rule) => {
2780
+ ctx.__fillRule = rule;
2781
+ },
1328
2782
  line: (ctx) => (x1, y1, x2, y2) => {
1329
2783
  if (!ctx.checkTransparency("stroke")) return;
1330
2784
  ctx.beginPath();
@@ -1386,17 +2840,29 @@ var KlintFunctions = {
1386
2840
  ctx.__currentContours = [];
1387
2841
  },
1388
2842
  beginContour: (ctx) => () => {
1389
- if (!ctx.__startedShape) return;
1390
- if (ctx.__startedContour && ctx.__currentContour?.length) {
1391
- ctx.__currentContours?.push([...ctx.__currentContour]);
1392
- }
2843
+ if (!ctx.__startedShape || ctx.__startedContour) return;
1393
2844
  ctx.__startedContour = true;
1394
2845
  ctx.__currentContour = [];
1395
2846
  },
1396
2847
  vertex: (ctx) => (x, y) => {
1397
2848
  if (!ctx.__startedShape) return;
1398
2849
  const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
1399
- points?.push([x, y]);
2850
+ points?.push({ type: "line", x, y });
2851
+ },
2852
+ bezierVertex: (ctx) => (cp1x, cp1y, cp2x, cp2y, x, y) => {
2853
+ if (!ctx.__startedShape) return;
2854
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2855
+ points?.push({ type: "bezier", cp1x, cp1y, cp2x, cp2y, x, y });
2856
+ },
2857
+ quadraticVertex: (ctx) => (cpx, cpy, x, y) => {
2858
+ if (!ctx.__startedShape) return;
2859
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2860
+ points?.push({ type: "quadratic", cpx, cpy, x, y });
2861
+ },
2862
+ arcVertex: (ctx) => (x1, y1, x2, y2, radius) => {
2863
+ if (!ctx.__startedShape) return;
2864
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2865
+ points?.push({ type: "arc", x1, y1, x2, y2, radius });
1400
2866
  },
1401
2867
  endContour: (ctx) => (forceRevert = true) => {
1402
2868
  if (!ctx.__startedContour || !ctx.__currentContour?.length) return;
@@ -1414,17 +2880,38 @@ var KlintFunctions = {
1414
2880
  const points = ctx.__currentShape;
1415
2881
  if (!points?.length) return;
1416
2882
  const drawPath = (points2, close2 = false) => {
1417
- ctx.moveTo(points2[0][0], points2[0][1]);
1418
- for (let i = 1; i < points2.length; i++) {
1419
- ctx.lineTo(points2[i][0], points2[i][1]);
1420
- }
1421
- if (close2) {
1422
- const [firstX, firstY] = points2[0];
1423
- const lastPoint = points2[points2.length - 1];
1424
- if (lastPoint[0] !== firstX || lastPoint[1] !== firstY) {
1425
- ctx.lineTo(firstX, firstY);
2883
+ if (points2.length === 0) return;
2884
+ const firstPoint = points2[0];
2885
+ const startX = firstPoint.type === "line" ? firstPoint.x : firstPoint.type === "bezier" ? firstPoint.x : firstPoint.type === "quadratic" ? firstPoint.x : firstPoint.x2;
2886
+ const startY = firstPoint.type === "line" ? firstPoint.y : firstPoint.type === "bezier" ? firstPoint.y : firstPoint.type === "quadratic" ? firstPoint.y : firstPoint.y2;
2887
+ ctx.moveTo(startX, startY);
2888
+ for (let i = 0; i < points2.length; i++) {
2889
+ const point = points2[i];
2890
+ switch (point.type) {
2891
+ case "line":
2892
+ if (i > 0) ctx.lineTo(point.x, point.y);
2893
+ break;
2894
+ case "bezier":
2895
+ ctx.bezierCurveTo(
2896
+ point.cp1x,
2897
+ point.cp1y,
2898
+ point.cp2x,
2899
+ point.cp2y,
2900
+ point.x,
2901
+ point.y
2902
+ );
2903
+ break;
2904
+ case "quadratic":
2905
+ ctx.quadraticCurveTo(point.cpx, point.cpy, point.x, point.y);
2906
+ break;
2907
+ case "arc":
2908
+ ctx.arcTo(point.x1, point.y1, point.x2, point.y2, point.radius);
2909
+ break;
1426
2910
  }
1427
2911
  }
2912
+ if (close2 && points2.length > 1) {
2913
+ ctx.closePath();
2914
+ }
1428
2915
  };
1429
2916
  ctx.beginPath();
1430
2917
  drawPath(points, close);
@@ -1448,6 +2935,9 @@ var KlintFunctions = {
1448
2935
  addColorStop: () => (gradient, offset = 0, color = "#000") => {
1449
2936
  return gradient.addColorStop(offset, color);
1450
2937
  },
2938
+ PI: () => Math.PI,
2939
+ TWO_PI: () => Math.PI * 2,
2940
+ TAU: () => Math.PI * 2,
1451
2941
  constrain: () => (val, floor, ceil) => {
1452
2942
  return Math.max(floor, Math.min(val, ceil));
1453
2943
  },
@@ -1485,6 +2975,14 @@ var KlintFunctions = {
1485
2975
  const t = (n - A) / (B - A);
1486
2976
  return ctx.lerp(C, D, t, bounded);
1487
2977
  },
2978
+ bezierLerp: () => (a, b, c, d, t) => {
2979
+ const u = 1 - t;
2980
+ return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
2981
+ },
2982
+ bezierTangent: () => (a, b, c, d, t) => {
2983
+ const u = 1 - t;
2984
+ return 3 * d * t * t - 3 * c * t * t + 6 * c * u * t - 6 * b * u * t + 3 * b * u * u - 3 * a * u * u;
2985
+ },
1488
2986
  textFont: (ctx) => (font) => {
1489
2987
  ctx.__textFont = font;
1490
2988
  },
@@ -1520,7 +3018,8 @@ var KlintFunctions = {
1520
3018
  ctx.__textAlignment.vertical = vertical ?? ctx.__textAlignment.vertical;
1521
3019
  },
1522
3020
  textLeading: (ctx) => (spacing) => {
1523
- ctx.lineHeight = `${spacing}px`;
3021
+ ctx.__textLeading = spacing;
3022
+ return ctx.__textLeading;
1524
3023
  },
1525
3024
  computeFont: (ctx) => () => {
1526
3025
  ctx.computeTextStyle();
@@ -1539,10 +3038,110 @@ var KlintFunctions = {
1539
3038
  if (ctx.textBaseline !== ctx.__textAlignment.vertical) {
1540
3039
  ctx.textBaseline = ctx.__textAlignment.vertical;
1541
3040
  }
1542
- if (ctx.checkTransparency("fill"))
1543
- ctx.fillText(String(text), x, y, maxWidth);
1544
- if (ctx.checkTransparency("stroke"))
1545
- ctx.strokeText(String(text), x, y, maxWidth);
3041
+ const textString = String(text);
3042
+ if (textString.includes("\n")) {
3043
+ const lines = textString.split("\n");
3044
+ const firstLineMetrics = ctx.measureText(lines[0] || "M");
3045
+ const defaultLineHeight = firstLineMetrics.actualBoundingBoxAscent + firstLineMetrics.actualBoundingBoxDescent;
3046
+ const lineHeight = ctx.__textLeading ?? ctx.__textSize * 1.2;
3047
+ const totalHeight = lines.length * lineHeight - (lineHeight - defaultLineHeight);
3048
+ let startY = y;
3049
+ if (ctx.__textAlignment.vertical === "middle") {
3050
+ startY = y - totalHeight / 2 + defaultLineHeight / 2;
3051
+ } else if (ctx.__textAlignment.vertical === "bottom") {
3052
+ startY = y - totalHeight + defaultLineHeight;
3053
+ } else if (ctx.__textAlignment.vertical === "top") {
3054
+ startY = y + defaultLineHeight / 2;
3055
+ }
3056
+ lines.forEach((line, index) => {
3057
+ const lineY = startY + index * lineHeight;
3058
+ if (ctx.checkTransparency("fill"))
3059
+ ctx.fillText(line, x, lineY, maxWidth);
3060
+ if (ctx.checkTransparency("stroke"))
3061
+ ctx.strokeText(line, x, lineY, maxWidth);
3062
+ });
3063
+ } else {
3064
+ if (ctx.checkTransparency("fill"))
3065
+ ctx.fillText(textString, x, y, maxWidth);
3066
+ if (ctx.checkTransparency("stroke"))
3067
+ ctx.strokeText(textString, x, y, maxWidth);
3068
+ }
3069
+ },
3070
+ paragraph: (ctx) => (text, x, y, width, options) => {
3071
+ if (text === void 0) return;
3072
+ ctx.computeFont();
3073
+ const textString = String(text);
3074
+ const justification = options?.justification || "left";
3075
+ const overflow = options?.overflow || 0;
3076
+ const breakMode = options?.break || "words";
3077
+ const originalAlign = ctx.textAlign;
3078
+ const originalBaseline = ctx.textBaseline;
3079
+ if (justification === "center") {
3080
+ ctx.textAlign = "center";
3081
+ } else if (justification === "right") {
3082
+ ctx.textAlign = "right";
3083
+ } else {
3084
+ ctx.textAlign = "left";
3085
+ }
3086
+ ctx.textBaseline = "top";
3087
+ const tokens = breakMode === "letters" ? textString.split("") : textString.split(/\s+/);
3088
+ const lines = [];
3089
+ let currentLine = "";
3090
+ for (let i = 0; i < tokens.length; i++) {
3091
+ const token = tokens[i];
3092
+ const testLine = currentLine ? breakMode === "letters" ? currentLine + token : currentLine + " " + token : token;
3093
+ const metrics = ctx.measureText(testLine);
3094
+ if (metrics.width > width && currentLine !== "") {
3095
+ lines.push(currentLine);
3096
+ currentLine = token;
3097
+ } else {
3098
+ currentLine = testLine;
3099
+ }
3100
+ }
3101
+ if (currentLine) {
3102
+ lines.push(currentLine);
3103
+ }
3104
+ const lineHeight = ctx.__textLeading ?? ctx.__textSize * 1.2;
3105
+ let linesToDraw = lines;
3106
+ if (overflow > 0 && lines.length * lineHeight > overflow) {
3107
+ const maxLines = Math.floor(overflow / lineHeight);
3108
+ linesToDraw = lines.slice(0, maxLines);
3109
+ }
3110
+ linesToDraw.forEach((line, index) => {
3111
+ const lineY = y + index * lineHeight;
3112
+ let lineX = x;
3113
+ if (justification === "center") {
3114
+ lineX = x + width / 2;
3115
+ } else if (justification === "right") {
3116
+ lineX = x + width;
3117
+ } else if (justification === "justified" && index < linesToDraw.length - 1) {
3118
+ const words = line.split(/\s+/);
3119
+ if (words.length > 1) {
3120
+ const textWidth = ctx.measureText(words.join("")).width;
3121
+ const totalSpaceWidth = width - textWidth;
3122
+ const spaceWidth = totalSpaceWidth / (words.length - 1);
3123
+ let currentX = x;
3124
+ words.forEach((word, wordIndex) => {
3125
+ if (ctx.checkTransparency("fill")) {
3126
+ ctx.fillText(word, currentX, lineY);
3127
+ }
3128
+ if (ctx.checkTransparency("stroke")) {
3129
+ ctx.strokeText(word, currentX, lineY);
3130
+ }
3131
+ currentX += ctx.measureText(word).width + spaceWidth;
3132
+ });
3133
+ return;
3134
+ }
3135
+ }
3136
+ if (ctx.checkTransparency("fill")) {
3137
+ ctx.fillText(line, lineX, lineY);
3138
+ }
3139
+ if (ctx.checkTransparency("stroke")) {
3140
+ ctx.strokeText(line, lineX, lineY);
3141
+ }
3142
+ });
3143
+ ctx.textAlign = originalAlign;
3144
+ ctx.textBaseline = originalBaseline;
1546
3145
  },
1547
3146
  // DO NOT use putImageData for images you can draw : https://www.measurethat.net/Benchmarks/Show/9510/0/putimagedata-vs-drawimage
1548
3147
  image: (ctx) => (image, x, y, arg3, arg4, arg5, arg6, arg7, arg8) => {
@@ -1592,11 +3191,8 @@ var KlintFunctions = {
1592
3191
  return ctx.getImageData(0, 0, ctx.width, ctx.height);
1593
3192
  },
1594
3193
  updatePixels: (ctx) => (pixels) => {
1595
- const imageData = new ImageData(
1596
- pixels instanceof Uint8ClampedArray ? pixels : new Uint8ClampedArray(pixels),
1597
- ctx.width,
1598
- ctx.height
1599
- );
3194
+ const pixelArray = pixels instanceof Uint8ClampedArray ? new Uint8ClampedArray(pixels) : new Uint8ClampedArray(pixels);
3195
+ const imageData = new ImageData(pixelArray, ctx.width, ctx.height);
1600
3196
  ctx.putImageData(imageData, 0, 0);
1601
3197
  },
1602
3198
  readPixels: (ctx) => (x, y, w = 1, h = 1) => {
@@ -1612,7 +3208,7 @@ var KlintFunctions = {
1612
3208
  ctx.globalAlpha = ctx.constrain(value, 0, 1);
1613
3209
  },
1614
3210
  blend: (ctx) => (blend) => {
1615
- ctx.globalCompositeOperation = blend;
3211
+ ctx.globalCompositeOperation = blend === "default" ? "source-over" : blend;
1616
3212
  },
1617
3213
  setCanvasOrigin: (ctx) => (type) => {
1618
3214
  ctx.__canvasOrigin = type;
@@ -1650,6 +3246,68 @@ var KlintFunctions = {
1650
3246
  if (ctx.__canvasOrigin === "center") {
1651
3247
  ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
1652
3248
  }
3249
+ },
3250
+ clipTo: (ctx) => (callback, fillRule) => {
3251
+ const originalFill = ctx.fill;
3252
+ const originalStroke = ctx.stroke;
3253
+ const originalDrawIfVisible = ctx.drawIfVisible;
3254
+ const originalBeginPath = ctx.beginPath;
3255
+ ctx.fill = () => {
3256
+ };
3257
+ ctx.stroke = () => {
3258
+ };
3259
+ ctx.drawIfVisible = () => {
3260
+ };
3261
+ let allowBeginPath = true;
3262
+ ctx.beginPath = () => {
3263
+ if (allowBeginPath) {
3264
+ originalBeginPath.call(ctx);
3265
+ allowBeginPath = false;
3266
+ }
3267
+ };
3268
+ ctx.beginPath();
3269
+ callback(ctx);
3270
+ ctx.clip(fillRule || ctx.__fillRule || "nonzero");
3271
+ ctx.beginPath = originalBeginPath;
3272
+ ctx.drawIfVisible = originalDrawIfVisible;
3273
+ ctx.fill = originalFill;
3274
+ ctx.stroke = originalStroke;
3275
+ },
3276
+ canIuseFilter: (ctx) => () => {
3277
+ return ctx.filter !== void 0 && ctx.filter !== null;
3278
+ },
3279
+ blur: (ctx) => (radius) => {
3280
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3281
+ ctx.filter = `blur(${radius}px)`;
3282
+ },
3283
+ SVGfilter: (ctx) => (url) => {
3284
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3285
+ if (!url || !url.startsWith("url(")) return;
3286
+ ctx.filter = url;
3287
+ },
3288
+ dropShadow: (ctx) => (offsetX, offsetY, blurRadius, color) => {
3289
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3290
+ ctx.filter = `drop-shadow(${offsetX}px ${offsetY}px ${blurRadius}px ${color})`;
3291
+ },
3292
+ grayscale: (ctx) => (amount) => {
3293
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3294
+ const value = ctx.constrain(amount, 0, 1);
3295
+ ctx.filter = `grayscale(${value})`;
3296
+ },
3297
+ hue: (ctx) => (angle) => {
3298
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3299
+ const degrees = angle * 180 / Math.PI;
3300
+ ctx.filter = `hue-rotate(${degrees}deg)`;
3301
+ },
3302
+ invert: (ctx) => (amount) => {
3303
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3304
+ const value = ctx.constrain(amount, 0, 1);
3305
+ ctx.filter = `invert(${value})`;
3306
+ },
3307
+ filterOpacity: (ctx) => (value) => {
3308
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3309
+ const amount = ctx.constrain(value, 0, 1);
3310
+ ctx.filter = `opacity(${amount})`;
1653
3311
  }
1654
3312
  };
1655
3313
 
@@ -1687,13 +3345,50 @@ var DEFAULT_GESTURE_STATE = {
1687
3345
  lastX: 0,
1688
3346
  lastY: 0
1689
3347
  };
3348
+ var DEFAULT_KEYBOARD_STATE = {
3349
+ pressedKeys: /* @__PURE__ */ new Set(),
3350
+ modifiers: {
3351
+ alt: false,
3352
+ shift: false,
3353
+ ctrl: false,
3354
+ meta: false
3355
+ },
3356
+ lastKey: null,
3357
+ lastKeyTime: 0
3358
+ };
1690
3359
  function useKlint() {
1691
3360
  const contextRef = useRef2(null);
1692
3361
  const mouseRef = useRef2(null);
1693
3362
  const scrollRef = useRef2(null);
1694
3363
  const gestureRef = useRef2(null);
3364
+ const keyboardRef = useRef2(null);
1695
3365
  const useDev = () => {
1696
- return;
3366
+ useEffect2(() => {
3367
+ if (process.env.NODE_ENV === "development") {
3368
+ if (typeof import.meta !== "undefined" && import.meta.hot) {
3369
+ console.log("[Klint] hot updated - clearing non-context state");
3370
+ if (contextRef.current) {
3371
+ const ctx = contextRef.current;
3372
+ ctx.frame = 0;
3373
+ ctx.time = 0;
3374
+ ctx.__offscreens?.clear();
3375
+ ctx.__startedShape = false;
3376
+ ctx.__currentShape = null;
3377
+ ctx.__startedContour = false;
3378
+ ctx.__currentContours = null;
3379
+ ctx.__currentContour = null;
3380
+ ctx.__isReadyToDraw = true;
3381
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
3382
+ }
3383
+ }
3384
+ }
3385
+ }, []);
3386
+ useEffect2(() => {
3387
+ if (process.env.NODE_ENV === "development" && contextRef.current) {
3388
+ console.log("[Klint] hot updated - clearing context state");
3389
+ contextRef.current.__isReadyToDraw = true;
3390
+ }
3391
+ });
1697
3392
  };
1698
3393
  const KlintImage = () => {
1699
3394
  const imagesRef = useRef2(/* @__PURE__ */ new Map());
@@ -1767,6 +3462,8 @@ function useKlint() {
1767
3462
  if (!contextRef.current?.canvas) return;
1768
3463
  const canvas = contextRef.current.canvas;
1769
3464
  const ctx = contextRef.current;
3465
+ const controller = new AbortController();
3466
+ const { signal } = controller;
1770
3467
  const updateMousePosition = (e) => {
1771
3468
  const rect = canvas.getBoundingClientRect();
1772
3469
  const dpr = window.devicePixelRatio || 1;
@@ -1805,20 +3502,13 @@ function useKlint() {
1805
3502
  const handleClick = (e) => {
1806
3503
  if (clickCallbackRef.current) clickCallbackRef.current(ctx, e);
1807
3504
  };
1808
- canvas.addEventListener("mousemove", updateMousePosition);
1809
- canvas.addEventListener("mousedown", handleMouseDown);
1810
- canvas.addEventListener("mouseup", handleMouseUp);
1811
- canvas.addEventListener("mouseenter", handleMouseEnter);
1812
- canvas.addEventListener("mouseleave", handleMouseLeave);
1813
- canvas.addEventListener("click", handleClick);
1814
- return () => {
1815
- canvas.removeEventListener("mousemove", updateMousePosition);
1816
- canvas.removeEventListener("mousedown", handleMouseDown);
1817
- canvas.removeEventListener("mouseup", handleMouseUp);
1818
- canvas.removeEventListener("mouseenter", handleMouseEnter);
1819
- canvas.removeEventListener("mouseleave", handleMouseLeave);
1820
- canvas.removeEventListener("click", handleClick);
1821
- };
3505
+ canvas.addEventListener("mousemove", updateMousePosition, { signal });
3506
+ canvas.addEventListener("mousedown", handleMouseDown, { signal });
3507
+ canvas.addEventListener("mouseup", handleMouseUp, { signal });
3508
+ canvas.addEventListener("mouseenter", handleMouseEnter, { signal });
3509
+ canvas.addEventListener("mouseleave", handleMouseLeave, { signal });
3510
+ canvas.addEventListener("click", handleClick, { signal });
3511
+ return () => controller.abort();
1822
3512
  });
1823
3513
  return {
1824
3514
  mouse: mouseRef.current,
@@ -1838,6 +3528,8 @@ function useKlint() {
1838
3528
  if (!contextRef.current?.canvas) return;
1839
3529
  const canvas = contextRef.current.canvas;
1840
3530
  const ctx = contextRef.current;
3531
+ const controller = new AbortController();
3532
+ const { signal } = controller;
1841
3533
  const handleScroll = (e) => {
1842
3534
  e.preventDefault();
1843
3535
  if (!scrollRef.current) return;
@@ -1850,8 +3542,8 @@ function useKlint() {
1850
3542
  scrollCallbackRef.current(ctx, scrollRef.current, e);
1851
3543
  }
1852
3544
  };
1853
- canvas.addEventListener("wheel", handleScroll);
1854
- return () => canvas.removeEventListener("wheel", handleScroll);
3545
+ canvas.addEventListener("wheel", handleScroll, { signal });
3546
+ return () => controller.abort();
1855
3547
  });
1856
3548
  return {
1857
3549
  scroll: scrollRef.current,
@@ -1997,18 +3689,19 @@ function useKlint() {
1997
3689
  touchEndCallbackRef.current(ctx, e, gestureRef.current);
1998
3690
  }
1999
3691
  };
3692
+ const controller = new AbortController();
3693
+ const { signal } = controller;
2000
3694
  canvas.addEventListener("touchstart", handleTouchStart, {
2001
- passive: false
3695
+ passive: false,
3696
+ signal
2002
3697
  });
2003
- canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
2004
- canvas.addEventListener("touchend", handleTouchEnd);
2005
- canvas.addEventListener("touchcancel", handleTouchCancel);
2006
- return () => {
2007
- canvas.removeEventListener("touchstart", handleTouchStart);
2008
- canvas.removeEventListener("touchmove", handleTouchMove);
2009
- canvas.removeEventListener("touchend", handleTouchEnd);
2010
- canvas.removeEventListener("touchcancel", handleTouchCancel);
2011
- };
3698
+ canvas.addEventListener("touchmove", handleTouchMove, {
3699
+ passive: false,
3700
+ signal
3701
+ });
3702
+ canvas.addEventListener("touchend", handleTouchEnd, { signal });
3703
+ canvas.addEventListener("touchcancel", handleTouchCancel, { signal });
3704
+ return () => controller.abort();
2012
3705
  }, []);
2013
3706
  return {
2014
3707
  gesture: gestureRef.current,
@@ -2021,6 +3714,315 @@ function useKlint() {
2021
3714
  onTouchEnd: (callback) => touchEndCallbackRef.current = callback
2022
3715
  };
2023
3716
  };
3717
+ const KlintKeyboard = () => {
3718
+ if (!keyboardRef.current) {
3719
+ keyboardRef.current = { ...DEFAULT_KEYBOARD_STATE };
3720
+ }
3721
+ const keyPressedCallbackRef = useRef2(/* @__PURE__ */ new Map());
3722
+ const keyReleasedCallbackRef = useRef2(/* @__PURE__ */ new Map());
3723
+ const keyComboCallbackRef = useRef2(/* @__PURE__ */ new Map());
3724
+ useEffect2(() => {
3725
+ if (!contextRef.current) return;
3726
+ const ctx = contextRef.current;
3727
+ const controller = new AbortController();
3728
+ const { signal } = controller;
3729
+ const updateModifiers = (e) => {
3730
+ if (!keyboardRef.current) return;
3731
+ keyboardRef.current.modifiers.alt = e.altKey;
3732
+ keyboardRef.current.modifiers.shift = e.shiftKey;
3733
+ keyboardRef.current.modifiers.ctrl = e.ctrlKey;
3734
+ keyboardRef.current.modifiers.meta = e.metaKey;
3735
+ };
3736
+ const normalizeKey2 = (key) => {
3737
+ const keyMap = {
3738
+ " ": "Space",
3739
+ Control: "Ctrl",
3740
+ Escape: "Esc"
3741
+ };
3742
+ return keyMap[key] || key;
3743
+ };
3744
+ const handleKeyDown = (e) => {
3745
+ if (!keyboardRef.current) return;
3746
+ const normalizedKey = normalizeKey2(e.key);
3747
+ keyboardRef.current.pressedKeys.add(normalizedKey);
3748
+ keyboardRef.current.lastKey = normalizedKey;
3749
+ keyboardRef.current.lastKeyTime = performance.now();
3750
+ updateModifiers(e);
3751
+ const keyCallback = keyPressedCallbackRef.current.get(normalizedKey);
3752
+ if (keyCallback) {
3753
+ keyCallback(ctx, e);
3754
+ }
3755
+ const pressedKeysArray = Array.from(
3756
+ keyboardRef.current.pressedKeys
3757
+ ).sort();
3758
+ const comboKey = pressedKeysArray.join("+");
3759
+ const comboCallback = keyComboCallbackRef.current.get(comboKey);
3760
+ if (comboCallback) {
3761
+ comboCallback(ctx, e);
3762
+ }
3763
+ };
3764
+ const handleKeyUp = (e) => {
3765
+ if (!keyboardRef.current) return;
3766
+ const normalizedKey = normalizeKey2(e.key);
3767
+ keyboardRef.current.pressedKeys.delete(normalizedKey);
3768
+ updateModifiers(e);
3769
+ const keyCallback = keyReleasedCallbackRef.current.get(normalizedKey);
3770
+ if (keyCallback) {
3771
+ keyCallback(ctx, e);
3772
+ }
3773
+ };
3774
+ window.addEventListener("keydown", handleKeyDown, { signal });
3775
+ window.addEventListener("keyup", handleKeyUp, { signal });
3776
+ return () => controller.abort();
3777
+ }, []);
3778
+ const createComboKey = (keys) => {
3779
+ return keys.map(normalizeKey).sort().join("+");
3780
+ };
3781
+ const normalizeKey = (key) => {
3782
+ const keyMap = {
3783
+ " ": "Space",
3784
+ Control: "Ctrl",
3785
+ Escape: "Esc"
3786
+ };
3787
+ return keyMap[key] || key;
3788
+ };
3789
+ return {
3790
+ keyboard: keyboardRef.current,
3791
+ // Register callback for single key press
3792
+ keyPressed: (key, callback) => {
3793
+ keyPressedCallbackRef.current.set(normalizeKey(key), callback);
3794
+ },
3795
+ // Register callback for single key release
3796
+ keyReleased: (key, callback) => {
3797
+ keyReleasedCallbackRef.current.set(normalizeKey(key), callback);
3798
+ },
3799
+ // Register callback for key combination (e.g., ['Alt', 'Shift'])
3800
+ keyCombo: (keys, callback) => {
3801
+ const comboKey = createComboKey(keys);
3802
+ keyComboCallbackRef.current.set(comboKey, callback);
3803
+ },
3804
+ // Utility functions
3805
+ isPressed: (key) => {
3806
+ return keyboardRef.current?.pressedKeys.has(normalizeKey(key)) || false;
3807
+ },
3808
+ arePressed: (keys) => {
3809
+ if (!keyboardRef.current) return false;
3810
+ return keys.every(
3811
+ (key) => keyboardRef.current.pressedKeys.has(normalizeKey(key))
3812
+ );
3813
+ },
3814
+ // Clear all callbacks
3815
+ clearCallbacks: () => {
3816
+ keyPressedCallbackRef.current.clear();
3817
+ keyReleasedCallbackRef.current.clear();
3818
+ keyComboCallbackRef.current.clear();
3819
+ }
3820
+ };
3821
+ };
3822
+ const KlintTimeline = () => {
3823
+ const timelinesRef = useRef2(/* @__PURE__ */ new Map());
3824
+ const callbacksRef = useRef2({ start: [], end: [], loop: [] });
3825
+ const Timeline = {
3826
+ create: (setup, options = {}) => {
3827
+ const tracks = /* @__PURE__ */ new Map();
3828
+ const parents = /* @__PURE__ */ new Set();
3829
+ let currentProgress = 0;
3830
+ let hasStarted = false;
3831
+ let hasEnded = false;
3832
+ const defaultEasing = options.defaultEasing || ((t) => t);
3833
+ const defaultLoop = options.defaultLoop || 0;
3834
+ const executeCallback = (callback, errorMsg = "Callback error") => {
3835
+ try {
3836
+ callback();
3837
+ } catch (e) {
3838
+ console.warn(`${errorMsg}:`, e);
3839
+ }
3840
+ };
3841
+ function createKeyframes() {
3842
+ const segments = [];
3843
+ let currentPos = 0;
3844
+ let loopCount = defaultLoop;
3845
+ let parentTrack = void 0;
3846
+ const parseEasingCallback = (easing, callback) => {
3847
+ if (typeof easing === "function" && typeof callback === "undefined") {
3848
+ if (easing.length === 0) {
3849
+ return {
3850
+ easing: defaultEasing,
3851
+ callback: easing
3852
+ };
3853
+ }
3854
+ return {
3855
+ easing,
3856
+ callback: void 0
3857
+ };
3858
+ }
3859
+ return {
3860
+ easing: easing || defaultEasing,
3861
+ callback
3862
+ };
3863
+ };
3864
+ const builder = {
3865
+ start(value, delay = 0, callback) {
3866
+ segments.push({ pos: delay, value, callback, type: "start" });
3867
+ currentPos = delay;
3868
+ return builder;
3869
+ },
3870
+ at(progress, value, easing, callback) {
3871
+ const { easing: finalEasing, callback: finalCallback } = parseEasingCallback(easing, callback);
3872
+ segments.push({
3873
+ pos: progress,
3874
+ value,
3875
+ easing: finalEasing,
3876
+ callback: finalCallback,
3877
+ type: "tween"
3878
+ });
3879
+ currentPos = progress;
3880
+ return builder;
3881
+ },
3882
+ then(value, duration, easing, callback) {
3883
+ const { easing: finalEasing, callback: finalCallback } = parseEasingCallback(easing, callback);
3884
+ const nextPos = currentPos + duration;
3885
+ segments.push({
3886
+ pos: nextPos,
3887
+ value,
3888
+ easing: finalEasing,
3889
+ callback: finalCallback,
3890
+ type: "tween"
3891
+ });
3892
+ currentPos = nextPos;
3893
+ return builder;
3894
+ },
3895
+ loop(count = Infinity) {
3896
+ loopCount = count;
3897
+ return builder;
3898
+ },
3899
+ _compile: () => {
3900
+ segments.sort((a, b) => a.pos - b.pos);
3901
+ return { segments, loopCount, parentTrack };
3902
+ }
3903
+ };
3904
+ return builder;
3905
+ }
3906
+ function interpolateTrack(compiled, progress) {
3907
+ const { segments } = compiled;
3908
+ if (!segments.length) return 0;
3909
+ progress = Math.max(0, Math.min(1, progress));
3910
+ const valueSegments = segments.filter(
3911
+ (seg) => seg.type !== "callback"
3912
+ );
3913
+ if (!valueSegments.length) return 0;
3914
+ let prevSeg = valueSegments[0];
3915
+ for (let i = 1; i < valueSegments.length; i++) {
3916
+ const seg = valueSegments[i];
3917
+ if (progress <= seg.pos) {
3918
+ if (seg.type === "tween" && prevSeg.value !== void 0 && seg.value !== void 0) {
3919
+ const t = (progress - prevSeg.pos) / (seg.pos - prevSeg.pos);
3920
+ const easedT = seg.easing ? seg.easing(t) : t;
3921
+ return prevSeg.value + (seg.value - prevSeg.value) * easedT;
3922
+ }
3923
+ return seg.value || 0;
3924
+ }
3925
+ if (seg.value !== void 0) {
3926
+ prevSeg = seg;
3927
+ }
3928
+ }
3929
+ return prevSeg.value || 0;
3930
+ }
3931
+ const timeline = {
3932
+ track: (keyframesOrFn) => {
3933
+ const compiled = typeof keyframesOrFn === "function" ? timeline.keyframes(keyframesOrFn) : keyframesOrFn;
3934
+ const track = {
3935
+ ...compiled,
3936
+ currentValue: 0,
3937
+ getValue: (progress) => interpolateTrack(compiled, progress),
3938
+ get current() {
3939
+ return this.currentValue;
3940
+ },
3941
+ value: function() {
3942
+ return this.currentValue;
3943
+ }
3944
+ };
3945
+ tracks.set(track, compiled);
3946
+ return track;
3947
+ },
3948
+ keyframes: (fn) => {
3949
+ const kf = createKeyframes();
3950
+ fn(kf);
3951
+ return kf._compile();
3952
+ },
3953
+ stagger: (count, offset, keyframesFn) => {
3954
+ const compiled = timeline.keyframes(keyframesFn);
3955
+ return Array.from({ length: count }, (_, i) => {
3956
+ const track = {
3957
+ ...compiled,
3958
+ currentValue: 0,
3959
+ staggerDelay: i * offset,
3960
+ getValue: (progress) => {
3961
+ const staggeredProgress = Math.max(0, progress - i * offset);
3962
+ const normalizedProgress = Math.min(
3963
+ 1,
3964
+ staggeredProgress / (1 - i * offset)
3965
+ );
3966
+ return normalizedProgress > 0 ? interpolateTrack(compiled, normalizedProgress) : 0;
3967
+ },
3968
+ get current() {
3969
+ return this.currentValue;
3970
+ },
3971
+ value: function() {
3972
+ return this.currentValue;
3973
+ }
3974
+ };
3975
+ tracks.set(track, compiled);
3976
+ return track;
3977
+ });
3978
+ },
3979
+ update: (progress) => {
3980
+ const prevProgress = currentProgress;
3981
+ currentProgress = progress;
3982
+ if (!hasStarted && progress > 0) {
3983
+ hasStarted = true;
3984
+ callbacksRef.current.start.forEach(
3985
+ (cb) => executeCallback(cb, "Start callback error")
3986
+ );
3987
+ }
3988
+ if (!hasEnded && progress >= 1) {
3989
+ hasEnded = true;
3990
+ callbacksRef.current.end.forEach(
3991
+ (cb) => executeCallback(cb, "End callback error")
3992
+ );
3993
+ }
3994
+ if (progress < prevProgress) {
3995
+ if (progress === 0) {
3996
+ hasStarted = false;
3997
+ hasEnded = false;
3998
+ } else if (progress < 1) {
3999
+ hasEnded = false;
4000
+ }
4001
+ }
4002
+ for (const [track, compiled] of tracks) {
4003
+ track.currentValue = track.getValue(progress);
4004
+ if (compiled.segments) {
4005
+ compiled.segments.forEach((seg) => {
4006
+ if (seg.callback && seg.pos <= progress && seg.pos > prevProgress) {
4007
+ executeCallback(seg.callback);
4008
+ }
4009
+ });
4010
+ }
4011
+ }
4012
+ },
4013
+ progress: () => currentProgress
4014
+ };
4015
+ const result = setup(timeline);
4016
+ return { ...result, update: timeline.update };
4017
+ }
4018
+ };
4019
+ return {
4020
+ Timeline,
4021
+ onStart: (fn) => callbacksRef.current.start.push(fn),
4022
+ onEnd: (fn) => callbacksRef.current.end.push(fn),
4023
+ onLoop: (fn) => callbacksRef.current.loop.push(fn)
4024
+ };
4025
+ };
2024
4026
  const KlintWindow = () => {
2025
4027
  const resizeCallbackRef = useRef2(
2026
4028
  null
@@ -2031,6 +4033,8 @@ function useKlint() {
2031
4033
  useEffect2(() => {
2032
4034
  if (!contextRef.current) return;
2033
4035
  const ctx = contextRef.current;
4036
+ const controller = new AbortController();
4037
+ const { signal } = controller;
2034
4038
  const handleResize = () => {
2035
4039
  if (resizeCallbackRef.current) resizeCallbackRef.current(ctx);
2036
4040
  };
@@ -2046,19 +4050,13 @@ function useKlint() {
2046
4050
  visibilityChangeCallbackRef.current(ctx, isVisible);
2047
4051
  }
2048
4052
  };
2049
- window.addEventListener("resize", handleResize);
2050
- window.addEventListener("blur", handleBlur);
2051
- window.addEventListener("focus", handleFocus);
2052
- document.addEventListener("visibilitychange", handleVisibilityChange);
2053
- return () => {
2054
- window.removeEventListener("resize", handleResize);
2055
- window.removeEventListener("blur", handleBlur);
2056
- window.removeEventListener("focus", handleFocus);
2057
- document.removeEventListener(
2058
- "visibilitychange",
2059
- handleVisibilityChange
2060
- );
2061
- };
4053
+ window.addEventListener("resize", handleResize, { signal });
4054
+ window.addEventListener("blur", handleBlur, { signal });
4055
+ window.addEventListener("focus", handleFocus, { signal });
4056
+ document.addEventListener("visibilitychange", handleVisibilityChange, {
4057
+ signal
4058
+ });
4059
+ return () => controller.abort();
2062
4060
  }, []);
2063
4061
  return {
2064
4062
  onResize: (callback) => resizeCallbackRef.current = callback,
@@ -2081,20 +4079,27 @@ function useKlint() {
2081
4079
  context.__textWeight = "normal";
2082
4080
  context.__textStyle = "normal";
2083
4081
  context.__textSize = 72;
4082
+ context.__textLeading = void 0;
2084
4083
  context.__textAlignment = {
2085
4084
  horizontal: "left",
2086
4085
  vertical: "top"
2087
4086
  };
4087
+ context.__fillRule = "nonzero";
2088
4088
  context.__offscreens = /* @__PURE__ */ new Map();
2089
4089
  context.__isPlaying = true;
2090
4090
  context.__currentContext = context;
2091
4091
  context.Color = new Color_default();
2092
4092
  context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
2093
- context.Easing = new Easing_default(context);
2094
- context.State = new State_default();
2095
- context.Time = new Time_default(context);
4093
+ context.Vector = new Vector_default();
4094
+ context.Easing = new Easing_default();
2096
4095
  context.Text = new Text_default(context);
2097
4096
  context.Thing = new Thing_default(context);
4097
+ context.Grid = new Grid_default(context);
4098
+ context.Strip = new Strip_default(context);
4099
+ context.Noise = new Noise_default(context);
4100
+ context.Hotspot = new Hotspot_default(context);
4101
+ context.Performance = new Performance_default(context);
4102
+ context.SSR = new SSR_default(context);
2098
4103
  Object.entries(KlintCoreFunctions).forEach(([name, fn]) => {
2099
4104
  context[name] = fn(context);
2100
4105
  });
@@ -2125,6 +4130,30 @@ function useKlint() {
2125
4130
  contextRef.current.__isPlaying = !contextRef.current.__isPlaying;
2126
4131
  }
2127
4132
  }, []);
4133
+ const KlintPerformance = () => {
4134
+ const metricsRef = useRef2(null);
4135
+ useEffect2(() => {
4136
+ if (!contextRef.current?.__performance) return;
4137
+ const updateMetrics = () => {
4138
+ if (contextRef.current?.__performance) {
4139
+ metricsRef.current = { ...contextRef.current.__performance };
4140
+ }
4141
+ };
4142
+ const interval = setInterval(updateMetrics, 100);
4143
+ return () => clearInterval(interval);
4144
+ }, []);
4145
+ return {
4146
+ metrics: metricsRef.current,
4147
+ getMetrics: () => contextRef.current?.__performance || null,
4148
+ reset: () => {
4149
+ if (contextRef.current?.__performance) {
4150
+ contextRef.current.__performance.droppedFrames = 0;
4151
+ contextRef.current.__performance.minFrameTime = Infinity;
4152
+ contextRef.current.__performance.maxFrameTime = 0;
4153
+ }
4154
+ }
4155
+ };
4156
+ };
2128
4157
  return {
2129
4158
  context: {
2130
4159
  context: contextRef.current,
@@ -2133,8 +4162,11 @@ function useKlint() {
2133
4162
  KlintMouse,
2134
4163
  KlintScroll,
2135
4164
  KlintGesture,
4165
+ KlintKeyboard,
2136
4166
  KlintWindow,
2137
4167
  KlintImage,
4168
+ KlintTimeline,
4169
+ KlintPerformance,
2138
4170
  togglePlay,
2139
4171
  useDev
2140
4172
  };
@@ -2180,10 +4212,21 @@ var useStorage = (initialProps = {}) => {
2180
4212
  };
2181
4213
  export {
2182
4214
  CONFIG_PROPS,
4215
+ Color_default as Color,
2183
4216
  EPSILON,
4217
+ Easing_default as Easing,
4218
+ Grid_default as Grid,
4219
+ Hotspot_default as Hotspot,
2184
4220
  Klint,
2185
4221
  KlintCoreFunctions,
2186
4222
  KlintFunctions,
4223
+ Noise_default as Noise,
4224
+ Performance_default as Performance,
4225
+ SSR_default as SSR,
4226
+ Strip_default as Strip,
4227
+ Text_default as Text,
4228
+ Thing_default as Thing,
4229
+ Vector_default as Vector,
2187
4230
  useKlint,
2188
4231
  useProps,
2189
4232
  useStorage