@shopify/klint 0.0.97 → 0.2.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.cjs CHANGED
@@ -31,10 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  CONFIG_PROPS: () => CONFIG_PROPS,
34
+ Color: () => Color_default,
34
35
  EPSILON: () => EPSILON,
36
+ Easing: () => Easing_default,
37
+ Grid: () => Grid_default,
38
+ Hotspot: () => Hotspot_default,
35
39
  Klint: () => Klint,
36
40
  KlintCoreFunctions: () => KlintCoreFunctions,
37
41
  KlintFunctions: () => KlintFunctions,
42
+ Noise: () => Noise_default,
43
+ Performance: () => Performance_default,
44
+ SSR: () => SSR_default,
45
+ Strip: () => Strip_default,
46
+ Text: () => Text_default,
47
+ Thing: () => Thing_default,
48
+ Vector: () => Vector_default,
38
49
  useKlint: () => useKlint,
39
50
  useProps: () => useProps,
40
51
  useStorage: () => useStorage
@@ -43,6 +54,7 @@ module.exports = __toCommonJS(index_exports);
43
54
 
44
55
  // src/Klint.tsx
45
56
  var import_react = __toESM(require("react"), 1);
57
+ var import_meta = {};
46
58
  var DEFAULT_FPS = 60;
47
59
  var DEFAULT_ALT = "A beautiful artwork made with Klint Canvas";
48
60
  var EPSILON = 1e-4;
@@ -80,11 +92,18 @@ var CONFIG_PROPS = [
80
92
  "__textWeight",
81
93
  "__textStyle",
82
94
  "__textSize",
95
+ "__textLeading",
83
96
  "__textAlignment",
97
+ "__fillRule",
84
98
  "__isPlaying"
85
99
  ];
86
- function useAnimate(contextRef, draw, isVisible) {
100
+ function useAnimate(contextRef, draw, isVisible, enablePerformanceTracking = false) {
87
101
  const animationFrameId = (0, import_react.useRef)(0);
102
+ const frameTimeHistoryRef = (0, import_react.useRef)([]);
103
+ const frameStartTimeRef = (0, import_react.useRef)(0);
104
+ const frameCountRef = (0, import_react.useRef)(0);
105
+ const lastFpsUpdateRef = (0, import_react.useRef)(0);
106
+ const droppedFramesRef = (0, import_react.useRef)(0);
88
107
  const animate = (0, import_react.useCallback)(
89
108
  (timestamp = 0) => {
90
109
  if (!contextRef.current || !isVisible) return;
@@ -98,22 +117,55 @@ function useAnimate(contextRef, draw, isVisible) {
98
117
  if (!context.__lastTargetTime) {
99
118
  context.__lastTargetTime = now;
100
119
  context.__lastRealTime = now;
120
+ frameStartTimeRef.current = now;
121
+ lastFpsUpdateRef.current = now;
101
122
  }
102
123
  const sinceLast = now - context.__lastTargetTime;
103
124
  const epsilon = 5;
104
125
  if (sinceLast >= target - epsilon) {
126
+ const frameStart = enablePerformanceTracking && context.__performance ? performance.now() : 0;
105
127
  context.deltaTime = now - context.__lastRealTime;
106
128
  draw(context);
107
129
  if (context.time > 1e7) context.time = 0;
108
130
  if (context.frame > 1e7) context.frame = 0;
109
- context.time += context.deltaTime / DEFAULT_FPS;
131
+ context.time += context.deltaTime / 1e3;
110
132
  context.frame++;
111
133
  context.__lastTargetTime = now;
112
134
  context.__lastRealTime = now;
135
+ if (enablePerformanceTracking && context.__performance) {
136
+ const frameTime = performance.now() - frameStart;
137
+ const targetFrameTime = 1e3 / context.fps;
138
+ frameTimeHistoryRef.current.push(frameTime);
139
+ if (frameTimeHistoryRef.current.length > 60) {
140
+ frameTimeHistoryRef.current.shift();
141
+ }
142
+ const avgFrameTime = frameTimeHistoryRef.current.reduce((a, b) => a + b, 0) / frameTimeHistoryRef.current.length;
143
+ context.__performance.frameTime = frameTime;
144
+ context.__performance.averageFrameTime = avgFrameTime;
145
+ context.__performance.minFrameTime = Math.min(
146
+ ...frameTimeHistoryRef.current
147
+ );
148
+ context.__performance.maxFrameTime = Math.max(
149
+ ...frameTimeHistoryRef.current
150
+ );
151
+ if (frameTime > targetFrameTime * 1.1) {
152
+ droppedFramesRef.current++;
153
+ }
154
+ context.__performance.droppedFrames = droppedFramesRef.current;
155
+ frameCountRef.current++;
156
+ if (now - lastFpsUpdateRef.current >= 1e3) {
157
+ context.__performance.fps = frameCountRef.current;
158
+ frameCountRef.current = 0;
159
+ lastFpsUpdateRef.current = now;
160
+ if (typeof performance !== "undefined" && performance.memory) {
161
+ context.__performance.memoryUsage = performance.memory.usedJSHeapSize / 1048576;
162
+ }
163
+ }
164
+ }
113
165
  }
114
166
  animationFrameId.current = requestAnimationFrame(animate);
115
167
  },
116
- [draw, isVisible, contextRef]
168
+ [draw, isVisible, contextRef, enablePerformanceTracking]
117
169
  );
118
170
  return {
119
171
  animate,
@@ -126,7 +178,8 @@ function Klint({
126
178
  draw,
127
179
  options = {},
128
180
  preload,
129
- onVisible
181
+ onVisible,
182
+ enablePerformanceTracking = false
130
183
  }) {
131
184
  const canvasRef = (0, import_react.useRef)(null);
132
185
  const containerRef = (0, import_react.useRef)(null);
@@ -136,13 +189,36 @@ function Klint({
136
189
  null
137
190
  );
138
191
  const [isVisible, setIsVisible] = (0, import_react.useState)(true);
192
+ (0, import_react.useEffect)(() => {
193
+ if (typeof import_meta !== "undefined" && import_meta.hot) {
194
+ import_meta.hot.dispose(() => {
195
+ console.log("[Klint] Component unmounting due to HMR");
196
+ if (contextRef.current) {
197
+ contextRef.current.__isPlaying = false;
198
+ }
199
+ });
200
+ }
201
+ if (typeof module !== "undefined" && module.hot) {
202
+ module.hot.dispose(() => {
203
+ console.log("[Klint] Component unmounting due to Webpack HMR");
204
+ if (contextRef.current) {
205
+ contextRef.current.__isPlaying = false;
206
+ }
207
+ });
208
+ }
209
+ }, []);
139
210
  const __options = {
140
211
  ...DEFAULT_OPTIONS,
141
212
  ...options
142
213
  };
143
214
  const [toStaticImage, setStaticImage] = (0, import_react.useState)(null);
144
215
  const initContext = context?.initCoreContext;
145
- const { animate, animationFrameId } = useAnimate(contextRef, draw, isVisible);
216
+ const { animate, animationFrameId } = useAnimate(
217
+ contextRef,
218
+ draw,
219
+ isVisible,
220
+ enablePerformanceTracking
221
+ );
146
222
  const updateCanvasSize = (shouldRedraw = false) => {
147
223
  if (!containerRef.current || !contextRef.current || !canvasRef.current)
148
224
  return;
@@ -172,6 +248,16 @@ function Klint({
172
248
  const context2 = contextRef.current;
173
249
  if (!context2) return;
174
250
  context2.__dpr = dpr;
251
+ if (enablePerformanceTracking) {
252
+ context2.__performance = {
253
+ fps: 0,
254
+ frameTime: 0,
255
+ averageFrameTime: 0,
256
+ minFrameTime: Infinity,
257
+ maxFrameTime: 0,
258
+ droppedFrames: 0
259
+ };
260
+ }
175
261
  if (__options.fps && __options.fps !== context2.fps) {
176
262
  context2.fps = __options.fps;
177
263
  }
@@ -634,7 +720,8 @@ var Color_default = Color;
634
720
 
635
721
  // src/elements/Easing.tsx
636
722
  var Easing = class {
637
- constructor(ctx) {
723
+ constructor() {
724
+ this.zeroOut = (val) => Object.is(val, -0) || Math.abs(val) < 1e-12 ? 0 : val;
638
725
  this.normalize = (val) => {
639
726
  return val * 0.5 + 0.5;
640
727
  };
@@ -658,19 +745,19 @@ var Easing = class {
658
745
  };
659
746
  this.overshootIn = (val) => {
660
747
  const k = 1.70158;
661
- return val * val * (val * (k + 1) - k);
748
+ return this.zeroOut(val * val * (val * (k + 1) - k));
662
749
  };
663
750
  this.overshootOut = (val) => {
664
751
  const m = val - 1;
665
752
  const k = 1.70158;
666
- return 1 + m * m * (m * (k + 1) + k);
753
+ return this.zeroOut(1 + m * m * (m * (k + 1) + k));
667
754
  };
668
755
  this.overshootInOut = (val) => {
669
756
  const m = val - 1;
670
757
  const t = val * 2;
671
758
  const k = 1.70158 * 1.525;
672
- if (val < 0.5) return val * t * (t * (k + 1) - k);
673
- return 1 + 2 * m * m * (2 * m * (k + 1) + k);
759
+ if (val < 0.5) return this.zeroOut(val * t * (t * (k + 1) - k));
760
+ return this.zeroOut(1 + 2 * m * m * (2 * m * (k + 1) + k));
674
761
  };
675
762
  this.bounceOut = (val) => {
676
763
  const r = 1 / 2.75;
@@ -723,48 +810,22 @@ var Easing = class {
723
810
  this.log = () => {
724
811
  console.log(this);
725
812
  };
726
- this.context = ctx;
727
813
  }
728
814
  };
729
815
  var Easing_default = Easing;
730
816
 
731
- // src/elements/State.tsx
732
- var State = class {
733
- constructor() {
734
- this.store = /* @__PURE__ */ new Map();
735
- }
736
- set(key, value, callback) {
737
- this.store.set(key, value);
738
- callback?.(key, value);
739
- }
740
- get(key, callback) {
741
- const value = this.store.get(key);
742
- callback?.(key, value);
743
- return value;
744
- }
745
- has(key) {
746
- return this.store.has(key);
747
- }
748
- delete(key, callback) {
749
- this.store.delete(key);
750
- callback?.(key);
751
- }
752
- log() {
753
- return this.store;
754
- }
755
- };
756
- var State_default = State;
757
-
758
817
  // src/elements/Vector.tsx
759
818
  var Vector = class _Vector {
760
819
  /**
761
820
  * Creates a new Vector
762
821
  * @param x - X-coordinate (default: 0)
763
822
  * @param y - Y-coordinate (default: 0)
823
+ * @param z - Z-coordinate (default: 0)
764
824
  */
765
- constructor(x = 0, y = 0) {
825
+ constructor(x = 0, y = 0, z = 0) {
766
826
  this.x = x;
767
827
  this.y = y;
828
+ this.z = z;
768
829
  }
769
830
  /**
770
831
  * Adds another vector to this vector
@@ -774,6 +835,7 @@ var Vector = class _Vector {
774
835
  add(v) {
775
836
  this.x += v.x;
776
837
  this.y += v.y;
838
+ this.z += v.z;
777
839
  return this;
778
840
  }
779
841
  /**
@@ -784,6 +846,7 @@ var Vector = class _Vector {
784
846
  sub(v) {
785
847
  this.x -= v.x;
786
848
  this.y -= v.y;
849
+ this.z -= v.z;
787
850
  return this;
788
851
  }
789
852
  /**
@@ -794,6 +857,7 @@ var Vector = class _Vector {
794
857
  mult(n) {
795
858
  this.x *= n;
796
859
  this.y *= n;
860
+ this.z *= n;
797
861
  return this;
798
862
  }
799
863
  /**
@@ -804,11 +868,11 @@ var Vector = class _Vector {
804
868
  div(n) {
805
869
  this.x /= n;
806
870
  this.y /= n;
871
+ this.z /= n;
807
872
  return this;
808
873
  }
809
- // to do : project, perp, slerp
810
874
  /**
811
- * Rotates this vector by an angle
875
+ * Rotates this vector by an angle (around z-axis for 2D rotation)
812
876
  * @param angle - Angle in radians
813
877
  * @returns This vector after rotation
814
878
  */
@@ -826,7 +890,7 @@ var Vector = class _Vector {
826
890
  * @returns The magnitude of the vector
827
891
  */
828
892
  mag() {
829
- return Math.sqrt(this.x * this.x + this.y * this.y);
893
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
830
894
  }
831
895
  /**
832
896
  * Alias for mag() - calculates the length of this vector
@@ -841,7 +905,7 @@ var Vector = class _Vector {
841
905
  * @returns The dot product
842
906
  */
843
907
  dot(v) {
844
- return this.x * v.x + this.y * v.y;
908
+ return this.x * v.x + this.y * v.y + this.z * v.z;
845
909
  }
846
910
  /**
847
911
  * Calculates the distance between this vector and another vector
@@ -849,21 +913,21 @@ var Vector = class _Vector {
849
913
  * @returns The distance between the vectors
850
914
  */
851
915
  dist(v) {
852
- return Math.hypot(this.x - v.x, this.y - v.y);
916
+ return Math.hypot(this.x - v.x, this.y - v.y, this.z - v.z);
853
917
  }
854
918
  /**
855
- * Calculates the angle of this vector
919
+ * Calculates the angle of this vector (in 2D, ignoring z)
856
920
  * @returns The angle in radians
857
921
  */
858
922
  angle() {
859
- return Math.atan2(-this.x, -this.y) + Math.PI;
923
+ return Math.atan2(this.y, this.x);
860
924
  }
861
925
  /**
862
926
  * Creates a copy of this vector
863
927
  * @returns A new Vector with the same coordinates
864
928
  */
865
929
  copy() {
866
- return new _Vector(this.x, this.y);
930
+ return new _Vector(this.x, this.y, this.z);
867
931
  }
868
932
  /**
869
933
  * Normalizes this vector (sets its magnitude to 1)
@@ -877,107 +941,128 @@ var Vector = class _Vector {
877
941
  * Sets the coordinates of this vector
878
942
  * @param x - New X-coordinate
879
943
  * @param y - New Y-coordinate
944
+ * @param z - New Z-coordinate (default: 0)
880
945
  * @returns This vector after setting coordinates
881
946
  */
882
- set(x, y) {
947
+ set(x, y, z = 0) {
883
948
  this.x = x;
884
949
  this.y = y;
950
+ this.z = z;
885
951
  return this;
886
952
  }
887
953
  /**
888
- * Creates a new vector at a specified angle and distance from a center point
889
- * @param center - The center point vector
890
- * @param a - The angle in radians
891
- * @param r - The radius (distance from center)
892
- * @returns A new Vector at the calculated position
954
+ * Calculates the cross product of this vector with another vector
955
+ * @param v - The other vector
956
+ * @returns A new Vector representing the cross product
893
957
  */
894
- static fromAngle(center, a, r) {
895
- const x = Math.cos(a) * r + center.x;
896
- const y = Math.sin(a) * r + center.y;
897
- return new _Vector(x, y);
898
- }
899
- };
900
- var Vector_default = Vector;
901
-
902
- // src/elements/Time.tsx
903
- var Time = class {
904
- constructor(ctx) {
905
- this.timelines = /* @__PURE__ */ new Map();
906
- this.currentTimeline = "default";
907
- this.DEFAULT_DURATION = 8 * 60;
908
- this.staggers = [];
909
- this.context = ctx;
910
- this.timelines.set("default", {
911
- progress: 0,
912
- duration: this.DEFAULT_DURATION
913
- });
958
+ cross(v) {
959
+ return new _Vector(
960
+ this.y * v.z - this.z * v.y,
961
+ this.z * v.x - this.x * v.z,
962
+ this.x * v.y - this.y * v.x
963
+ );
914
964
  }
915
- timeline(key) {
916
- if (!this.timelines.has(key)) {
917
- this.timelines.set(key, { progress: 0, duration: this.DEFAULT_DURATION });
918
- }
919
- this.currentTimeline = key;
965
+ /**
966
+ * Calculates this vector's position relative to another vector
967
+ * @param v - The reference vector
968
+ * @returns This vector after making it relative to v
969
+ */
970
+ relativeTo(v) {
971
+ this.x -= v.x;
972
+ this.y -= v.y;
973
+ this.z -= v.z;
920
974
  return this;
921
975
  }
922
- use(progress) {
923
- const timeline = this.timelines.get(this.currentTimeline);
924
- if (timeline.duration <= 0) {
925
- timeline.progress = 0;
926
- return this;
927
- }
928
- timeline.progress = timeline.duration === 1 ? Math.min(progress, 1) : progress / timeline.duration % 1;
976
+ /**
977
+ * Makes this vector point towards a target vector
978
+ * @param target - The target vector to look at
979
+ * @returns This vector after pointing towards target
980
+ */
981
+ lookAt(target) {
982
+ const direction = new _Vector(
983
+ target.x - this.x,
984
+ target.y - this.y,
985
+ target.z - this.z
986
+ );
987
+ direction.normalize();
988
+ this.x = direction.x;
989
+ this.y = direction.y;
990
+ this.z = direction.z;
929
991
  return this;
930
992
  }
931
- for(duration) {
932
- const timeline = this.timelines.get(this.currentTimeline);
933
- timeline.duration = duration;
993
+ /**
994
+ * Converts this vector from world space to screen space
995
+ * @param width - Screen width
996
+ * @param height - Screen height
997
+ * @returns This vector after screen space conversion
998
+ */
999
+ toScreen(width, height) {
1000
+ this.x = (this.x + 1) * width / 2;
1001
+ this.y = (1 - this.y) * height / 2;
934
1002
  return this;
935
1003
  }
936
- stagger(num, offset = 0, callback) {
937
- const timeline = this.timelines.get(this.currentTimeline);
938
- const totalduration = this.context.remap(
939
- timeline.progress,
940
- 0,
941
- 1,
942
- 0,
943
- 1 + offset
944
- );
945
- for (let i = 0; i < num; i++) {
946
- const id = 1 - i / (num - 1);
947
- const progress = this.context.constrain(
948
- totalduration - id * offset,
949
- 0,
950
- 1
951
- );
952
- if (!callback) {
953
- if (this.staggers[i]) {
954
- this.staggers[i].progress = progress;
955
- } else {
956
- this.staggers[i] = { progress, id };
957
- }
1004
+ /**
1005
+ * Spherical linear interpolation between this vector and another vector
1006
+ * @param v - The target vector
1007
+ * @param amt - Interpolation amount (0-1)
1008
+ * @returns This vector after interpolation
1009
+ */
1010
+ slerp(v, amt) {
1011
+ if (amt === 0) {
1012
+ return this;
1013
+ }
1014
+ if (amt === 1) {
1015
+ return this.set(v.x, v.y, v.z);
1016
+ }
1017
+ const selfMag = this.mag();
1018
+ const vMag = v.mag();
1019
+ const magmag = selfMag * vMag;
1020
+ if (magmag === 0) {
1021
+ this.mult(1 - amt).add(new _Vector(v.x * amt, v.y * amt, v.z * amt));
1022
+ return this;
1023
+ }
1024
+ const axis = this.cross(v);
1025
+ const axisMag = axis.mag();
1026
+ const theta = Math.atan2(axisMag, this.dot(v));
1027
+ if (axisMag > 0) {
1028
+ axis.x /= axisMag;
1029
+ axis.y /= axisMag;
1030
+ axis.z /= axisMag;
1031
+ } else if (theta < Math.PI * 0.5) {
1032
+ this.mult(1 - amt).add(new _Vector(v.x * amt, v.y * amt, v.z * amt));
1033
+ return this;
1034
+ } else {
1035
+ if (this.z === 0 && v.z === 0) {
1036
+ axis.set(0, 0, 1);
1037
+ } else if (this.x !== 0) {
1038
+ axis.set(this.y, -this.x, 0).normalize();
958
1039
  } else {
959
- callback?.(progress, id, num);
1040
+ axis.set(1, 0, 0);
960
1041
  }
961
1042
  }
962
- return callback ? this : this.staggers;
963
- }
964
- between(from = 0, to = 1, callback) {
965
- const timeline = this.timelines.get(this.currentTimeline);
966
- const localProgress = this.context.remap(
967
- timeline.progress,
968
- Math.max(0, from),
969
- Math.min(1, to),
970
- 0,
971
- 1
972
- );
973
- callback(localProgress);
1043
+ const ey = axis.cross(this);
1044
+ const lerpedMagFactor = 1 - amt + amt * vMag / selfMag;
1045
+ const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta);
1046
+ const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta);
1047
+ this.x = this.x * cosMultiplier + ey.x * sinMultiplier;
1048
+ this.y = this.y * cosMultiplier + ey.y * sinMultiplier;
1049
+ this.z = this.z * cosMultiplier + ey.z * sinMultiplier;
974
1050
  return this;
975
1051
  }
976
- progress() {
977
- return this.timelines.get(this.currentTimeline)?.progress || 0;
1052
+ /**
1053
+ * Creates a new vector at a specified angle and distance from a center point
1054
+ * @param center - The center point vector
1055
+ * @param a - The angle in radians
1056
+ * @param r - The radius (distance from center)
1057
+ * @returns A new Vector at the calculated position
1058
+ */
1059
+ static fromAngle(center, a, r) {
1060
+ const x = Math.cos(a) * r + center.x;
1061
+ const y = Math.sin(a) * r + center.y;
1062
+ return new _Vector(x, y, center.z);
978
1063
  }
979
1064
  };
980
- var Time_default = Time;
1065
+ var Vector_default = Vector;
981
1066
 
982
1067
  // src/elements/Text.tsx
983
1068
  var Text = class {
@@ -1186,139 +1271,1513 @@ var Thing = class {
1186
1271
  };
1187
1272
  var Thing_default = Thing;
1188
1273
 
1189
- // src/KlintFunctions.tsx
1190
- var KlintCoreFunctions = {
1191
- saveCanvas: (ctx) => () => {
1192
- const link = document.createElement("a");
1193
- link.download = "canvas.png";
1194
- link.href = ctx.canvas.toDataURL();
1195
- link.click();
1196
- },
1197
- fullscreen: (ctx) => () => {
1198
- ctx.canvas.requestFullscreen?.();
1199
- },
1200
- play: (ctx) => () => {
1201
- if (!ctx.__isPlaying) ctx.__isPlaying = true;
1202
- },
1203
- pause: (ctx) => () => {
1204
- if (ctx.__isPlaying) ctx.__isPlaying = false;
1205
- },
1206
- // to do
1207
- redraw: () => () => {
1208
- },
1209
- extend: (ctx) => (name, data, enforceReplace = false) => {
1210
- if (name in ctx && !enforceReplace) return;
1211
- ctx[name] = data;
1212
- },
1213
- passImage: () => (element) => {
1214
- if (!element.complete) {
1215
- console.warn("Image passed to passImage() is not fully loaded");
1216
- return null;
1274
+ // src/elements/Grid.tsx
1275
+ var Grid = class {
1276
+ /**
1277
+ * Creates a new Grid instance
1278
+ * @param ctx - The Klint context
1279
+ */
1280
+ constructor(ctx) {
1281
+ this.context = ctx;
1282
+ }
1283
+ /**
1284
+ * Create a rectangular grid of points
1285
+ * @param x - X position of the grid
1286
+ * @param y - Y position of the grid
1287
+ * @param width - Width of the grid
1288
+ * @param height - Height of the grid
1289
+ * @param countX - Number of points horizontally
1290
+ * @param countY - Number of points vertically
1291
+ * @param options - Grid options
1292
+ * @returns Array of grid points
1293
+ */
1294
+ rect(x, y, width, height, countX, countY, options) {
1295
+ const origin = options?.origin || "corner";
1296
+ const points = [];
1297
+ let startX = x;
1298
+ let startY = y;
1299
+ if (origin === "center") {
1300
+ startX = x - width / 2;
1301
+ startY = y - height / 2;
1217
1302
  }
1218
- return element;
1219
- },
1220
- passImages: () => (elements) => {
1221
- return elements.map((element) => {
1222
- if (!element.complete) {
1223
- console.warn("Image passed to passImages() is not fully loaded");
1224
- return null;
1303
+ const spacingX = countX > 1 ? width / (countX - 1) : 0;
1304
+ const spacingY = countY > 1 ? height / (countY - 1) : 0;
1305
+ for (let j = 0; j < countY; j++) {
1306
+ for (let i = 0; i < countX; i++) {
1307
+ const pointX = startX + i * spacingX;
1308
+ const pointY = startY + j * spacingY;
1309
+ const id = j * countX + i;
1310
+ points.push({
1311
+ x: pointX,
1312
+ y: pointY,
1313
+ i,
1314
+ j,
1315
+ id
1316
+ });
1225
1317
  }
1226
- return element;
1227
- });
1228
- },
1229
- saveConfig: (ctx) => (from) => {
1230
- return Object.fromEntries(
1231
- CONFIG_PROPS.map((key) => [
1232
- key,
1233
- from?.[key] ?? ctx[key]
1234
- ])
1235
- );
1236
- },
1237
- restoreConfig: (ctx) => (config) => {
1238
- Object.assign(ctx, config);
1239
- },
1240
- describe: (ctx) => (description) => {
1241
- ctx.__description = description;
1242
- },
1243
- createOffscreen: (ctx) => (id, width, height, options, callback) => {
1244
- const offscreen = document.createElement("canvas");
1245
- offscreen.width = width * ctx.__dpr;
1246
- offscreen.height = height * ctx.__dpr;
1247
- const context = offscreen.getContext("2d", {
1248
- alpha: options?.alpha ?? true,
1249
- willReadFrequently: options?.willreadfrequently ?? false
1250
- });
1251
- if (!context) throw new Error("Failed to create offscreen context");
1252
- context.__dpr = ctx.__dpr;
1253
- context.width = width * ctx.__dpr;
1254
- context.height = height * ctx.__dpr;
1255
- context.__isMainContext = false;
1256
- context.__imageOrigin = "corner";
1257
- context.__rectangleOrigin = "corner";
1258
- context.__canvasOrigin = "corner";
1259
- context.__textFont = "sans-serif";
1260
- context.__textWeight = "normal";
1261
- context.__textStyle = "normal";
1262
- context.__textSize = 120;
1263
- context.__textAlignment = {
1264
- horizontal: "left",
1265
- vertical: "top"
1266
- };
1267
- if (!options?.ignoreFunctions) {
1268
- context.Color = ctx.Color;
1269
- context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
1270
- context.Easing = ctx.Easing;
1271
- context.Text = ctx.Text;
1272
- Object.entries(KlintFunctions).forEach(([name, fn]) => {
1273
- context[name] = fn(context);
1318
+ }
1319
+ return points;
1320
+ }
1321
+ /**
1322
+ * Create a radial grid of points
1323
+ * @param x - Center X position
1324
+ * @param y - Center Y position
1325
+ * @param radius - Maximum radius
1326
+ * @param count - Number of points per ring
1327
+ * @param ringCount - Number of rings
1328
+ * @param ringSpace - Space between rings
1329
+ * @param options - Grid options
1330
+ * @returns Array of grid points
1331
+ */
1332
+ radial(x, y, radius, count, ringCount, ringSpace, options) {
1333
+ const perStepCount = options?.perStepCount || 0;
1334
+ const points = [];
1335
+ let id = 0;
1336
+ for (let ring = 0; ring < ringCount; ring++) {
1337
+ const ringRadius = ring * ringSpace;
1338
+ if (ringRadius > radius) break;
1339
+ const ringPointCount = count + perStepCount * ring;
1340
+ for (let i = 0; i < ringPointCount; i++) {
1341
+ const angle = Math.PI * 2 * i / ringPointCount;
1342
+ const pointX = x + Math.cos(angle) * ringRadius;
1343
+ const pointY = y + Math.sin(angle) * ringRadius;
1344
+ points.push({
1345
+ x: pointX,
1346
+ y: pointY,
1347
+ i,
1348
+ j: ring,
1349
+ id: id++
1350
+ });
1351
+ }
1352
+ }
1353
+ if (ringCount > 0 && points.length === 0) {
1354
+ points.push({
1355
+ x,
1356
+ y,
1357
+ i: 0,
1358
+ j: 0,
1359
+ id: 0
1274
1360
  });
1275
1361
  }
1276
- if (options?.origin) {
1277
- context.__canvasOrigin = options.origin;
1278
- if (options.origin === "center") {
1279
- context.translate(context.width * 0.5, context.height * 0.5);
1362
+ return points;
1363
+ }
1364
+ /**
1365
+ * Create a hexagonal grid of points
1366
+ * @param x - X position of the grid
1367
+ * @param y - Y position of the grid
1368
+ * @param width - Width of the grid area
1369
+ * @param height - Height of the grid area
1370
+ * @param size - Size of each hexagon
1371
+ * @param options - Grid options
1372
+ * @returns Array of grid points
1373
+ */
1374
+ hex(x, y, width, height, size, options) {
1375
+ const origin = options?.origin || "corner";
1376
+ const pointy = options?.pointy !== false;
1377
+ const points = [];
1378
+ let startX = x;
1379
+ let startY = y;
1380
+ if (origin === "center") {
1381
+ startX = x - width / 2;
1382
+ startY = y - height / 2;
1383
+ }
1384
+ const hexWidth = pointy ? Math.sqrt(3) * size : 2 * size;
1385
+ const hexHeight = pointy ? 2 * size : Math.sqrt(3) * size;
1386
+ const cols = Math.ceil(width / hexWidth) + 1;
1387
+ const rows = Math.ceil(height / (hexHeight * 0.75)) + 1;
1388
+ let id = 0;
1389
+ for (let j = 0; j < rows; j++) {
1390
+ for (let i = 0; i < cols; i++) {
1391
+ let pointX;
1392
+ let pointY;
1393
+ if (pointy) {
1394
+ pointX = startX + i * hexWidth;
1395
+ pointY = startY + j * hexHeight * 0.75;
1396
+ if (j % 2 === 1) {
1397
+ pointX += hexWidth / 2;
1398
+ }
1399
+ } else {
1400
+ pointX = startX + i * hexWidth * 0.75;
1401
+ pointY = startY + j * hexHeight;
1402
+ if (i % 2 === 1) {
1403
+ pointY += hexHeight / 2;
1404
+ }
1405
+ }
1406
+ if (pointX <= startX + width && pointY <= startY + height) {
1407
+ points.push({
1408
+ x: pointX,
1409
+ y: pointY,
1410
+ i,
1411
+ j,
1412
+ id: id++
1413
+ });
1414
+ }
1280
1415
  }
1281
1416
  }
1282
- if (callback) {
1283
- callback(context);
1417
+ return points;
1418
+ }
1419
+ /**
1420
+ * Create a triangular grid of points
1421
+ * @param x - X position of the grid
1422
+ * @param y - Y position of the grid
1423
+ * @param width - Width of the grid
1424
+ * @param height - Height of the grid
1425
+ * @param size - Size of each triangle
1426
+ * @param options - Grid options
1427
+ * @returns Array of grid points
1428
+ */
1429
+ triangle(x, y, width, height, size, options) {
1430
+ const origin = options?.origin || "corner";
1431
+ const points = [];
1432
+ let startX = x;
1433
+ let startY = y;
1434
+ if (origin === "center") {
1435
+ startX = x - width / 2;
1436
+ startY = y - height / 2;
1284
1437
  }
1285
- if (options?.static === "true") {
1286
- const base64 = offscreen.toDataURL();
1287
- const img = new Image();
1288
- img.src = base64;
1289
- ctx.__offscreens.set(id, img);
1290
- return img;
1438
+ const triHeight = Math.sqrt(3) / 2 * size;
1439
+ const cols = Math.ceil(width / (size / 2)) + 1;
1440
+ const rows = Math.ceil(height / triHeight) + 1;
1441
+ let id = 0;
1442
+ for (let j = 0; j < rows; j++) {
1443
+ for (let i = 0; i < cols; i++) {
1444
+ const pointX = startX + i * (size / 2);
1445
+ const pointY = startY + j * triHeight;
1446
+ if (pointX <= startX + width && pointY <= startY + height) {
1447
+ points.push({
1448
+ x: pointX,
1449
+ y: pointY,
1450
+ i,
1451
+ j,
1452
+ id: id++
1453
+ });
1454
+ }
1455
+ }
1291
1456
  }
1292
- ctx.__offscreens.set(id, context);
1293
- return context;
1294
- },
1295
- getOffscreen: (ctx) => (id) => {
1296
- const offscreen = ctx.__offscreens.get(id);
1297
- if (!offscreen)
1298
- throw new Error(`No offscreen context found with id: ${id}`);
1299
- return offscreen;
1457
+ return points;
1300
1458
  }
1301
1459
  };
1302
- var KlintFunctions = {
1303
- extend: (ctx) => (name, data, enforceReplace = false) => {
1304
- if (name in ctx && !enforceReplace) return;
1305
- ctx[name] = data;
1306
- },
1307
- background: (ctx) => (color) => {
1308
- ctx.resetTransform();
1309
- ctx.push();
1310
- if (color && color !== "transparent") {
1311
- ctx.fillStyle = color;
1312
- ctx.fillRect(0, 0, ctx.width, ctx.height);
1313
- } else {
1314
- ctx.clearRect(0, 0, ctx.width, ctx.height);
1460
+ var Grid_default = Grid;
1461
+
1462
+ // src/elements/Strip.tsx
1463
+ var Strip = class {
1464
+ /**
1465
+ * Creates a new Strip instance
1466
+ * @param ctx - The Klint context
1467
+ */
1468
+ constructor(ctx) {
1469
+ this.context = ctx;
1470
+ }
1471
+ /**
1472
+ * Create a strip of triangles from points
1473
+ * Points are connected in a zigzag pattern:
1474
+ * 0 - 2 - 4 ...
1475
+ * | / | / |
1476
+ * 1 - 3 - 5 ...
1477
+ *
1478
+ * @param points - Array of points (must be even number for complete triangles)
1479
+ * @param draw - Optional callback to customize each triangle's appearance
1480
+ */
1481
+ triangles(points, draw) {
1482
+ if (points.length < 3) return;
1483
+ const numTriangles = Math.floor((points.length - 2) * 2);
1484
+ for (let i = 0; i < numTriangles; i++) {
1485
+ const baseIndex = Math.floor(i / 2) * 2;
1486
+ const isEven = i % 2 === 0;
1487
+ let p1;
1488
+ let p2;
1489
+ let p3;
1490
+ if (isEven) {
1491
+ p1 = points[baseIndex];
1492
+ p2 = points[baseIndex + 1];
1493
+ p3 = points[baseIndex + 2];
1494
+ } else {
1495
+ p1 = points[baseIndex + 1];
1496
+ p2 = points[baseIndex + 3];
1497
+ p3 = points[baseIndex + 2];
1498
+ }
1499
+ if (!p1 || !p2 || !p3) continue;
1500
+ const center = {
1501
+ x: (p1.x + p2.x + p3.x) / 3,
1502
+ y: (p1.y + p2.y + p3.y) / 3
1503
+ };
1504
+ const triangle = {
1505
+ id: i,
1506
+ center,
1507
+ points: [p1, p2, p3]
1508
+ };
1509
+ let fillColor;
1510
+ if (draw) {
1511
+ const result = draw(triangle);
1512
+ if (typeof result === "string") {
1513
+ fillColor = result;
1514
+ }
1515
+ }
1516
+ this.context.beginPath();
1517
+ this.context.moveTo(p1.x, p1.y);
1518
+ this.context.lineTo(p2.x, p2.y);
1519
+ this.context.lineTo(p3.x, p3.y);
1520
+ this.context.closePath();
1521
+ if (fillColor) {
1522
+ const prevFill = this.context.fillStyle;
1523
+ this.context.fillStyle = fillColor;
1524
+ this.context.fill();
1525
+ this.context.fillStyle = prevFill;
1526
+ } else if (this.context.checkTransparency("fill")) {
1527
+ this.context.fill();
1528
+ }
1529
+ if (this.context.checkTransparency("stroke")) {
1530
+ this.context.stroke();
1531
+ }
1315
1532
  }
1316
- ctx.pop();
1317
- if (ctx.__canvasOrigin === "center")
1318
- ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
1319
- },
1320
- reset: (ctx) => () => {
1321
- ctx.clearRect(0, 0, ctx.width, ctx.height);
1533
+ }
1534
+ /**
1535
+ * Create a strip of quads from points
1536
+ * Points are connected in a grid pattern:
1537
+ * 0 - 2 - 4 ...
1538
+ * | | |
1539
+ * 1 - 3 - 5 ...
1540
+ *
1541
+ * @param points - Array of points (must be even number for complete quads)
1542
+ * @param draw - Optional callback to customize each quad's appearance
1543
+ */
1544
+ quads(points, draw) {
1545
+ if (points.length < 4) return;
1546
+ const numQuads = Math.floor((points.length - 2) / 2);
1547
+ for (let i = 0; i < numQuads; i++) {
1548
+ const baseIndex = i * 2;
1549
+ const p1 = points[baseIndex];
1550
+ const p2 = points[baseIndex + 1];
1551
+ const p3 = points[baseIndex + 3];
1552
+ const p4 = points[baseIndex + 2];
1553
+ if (!p1 || !p2 || !p3 || !p4) continue;
1554
+ const center = {
1555
+ x: (p1.x + p2.x + p3.x + p4.x) / 4,
1556
+ y: (p1.y + p2.y + p3.y + p4.y) / 4
1557
+ };
1558
+ const quad = {
1559
+ id: i,
1560
+ center,
1561
+ points: [p1, p2, p3, p4]
1562
+ };
1563
+ let fillColor;
1564
+ if (draw) {
1565
+ const result = draw(quad);
1566
+ if (typeof result === "string") {
1567
+ fillColor = result;
1568
+ }
1569
+ }
1570
+ this.context.beginPath();
1571
+ this.context.moveTo(p1.x, p1.y);
1572
+ this.context.lineTo(p2.x, p2.y);
1573
+ this.context.lineTo(p3.x, p3.y);
1574
+ this.context.lineTo(p4.x, p4.y);
1575
+ this.context.closePath();
1576
+ if (fillColor) {
1577
+ const prevFill = this.context.fillStyle;
1578
+ this.context.fillStyle = fillColor;
1579
+ this.context.fill();
1580
+ this.context.fillStyle = prevFill;
1581
+ } else if (this.context.checkTransparency("fill")) {
1582
+ this.context.fill();
1583
+ }
1584
+ if (this.context.checkTransparency("stroke")) {
1585
+ this.context.stroke();
1586
+ }
1587
+ }
1588
+ }
1589
+ /**
1590
+ * Create a single hull shape from points
1591
+ * Points are connected following the winding order:
1592
+ * 0 - 2 - 4 - ... n-1
1593
+ * | |
1594
+ * 1 - 3 - 5 - ... n
1595
+ *
1596
+ * Final order: 0 - 2 - 4 ... n-1, n, ... 5 - 3 - 1
1597
+ *
1598
+ * @param points - Array of points
1599
+ * @param draw - Optional callback to add elements along the hull
1600
+ */
1601
+ hull(points, draw) {
1602
+ if (points.length < 2) return;
1603
+ const hullPath = [];
1604
+ for (let i = 0; i < points.length; i += 2) {
1605
+ hullPath.push(points[i]);
1606
+ }
1607
+ for (let i = points.length - 1 - points.length % 2; i >= 1; i -= 2) {
1608
+ hullPath.push(points[i]);
1609
+ }
1610
+ this.context.beginPath();
1611
+ this.context.moveTo(hullPath[0].x, hullPath[0].y);
1612
+ for (let i = 1; i < hullPath.length; i++) {
1613
+ this.context.lineTo(hullPath[i].x, hullPath[i].y);
1614
+ }
1615
+ this.context.closePath();
1616
+ if (this.context.checkTransparency("fill")) {
1617
+ this.context.fill();
1618
+ }
1619
+ if (this.context.checkTransparency("stroke")) {
1620
+ this.context.stroke();
1621
+ }
1622
+ if (draw) {
1623
+ const numPairs = Math.floor(points.length / 2);
1624
+ for (let i = 0; i < numPairs; i++) {
1625
+ const topPoint = points[i * 2];
1626
+ const bottomPoint = points[i * 2 + 1];
1627
+ if (topPoint && bottomPoint) {
1628
+ const center = {
1629
+ x: (topPoint.x + bottomPoint.x) / 2,
1630
+ y: (topPoint.y + bottomPoint.y) / 2
1631
+ };
1632
+ const hull = {
1633
+ id: i,
1634
+ center
1635
+ };
1636
+ draw(hull);
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ /**
1642
+ * Create a ribbon/tape effect from points
1643
+ * Similar to hull but with configurable width
1644
+ *
1645
+ * @param points - Array of center points
1646
+ * @param width - Width of the ribbon
1647
+ * @param draw - Optional callback
1648
+ */
1649
+ ribbon(points, width, draw) {
1650
+ if (points.length < 2) return;
1651
+ const offsetPoints = [];
1652
+ for (let i = 0; i < points.length; i++) {
1653
+ const curr = points[i];
1654
+ const prev = points[i - 1] || curr;
1655
+ const next = points[i + 1] || curr;
1656
+ const dx = next.x - prev.x;
1657
+ const dy = next.y - prev.y;
1658
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
1659
+ const perpX = -dy / len * (width / 2);
1660
+ const perpY = dx / len * (width / 2);
1661
+ offsetPoints.push({
1662
+ top: { x: curr.x + perpX, y: curr.y + perpY },
1663
+ bottom: { x: curr.x - perpX, y: curr.y - perpY }
1664
+ });
1665
+ }
1666
+ this.context.beginPath();
1667
+ this.context.moveTo(offsetPoints[0].top.x, offsetPoints[0].top.y);
1668
+ for (let i = 1; i < offsetPoints.length; i++) {
1669
+ this.context.lineTo(offsetPoints[i].top.x, offsetPoints[i].top.y);
1670
+ }
1671
+ for (let i = offsetPoints.length - 1; i >= 0; i--) {
1672
+ this.context.lineTo(offsetPoints[i].bottom.x, offsetPoints[i].bottom.y);
1673
+ }
1674
+ this.context.closePath();
1675
+ if (this.context.checkTransparency("fill")) {
1676
+ this.context.fill();
1677
+ }
1678
+ if (this.context.checkTransparency("stroke")) {
1679
+ this.context.stroke();
1680
+ }
1681
+ if (draw) {
1682
+ for (let i = 0; i < points.length - 1; i++) {
1683
+ const center = {
1684
+ x: (points[i].x + points[i + 1].x) / 2,
1685
+ y: (points[i].y + points[i + 1].y) / 2
1686
+ };
1687
+ const result = draw({ id: i, center });
1688
+ if (typeof result === "string") {
1689
+ this.context.beginPath();
1690
+ this.context.moveTo(offsetPoints[i].top.x, offsetPoints[i].top.y);
1691
+ this.context.lineTo(offsetPoints[i + 1].top.x, offsetPoints[i + 1].top.y);
1692
+ this.context.lineTo(offsetPoints[i + 1].bottom.x, offsetPoints[i + 1].bottom.y);
1693
+ this.context.lineTo(offsetPoints[i].bottom.x, offsetPoints[i].bottom.y);
1694
+ this.context.closePath();
1695
+ const prevFill = this.context.fillStyle;
1696
+ this.context.fillStyle = result;
1697
+ this.context.fill();
1698
+ this.context.fillStyle = prevFill;
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1703
+ };
1704
+ var Strip_default = Strip;
1705
+
1706
+ // src/elements/Noise.tsx
1707
+ var Noise = class {
1708
+ /**
1709
+ * Creates a new Noise instance
1710
+ * @param ctx - The Klint context
1711
+ */
1712
+ constructor(ctx) {
1713
+ this.perm = [];
1714
+ this.permMod12 = [];
1715
+ this.grad3 = [
1716
+ [1, 1, 0],
1717
+ [-1, 1, 0],
1718
+ [1, -1, 0],
1719
+ [-1, -1, 0],
1720
+ [1, 0, 1],
1721
+ [-1, 0, 1],
1722
+ [1, 0, -1],
1723
+ [-1, 0, -1],
1724
+ [0, 1, 1],
1725
+ [0, -1, 1],
1726
+ [0, 1, -1],
1727
+ [0, -1, -1]
1728
+ ];
1729
+ this.grad4 = [
1730
+ [0, 1, 1, 1],
1731
+ [0, 1, 1, -1],
1732
+ [0, 1, -1, 1],
1733
+ [0, 1, -1, -1],
1734
+ [0, -1, 1, 1],
1735
+ [0, -1, 1, -1],
1736
+ [0, -1, -1, 1],
1737
+ [0, -1, -1, -1],
1738
+ [1, 0, 1, 1],
1739
+ [1, 0, 1, -1],
1740
+ [1, 0, -1, 1],
1741
+ [1, 0, -1, -1],
1742
+ [-1, 0, 1, 1],
1743
+ [-1, 0, 1, -1],
1744
+ [-1, 0, -1, 1],
1745
+ [-1, 0, -1, -1],
1746
+ [1, 1, 0, 1],
1747
+ [1, 1, 0, -1],
1748
+ [1, -1, 0, 1],
1749
+ [1, -1, 0, -1],
1750
+ [-1, 1, 0, 1],
1751
+ [-1, 1, 0, -1],
1752
+ [-1, -1, 0, 1],
1753
+ [-1, -1, 0, -1],
1754
+ [1, 1, 1, 0],
1755
+ [1, 1, -1, 0],
1756
+ [1, -1, 1, 0],
1757
+ [1, -1, -1, 0],
1758
+ [-1, 1, 1, 0],
1759
+ [-1, 1, -1, 0],
1760
+ [-1, -1, 1, 0],
1761
+ [-1, -1, -1, 0]
1762
+ ];
1763
+ this.currentSeed = Math.random();
1764
+ this.F2 = 0.5 * (Math.sqrt(3) - 1);
1765
+ this.G2 = (3 - Math.sqrt(3)) / 6;
1766
+ this.F3 = 1 / 3;
1767
+ this.G3 = 1 / 6;
1768
+ this.F4 = (Math.sqrt(5) - 1) / 4;
1769
+ this.G4 = (5 - Math.sqrt(5)) / 20;
1770
+ this.context = ctx;
1771
+ this.buildPermutationTable();
1772
+ }
1773
+ /**
1774
+ * Build permutation table for noise generation
1775
+ */
1776
+ buildPermutationTable() {
1777
+ const p = [];
1778
+ for (let i = 0; i < 256; i++) {
1779
+ p[i] = i;
1780
+ }
1781
+ let n = 256;
1782
+ while (n > 0) {
1783
+ const index = Math.floor(this.random() * n--);
1784
+ const temp = p[n];
1785
+ p[n] = p[index];
1786
+ p[index] = temp;
1787
+ }
1788
+ this.perm = [];
1789
+ this.permMod12 = [];
1790
+ for (let i = 0; i < 512; i++) {
1791
+ this.perm[i] = p[i & 255];
1792
+ this.permMod12[i] = this.perm[i] % 12;
1793
+ }
1794
+ }
1795
+ /**
1796
+ * Seeded random number generator
1797
+ */
1798
+ random() {
1799
+ const x = Math.sin(this.currentSeed++) * 1e4;
1800
+ return x - Math.floor(x);
1801
+ }
1802
+ /**
1803
+ * Set seed for noise generation
1804
+ * @param seed - Seed value for reproducible noise
1805
+ */
1806
+ seed(seed) {
1807
+ this.currentSeed = seed !== void 0 ? seed : Math.random() * 1e4;
1808
+ this.buildPermutationTable();
1809
+ }
1810
+ /**
1811
+ * Fade function for Perlin noise
1812
+ */
1813
+ fade(t) {
1814
+ return t * t * t * (t * (t * 6 - 15) + 10);
1815
+ }
1816
+ /**
1817
+ * Linear interpolation
1818
+ */
1819
+ lerp(t, a, b) {
1820
+ return a + t * (b - a);
1821
+ }
1822
+ perlin(x, y, z, w) {
1823
+ if (y === void 0) {
1824
+ const xi = Math.floor(x) & 255;
1825
+ const xf = x - Math.floor(x);
1826
+ const u = this.fade(xf);
1827
+ const a = this.perm[xi];
1828
+ const b = this.perm[xi + 1];
1829
+ const grad1 = (hash, x2) => (hash & 1) === 0 ? x2 : -x2;
1830
+ return this.lerp(u, grad1(a, xf), grad1(b, xf - 1));
1831
+ } else if (z === void 0) {
1832
+ const xi = Math.floor(x) & 255;
1833
+ const yi = Math.floor(y) & 255;
1834
+ const xf = x - Math.floor(x);
1835
+ const yf = y - Math.floor(y);
1836
+ const u = this.fade(xf);
1837
+ const v = this.fade(yf);
1838
+ const aa = this.perm[this.perm[xi] + yi];
1839
+ const ab = this.perm[this.perm[xi] + yi + 1];
1840
+ const ba = this.perm[this.perm[xi + 1] + yi];
1841
+ const bb = this.perm[this.perm[xi + 1] + yi + 1];
1842
+ const grad2 = (hash, x3, y2) => {
1843
+ const h = hash & 3;
1844
+ const u2 = h < 2 ? x3 : y2;
1845
+ const v2 = h < 2 ? y2 : x3;
1846
+ return ((h & 1) === 0 ? u2 : -u2) + ((h & 2) === 0 ? v2 : -v2);
1847
+ };
1848
+ const x1 = this.lerp(u, grad2(aa, xf, yf), grad2(ba, xf - 1, yf));
1849
+ const x2 = this.lerp(u, grad2(ab, xf, yf - 1), grad2(bb, xf - 1, yf - 1));
1850
+ return this.lerp(v, x1, x2);
1851
+ } else if (w === void 0) {
1852
+ const xi = Math.floor(x) & 255;
1853
+ const yi = Math.floor(y) & 255;
1854
+ const zi = Math.floor(z) & 255;
1855
+ const xf = x - Math.floor(x);
1856
+ const yf = y - Math.floor(y);
1857
+ const zf = z - Math.floor(z);
1858
+ const u = this.fade(xf);
1859
+ const v = this.fade(yf);
1860
+ const w2 = this.fade(zf);
1861
+ const aaa = this.perm[this.perm[this.perm[xi] + yi] + zi];
1862
+ const aba = this.perm[this.perm[this.perm[xi] + yi + 1] + zi];
1863
+ const aab = this.perm[this.perm[this.perm[xi] + yi] + zi + 1];
1864
+ const abb = this.perm[this.perm[this.perm[xi] + yi + 1] + zi + 1];
1865
+ const baa = this.perm[this.perm[this.perm[xi + 1] + yi] + zi];
1866
+ const bba = this.perm[this.perm[this.perm[xi + 1] + yi + 1] + zi];
1867
+ const bab = this.perm[this.perm[this.perm[xi + 1] + yi] + zi + 1];
1868
+ const bbb = this.perm[this.perm[this.perm[xi + 1] + yi + 1] + zi + 1];
1869
+ const grad3 = (hash, x2, y2, z2) => {
1870
+ const h = hash & 15;
1871
+ const u2 = h < 8 ? x2 : y2;
1872
+ const v2 = h < 4 ? y2 : h === 12 || h === 14 ? x2 : z2;
1873
+ return ((h & 1) === 0 ? u2 : -u2) + ((h & 2) === 0 ? v2 : -v2);
1874
+ };
1875
+ const x1 = this.lerp(
1876
+ w2,
1877
+ this.lerp(
1878
+ v,
1879
+ this.lerp(u, grad3(aaa, xf, yf, zf), grad3(baa, xf - 1, yf, zf)),
1880
+ this.lerp(u, grad3(aba, xf, yf - 1, zf), grad3(bba, xf - 1, yf - 1, zf))
1881
+ ),
1882
+ this.lerp(
1883
+ v,
1884
+ this.lerp(u, grad3(aab, xf, yf, zf - 1), grad3(bab, xf - 1, yf, zf - 1)),
1885
+ this.lerp(u, grad3(abb, xf, yf - 1, zf - 1), grad3(bbb, xf - 1, yf - 1, zf - 1))
1886
+ )
1887
+ );
1888
+ return x1;
1889
+ } else {
1890
+ return this.perlin(x, y, z) * 0.5 + this.perlin(x + w, y + w, z + w) * 0.5;
1891
+ }
1892
+ }
1893
+ simplex(x, y, z, w) {
1894
+ if (y === void 0) {
1895
+ return this.perlin(x);
1896
+ } else if (z === void 0) {
1897
+ let n0 = 0, n1 = 0, n2 = 0;
1898
+ const s = (x + y) * this.F2;
1899
+ const i = Math.floor(x + s);
1900
+ const j = Math.floor(y + s);
1901
+ const t = (i + j) * this.G2;
1902
+ const X0 = i - t;
1903
+ const Y0 = j - t;
1904
+ const x0 = x - X0;
1905
+ const y0 = y - Y0;
1906
+ let i1, j1;
1907
+ if (x0 > y0) {
1908
+ i1 = 1;
1909
+ j1 = 0;
1910
+ } else {
1911
+ i1 = 0;
1912
+ j1 = 1;
1913
+ }
1914
+ const x1 = x0 - i1 + this.G2;
1915
+ const y1 = y0 - j1 + this.G2;
1916
+ const x2 = x0 - 1 + 2 * this.G2;
1917
+ const y2 = y0 - 1 + 2 * this.G2;
1918
+ const ii = i & 255;
1919
+ const jj = j & 255;
1920
+ const gi0 = this.permMod12[ii + this.perm[jj]];
1921
+ const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]];
1922
+ const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]];
1923
+ let t0 = 0.5 - x0 * x0 - y0 * y0;
1924
+ if (t0 < 0) {
1925
+ n0 = 0;
1926
+ } else {
1927
+ t0 *= t0;
1928
+ n0 = t0 * t0 * this.dot2(this.grad3[gi0], x0, y0);
1929
+ }
1930
+ let t1 = 0.5 - x1 * x1 - y1 * y1;
1931
+ if (t1 < 0) {
1932
+ n1 = 0;
1933
+ } else {
1934
+ t1 *= t1;
1935
+ n1 = t1 * t1 * this.dot2(this.grad3[gi1], x1, y1);
1936
+ }
1937
+ let t2 = 0.5 - x2 * x2 - y2 * y2;
1938
+ if (t2 < 0) {
1939
+ n2 = 0;
1940
+ } else {
1941
+ t2 *= t2;
1942
+ n2 = t2 * t2 * this.dot2(this.grad3[gi2], x2, y2);
1943
+ }
1944
+ return 70 * (n0 + n1 + n2);
1945
+ } else if (w === void 0) {
1946
+ let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
1947
+ const s = (x + y + z) * this.F3;
1948
+ const i = Math.floor(x + s);
1949
+ const j = Math.floor(y + s);
1950
+ const k = Math.floor(z + s);
1951
+ const t = (i + j + k) * this.G3;
1952
+ const X0 = i - t;
1953
+ const Y0 = j - t;
1954
+ const Z0 = k - t;
1955
+ const x0 = x - X0;
1956
+ const y0 = y - Y0;
1957
+ const z0 = z - Z0;
1958
+ let i1, j1, k1;
1959
+ let i2, j2, k2;
1960
+ if (x0 >= y0) {
1961
+ if (y0 >= z0) {
1962
+ i1 = 1;
1963
+ j1 = 0;
1964
+ k1 = 0;
1965
+ i2 = 1;
1966
+ j2 = 1;
1967
+ k2 = 0;
1968
+ } else if (x0 >= z0) {
1969
+ i1 = 1;
1970
+ j1 = 0;
1971
+ k1 = 0;
1972
+ i2 = 1;
1973
+ j2 = 0;
1974
+ k2 = 1;
1975
+ } else {
1976
+ i1 = 0;
1977
+ j1 = 0;
1978
+ k1 = 1;
1979
+ i2 = 1;
1980
+ j2 = 0;
1981
+ k2 = 1;
1982
+ }
1983
+ } else {
1984
+ if (y0 < z0) {
1985
+ i1 = 0;
1986
+ j1 = 0;
1987
+ k1 = 1;
1988
+ i2 = 0;
1989
+ j2 = 1;
1990
+ k2 = 1;
1991
+ } else if (x0 < z0) {
1992
+ i1 = 0;
1993
+ j1 = 1;
1994
+ k1 = 0;
1995
+ i2 = 0;
1996
+ j2 = 1;
1997
+ k2 = 1;
1998
+ } else {
1999
+ i1 = 0;
2000
+ j1 = 1;
2001
+ k1 = 0;
2002
+ i2 = 1;
2003
+ j2 = 1;
2004
+ k2 = 0;
2005
+ }
2006
+ }
2007
+ const x1 = x0 - i1 + this.G3;
2008
+ const y1 = y0 - j1 + this.G3;
2009
+ const z1 = z0 - k1 + this.G3;
2010
+ const x2 = x0 - i2 + 2 * this.G3;
2011
+ const y2 = y0 - j2 + 2 * this.G3;
2012
+ const z2 = z0 - k2 + 2 * this.G3;
2013
+ const x3 = x0 - 1 + 3 * this.G3;
2014
+ const y3 = y0 - 1 + 3 * this.G3;
2015
+ const z3 = z0 - 1 + 3 * this.G3;
2016
+ const ii = i & 255;
2017
+ const jj = j & 255;
2018
+ const kk = k & 255;
2019
+ const gi0 = this.permMod12[ii + this.perm[jj + this.perm[kk]]];
2020
+ const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1 + this.perm[kk + k1]]];
2021
+ const gi2 = this.permMod12[ii + i2 + this.perm[jj + j2 + this.perm[kk + k2]]];
2022
+ const gi3 = this.permMod12[ii + 1 + this.perm[jj + 1 + this.perm[kk + 1]]];
2023
+ let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
2024
+ if (t0 < 0) {
2025
+ n0 = 0;
2026
+ } else {
2027
+ t0 *= t0;
2028
+ n0 = t0 * t0 * this.dot3(this.grad3[gi0], x0, y0, z0);
2029
+ }
2030
+ let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
2031
+ if (t1 < 0) {
2032
+ n1 = 0;
2033
+ } else {
2034
+ t1 *= t1;
2035
+ n1 = t1 * t1 * this.dot3(this.grad3[gi1], x1, y1, z1);
2036
+ }
2037
+ let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
2038
+ if (t2 < 0) {
2039
+ n2 = 0;
2040
+ } else {
2041
+ t2 *= t2;
2042
+ n2 = t2 * t2 * this.dot3(this.grad3[gi2], x2, y2, z2);
2043
+ }
2044
+ let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
2045
+ if (t3 < 0) {
2046
+ n3 = 0;
2047
+ } else {
2048
+ t3 *= t3;
2049
+ n3 = t3 * t3 * this.dot3(this.grad3[gi3], x3, y3, z3);
2050
+ }
2051
+ return 32 * (n0 + n1 + n2 + n3);
2052
+ } else {
2053
+ return this.simplex(x, y, z) * 0.5 + this.simplex(x + w, y + w, z + w) * 0.5;
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Dot product for 2D
2058
+ */
2059
+ dot2(g, x, y) {
2060
+ return g[0] * x + g[1] * y;
2061
+ }
2062
+ /**
2063
+ * Dot product for 3D
2064
+ */
2065
+ dot3(g, x, y, z) {
2066
+ return g[0] * x + g[1] * y + g[2] * z;
2067
+ }
2068
+ hash(x, y, z, w) {
2069
+ let n = 0;
2070
+ if (y === void 0) {
2071
+ n = Math.sin(x * 12.9898 + this.currentSeed) * 43758.5453;
2072
+ } else if (z === void 0) {
2073
+ n = Math.sin(x * 12.9898 + y * 78.233 + this.currentSeed) * 43758.5453;
2074
+ } else if (w === void 0) {
2075
+ n = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + this.currentSeed) * 43758.5453;
2076
+ } else {
2077
+ n = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + w * 59.1337 + this.currentSeed) * 43758.5453;
2078
+ }
2079
+ return n - Math.floor(n);
2080
+ }
2081
+ /**
2082
+ * Fractal Brownian Motion (fBm) noise
2083
+ * Supports options object for amplitude/frequency/lacunarity/gain/octaves.
2084
+ */
2085
+ fbm(x, y, z, options) {
2086
+ let yVal = void 0;
2087
+ let zVal = void 0;
2088
+ let opts = typeof y === "object" ? y : typeof z === "object" ? z : {};
2089
+ if (typeof y === "number") yVal = y;
2090
+ if (typeof z === "number") zVal = z;
2091
+ if (options) opts = { ...opts, ...options };
2092
+ const octaves = opts.octaves ?? 4;
2093
+ if (octaves <= 0) return 0;
2094
+ let amplitude = opts.amplitude ?? 1;
2095
+ let frequency = opts.frequency ?? 1;
2096
+ const lacunarity = opts.lacunarity ?? 2;
2097
+ const gain = opts.gain ?? 0.5;
2098
+ let value = 0;
2099
+ let maxValue = 0;
2100
+ for (let i = 0; i < octaves; i++) {
2101
+ if (yVal === void 0) {
2102
+ value += amplitude * this.perlin(x * frequency);
2103
+ } else if (zVal === void 0) {
2104
+ value += amplitude * this.perlin(x * frequency, yVal * frequency);
2105
+ } else {
2106
+ value += amplitude * this.perlin(x * frequency, yVal * frequency, zVal * frequency);
2107
+ }
2108
+ maxValue += amplitude;
2109
+ amplitude *= gain;
2110
+ frequency *= lacunarity;
2111
+ }
2112
+ return maxValue === 0 ? 0 : value / maxValue;
2113
+ }
2114
+ /**
2115
+ * Turbulence noise (absolute value of noise)
2116
+ * Supports options object for octaves.
2117
+ */
2118
+ turbulence(x, y, z, options) {
2119
+ let yVal = void 0;
2120
+ let zVal = void 0;
2121
+ let opts = typeof y === "object" ? y : typeof z === "object" ? z : {};
2122
+ if (typeof y === "number") yVal = y;
2123
+ if (typeof z === "number") zVal = z;
2124
+ if (options) opts = { ...opts, ...options };
2125
+ const octaves = opts.octaves ?? 4;
2126
+ if (octaves <= 0) return 0;
2127
+ let value = 0;
2128
+ let amplitude = 1;
2129
+ let frequency = 1;
2130
+ let maxValue = 0;
2131
+ for (let i = 0; i < octaves; i++) {
2132
+ let noise = 0;
2133
+ if (yVal === void 0) {
2134
+ noise = this.perlin(x * frequency);
2135
+ } else if (zVal === void 0) {
2136
+ noise = this.perlin(x * frequency, yVal * frequency);
2137
+ } else {
2138
+ noise = this.perlin(x * frequency, yVal * frequency, zVal * frequency);
2139
+ }
2140
+ value += amplitude * Math.abs(noise);
2141
+ maxValue += amplitude;
2142
+ amplitude *= 0.5;
2143
+ frequency *= 2;
2144
+ }
2145
+ return maxValue === 0 ? 0 : value / maxValue;
2146
+ }
2147
+ /**
2148
+ * Ridged multifractal noise (simple implementation)
2149
+ */
2150
+ ridge(x, y, options) {
2151
+ let yVal = void 0;
2152
+ let opts = typeof y === "object" ? y : {};
2153
+ if (typeof y === "number") yVal = y;
2154
+ if (options) opts = { ...opts, ...options };
2155
+ const octaves = opts.octaves ?? 4;
2156
+ if (octaves <= 0) return 0;
2157
+ let amplitude = opts.amplitude ?? 1;
2158
+ let frequency = opts.frequency ?? 1;
2159
+ const lacunarity = opts.lacunarity ?? 2;
2160
+ const gain = opts.gain ?? 0.5;
2161
+ let value = 0;
2162
+ let weight = 1;
2163
+ let maxValue = 0;
2164
+ for (let i = 0; i < octaves; i++) {
2165
+ const n = yVal === void 0 ? this.perlin(x * frequency) : this.perlin(x * frequency, yVal * frequency);
2166
+ let signal = 1 - Math.abs(n);
2167
+ signal *= signal;
2168
+ signal *= weight;
2169
+ weight = signal * 2;
2170
+ weight = Math.min(Math.max(weight, 0), 1);
2171
+ value += signal * amplitude;
2172
+ maxValue += amplitude;
2173
+ amplitude *= gain;
2174
+ frequency *= lacunarity;
2175
+ }
2176
+ return maxValue === 0 ? 0 : value / maxValue;
2177
+ }
2178
+ /**
2179
+ * Cellular / Worley noise (2D simple implementation)
2180
+ */
2181
+ cellular(x, y, options) {
2182
+ let yVal = void 0;
2183
+ let opts = typeof y === "object" ? y : {};
2184
+ if (typeof y === "number") yVal = y;
2185
+ if (options) opts = { ...opts, ...options };
2186
+ if (yVal === void 0) yVal = 0;
2187
+ const xi = Math.floor(x);
2188
+ const yi = Math.floor(yVal);
2189
+ const distance = opts.distance ?? "euclidean";
2190
+ let minDist = Infinity;
2191
+ for (let j = -1; j <= 1; j++) {
2192
+ for (let i = -1; i <= 1; i++) {
2193
+ const fx = i + this.hash(xi + i, yi + j);
2194
+ const fy = j + this.hash(yi + j, xi + i);
2195
+ const dx = fx + xi - x;
2196
+ const dy = fy + yi - yVal;
2197
+ const dist = distance === "manhattan" ? Math.abs(dx) + Math.abs(dy) : Math.sqrt(dx * dx + dy * dy);
2198
+ if (dist < minDist) minDist = dist;
2199
+ }
2200
+ }
2201
+ const maxDist = Math.SQRT2;
2202
+ const normalized = 1 - Math.min(minDist / maxDist, 1);
2203
+ return normalized;
2204
+ }
2205
+ };
2206
+ var Noise_default = Noise;
2207
+
2208
+ // src/elements/Hotspot.tsx
2209
+ var Hotspot = class {
2210
+ /**
2211
+ * Creates a new Hotspot instance
2212
+ * @param ctx - The Klint context
2213
+ */
2214
+ constructor(ctx) {
2215
+ this.context = ctx;
2216
+ }
2217
+ /**
2218
+ * Check if point is inside a circle
2219
+ * @param point - Point to check (usually mouse position)
2220
+ * @param x - Circle center X
2221
+ * @param y - Circle center Y
2222
+ * @param radius - Circle radius
2223
+ * @returns True if point is inside the circle
2224
+ */
2225
+ circle(point, x, y, radius) {
2226
+ const dx = point.x - x;
2227
+ const dy = point.y - y;
2228
+ const distance = Math.sqrt(dx * dx + dy * dy);
2229
+ return distance <= radius;
2230
+ }
2231
+ /**
2232
+ * Check if point is inside a rectangle
2233
+ * @param point - Point to check (usually mouse position)
2234
+ * @param x - Rectangle X position
2235
+ * @param y - Rectangle Y position
2236
+ * @param width - Rectangle width
2237
+ * @param height - Rectangle height
2238
+ * @returns True if point is inside the rectangle
2239
+ */
2240
+ rect(point, x, y, width, height) {
2241
+ const origin = this.context.__rectangleOrigin || "corner";
2242
+ let left, top;
2243
+ if (origin === "center") {
2244
+ left = x - width / 2;
2245
+ top = y - height / 2;
2246
+ } else {
2247
+ left = x;
2248
+ top = y;
2249
+ }
2250
+ const right = left + width;
2251
+ const bottom = top + height;
2252
+ return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
2253
+ }
2254
+ /**
2255
+ * Check if point is inside an ellipse
2256
+ * @param point - Point to check
2257
+ * @param x - Ellipse center X
2258
+ * @param y - Ellipse center Y
2259
+ * @param radiusX - Horizontal radius
2260
+ * @param radiusY - Vertical radius
2261
+ * @param rotation - Rotation angle in radians
2262
+ * @returns True if point is inside the ellipse
2263
+ */
2264
+ ellipse(point, x, y, radiusX, radiusY, rotation = 0) {
2265
+ const cos = Math.cos(-rotation);
2266
+ const sin = Math.sin(-rotation);
2267
+ const dx = point.x - x;
2268
+ const dy = point.y - y;
2269
+ const localX = dx * cos - dy * sin;
2270
+ const localY = dx * sin + dy * cos;
2271
+ return localX * localX / (radiusX * radiusX) + localY * localY / (radiusY * radiusY) <= 1;
2272
+ }
2273
+ /**
2274
+ * Check if point is inside a polygon
2275
+ * @param point - Point to check
2276
+ * @param vertices - Array of polygon vertices
2277
+ * @returns True if point is inside the polygon
2278
+ */
2279
+ polygon(point, vertices) {
2280
+ let inside = false;
2281
+ const n = vertices.length;
2282
+ for (let i = 0, j = n - 1; i < n; j = i++) {
2283
+ const xi = vertices[i].x, yi = vertices[i].y;
2284
+ const xj = vertices[j].x, yj = vertices[j].y;
2285
+ const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
2286
+ if (intersect) inside = !inside;
2287
+ }
2288
+ return inside;
2289
+ }
2290
+ /**
2291
+ * Check if point is inside a Path2D
2292
+ * @param point - Point to check
2293
+ * @param path - Path2D object
2294
+ * @returns True if point is inside the path
2295
+ */
2296
+ path(point, path) {
2297
+ return this.context.isPointInPath(path, point.x, point.y);
2298
+ }
2299
+ };
2300
+ var Hotspot_default = Hotspot;
2301
+
2302
+ // src/elements/Performance.tsx
2303
+ var Performance = class {
2304
+ constructor(ctx) {
2305
+ this.textMetricsCache = /* @__PURE__ */ new Map();
2306
+ this.frameTimeHistory = [];
2307
+ this.MAX_HISTORY = 60;
2308
+ this.context = ctx;
2309
+ }
2310
+ /**
2311
+ * Batch multiple canvas operations together for better performance
2312
+ */
2313
+ batchDraw(drawFn) {
2314
+ this.context.save();
2315
+ this.context.beginPath();
2316
+ try {
2317
+ drawFn();
2318
+ } finally {
2319
+ this.context.restore();
2320
+ }
2321
+ }
2322
+ /**
2323
+ * Optimize drawing by using offscreen canvas for static elements
2324
+ */
2325
+ useOffscreenCache(id, width, height, renderFn) {
2326
+ let offscreen = this.context.__offscreens.get(id);
2327
+ if (!offscreen || offscreen instanceof HTMLImageElement) {
2328
+ offscreen = this.context.createOffscreen(id, width, height);
2329
+ if (offscreen && !(offscreen instanceof HTMLImageElement)) {
2330
+ renderFn(offscreen);
2331
+ }
2332
+ }
2333
+ if (offscreen) {
2334
+ if (offscreen instanceof HTMLImageElement) {
2335
+ this.context.image(offscreen, 0, 0);
2336
+ } else {
2337
+ this.context.image(offscreen.canvas, 0, 0);
2338
+ }
2339
+ }
2340
+ }
2341
+ /**
2342
+ * Throttle expensive operations to run less frequently
2343
+ */
2344
+ throttleFrame(interval, fn) {
2345
+ const cacheKey = `__throttle_${fn.toString().slice(0, 50)}`;
2346
+ const cached = this.context[cacheKey];
2347
+ if (!cached || this.context.frame % interval === 0) {
2348
+ const result = fn();
2349
+ this.context[cacheKey] = { value: result, frame: this.context.frame };
2350
+ return result;
2351
+ }
2352
+ return cached.value;
2353
+ }
2354
+ /**
2355
+ * Detect potential memory leaks by tracking object creation
2356
+ */
2357
+ useLeakDetection() {
2358
+ const objectCounts = /* @__PURE__ */ new Map();
2359
+ let lastCheckFrame = 0;
2360
+ return {
2361
+ track: (type) => {
2362
+ const count = objectCounts.get(type) || 0;
2363
+ objectCounts.set(type, count + 1);
2364
+ },
2365
+ check: () => {
2366
+ if (this.context.frame - lastCheckFrame < 60) return;
2367
+ lastCheckFrame = this.context.frame;
2368
+ const warnings = [];
2369
+ objectCounts.forEach((count, type) => {
2370
+ if (count > 1e4) {
2371
+ warnings.push(
2372
+ `Potential memory leak: ${type} has ${count} instances`
2373
+ );
2374
+ }
2375
+ });
2376
+ if (warnings.length > 0) {
2377
+ console.warn("[Klint] Memory leak warnings:", warnings);
2378
+ }
2379
+ objectCounts.clear();
2380
+ }
2381
+ };
2382
+ }
2383
+ /**
2384
+ * Optimize text rendering by caching text measurements
2385
+ */
2386
+ getCachedTextMetrics(text, font) {
2387
+ const cacheKey = `${font}:${text}`;
2388
+ if (!this.textMetricsCache.has(cacheKey)) {
2389
+ this.context.save();
2390
+ this.context.font = font;
2391
+ const metrics = this.context.measureText(text);
2392
+ this.context.restore();
2393
+ this.textMetricsCache.set(cacheKey, metrics);
2394
+ if (this.textMetricsCache.size > 1e3) {
2395
+ const firstKey = this.textMetricsCache.keys().next().value;
2396
+ if (firstKey) this.textMetricsCache.delete(firstKey);
2397
+ }
2398
+ }
2399
+ return this.textMetricsCache.get(cacheKey);
2400
+ }
2401
+ /**
2402
+ * Clear all performance caches
2403
+ */
2404
+ clearCaches() {
2405
+ this.textMetricsCache.clear();
2406
+ }
2407
+ /**
2408
+ * Get current performance metrics
2409
+ */
2410
+ getMetrics() {
2411
+ return this.context.__performance || null;
2412
+ }
2413
+ /**
2414
+ * Render performance widget on canvas
2415
+ */
2416
+ render(options = {}) {
2417
+ const metrics = this.getMetrics();
2418
+ if (!metrics) return;
2419
+ const {
2420
+ x = 10,
2421
+ y = 10,
2422
+ width = 200,
2423
+ height = 120,
2424
+ backgroundColor = "rgba(0, 0, 0, 0.8)",
2425
+ textColor = "#ffffff",
2426
+ accentColor = "#4ecdc4",
2427
+ showGraph = true,
2428
+ graphHeight = 40,
2429
+ fontSize = 12,
2430
+ showMemory = true
2431
+ } = options;
2432
+ if (showGraph) {
2433
+ this.frameTimeHistory.push(metrics.frameTime);
2434
+ if (this.frameTimeHistory.length > this.MAX_HISTORY) {
2435
+ this.frameTimeHistory.shift();
2436
+ }
2437
+ }
2438
+ this.context.save();
2439
+ this.context.fillStyle = backgroundColor;
2440
+ this.context.fillRect(x, y, width, height);
2441
+ this.context.strokeStyle = accentColor;
2442
+ this.context.lineWidth = 1;
2443
+ this.context.strokeRect(x, y, width, height);
2444
+ this.context.fillStyle = textColor;
2445
+ this.context.font = `${fontSize}px monospace`;
2446
+ this.context.textAlign = "left";
2447
+ this.context.textBaseline = "top";
2448
+ let currentY = y + 8;
2449
+ const fpsColor = metrics.fps >= 55 ? "#4ecdc4" : metrics.fps >= 30 ? "#ffd93d" : "#ff6b6b";
2450
+ this.context.fillStyle = fpsColor;
2451
+ this.context.fillText(`FPS: ${metrics.fps}`, x + 8, currentY);
2452
+ currentY += fontSize + 4;
2453
+ this.context.fillStyle = textColor;
2454
+ this.context.fillText(`Frame: ${metrics.frameTime.toFixed(2)}ms`, x + 8, currentY);
2455
+ currentY += fontSize + 4;
2456
+ this.context.fillText(`Avg: ${metrics.averageFrameTime.toFixed(2)}ms`, x + 8, currentY);
2457
+ currentY += fontSize + 4;
2458
+ this.context.fillText(
2459
+ `Min: ${metrics.minFrameTime.toFixed(2)}ms / Max: ${metrics.maxFrameTime.toFixed(2)}ms`,
2460
+ x + 8,
2461
+ currentY
2462
+ );
2463
+ currentY += fontSize + 4;
2464
+ if (metrics.droppedFrames > 0) {
2465
+ this.context.fillStyle = "#ff6b6b";
2466
+ this.context.fillText(`Dropped: ${metrics.droppedFrames}`, x + 8, currentY);
2467
+ currentY += fontSize + 4;
2468
+ }
2469
+ if (showMemory && metrics.memoryUsage !== void 0) {
2470
+ this.context.fillStyle = textColor;
2471
+ this.context.fillText(`Memory: ${metrics.memoryUsage.toFixed(2)}MB`, x + 8, currentY);
2472
+ currentY += fontSize + 4;
2473
+ }
2474
+ if (showGraph && this.frameTimeHistory.length > 1) {
2475
+ const graphX = x + 8;
2476
+ const graphY = currentY + 4;
2477
+ const graphWidth = width - 16;
2478
+ const targetFrameTime = 1e3 / this.context.fps;
2479
+ this.context.fillStyle = "rgba(255, 255, 255, 0.1)";
2480
+ this.context.fillRect(graphX, graphY, graphWidth, graphHeight);
2481
+ const targetY = graphY + graphHeight - targetFrameTime / (targetFrameTime * 2) * graphHeight;
2482
+ this.context.strokeStyle = "rgba(255, 255, 255, 0.3)";
2483
+ this.context.lineWidth = 1;
2484
+ this.context.beginPath();
2485
+ this.context.moveTo(graphX, targetY);
2486
+ this.context.lineTo(graphX + graphWidth, targetY);
2487
+ this.context.stroke();
2488
+ this.context.strokeStyle = accentColor;
2489
+ this.context.lineWidth = 2;
2490
+ this.context.beginPath();
2491
+ const maxFrameTime = Math.max(...this.frameTimeHistory, targetFrameTime * 2);
2492
+ const stepX = graphWidth / (this.frameTimeHistory.length - 1);
2493
+ this.frameTimeHistory.forEach((frameTime, index) => {
2494
+ const normalizedTime = Math.min(frameTime / maxFrameTime, 1);
2495
+ const pointY = graphY + graphHeight - normalizedTime * graphHeight;
2496
+ const pointX = graphX + index * stepX;
2497
+ if (index === 0) {
2498
+ this.context.moveTo(pointX, pointY);
2499
+ } else {
2500
+ this.context.lineTo(pointX, pointY);
2501
+ }
2502
+ });
2503
+ this.context.stroke();
2504
+ }
2505
+ this.context.restore();
2506
+ }
2507
+ /**
2508
+ * Alias for render() - shows performance widget
2509
+ */
2510
+ show(options) {
2511
+ this.render(options);
2512
+ }
2513
+ };
2514
+ var Performance_default = Performance;
2515
+
2516
+ // src/elements/SSR.tsx
2517
+ var SSR = class {
2518
+ constructor(ctx) {
2519
+ this.context = ctx || null;
2520
+ }
2521
+ /**
2522
+ * Render a Klint sketch to a static image (base64 data URL)
2523
+ */
2524
+ async renderToImage(draw, options) {
2525
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
2526
+ return this.renderInBrowser(draw, options);
2527
+ }
2528
+ throw new Error(
2529
+ "Server-side rendering requires 'canvas' package. Install it with: npm install canvas\nAlternatively, use SSR.generateImageUrl() to generate images via an API endpoint."
2530
+ );
2531
+ }
2532
+ /**
2533
+ * Generate a static image URL for a Klint sketch
2534
+ */
2535
+ async generateImageUrl(draw, options) {
2536
+ if (typeof window === "undefined") {
2537
+ throw new Error(
2538
+ "generateImageUrl() requires a browser environment. For server-side rendering, use renderToImage() with the 'canvas' package."
2539
+ );
2540
+ }
2541
+ return this.renderInBrowser(draw, options);
2542
+ }
2543
+ /**
2544
+ * Check if Klint can run in the current environment
2545
+ */
2546
+ canRender() {
2547
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
2548
+ return true;
2549
+ }
2550
+ try {
2551
+ require("canvas");
2552
+ return true;
2553
+ } catch {
2554
+ return false;
2555
+ }
2556
+ }
2557
+ /**
2558
+ * Render in browser environment (client-side)
2559
+ */
2560
+ async renderInBrowser(draw, options) {
2561
+ return new Promise((resolve, reject) => {
2562
+ try {
2563
+ const canvas = document.createElement("canvas");
2564
+ const dpr = options.dpr || (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
2565
+ canvas.width = options.width * dpr;
2566
+ canvas.height = options.height * dpr;
2567
+ const ctx = canvas.getContext("2d");
2568
+ if (!ctx) {
2569
+ reject(new Error("Failed to get canvas context"));
2570
+ return;
2571
+ }
2572
+ const klintContext = this.createMinimalContext(
2573
+ ctx,
2574
+ canvas,
2575
+ options.width,
2576
+ options.height,
2577
+ dpr
2578
+ );
2579
+ draw(klintContext);
2580
+ const mimeType = options.format === "jpeg" ? "image/jpeg" : options.format === "webp" ? "image/webp" : "image/png";
2581
+ const quality = options.quality !== void 0 ? options.quality : 0.85;
2582
+ const dataUrl = canvas.toDataURL(mimeType, quality);
2583
+ resolve(dataUrl);
2584
+ } catch (error) {
2585
+ reject(error);
2586
+ }
2587
+ });
2588
+ }
2589
+ /**
2590
+ * Create a minimal KlintContext for server rendering
2591
+ * This provides basic functionality without full Klint features
2592
+ */
2593
+ createMinimalContext(ctx, canvas, width, height, dpr) {
2594
+ return {
2595
+ canvas,
2596
+ width: width * dpr,
2597
+ height: height * dpr,
2598
+ // Basic canvas methods
2599
+ fillRect: ctx.fillRect.bind(ctx),
2600
+ strokeRect: ctx.strokeRect.bind(ctx),
2601
+ clearRect: ctx.clearRect.bind(ctx),
2602
+ beginPath: ctx.beginPath.bind(ctx),
2603
+ moveTo: ctx.moveTo.bind(ctx),
2604
+ lineTo: ctx.lineTo.bind(ctx),
2605
+ arc: ctx.arc.bind(ctx),
2606
+ fill: ctx.fill.bind(ctx),
2607
+ stroke: ctx.stroke.bind(ctx),
2608
+ save: ctx.save.bind(ctx),
2609
+ restore: ctx.restore.bind(ctx),
2610
+ translate: ctx.translate.bind(ctx),
2611
+ rotate: ctx.rotate.bind(ctx),
2612
+ scale: ctx.scale.bind(ctx),
2613
+ // Basic properties
2614
+ fillStyle: ctx.fillStyle,
2615
+ strokeStyle: ctx.strokeStyle,
2616
+ lineWidth: ctx.lineWidth,
2617
+ // Minimal Klint-like API
2618
+ background: (color) => {
2619
+ ctx.fillStyle = color || "#000";
2620
+ ctx.fillRect(0, 0, width * dpr, height * dpr);
2621
+ },
2622
+ fillColor: (color) => {
2623
+ ctx.fillStyle = color;
2624
+ },
2625
+ strokeColor: (color) => {
2626
+ ctx.strokeStyle = color;
2627
+ },
2628
+ circle: (x, y, radius) => {
2629
+ ctx.beginPath();
2630
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
2631
+ ctx.fill();
2632
+ },
2633
+ rectangle: (x, y, w, h) => {
2634
+ ctx.fillRect(x, y, w, h || w);
2635
+ },
2636
+ // Time properties (static for server rendering)
2637
+ frame: 0,
2638
+ time: 0,
2639
+ deltaTime: 0,
2640
+ fps: 60
2641
+ };
2642
+ }
2643
+ };
2644
+ var SSR_default = SSR;
2645
+
2646
+ // src/KlintFunctions.tsx
2647
+ var KlintCoreFunctions = {
2648
+ saveCanvas: (ctx) => () => {
2649
+ const link = document.createElement("a");
2650
+ link.download = "canvas.png";
2651
+ link.href = ctx.canvas.toDataURL();
2652
+ link.click();
2653
+ },
2654
+ fullscreen: (ctx) => () => {
2655
+ ctx.canvas.requestFullscreen?.();
2656
+ },
2657
+ play: (ctx) => () => {
2658
+ if (!ctx.__isPlaying) ctx.__isPlaying = true;
2659
+ },
2660
+ pause: (ctx) => () => {
2661
+ if (ctx.__isPlaying) ctx.__isPlaying = false;
2662
+ },
2663
+ // to do
2664
+ redraw: () => () => {
2665
+ },
2666
+ extend: (ctx) => (name, data, enforceReplace = false) => {
2667
+ if (name in ctx && !enforceReplace) return;
2668
+ ctx[name] = data;
2669
+ },
2670
+ passImage: () => (element) => {
2671
+ if (!element.complete) {
2672
+ console.warn("Image passed to passImage() is not fully loaded");
2673
+ return null;
2674
+ }
2675
+ return element;
2676
+ },
2677
+ passImages: () => (elements) => {
2678
+ return elements.map((element) => {
2679
+ if (!element.complete) {
2680
+ console.warn("Image passed to passImages() is not fully loaded");
2681
+ return null;
2682
+ }
2683
+ return element;
2684
+ });
2685
+ },
2686
+ saveConfig: (ctx) => (from) => {
2687
+ return Object.fromEntries(
2688
+ CONFIG_PROPS.map((key) => [
2689
+ key,
2690
+ from?.[key] ?? ctx[key]
2691
+ ])
2692
+ );
2693
+ },
2694
+ restoreConfig: (ctx) => (config) => {
2695
+ Object.assign(ctx, config);
2696
+ },
2697
+ describe: (ctx) => (description) => {
2698
+ ctx.__description = description;
2699
+ },
2700
+ createOffscreen: (ctx) => (id, width, height, options, callback) => {
2701
+ const offscreen = document.createElement("canvas");
2702
+ offscreen.width = width * ctx.__dpr;
2703
+ offscreen.height = height * ctx.__dpr;
2704
+ const context = offscreen.getContext("2d", {
2705
+ alpha: options?.alpha ?? true,
2706
+ willReadFrequently: options?.willreadfrequently ?? false
2707
+ });
2708
+ if (!context) throw new Error("Failed to create offscreen context");
2709
+ context.__dpr = ctx.__dpr;
2710
+ context.width = width * ctx.__dpr;
2711
+ context.height = height * ctx.__dpr;
2712
+ context.__isMainContext = false;
2713
+ context.__imageOrigin = "corner";
2714
+ context.__rectangleOrigin = "corner";
2715
+ context.__canvasOrigin = "corner";
2716
+ context.__textFont = "sans-serif";
2717
+ context.__textWeight = "normal";
2718
+ context.__textStyle = "normal";
2719
+ context.__textSize = 120;
2720
+ context.__textLeading = void 0;
2721
+ context.__textAlignment = {
2722
+ horizontal: "left",
2723
+ vertical: "top"
2724
+ };
2725
+ context.__fillRule = "nonzero";
2726
+ if (!options?.ignoreFunctions) {
2727
+ context.Color = ctx.Color;
2728
+ context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
2729
+ context.Easing = ctx.Easing;
2730
+ context.Text = ctx.Text;
2731
+ Object.entries(KlintFunctions).forEach(([name, fn]) => {
2732
+ context[name] = fn(context);
2733
+ });
2734
+ }
2735
+ if (options?.origin) {
2736
+ context.__canvasOrigin = options.origin;
2737
+ if (options.origin === "center") {
2738
+ context.translate(context.width * 0.5, context.height * 0.5);
2739
+ }
2740
+ }
2741
+ if (callback) {
2742
+ callback(context);
2743
+ }
2744
+ if (options?.static === "true") {
2745
+ const base64 = offscreen.toDataURL();
2746
+ const img = new Image();
2747
+ img.src = base64;
2748
+ ctx.__offscreens.set(id, img);
2749
+ return img;
2750
+ }
2751
+ ctx.__offscreens.set(id, context);
2752
+ return context;
2753
+ },
2754
+ getOffscreen: (ctx) => (id) => {
2755
+ const offscreen = ctx.__offscreens.get(id);
2756
+ if (!offscreen)
2757
+ throw new Error(`No offscreen context found with id: ${id}`);
2758
+ return offscreen;
2759
+ }
2760
+ };
2761
+ var KlintFunctions = {
2762
+ extend: (ctx) => (name, data, enforceReplace = false) => {
2763
+ if (name in ctx && !enforceReplace) return;
2764
+ ctx[name] = data;
2765
+ },
2766
+ background: (ctx) => (color) => {
2767
+ ctx.resetTransform();
2768
+ ctx.push();
2769
+ if (color && color !== "transparent") {
2770
+ ctx.fillStyle = color;
2771
+ ctx.fillRect(0, 0, ctx.width, ctx.height);
2772
+ } else {
2773
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
2774
+ }
2775
+ ctx.pop();
2776
+ if (ctx.__canvasOrigin === "center")
2777
+ ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
2778
+ },
2779
+ reset: (ctx) => () => {
2780
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
1322
2781
  ctx.resetTransform();
1323
2782
  },
1324
2783
  clear: (ctx) => () => {
@@ -1365,9 +2824,12 @@ var KlintFunctions = {
1365
2824
  return true;
1366
2825
  },
1367
2826
  drawIfVisible: (ctx) => () => {
1368
- if (ctx.checkTransparency("fill")) ctx.fill();
2827
+ if (ctx.checkTransparency("fill")) ctx.fill(ctx.__fillRule || "nonzero");
1369
2828
  if (ctx.checkTransparency("stroke")) ctx.stroke();
1370
2829
  },
2830
+ fillRule: (ctx) => (rule) => {
2831
+ ctx.__fillRule = rule;
2832
+ },
1371
2833
  line: (ctx) => (x1, y1, x2, y2) => {
1372
2834
  if (!ctx.checkTransparency("stroke")) return;
1373
2835
  ctx.beginPath();
@@ -1429,17 +2891,29 @@ var KlintFunctions = {
1429
2891
  ctx.__currentContours = [];
1430
2892
  },
1431
2893
  beginContour: (ctx) => () => {
1432
- if (!ctx.__startedShape) return;
1433
- if (ctx.__startedContour && ctx.__currentContour?.length) {
1434
- ctx.__currentContours?.push([...ctx.__currentContour]);
1435
- }
2894
+ if (!ctx.__startedShape || ctx.__startedContour) return;
1436
2895
  ctx.__startedContour = true;
1437
2896
  ctx.__currentContour = [];
1438
2897
  },
1439
2898
  vertex: (ctx) => (x, y) => {
1440
2899
  if (!ctx.__startedShape) return;
1441
2900
  const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
1442
- points?.push([x, y]);
2901
+ points?.push({ type: "line", x, y });
2902
+ },
2903
+ bezierVertex: (ctx) => (cp1x, cp1y, cp2x, cp2y, x, y) => {
2904
+ if (!ctx.__startedShape) return;
2905
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2906
+ points?.push({ type: "bezier", cp1x, cp1y, cp2x, cp2y, x, y });
2907
+ },
2908
+ quadraticVertex: (ctx) => (cpx, cpy, x, y) => {
2909
+ if (!ctx.__startedShape) return;
2910
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2911
+ points?.push({ type: "quadratic", cpx, cpy, x, y });
2912
+ },
2913
+ arcVertex: (ctx) => (x1, y1, x2, y2, radius) => {
2914
+ if (!ctx.__startedShape) return;
2915
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
2916
+ points?.push({ type: "arc", x1, y1, x2, y2, radius });
1443
2917
  },
1444
2918
  endContour: (ctx) => (forceRevert = true) => {
1445
2919
  if (!ctx.__startedContour || !ctx.__currentContour?.length) return;
@@ -1457,17 +2931,38 @@ var KlintFunctions = {
1457
2931
  const points = ctx.__currentShape;
1458
2932
  if (!points?.length) return;
1459
2933
  const drawPath = (points2, close2 = false) => {
1460
- ctx.moveTo(points2[0][0], points2[0][1]);
1461
- for (let i = 1; i < points2.length; i++) {
1462
- ctx.lineTo(points2[i][0], points2[i][1]);
1463
- }
1464
- if (close2) {
1465
- const [firstX, firstY] = points2[0];
1466
- const lastPoint = points2[points2.length - 1];
1467
- if (lastPoint[0] !== firstX || lastPoint[1] !== firstY) {
1468
- ctx.lineTo(firstX, firstY);
2934
+ if (points2.length === 0) return;
2935
+ const firstPoint = points2[0];
2936
+ const startX = firstPoint.type === "line" ? firstPoint.x : firstPoint.type === "bezier" ? firstPoint.x : firstPoint.type === "quadratic" ? firstPoint.x : firstPoint.x2;
2937
+ const startY = firstPoint.type === "line" ? firstPoint.y : firstPoint.type === "bezier" ? firstPoint.y : firstPoint.type === "quadratic" ? firstPoint.y : firstPoint.y2;
2938
+ ctx.moveTo(startX, startY);
2939
+ for (let i = 0; i < points2.length; i++) {
2940
+ const point = points2[i];
2941
+ switch (point.type) {
2942
+ case "line":
2943
+ if (i > 0) ctx.lineTo(point.x, point.y);
2944
+ break;
2945
+ case "bezier":
2946
+ ctx.bezierCurveTo(
2947
+ point.cp1x,
2948
+ point.cp1y,
2949
+ point.cp2x,
2950
+ point.cp2y,
2951
+ point.x,
2952
+ point.y
2953
+ );
2954
+ break;
2955
+ case "quadratic":
2956
+ ctx.quadraticCurveTo(point.cpx, point.cpy, point.x, point.y);
2957
+ break;
2958
+ case "arc":
2959
+ ctx.arcTo(point.x1, point.y1, point.x2, point.y2, point.radius);
2960
+ break;
1469
2961
  }
1470
2962
  }
2963
+ if (close2 && points2.length > 1) {
2964
+ ctx.closePath();
2965
+ }
1471
2966
  };
1472
2967
  ctx.beginPath();
1473
2968
  drawPath(points, close);
@@ -1491,6 +2986,9 @@ var KlintFunctions = {
1491
2986
  addColorStop: () => (gradient, offset = 0, color = "#000") => {
1492
2987
  return gradient.addColorStop(offset, color);
1493
2988
  },
2989
+ PI: () => Math.PI,
2990
+ TWO_PI: () => Math.PI * 2,
2991
+ TAU: () => Math.PI * 2,
1494
2992
  constrain: () => (val, floor, ceil) => {
1495
2993
  return Math.max(floor, Math.min(val, ceil));
1496
2994
  },
@@ -1528,6 +3026,14 @@ var KlintFunctions = {
1528
3026
  const t = (n - A) / (B - A);
1529
3027
  return ctx.lerp(C, D, t, bounded);
1530
3028
  },
3029
+ bezierLerp: () => (a, b, c, d, t) => {
3030
+ const u = 1 - t;
3031
+ return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
3032
+ },
3033
+ bezierTangent: () => (a, b, c, d, t) => {
3034
+ const u = 1 - t;
3035
+ 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;
3036
+ },
1531
3037
  textFont: (ctx) => (font) => {
1532
3038
  ctx.__textFont = font;
1533
3039
  },
@@ -1563,7 +3069,8 @@ var KlintFunctions = {
1563
3069
  ctx.__textAlignment.vertical = vertical ?? ctx.__textAlignment.vertical;
1564
3070
  },
1565
3071
  textLeading: (ctx) => (spacing) => {
1566
- ctx.lineHeight = `${spacing}px`;
3072
+ ctx.__textLeading = spacing;
3073
+ return ctx.__textLeading;
1567
3074
  },
1568
3075
  computeFont: (ctx) => () => {
1569
3076
  ctx.computeTextStyle();
@@ -1582,10 +3089,110 @@ var KlintFunctions = {
1582
3089
  if (ctx.textBaseline !== ctx.__textAlignment.vertical) {
1583
3090
  ctx.textBaseline = ctx.__textAlignment.vertical;
1584
3091
  }
1585
- if (ctx.checkTransparency("fill"))
1586
- ctx.fillText(String(text), x, y, maxWidth);
1587
- if (ctx.checkTransparency("stroke"))
1588
- ctx.strokeText(String(text), x, y, maxWidth);
3092
+ const textString = String(text);
3093
+ if (textString.includes("\n")) {
3094
+ const lines = textString.split("\n");
3095
+ const firstLineMetrics = ctx.measureText(lines[0] || "M");
3096
+ const defaultLineHeight = firstLineMetrics.actualBoundingBoxAscent + firstLineMetrics.actualBoundingBoxDescent;
3097
+ const lineHeight = ctx.__textLeading ?? ctx.__textSize * 1.2;
3098
+ const totalHeight = lines.length * lineHeight - (lineHeight - defaultLineHeight);
3099
+ let startY = y;
3100
+ if (ctx.__textAlignment.vertical === "middle") {
3101
+ startY = y - totalHeight / 2 + defaultLineHeight / 2;
3102
+ } else if (ctx.__textAlignment.vertical === "bottom") {
3103
+ startY = y - totalHeight + defaultLineHeight;
3104
+ } else if (ctx.__textAlignment.vertical === "top") {
3105
+ startY = y + defaultLineHeight / 2;
3106
+ }
3107
+ lines.forEach((line, index) => {
3108
+ const lineY = startY + index * lineHeight;
3109
+ if (ctx.checkTransparency("fill"))
3110
+ ctx.fillText(line, x, lineY, maxWidth);
3111
+ if (ctx.checkTransparency("stroke"))
3112
+ ctx.strokeText(line, x, lineY, maxWidth);
3113
+ });
3114
+ } else {
3115
+ if (ctx.checkTransparency("fill"))
3116
+ ctx.fillText(textString, x, y, maxWidth);
3117
+ if (ctx.checkTransparency("stroke"))
3118
+ ctx.strokeText(textString, x, y, maxWidth);
3119
+ }
3120
+ },
3121
+ paragraph: (ctx) => (text, x, y, width, options) => {
3122
+ if (text === void 0) return;
3123
+ ctx.computeFont();
3124
+ const textString = String(text);
3125
+ const justification = options?.justification || "left";
3126
+ const overflow = options?.overflow || 0;
3127
+ const breakMode = options?.break || "words";
3128
+ const originalAlign = ctx.textAlign;
3129
+ const originalBaseline = ctx.textBaseline;
3130
+ if (justification === "center") {
3131
+ ctx.textAlign = "center";
3132
+ } else if (justification === "right") {
3133
+ ctx.textAlign = "right";
3134
+ } else {
3135
+ ctx.textAlign = "left";
3136
+ }
3137
+ ctx.textBaseline = "top";
3138
+ const tokens = breakMode === "letters" ? textString.split("") : textString.split(/\s+/);
3139
+ const lines = [];
3140
+ let currentLine = "";
3141
+ for (let i = 0; i < tokens.length; i++) {
3142
+ const token = tokens[i];
3143
+ const testLine = currentLine ? breakMode === "letters" ? currentLine + token : currentLine + " " + token : token;
3144
+ const metrics = ctx.measureText(testLine);
3145
+ if (metrics.width > width && currentLine !== "") {
3146
+ lines.push(currentLine);
3147
+ currentLine = token;
3148
+ } else {
3149
+ currentLine = testLine;
3150
+ }
3151
+ }
3152
+ if (currentLine) {
3153
+ lines.push(currentLine);
3154
+ }
3155
+ const lineHeight = ctx.__textLeading ?? ctx.__textSize * 1.2;
3156
+ let linesToDraw = lines;
3157
+ if (overflow > 0 && lines.length * lineHeight > overflow) {
3158
+ const maxLines = Math.floor(overflow / lineHeight);
3159
+ linesToDraw = lines.slice(0, maxLines);
3160
+ }
3161
+ linesToDraw.forEach((line, index) => {
3162
+ const lineY = y + index * lineHeight;
3163
+ let lineX = x;
3164
+ if (justification === "center") {
3165
+ lineX = x + width / 2;
3166
+ } else if (justification === "right") {
3167
+ lineX = x + width;
3168
+ } else if (justification === "justified" && index < linesToDraw.length - 1) {
3169
+ const words = line.split(/\s+/);
3170
+ if (words.length > 1) {
3171
+ const textWidth = ctx.measureText(words.join("")).width;
3172
+ const totalSpaceWidth = width - textWidth;
3173
+ const spaceWidth = totalSpaceWidth / (words.length - 1);
3174
+ let currentX = x;
3175
+ words.forEach((word, wordIndex) => {
3176
+ if (ctx.checkTransparency("fill")) {
3177
+ ctx.fillText(word, currentX, lineY);
3178
+ }
3179
+ if (ctx.checkTransparency("stroke")) {
3180
+ ctx.strokeText(word, currentX, lineY);
3181
+ }
3182
+ currentX += ctx.measureText(word).width + spaceWidth;
3183
+ });
3184
+ return;
3185
+ }
3186
+ }
3187
+ if (ctx.checkTransparency("fill")) {
3188
+ ctx.fillText(line, lineX, lineY);
3189
+ }
3190
+ if (ctx.checkTransparency("stroke")) {
3191
+ ctx.strokeText(line, lineX, lineY);
3192
+ }
3193
+ });
3194
+ ctx.textAlign = originalAlign;
3195
+ ctx.textBaseline = originalBaseline;
1589
3196
  },
1590
3197
  // DO NOT use putImageData for images you can draw : https://www.measurethat.net/Benchmarks/Show/9510/0/putimagedata-vs-drawimage
1591
3198
  image: (ctx) => (image, x, y, arg3, arg4, arg5, arg6, arg7, arg8) => {
@@ -1635,11 +3242,8 @@ var KlintFunctions = {
1635
3242
  return ctx.getImageData(0, 0, ctx.width, ctx.height);
1636
3243
  },
1637
3244
  updatePixels: (ctx) => (pixels) => {
1638
- const imageData = new ImageData(
1639
- pixels instanceof Uint8ClampedArray ? pixels : new Uint8ClampedArray(pixels),
1640
- ctx.width,
1641
- ctx.height
1642
- );
3245
+ const pixelArray = pixels instanceof Uint8ClampedArray ? new Uint8ClampedArray(pixels) : new Uint8ClampedArray(pixels);
3246
+ const imageData = new ImageData(pixelArray, ctx.width, ctx.height);
1643
3247
  ctx.putImageData(imageData, 0, 0);
1644
3248
  },
1645
3249
  readPixels: (ctx) => (x, y, w = 1, h = 1) => {
@@ -1655,7 +3259,7 @@ var KlintFunctions = {
1655
3259
  ctx.globalAlpha = ctx.constrain(value, 0, 1);
1656
3260
  },
1657
3261
  blend: (ctx) => (blend) => {
1658
- ctx.globalCompositeOperation = blend;
3262
+ ctx.globalCompositeOperation = blend === "default" ? "source-over" : blend;
1659
3263
  },
1660
3264
  setCanvasOrigin: (ctx) => (type) => {
1661
3265
  ctx.__canvasOrigin = type;
@@ -1693,10 +3297,73 @@ var KlintFunctions = {
1693
3297
  if (ctx.__canvasOrigin === "center") {
1694
3298
  ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
1695
3299
  }
3300
+ },
3301
+ clipTo: (ctx) => (callback, fillRule) => {
3302
+ const originalFill = ctx.fill;
3303
+ const originalStroke = ctx.stroke;
3304
+ const originalDrawIfVisible = ctx.drawIfVisible;
3305
+ const originalBeginPath = ctx.beginPath;
3306
+ ctx.fill = () => {
3307
+ };
3308
+ ctx.stroke = () => {
3309
+ };
3310
+ ctx.drawIfVisible = () => {
3311
+ };
3312
+ let allowBeginPath = true;
3313
+ ctx.beginPath = () => {
3314
+ if (allowBeginPath) {
3315
+ originalBeginPath.call(ctx);
3316
+ allowBeginPath = false;
3317
+ }
3318
+ };
3319
+ ctx.beginPath();
3320
+ callback(ctx);
3321
+ ctx.clip(fillRule || ctx.__fillRule || "nonzero");
3322
+ ctx.beginPath = originalBeginPath;
3323
+ ctx.drawIfVisible = originalDrawIfVisible;
3324
+ ctx.fill = originalFill;
3325
+ ctx.stroke = originalStroke;
3326
+ },
3327
+ canIuseFilter: (ctx) => () => {
3328
+ return ctx.filter !== void 0 && ctx.filter !== null;
3329
+ },
3330
+ blur: (ctx) => (radius) => {
3331
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3332
+ ctx.filter = `blur(${radius}px)`;
3333
+ },
3334
+ SVGfilter: (ctx) => (url) => {
3335
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3336
+ if (!url || !url.startsWith("url(")) return;
3337
+ ctx.filter = url;
3338
+ },
3339
+ dropShadow: (ctx) => (offsetX, offsetY, blurRadius, color) => {
3340
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3341
+ ctx.filter = `drop-shadow(${offsetX}px ${offsetY}px ${blurRadius}px ${color})`;
3342
+ },
3343
+ grayscale: (ctx) => (amount) => {
3344
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3345
+ const value = ctx.constrain(amount, 0, 1);
3346
+ ctx.filter = `grayscale(${value})`;
3347
+ },
3348
+ hue: (ctx) => (angle) => {
3349
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3350
+ const degrees = angle * 180 / Math.PI;
3351
+ ctx.filter = `hue-rotate(${degrees}deg)`;
3352
+ },
3353
+ invert: (ctx) => (amount) => {
3354
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3355
+ const value = ctx.constrain(amount, 0, 1);
3356
+ ctx.filter = `invert(${value})`;
3357
+ },
3358
+ filterOpacity: (ctx) => (value) => {
3359
+ if (ctx.filter === void 0 || ctx.filter === null) return;
3360
+ const amount = ctx.constrain(value, 0, 1);
3361
+ ctx.filter = `opacity(${amount})`;
1696
3362
  }
1697
3363
  };
1698
3364
 
1699
3365
  // src/useKlint.tsx
3366
+ var import_meta2 = {};
1700
3367
  var DEFAULT_MOUSE_STATE = {
1701
3368
  x: 0,
1702
3369
  y: 0,
@@ -1730,13 +3397,50 @@ var DEFAULT_GESTURE_STATE = {
1730
3397
  lastX: 0,
1731
3398
  lastY: 0
1732
3399
  };
3400
+ var DEFAULT_KEYBOARD_STATE = {
3401
+ pressedKeys: /* @__PURE__ */ new Set(),
3402
+ modifiers: {
3403
+ alt: false,
3404
+ shift: false,
3405
+ ctrl: false,
3406
+ meta: false
3407
+ },
3408
+ lastKey: null,
3409
+ lastKeyTime: 0
3410
+ };
1733
3411
  function useKlint() {
1734
3412
  const contextRef = (0, import_react2.useRef)(null);
1735
3413
  const mouseRef = (0, import_react2.useRef)(null);
1736
3414
  const scrollRef = (0, import_react2.useRef)(null);
1737
3415
  const gestureRef = (0, import_react2.useRef)(null);
3416
+ const keyboardRef = (0, import_react2.useRef)(null);
1738
3417
  const useDev = () => {
1739
- return;
3418
+ (0, import_react2.useEffect)(() => {
3419
+ if (process.env.NODE_ENV === "development") {
3420
+ if (typeof import_meta2 !== "undefined" && import_meta2.hot) {
3421
+ console.log("[Klint] hot updated - clearing non-context state");
3422
+ if (contextRef.current) {
3423
+ const ctx = contextRef.current;
3424
+ ctx.frame = 0;
3425
+ ctx.time = 0;
3426
+ ctx.__offscreens?.clear();
3427
+ ctx.__startedShape = false;
3428
+ ctx.__currentShape = null;
3429
+ ctx.__startedContour = false;
3430
+ ctx.__currentContours = null;
3431
+ ctx.__currentContour = null;
3432
+ ctx.__isReadyToDraw = true;
3433
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
3434
+ }
3435
+ }
3436
+ }
3437
+ }, []);
3438
+ (0, import_react2.useEffect)(() => {
3439
+ if (process.env.NODE_ENV === "development" && contextRef.current) {
3440
+ console.log("[Klint] hot updated - clearing context state");
3441
+ contextRef.current.__isReadyToDraw = true;
3442
+ }
3443
+ });
1740
3444
  };
1741
3445
  const KlintImage = () => {
1742
3446
  const imagesRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
@@ -1810,6 +3514,8 @@ function useKlint() {
1810
3514
  if (!contextRef.current?.canvas) return;
1811
3515
  const canvas = contextRef.current.canvas;
1812
3516
  const ctx = contextRef.current;
3517
+ const controller = new AbortController();
3518
+ const { signal } = controller;
1813
3519
  const updateMousePosition = (e) => {
1814
3520
  const rect = canvas.getBoundingClientRect();
1815
3521
  const dpr = window.devicePixelRatio || 1;
@@ -1848,20 +3554,13 @@ function useKlint() {
1848
3554
  const handleClick = (e) => {
1849
3555
  if (clickCallbackRef.current) clickCallbackRef.current(ctx, e);
1850
3556
  };
1851
- canvas.addEventListener("mousemove", updateMousePosition);
1852
- canvas.addEventListener("mousedown", handleMouseDown);
1853
- canvas.addEventListener("mouseup", handleMouseUp);
1854
- canvas.addEventListener("mouseenter", handleMouseEnter);
1855
- canvas.addEventListener("mouseleave", handleMouseLeave);
1856
- canvas.addEventListener("click", handleClick);
1857
- return () => {
1858
- canvas.removeEventListener("mousemove", updateMousePosition);
1859
- canvas.removeEventListener("mousedown", handleMouseDown);
1860
- canvas.removeEventListener("mouseup", handleMouseUp);
1861
- canvas.removeEventListener("mouseenter", handleMouseEnter);
1862
- canvas.removeEventListener("mouseleave", handleMouseLeave);
1863
- canvas.removeEventListener("click", handleClick);
1864
- };
3557
+ canvas.addEventListener("mousemove", updateMousePosition, { signal });
3558
+ canvas.addEventListener("mousedown", handleMouseDown, { signal });
3559
+ canvas.addEventListener("mouseup", handleMouseUp, { signal });
3560
+ canvas.addEventListener("mouseenter", handleMouseEnter, { signal });
3561
+ canvas.addEventListener("mouseleave", handleMouseLeave, { signal });
3562
+ canvas.addEventListener("click", handleClick, { signal });
3563
+ return () => controller.abort();
1865
3564
  });
1866
3565
  return {
1867
3566
  mouse: mouseRef.current,
@@ -1881,6 +3580,8 @@ function useKlint() {
1881
3580
  if (!contextRef.current?.canvas) return;
1882
3581
  const canvas = contextRef.current.canvas;
1883
3582
  const ctx = contextRef.current;
3583
+ const controller = new AbortController();
3584
+ const { signal } = controller;
1884
3585
  const handleScroll = (e) => {
1885
3586
  e.preventDefault();
1886
3587
  if (!scrollRef.current) return;
@@ -1893,8 +3594,8 @@ function useKlint() {
1893
3594
  scrollCallbackRef.current(ctx, scrollRef.current, e);
1894
3595
  }
1895
3596
  };
1896
- canvas.addEventListener("wheel", handleScroll);
1897
- return () => canvas.removeEventListener("wheel", handleScroll);
3597
+ canvas.addEventListener("wheel", handleScroll, { signal });
3598
+ return () => controller.abort();
1898
3599
  });
1899
3600
  return {
1900
3601
  scroll: scrollRef.current,
@@ -2040,18 +3741,19 @@ function useKlint() {
2040
3741
  touchEndCallbackRef.current(ctx, e, gestureRef.current);
2041
3742
  }
2042
3743
  };
3744
+ const controller = new AbortController();
3745
+ const { signal } = controller;
2043
3746
  canvas.addEventListener("touchstart", handleTouchStart, {
2044
- passive: false
3747
+ passive: false,
3748
+ signal
2045
3749
  });
2046
- canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
2047
- canvas.addEventListener("touchend", handleTouchEnd);
2048
- canvas.addEventListener("touchcancel", handleTouchCancel);
2049
- return () => {
2050
- canvas.removeEventListener("touchstart", handleTouchStart);
2051
- canvas.removeEventListener("touchmove", handleTouchMove);
2052
- canvas.removeEventListener("touchend", handleTouchEnd);
2053
- canvas.removeEventListener("touchcancel", handleTouchCancel);
2054
- };
3750
+ canvas.addEventListener("touchmove", handleTouchMove, {
3751
+ passive: false,
3752
+ signal
3753
+ });
3754
+ canvas.addEventListener("touchend", handleTouchEnd, { signal });
3755
+ canvas.addEventListener("touchcancel", handleTouchCancel, { signal });
3756
+ return () => controller.abort();
2055
3757
  }, []);
2056
3758
  return {
2057
3759
  gesture: gestureRef.current,
@@ -2064,6 +3766,315 @@ function useKlint() {
2064
3766
  onTouchEnd: (callback) => touchEndCallbackRef.current = callback
2065
3767
  };
2066
3768
  };
3769
+ const KlintKeyboard = () => {
3770
+ if (!keyboardRef.current) {
3771
+ keyboardRef.current = { ...DEFAULT_KEYBOARD_STATE };
3772
+ }
3773
+ const keyPressedCallbackRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3774
+ const keyReleasedCallbackRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3775
+ const keyComboCallbackRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3776
+ (0, import_react2.useEffect)(() => {
3777
+ if (!contextRef.current) return;
3778
+ const ctx = contextRef.current;
3779
+ const controller = new AbortController();
3780
+ const { signal } = controller;
3781
+ const updateModifiers = (e) => {
3782
+ if (!keyboardRef.current) return;
3783
+ keyboardRef.current.modifiers.alt = e.altKey;
3784
+ keyboardRef.current.modifiers.shift = e.shiftKey;
3785
+ keyboardRef.current.modifiers.ctrl = e.ctrlKey;
3786
+ keyboardRef.current.modifiers.meta = e.metaKey;
3787
+ };
3788
+ const normalizeKey2 = (key) => {
3789
+ const keyMap = {
3790
+ " ": "Space",
3791
+ Control: "Ctrl",
3792
+ Escape: "Esc"
3793
+ };
3794
+ return keyMap[key] || key;
3795
+ };
3796
+ const handleKeyDown = (e) => {
3797
+ if (!keyboardRef.current) return;
3798
+ const normalizedKey = normalizeKey2(e.key);
3799
+ keyboardRef.current.pressedKeys.add(normalizedKey);
3800
+ keyboardRef.current.lastKey = normalizedKey;
3801
+ keyboardRef.current.lastKeyTime = performance.now();
3802
+ updateModifiers(e);
3803
+ const keyCallback = keyPressedCallbackRef.current.get(normalizedKey);
3804
+ if (keyCallback) {
3805
+ keyCallback(ctx, e);
3806
+ }
3807
+ const pressedKeysArray = Array.from(
3808
+ keyboardRef.current.pressedKeys
3809
+ ).sort();
3810
+ const comboKey = pressedKeysArray.join("+");
3811
+ const comboCallback = keyComboCallbackRef.current.get(comboKey);
3812
+ if (comboCallback) {
3813
+ comboCallback(ctx, e);
3814
+ }
3815
+ };
3816
+ const handleKeyUp = (e) => {
3817
+ if (!keyboardRef.current) return;
3818
+ const normalizedKey = normalizeKey2(e.key);
3819
+ keyboardRef.current.pressedKeys.delete(normalizedKey);
3820
+ updateModifiers(e);
3821
+ const keyCallback = keyReleasedCallbackRef.current.get(normalizedKey);
3822
+ if (keyCallback) {
3823
+ keyCallback(ctx, e);
3824
+ }
3825
+ };
3826
+ window.addEventListener("keydown", handleKeyDown, { signal });
3827
+ window.addEventListener("keyup", handleKeyUp, { signal });
3828
+ return () => controller.abort();
3829
+ }, []);
3830
+ const createComboKey = (keys) => {
3831
+ return keys.map(normalizeKey).sort().join("+");
3832
+ };
3833
+ const normalizeKey = (key) => {
3834
+ const keyMap = {
3835
+ " ": "Space",
3836
+ Control: "Ctrl",
3837
+ Escape: "Esc"
3838
+ };
3839
+ return keyMap[key] || key;
3840
+ };
3841
+ return {
3842
+ keyboard: keyboardRef.current,
3843
+ // Register callback for single key press
3844
+ keyPressed: (key, callback) => {
3845
+ keyPressedCallbackRef.current.set(normalizeKey(key), callback);
3846
+ },
3847
+ // Register callback for single key release
3848
+ keyReleased: (key, callback) => {
3849
+ keyReleasedCallbackRef.current.set(normalizeKey(key), callback);
3850
+ },
3851
+ // Register callback for key combination (e.g., ['Alt', 'Shift'])
3852
+ keyCombo: (keys, callback) => {
3853
+ const comboKey = createComboKey(keys);
3854
+ keyComboCallbackRef.current.set(comboKey, callback);
3855
+ },
3856
+ // Utility functions
3857
+ isPressed: (key) => {
3858
+ return keyboardRef.current?.pressedKeys.has(normalizeKey(key)) || false;
3859
+ },
3860
+ arePressed: (keys) => {
3861
+ if (!keyboardRef.current) return false;
3862
+ return keys.every(
3863
+ (key) => keyboardRef.current.pressedKeys.has(normalizeKey(key))
3864
+ );
3865
+ },
3866
+ // Clear all callbacks
3867
+ clearCallbacks: () => {
3868
+ keyPressedCallbackRef.current.clear();
3869
+ keyReleasedCallbackRef.current.clear();
3870
+ keyComboCallbackRef.current.clear();
3871
+ }
3872
+ };
3873
+ };
3874
+ const KlintTimeline = () => {
3875
+ const timelinesRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3876
+ const callbacksRef = (0, import_react2.useRef)({ start: [], end: [], loop: [] });
3877
+ const Timeline = {
3878
+ create: (setup, options = {}) => {
3879
+ const tracks = /* @__PURE__ */ new Map();
3880
+ const parents = /* @__PURE__ */ new Set();
3881
+ let currentProgress = 0;
3882
+ let hasStarted = false;
3883
+ let hasEnded = false;
3884
+ const defaultEasing = options.defaultEasing || ((t) => t);
3885
+ const defaultLoop = options.defaultLoop || 0;
3886
+ const executeCallback = (callback, errorMsg = "Callback error") => {
3887
+ try {
3888
+ callback();
3889
+ } catch (e) {
3890
+ console.warn(`${errorMsg}:`, e);
3891
+ }
3892
+ };
3893
+ function createKeyframes() {
3894
+ const segments = [];
3895
+ let currentPos = 0;
3896
+ let loopCount = defaultLoop;
3897
+ let parentTrack = void 0;
3898
+ const parseEasingCallback = (easing, callback) => {
3899
+ if (typeof easing === "function" && typeof callback === "undefined") {
3900
+ if (easing.length === 0) {
3901
+ return {
3902
+ easing: defaultEasing,
3903
+ callback: easing
3904
+ };
3905
+ }
3906
+ return {
3907
+ easing,
3908
+ callback: void 0
3909
+ };
3910
+ }
3911
+ return {
3912
+ easing: easing || defaultEasing,
3913
+ callback
3914
+ };
3915
+ };
3916
+ const builder = {
3917
+ start(value, delay = 0, callback) {
3918
+ segments.push({ pos: delay, value, callback, type: "start" });
3919
+ currentPos = delay;
3920
+ return builder;
3921
+ },
3922
+ at(progress, value, easing, callback) {
3923
+ const { easing: finalEasing, callback: finalCallback } = parseEasingCallback(easing, callback);
3924
+ segments.push({
3925
+ pos: progress,
3926
+ value,
3927
+ easing: finalEasing,
3928
+ callback: finalCallback,
3929
+ type: "tween"
3930
+ });
3931
+ currentPos = progress;
3932
+ return builder;
3933
+ },
3934
+ then(value, duration, easing, callback) {
3935
+ const { easing: finalEasing, callback: finalCallback } = parseEasingCallback(easing, callback);
3936
+ const nextPos = currentPos + duration;
3937
+ segments.push({
3938
+ pos: nextPos,
3939
+ value,
3940
+ easing: finalEasing,
3941
+ callback: finalCallback,
3942
+ type: "tween"
3943
+ });
3944
+ currentPos = nextPos;
3945
+ return builder;
3946
+ },
3947
+ loop(count = Infinity) {
3948
+ loopCount = count;
3949
+ return builder;
3950
+ },
3951
+ _compile: () => {
3952
+ segments.sort((a, b) => a.pos - b.pos);
3953
+ return { segments, loopCount, parentTrack };
3954
+ }
3955
+ };
3956
+ return builder;
3957
+ }
3958
+ function interpolateTrack(compiled, progress) {
3959
+ const { segments } = compiled;
3960
+ if (!segments.length) return 0;
3961
+ progress = Math.max(0, Math.min(1, progress));
3962
+ const valueSegments = segments.filter(
3963
+ (seg) => seg.type !== "callback"
3964
+ );
3965
+ if (!valueSegments.length) return 0;
3966
+ let prevSeg = valueSegments[0];
3967
+ for (let i = 1; i < valueSegments.length; i++) {
3968
+ const seg = valueSegments[i];
3969
+ if (progress <= seg.pos) {
3970
+ if (seg.type === "tween" && prevSeg.value !== void 0 && seg.value !== void 0) {
3971
+ const t = (progress - prevSeg.pos) / (seg.pos - prevSeg.pos);
3972
+ const easedT = seg.easing ? seg.easing(t) : t;
3973
+ return prevSeg.value + (seg.value - prevSeg.value) * easedT;
3974
+ }
3975
+ return seg.value || 0;
3976
+ }
3977
+ if (seg.value !== void 0) {
3978
+ prevSeg = seg;
3979
+ }
3980
+ }
3981
+ return prevSeg.value || 0;
3982
+ }
3983
+ const timeline = {
3984
+ track: (keyframesOrFn) => {
3985
+ const compiled = typeof keyframesOrFn === "function" ? timeline.keyframes(keyframesOrFn) : keyframesOrFn;
3986
+ const track = {
3987
+ ...compiled,
3988
+ currentValue: 0,
3989
+ getValue: (progress) => interpolateTrack(compiled, progress),
3990
+ get current() {
3991
+ return this.currentValue;
3992
+ },
3993
+ value: function() {
3994
+ return this.currentValue;
3995
+ }
3996
+ };
3997
+ tracks.set(track, compiled);
3998
+ return track;
3999
+ },
4000
+ keyframes: (fn) => {
4001
+ const kf = createKeyframes();
4002
+ fn(kf);
4003
+ return kf._compile();
4004
+ },
4005
+ stagger: (count, offset, keyframesFn) => {
4006
+ const compiled = timeline.keyframes(keyframesFn);
4007
+ return Array.from({ length: count }, (_, i) => {
4008
+ const track = {
4009
+ ...compiled,
4010
+ currentValue: 0,
4011
+ staggerDelay: i * offset,
4012
+ getValue: (progress) => {
4013
+ const staggeredProgress = Math.max(0, progress - i * offset);
4014
+ const normalizedProgress = Math.min(
4015
+ 1,
4016
+ staggeredProgress / (1 - i * offset)
4017
+ );
4018
+ return normalizedProgress > 0 ? interpolateTrack(compiled, normalizedProgress) : 0;
4019
+ },
4020
+ get current() {
4021
+ return this.currentValue;
4022
+ },
4023
+ value: function() {
4024
+ return this.currentValue;
4025
+ }
4026
+ };
4027
+ tracks.set(track, compiled);
4028
+ return track;
4029
+ });
4030
+ },
4031
+ update: (progress) => {
4032
+ const prevProgress = currentProgress;
4033
+ currentProgress = progress;
4034
+ if (!hasStarted && progress > 0) {
4035
+ hasStarted = true;
4036
+ callbacksRef.current.start.forEach(
4037
+ (cb) => executeCallback(cb, "Start callback error")
4038
+ );
4039
+ }
4040
+ if (!hasEnded && progress >= 1) {
4041
+ hasEnded = true;
4042
+ callbacksRef.current.end.forEach(
4043
+ (cb) => executeCallback(cb, "End callback error")
4044
+ );
4045
+ }
4046
+ if (progress < prevProgress) {
4047
+ if (progress === 0) {
4048
+ hasStarted = false;
4049
+ hasEnded = false;
4050
+ } else if (progress < 1) {
4051
+ hasEnded = false;
4052
+ }
4053
+ }
4054
+ for (const [track, compiled] of tracks) {
4055
+ track.currentValue = track.getValue(progress);
4056
+ if (compiled.segments) {
4057
+ compiled.segments.forEach((seg) => {
4058
+ if (seg.callback && seg.pos <= progress && seg.pos > prevProgress) {
4059
+ executeCallback(seg.callback);
4060
+ }
4061
+ });
4062
+ }
4063
+ }
4064
+ },
4065
+ progress: () => currentProgress
4066
+ };
4067
+ const result = setup(timeline);
4068
+ return { ...result, update: timeline.update };
4069
+ }
4070
+ };
4071
+ return {
4072
+ Timeline,
4073
+ onStart: (fn) => callbacksRef.current.start.push(fn),
4074
+ onEnd: (fn) => callbacksRef.current.end.push(fn),
4075
+ onLoop: (fn) => callbacksRef.current.loop.push(fn)
4076
+ };
4077
+ };
2067
4078
  const KlintWindow = () => {
2068
4079
  const resizeCallbackRef = (0, import_react2.useRef)(
2069
4080
  null
@@ -2074,6 +4085,8 @@ function useKlint() {
2074
4085
  (0, import_react2.useEffect)(() => {
2075
4086
  if (!contextRef.current) return;
2076
4087
  const ctx = contextRef.current;
4088
+ const controller = new AbortController();
4089
+ const { signal } = controller;
2077
4090
  const handleResize = () => {
2078
4091
  if (resizeCallbackRef.current) resizeCallbackRef.current(ctx);
2079
4092
  };
@@ -2089,19 +4102,13 @@ function useKlint() {
2089
4102
  visibilityChangeCallbackRef.current(ctx, isVisible);
2090
4103
  }
2091
4104
  };
2092
- window.addEventListener("resize", handleResize);
2093
- window.addEventListener("blur", handleBlur);
2094
- window.addEventListener("focus", handleFocus);
2095
- document.addEventListener("visibilitychange", handleVisibilityChange);
2096
- return () => {
2097
- window.removeEventListener("resize", handleResize);
2098
- window.removeEventListener("blur", handleBlur);
2099
- window.removeEventListener("focus", handleFocus);
2100
- document.removeEventListener(
2101
- "visibilitychange",
2102
- handleVisibilityChange
2103
- );
2104
- };
4105
+ window.addEventListener("resize", handleResize, { signal });
4106
+ window.addEventListener("blur", handleBlur, { signal });
4107
+ window.addEventListener("focus", handleFocus, { signal });
4108
+ document.addEventListener("visibilitychange", handleVisibilityChange, {
4109
+ signal
4110
+ });
4111
+ return () => controller.abort();
2105
4112
  }, []);
2106
4113
  return {
2107
4114
  onResize: (callback) => resizeCallbackRef.current = callback,
@@ -2124,20 +4131,27 @@ function useKlint() {
2124
4131
  context.__textWeight = "normal";
2125
4132
  context.__textStyle = "normal";
2126
4133
  context.__textSize = 72;
4134
+ context.__textLeading = void 0;
2127
4135
  context.__textAlignment = {
2128
4136
  horizontal: "left",
2129
4137
  vertical: "top"
2130
4138
  };
4139
+ context.__fillRule = "nonzero";
2131
4140
  context.__offscreens = /* @__PURE__ */ new Map();
2132
4141
  context.__isPlaying = true;
2133
4142
  context.__currentContext = context;
2134
4143
  context.Color = new Color_default();
2135
4144
  context.createVector = (x = 0, y = 0) => new Vector_default(x, y);
2136
- context.Easing = new Easing_default(context);
2137
- context.State = new State_default();
2138
- context.Time = new Time_default(context);
4145
+ context.Vector = new Vector_default();
4146
+ context.Easing = new Easing_default();
2139
4147
  context.Text = new Text_default(context);
2140
4148
  context.Thing = new Thing_default(context);
4149
+ context.Grid = new Grid_default(context);
4150
+ context.Strip = new Strip_default(context);
4151
+ context.Noise = new Noise_default(context);
4152
+ context.Hotspot = new Hotspot_default(context);
4153
+ context.Performance = new Performance_default(context);
4154
+ context.SSR = new SSR_default(context);
2141
4155
  Object.entries(KlintCoreFunctions).forEach(([name, fn]) => {
2142
4156
  context[name] = fn(context);
2143
4157
  });
@@ -2168,6 +4182,30 @@ function useKlint() {
2168
4182
  contextRef.current.__isPlaying = !contextRef.current.__isPlaying;
2169
4183
  }
2170
4184
  }, []);
4185
+ const KlintPerformance = () => {
4186
+ const metricsRef = (0, import_react2.useRef)(null);
4187
+ (0, import_react2.useEffect)(() => {
4188
+ if (!contextRef.current?.__performance) return;
4189
+ const updateMetrics = () => {
4190
+ if (contextRef.current?.__performance) {
4191
+ metricsRef.current = { ...contextRef.current.__performance };
4192
+ }
4193
+ };
4194
+ const interval = setInterval(updateMetrics, 100);
4195
+ return () => clearInterval(interval);
4196
+ }, []);
4197
+ return {
4198
+ metrics: metricsRef.current,
4199
+ getMetrics: () => contextRef.current?.__performance || null,
4200
+ reset: () => {
4201
+ if (contextRef.current?.__performance) {
4202
+ contextRef.current.__performance.droppedFrames = 0;
4203
+ contextRef.current.__performance.minFrameTime = Infinity;
4204
+ contextRef.current.__performance.maxFrameTime = 0;
4205
+ }
4206
+ }
4207
+ };
4208
+ };
2171
4209
  return {
2172
4210
  context: {
2173
4211
  context: contextRef.current,
@@ -2176,8 +4214,11 @@ function useKlint() {
2176
4214
  KlintMouse,
2177
4215
  KlintScroll,
2178
4216
  KlintGesture,
4217
+ KlintKeyboard,
2179
4218
  KlintWindow,
2180
4219
  KlintImage,
4220
+ KlintTimeline,
4221
+ KlintPerformance,
2181
4222
  togglePlay,
2182
4223
  useDev
2183
4224
  };
@@ -2224,10 +4265,21 @@ var useStorage = (initialProps = {}) => {
2224
4265
  // Annotate the CommonJS export names for ESM import in node:
2225
4266
  0 && (module.exports = {
2226
4267
  CONFIG_PROPS,
4268
+ Color,
2227
4269
  EPSILON,
4270
+ Easing,
4271
+ Grid,
4272
+ Hotspot,
2228
4273
  Klint,
2229
4274
  KlintCoreFunctions,
2230
4275
  KlintFunctions,
4276
+ Noise,
4277
+ Performance,
4278
+ SSR,
4279
+ Strip,
4280
+ Text,
4281
+ Thing,
4282
+ Vector,
2231
4283
  useKlint,
2232
4284
  useProps,
2233
4285
  useStorage