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