@shopify/klint 0.0.4

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 ADDED
@@ -0,0 +1,1135 @@
1
+ // src/Klint.tsx
2
+ import React, { useRef, useEffect, useState, useCallback } from "react";
3
+ var DEFAULT_FPS = 60;
4
+ var DEFAULT_ALT = "A beautiful artwork made with Klint Canvas";
5
+ var EPSILON = 1e-4;
6
+ var DEFAULT_OPTIONS = {
7
+ alpha: "true",
8
+ ignoreResize: "false",
9
+ noloop: "false",
10
+ static: "false",
11
+ nocanvas: "false",
12
+ unsafemode: "false",
13
+ willreadfrequently: "false",
14
+ fps: DEFAULT_FPS,
15
+ origin: "corner"
16
+ };
17
+ var CONFIG_PROPS = [
18
+ "lineWidth",
19
+ "strokeStyle",
20
+ "lineJoin",
21
+ "lineCap",
22
+ "fillStyle",
23
+ "font",
24
+ "textAlign",
25
+ "textBaseline",
26
+ "textRendering",
27
+ "wordSpacing",
28
+ "letterSpacing",
29
+ "globalAlpha",
30
+ "globalCompositeOperation",
31
+ "origin",
32
+ "transform",
33
+ "__imageOrigin",
34
+ "__rectangleOrigin",
35
+ "__textFont",
36
+ "__textWeight",
37
+ "__textStyle",
38
+ "__textSize",
39
+ "__textAlignment",
40
+ "__isPlaying"
41
+ ];
42
+ function useAnimate(contextRef, draw, isVisible) {
43
+ const animationFrameId = useRef(0);
44
+ const animate = useCallback(
45
+ (timestamp = 0) => {
46
+ if (!contextRef.current || !isVisible)
47
+ return;
48
+ if (!contextRef.current.__isReadyToDraw)
49
+ return;
50
+ if (!contextRef.current.__isPlaying) {
51
+ return;
52
+ }
53
+ const context = contextRef.current;
54
+ const now = timestamp;
55
+ const target = 1e3 / context.fps;
56
+ if (!context.__lastTargetTime) {
57
+ context.__lastTargetTime = now;
58
+ context.__lastRealTime = now;
59
+ }
60
+ const sinceLast = now - context.__lastTargetTime;
61
+ const epsilon = 5;
62
+ if (sinceLast >= target - epsilon) {
63
+ context.deltaTime = now - context.__lastRealTime;
64
+ draw(context);
65
+ if (context.time > 1e7)
66
+ context.time = 0;
67
+ if (context.frame > 1e7)
68
+ context.frame = 0;
69
+ context.time += context.deltaTime / DEFAULT_FPS;
70
+ context.frame++;
71
+ context.__lastTargetTime = now;
72
+ context.__lastRealTime = now;
73
+ }
74
+ animationFrameId.current = requestAnimationFrame(animate);
75
+ },
76
+ [draw, isVisible, contextRef]
77
+ );
78
+ return {
79
+ animate,
80
+ animationFrameId
81
+ };
82
+ }
83
+ function Klint({
84
+ context,
85
+ setup,
86
+ draw,
87
+ options = {},
88
+ preload,
89
+ onVisible
90
+ }) {
91
+ const canvasRef = useRef(null);
92
+ const containerRef = useRef(null);
93
+ const contextRef = useRef(null);
94
+ const intersectionObserverRef = useRef(null);
95
+ const resizeObserverRef = useRef(null);
96
+ const [isVisible, setIsVisible] = useState(true);
97
+ const __options = {
98
+ ...DEFAULT_OPTIONS,
99
+ ...options
100
+ };
101
+ const [toStaticImage, setStaticImage] = useState(null);
102
+ const initContext = context?.initCoreContext;
103
+ const { animate, animationFrameId } = useAnimate(contextRef, draw, isVisible);
104
+ const updateCanvasSize = (shouldRedraw = false) => {
105
+ if (!containerRef.current || !contextRef.current || !canvasRef.current)
106
+ return;
107
+ const container = containerRef.current;
108
+ const context2 = contextRef.current;
109
+ const canvas = canvasRef.current;
110
+ const { width, height } = container.getBoundingClientRect();
111
+ const config = context2.saveConfig();
112
+ context2.dpr = context2.__dpr;
113
+ canvas.width = context2.width = ~~(width * context2.__dpr);
114
+ canvas.height = context2.height = ~~(height * context2.__dpr);
115
+ canvas.style.width = `${width}px`;
116
+ canvas.style.height = `${height}px`;
117
+ context2.restoreConfig(config);
118
+ if (__options.origin === "center") {
119
+ context2.translate(canvas.width * 0.5, canvas.height * 0.5);
120
+ }
121
+ if (shouldRedraw)
122
+ draw(context2);
123
+ };
124
+ useEffect(() => {
125
+ if (!canvasRef.current || !containerRef.current)
126
+ return;
127
+ const canvas = canvasRef.current;
128
+ const container = containerRef.current;
129
+ const dpr = window.devicePixelRatio || 3;
130
+ contextRef.current = initContext ? initContext(canvas, __options) : null;
131
+ const context2 = contextRef.current;
132
+ if (!context2)
133
+ return;
134
+ context2.__dpr = dpr;
135
+ if (__options.fps && __options.fps !== context2.fps) {
136
+ context2.fps = __options.fps;
137
+ }
138
+ updateCanvasSize();
139
+ if (__options.ignoreResize !== "true") {
140
+ resizeObserverRef.current = new ResizeObserver(() => {
141
+ updateCanvasSize(context2.__isReadyToDraw);
142
+ });
143
+ resizeObserverRef.current.observe(container);
144
+ }
145
+ intersectionObserverRef.current = new IntersectionObserver(
146
+ (entries) => {
147
+ entries.forEach((entry) => {
148
+ setIsVisible(entry.isIntersecting);
149
+ onVisible?.(context2);
150
+ });
151
+ },
152
+ { threshold: 0.1, root: null, rootMargin: "50px" }
153
+ );
154
+ intersectionObserverRef.current.observe(container);
155
+ const initializeKlint = async () => {
156
+ if (!context2)
157
+ return;
158
+ const handleStaticMode = () => {
159
+ try {
160
+ const imageUrl = canvas.toDataURL("image/png");
161
+ setStaticImage(imageUrl);
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ throw new Error(`Klint draw error in static mode: ${message}`);
165
+ }
166
+ };
167
+ const initializeContext = async (unsafeReset = false) => {
168
+ if (preload && (unsafeReset || !context2.__isPreloaded)) {
169
+ try {
170
+ await preload(context2);
171
+ if (!unsafeReset)
172
+ context2.__isPreloaded = true;
173
+ } catch (error) {
174
+ const message = error instanceof Error ? error.message : String(error);
175
+ throw new Error(`Klint error in the preload: ${message}`);
176
+ }
177
+ }
178
+ if (setup && (unsafeReset || !context2.__isSetup)) {
179
+ try {
180
+ setup(context2);
181
+ if (!unsafeReset)
182
+ context2.__isSetup = true;
183
+ } catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ throw new Error(`Klint error in the setup: ${message}`);
186
+ }
187
+ }
188
+ if (draw && !context2.__isReadyToDraw) {
189
+ try {
190
+ draw(context2);
191
+ context2.__isReadyToDraw = true;
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ throw new Error(`Klint error in the draw: ${message}`);
195
+ }
196
+ }
197
+ };
198
+ const unsafeMode = __options.unsafemode === "true";
199
+ if (!unsafeMode && context2.__isReadyToDraw)
200
+ return;
201
+ await initializeContext(unsafeMode);
202
+ if (__options.static === "true") {
203
+ handleStaticMode();
204
+ } else {
205
+ if (__options.noloop !== "true")
206
+ animate();
207
+ }
208
+ };
209
+ initializeKlint();
210
+ const frameId = animationFrameId.current;
211
+ return () => {
212
+ resizeObserverRef.current?.disconnect();
213
+ intersectionObserverRef.current?.disconnect();
214
+ if (frameId) {
215
+ cancelAnimationFrame(frameId);
216
+ }
217
+ };
218
+ }, []);
219
+ return /* @__PURE__ */ React.createElement("div", { ref: containerRef, style: { width: "100%", height: "100%" } }, toStaticImage ? /* @__PURE__ */ React.createElement(
220
+ "img",
221
+ {
222
+ src: toStaticImage,
223
+ alt: contextRef.current?.__description || DEFAULT_ALT,
224
+ style: {
225
+ display: "block",
226
+ width: "100%",
227
+ height: "100%",
228
+ objectFit: "contain"
229
+ }
230
+ }
231
+ ) : /* @__PURE__ */ React.createElement(
232
+ "canvas",
233
+ {
234
+ ref: canvasRef,
235
+ style: {
236
+ display: __options.nocanvas === "true" ? "none" : "block"
237
+ },
238
+ "aria-label": contextRef.current?.__description || DEFAULT_ALT,
239
+ role: "img"
240
+ }
241
+ ));
242
+ }
243
+
244
+ // src/useKlint.tsx
245
+ import { useRef as useRef2, useCallback as useCallback2, useEffect as useEffect2, useMemo } from "react";
246
+
247
+ // src/KlintFunctions.tsx
248
+ var KlintCoreFunctions = {
249
+ saveCanvas: (ctx) => () => {
250
+ const link = document.createElement("a");
251
+ link.download = "canvas.png";
252
+ link.href = ctx.canvas.toDataURL();
253
+ link.click();
254
+ },
255
+ fullscreen: (ctx) => () => {
256
+ ctx.canvas.requestFullscreen?.();
257
+ },
258
+ play: (ctx) => () => {
259
+ if (!ctx.__isPlaying)
260
+ ctx.__isPlaying = true;
261
+ },
262
+ pause: (ctx) => () => {
263
+ if (ctx.__isPlaying)
264
+ ctx.__isPlaying = false;
265
+ },
266
+ // to do
267
+ redraw: () => () => {
268
+ },
269
+ extend: (ctx) => (name, data, enforceReplace = false) => {
270
+ if (name in ctx && !enforceReplace)
271
+ return;
272
+ ctx[name] = data;
273
+ },
274
+ passImage: () => (element) => {
275
+ if (!element.complete) {
276
+ console.warn("Image passed to passImage() is not fully loaded");
277
+ return null;
278
+ }
279
+ return element;
280
+ },
281
+ passImages: () => (elements) => {
282
+ return elements.map((element) => {
283
+ if (!element.complete) {
284
+ console.warn("Image passed to passImages() is not fully loaded");
285
+ return null;
286
+ }
287
+ return element;
288
+ });
289
+ },
290
+ saveConfig: (ctx) => (from) => {
291
+ return Object.fromEntries(
292
+ CONFIG_PROPS.map((key) => [
293
+ key,
294
+ from?.[key] ?? ctx[key]
295
+ ])
296
+ );
297
+ },
298
+ restoreConfig: (ctx) => (config) => {
299
+ Object.assign(ctx, config);
300
+ },
301
+ describe: (ctx) => (description) => {
302
+ ctx.__description = description;
303
+ },
304
+ createOffscreen: (ctx) => (id, width, height, options, callback) => {
305
+ const offscreen = document.createElement("canvas");
306
+ offscreen.width = width * ctx.__dpr;
307
+ offscreen.height = height * ctx.__dpr;
308
+ const context = offscreen.getContext("2d", {
309
+ alpha: options?.alpha ?? true,
310
+ willReadFrequently: options?.willreadfrequently ?? false
311
+ });
312
+ if (!context)
313
+ throw new Error("Failed to create offscreen context");
314
+ context.__dpr = ctx.__dpr;
315
+ context.width = width * ctx.__dpr;
316
+ context.height = height * ctx.__dpr;
317
+ context.__isMainContext = false;
318
+ context.__imageOrigin = "corner";
319
+ context.__rectangleOrigin = "corner";
320
+ context.__canvasOrigin = "corner";
321
+ context.__textFont = "sans-serif";
322
+ context.__textWeight = "normal";
323
+ context.__textStyle = "normal";
324
+ context.__textSize = 120;
325
+ context.__textAlignment = {
326
+ horizontal: "left",
327
+ vertical: "top"
328
+ };
329
+ if (!options?.ignoreFunctions) {
330
+ Object.entries(KlintFunctions).forEach(([name, fn]) => {
331
+ context[name] = fn(context);
332
+ });
333
+ }
334
+ if (options?.origin) {
335
+ context.__canvasOrigin = options.origin;
336
+ if (options.origin === "center") {
337
+ context.translate(context.width * 0.5, context.height * 0.5);
338
+ }
339
+ }
340
+ if (callback) {
341
+ callback(context);
342
+ }
343
+ if (options?.static === "true") {
344
+ const base64 = offscreen.toDataURL();
345
+ const img = new Image();
346
+ img.src = base64;
347
+ ctx.__offscreens.set(id, img);
348
+ return img;
349
+ }
350
+ ctx.__offscreens.set(id, context);
351
+ return context;
352
+ },
353
+ getOffscreen: (ctx) => (id) => {
354
+ const offscreen = ctx.__offscreens.get(id);
355
+ if (!offscreen)
356
+ throw new Error(`No offscreen context found with id: ${id}`);
357
+ return offscreen;
358
+ }
359
+ };
360
+ var KlintFunctions = {
361
+ extend: (ctx) => (name, data, enforceReplace = false) => {
362
+ if (name in ctx && !enforceReplace)
363
+ return;
364
+ ctx[name] = data;
365
+ },
366
+ background: (ctx) => (color) => {
367
+ ctx.resetTransform();
368
+ ctx.push();
369
+ if (color && color !== "transparent") {
370
+ ctx.fillStyle = color;
371
+ ctx.fillRect(0, 0, ctx.width, ctx.height);
372
+ } else {
373
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
374
+ }
375
+ ctx.pop();
376
+ if (ctx.__canvasOrigin === "center")
377
+ ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
378
+ },
379
+ reset: (ctx) => () => {
380
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
381
+ ctx.resetTransform();
382
+ },
383
+ clear: (ctx) => () => {
384
+ ctx.clearRect(0, 0, ctx.width, ctx.height);
385
+ },
386
+ fillColor: (ctx) => (color) => {
387
+ ctx.fillStyle = color;
388
+ },
389
+ strokeColor: (ctx) => (color) => {
390
+ ctx.strokeStyle = color;
391
+ },
392
+ noFill: (ctx) => () => {
393
+ ctx.fillStyle = "transparent";
394
+ },
395
+ noStroke: (ctx) => () => {
396
+ ctx.strokeStyle = "transparent";
397
+ },
398
+ strokeWidth: (ctx) => (width) => {
399
+ if (width <= 0) {
400
+ ctx.lineWidth = EPSILON;
401
+ }
402
+ ctx.lineWidth = width;
403
+ },
404
+ strokeJoin: (ctx) => (join) => {
405
+ ctx.lineJoin = join;
406
+ },
407
+ strokeCap: (ctx) => (cap) => {
408
+ ctx.lineCap = cap;
409
+ },
410
+ push: (ctx) => () => {
411
+ ctx.save();
412
+ },
413
+ pop: (ctx) => () => {
414
+ ctx.restore();
415
+ },
416
+ point: (ctx) => (x, y) => {
417
+ if (!ctx.checkTransparency("stroke"))
418
+ return;
419
+ ctx.beginPath();
420
+ ctx.strokeRect(x, y, 1, 1);
421
+ },
422
+ checkTransparency: (ctx) => (toCheck) => {
423
+ if (toCheck === "stroke" && ctx.strokeStyle === "transparent")
424
+ return false;
425
+ if (toCheck === "fill" && ctx.fillStyle === "transparent")
426
+ return false;
427
+ return true;
428
+ },
429
+ drawIfVisible: (ctx) => () => {
430
+ if (ctx.checkTransparency("fill"))
431
+ ctx.fill();
432
+ if (ctx.checkTransparency("stroke"))
433
+ ctx.stroke();
434
+ },
435
+ line: (ctx) => (x1, y1, x2, y2) => {
436
+ if (!ctx.checkTransparency("stroke"))
437
+ return;
438
+ ctx.beginPath();
439
+ ctx.moveTo(x1, y1);
440
+ ctx.lineTo(x2, y2);
441
+ ctx.stroke();
442
+ },
443
+ circle: (ctx) => (x, y, radius, radius2) => {
444
+ ctx.beginPath();
445
+ ctx.ellipse(x, y, radius, radius2 || radius, 0, 0, Math.PI * 2);
446
+ ctx.drawIfVisible();
447
+ },
448
+ disk: (ctx) => (x, y, radius, startAngle = 0, endAngle = Math.PI * 2, closed = true) => {
449
+ ctx.beginPath();
450
+ if (closed) {
451
+ ctx.moveTo(x, y);
452
+ ctx.arc(x, y, radius, startAngle, endAngle);
453
+ ctx.lineTo(x, y);
454
+ } else {
455
+ ctx.arc(x, y, radius, startAngle, endAngle);
456
+ }
457
+ ctx.drawIfVisible();
458
+ },
459
+ rectangle: (ctx) => (x, y, width, height) => {
460
+ const originType = ctx.__rectangleOrigin || ctx.origin;
461
+ const h = height ?? width;
462
+ const drawX = originType === "center" ? x - width / 2 : x;
463
+ const drawY = originType === "center" ? y - h / 2 : y;
464
+ ctx.beginPath();
465
+ ctx.rect(drawX, drawY, width, h);
466
+ ctx.drawIfVisible();
467
+ },
468
+ roundedRectangle: (ctx) => (x, y, width, radius, height) => {
469
+ const originType = ctx.__rectangleOrigin || ctx.origin;
470
+ const h = height ?? width;
471
+ const drawX = originType === "center" ? x - width / 2 : x;
472
+ const drawY = originType === "center" ? y - h / 2 : y;
473
+ ctx.beginPath();
474
+ ctx.roundRect(drawX, drawY, width, h, radius);
475
+ ctx.drawIfVisible();
476
+ },
477
+ polygon: (ctx) => (x, y, radius, sides, radius2, rotation = 0) => {
478
+ ctx.beginPath();
479
+ for (let i = 0; i < sides; i++) {
480
+ const angle = i * 2 * Math.PI / sides + rotation;
481
+ const pointX = x + radius * Math.cos(angle);
482
+ const pointY = y + (radius2 ? radius2 : radius) * Math.sin(angle);
483
+ if (i === 0)
484
+ ctx.moveTo(pointX, pointY);
485
+ else
486
+ ctx.lineTo(pointX, pointY);
487
+ }
488
+ ctx.closePath();
489
+ ctx.drawIfVisible();
490
+ },
491
+ beginShape: (ctx) => () => {
492
+ if (ctx.__startedShape)
493
+ return;
494
+ ctx.beginPath();
495
+ ctx.__startedShape = true;
496
+ ctx.__currentShape = [];
497
+ ctx.__currentContours = [];
498
+ },
499
+ beginContour: (ctx) => () => {
500
+ if (!ctx.__startedShape)
501
+ return;
502
+ if (ctx.__startedContour && ctx.__currentContour?.length) {
503
+ ctx.__currentContours?.push([...ctx.__currentContour]);
504
+ }
505
+ ctx.__startedContour = true;
506
+ ctx.__currentContour = [];
507
+ },
508
+ vertex: (ctx) => (x, y) => {
509
+ if (!ctx.__startedShape)
510
+ return;
511
+ const points = ctx.__startedContour ? ctx.__currentContour : ctx.__currentShape;
512
+ points?.push([x, y]);
513
+ },
514
+ endContour: (ctx) => (forceRevert = true) => {
515
+ if (!ctx.__startedContour || !ctx.__currentContour?.length)
516
+ return;
517
+ const contourPoints = [...ctx.__currentContour];
518
+ if (forceRevert) {
519
+ contourPoints.reverse();
520
+ }
521
+ ctx.__currentContours?.push(contourPoints);
522
+ ctx.__currentContour = null;
523
+ ctx.__startedContour = false;
524
+ },
525
+ endShape: (ctx) => (close = false) => {
526
+ if (!ctx.__startedShape)
527
+ return;
528
+ if (ctx.__startedContour)
529
+ ctx.endContour();
530
+ const points = ctx.__currentShape;
531
+ if (!points?.length)
532
+ return;
533
+ const drawPath = (points2, close2 = false) => {
534
+ ctx.moveTo(points2[0][0], points2[0][1]);
535
+ for (let i = 1; i < points2.length; i++) {
536
+ ctx.lineTo(points2[i][0], points2[i][1]);
537
+ }
538
+ if (close2) {
539
+ const [firstX, firstY] = points2[0];
540
+ const lastPoint = points2[points2.length - 1];
541
+ if (lastPoint[0] !== firstX || lastPoint[1] !== firstY) {
542
+ ctx.lineTo(firstX, firstY);
543
+ }
544
+ }
545
+ };
546
+ ctx.beginPath();
547
+ drawPath(points, close);
548
+ ctx.__currentContours?.forEach(
549
+ (contour) => drawPath(contour, true)
550
+ );
551
+ ctx.drawIfVisible();
552
+ ctx.__currentShape = null;
553
+ ctx.__currentContours = null;
554
+ ctx.__startedShape = false;
555
+ },
556
+ gradient: (ctx) => (x1 = 0, y1 = 0, x2 = ctx.width, y2 = ctx.width) => {
557
+ return ctx.createLinearGradient(x1, y1, x2, y2);
558
+ },
559
+ radialGradient: (ctx) => (x1 = ctx.width / 2, y1 = ctx.height / 2, r1 = 0, x2 = ctx.width / 2, y2 = ctx.height / 2, r2 = Math.min(ctx.width, ctx.height)) => {
560
+ return ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
561
+ },
562
+ conicGradient: (ctx) => (angle = 0, x1 = ctx.width / 2, y1 = ctx.height / 2) => {
563
+ return ctx.createConicGradient(angle, x1, y1);
564
+ },
565
+ addColorStop: () => (gradient, offset = 0, color = "#000") => {
566
+ return gradient.addColorStop(offset, color);
567
+ },
568
+ constrain: () => (val, floor, ceil) => {
569
+ return Math.max(floor, Math.min(val, ceil));
570
+ },
571
+ lerp: (ctx) => (A, B, mix, bounded = true) => {
572
+ return A + (B - A) * (bounded ? ctx.constrain(mix, 0, 1) : mix);
573
+ },
574
+ fract: () => (n, mod, mode = "precise") => {
575
+ if (mode === "faster") {
576
+ const floor = (x) => x >> 0;
577
+ return n - floor(n / mod) * mod;
578
+ }
579
+ if (mode === "fast") {
580
+ return n - ~~(n / mod) * mod;
581
+ }
582
+ if (n >= 0)
583
+ return n % mod;
584
+ return mod - -n % mod;
585
+ },
586
+ distance: (ctx) => (x1, y1, x2, y2, mode = "precise") => {
587
+ if (mode === "faster") {
588
+ const dx = Math.abs(x2 - x1);
589
+ const dy = Math.abs(y2 - y1);
590
+ return dx + dy - Math.min(dx, dy) * 0.3;
591
+ }
592
+ if (mode === "fast")
593
+ return ctx.squareDistance(x1, y1, x2, y2) * Math.SQRT1_2;
594
+ return Math.hypot(x2 - x1, y2 - y1);
595
+ },
596
+ squareDistance: () => (x1, y1, x2, y2) => {
597
+ return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
598
+ },
599
+ dot: () => (x1, y1, x2, y2) => {
600
+ return x1 * x2 + y1 * y2;
601
+ },
602
+ remap: (ctx) => (n, A, B, C, D, bounded = true) => {
603
+ const t = (n - A) / (B - A);
604
+ return ctx.lerp(C, D, t, bounded);
605
+ },
606
+ textFont: (ctx) => (font) => {
607
+ ctx.__textFont = font;
608
+ },
609
+ textSize: (ctx) => (size) => {
610
+ ctx.__textSize = size * ctx.__dpr || ctx.__textSize;
611
+ },
612
+ textStyle: (ctx) => (style) => {
613
+ ctx.__textStyle = style || "normal";
614
+ },
615
+ textWeight: (ctx) => (weight) => {
616
+ ctx.__textWeight = weight || "normal";
617
+ },
618
+ textQuality: (ctx) => (quality = "auto") => {
619
+ if (quality === "speed") {
620
+ ctx.textRendering = "optimizeSpeed";
621
+ } else if (quality === "auto") {
622
+ ctx.textRendering = "auto";
623
+ } else if (quality === "legibility") {
624
+ ctx.textRendering = "optimizeLegibility";
625
+ } else if (quality === "precision") {
626
+ ctx.textRendering = "geometricPrecision";
627
+ }
628
+ },
629
+ textSpacing: (ctx) => (kind, value) => {
630
+ ctx[`${kind}Spacing`] = `${value}px`;
631
+ },
632
+ // TO DO : add variable axis handling
633
+ computeTextStyle: (ctx) => () => {
634
+ ctx.__computedTextFont = `${ctx.__textWeight} ${ctx.__textStyle} ${ctx.__textSize}px ${ctx.__textFont}`;
635
+ },
636
+ alignText: (ctx) => (horizontal, vertical) => {
637
+ ctx.__textAlignment.horizontal = horizontal;
638
+ ctx.__textAlignment.vertical = vertical ?? ctx.__textAlignment.vertical;
639
+ },
640
+ textLeading: (ctx) => (spacing) => {
641
+ ctx.lineHeight = `${spacing}px`;
642
+ },
643
+ computeFont: (ctx) => () => {
644
+ ctx.computeTextStyle();
645
+ if (ctx.font !== ctx.__computedTextFont)
646
+ ctx.font = ctx.__computedTextFont;
647
+ },
648
+ textWidth: (ctx) => (text) => {
649
+ ctx.computeFont();
650
+ return ctx.measureText(text).width;
651
+ },
652
+ text: (ctx) => (text, x, y, maxWidth = void 0) => {
653
+ if (text === void 0)
654
+ return;
655
+ ctx.computeFont();
656
+ if (ctx.textAlign !== ctx.__textAlignment.horizontal) {
657
+ ctx.textAlign = ctx.__textAlignment.horizontal;
658
+ }
659
+ if (ctx.textBaseline !== ctx.__textAlignment.vertical) {
660
+ ctx.textBaseline = ctx.__textAlignment.vertical;
661
+ }
662
+ if (ctx.checkTransparency("fill"))
663
+ ctx.fillText(String(text), x, y, maxWidth);
664
+ if (ctx.checkTransparency("stroke"))
665
+ ctx.strokeText(String(text), x, y, maxWidth);
666
+ },
667
+ // DO NOT use putImageData for images you can draw : https://www.measurethat.net/Benchmarks/Show/9510/0/putimagedata-vs-drawimage
668
+ image: (ctx) => (image, x, y, arg3, arg4, arg5, arg6, arg7, arg8) => {
669
+ const sourceImage = "canvas" in image ? image.canvas : image;
670
+ if (arg5 !== void 0) {
671
+ const [sx, sy, sWidth, sHeight, dx2, dy2, dWidth, dHeight] = [
672
+ x,
673
+ y,
674
+ arg3,
675
+ arg4,
676
+ arg5,
677
+ arg6,
678
+ arg7,
679
+ arg8
680
+ ];
681
+ const adjustedX2 = ctx.__imageOrigin === "center" ? dx2 - dWidth / 2 : dx2;
682
+ const adjustedY2 = ctx.__imageOrigin === "center" ? dy2 - dHeight / 2 : dy2;
683
+ ctx.drawImage(
684
+ sourceImage,
685
+ sx,
686
+ sy,
687
+ sWidth,
688
+ sHeight,
689
+ adjustedX2,
690
+ adjustedY2,
691
+ dWidth,
692
+ dHeight
693
+ );
694
+ return;
695
+ }
696
+ if (arg3 !== void 0) {
697
+ const [dx2, dy2, dWidth, dHeight] = [x, y, arg3, arg4];
698
+ const adjustedX2 = ctx.__imageOrigin === "center" ? dx2 - dWidth / 2 : dx2;
699
+ const adjustedY2 = ctx.__imageOrigin === "center" ? dy2 - dHeight / 2 : dy2;
700
+ ctx.drawImage(sourceImage, adjustedX2, adjustedY2, dWidth, dHeight);
701
+ return;
702
+ }
703
+ const [dx, dy] = [x, y];
704
+ const width = sourceImage instanceof HTMLImageElement ? sourceImage.naturalWidth : sourceImage.width;
705
+ const height = sourceImage instanceof HTMLImageElement ? sourceImage.naturalHeight : sourceImage.height;
706
+ const adjustedX = ctx.__imageOrigin === "center" ? dx - width / 2 : dx;
707
+ const adjustedY = ctx.__imageOrigin === "center" ? dy - height / 2 : dy;
708
+ ctx.drawImage(sourceImage, adjustedX, adjustedY);
709
+ },
710
+ // unsure about keeping those next two, maybe a shader plugin would be better
711
+ loadPixels: (ctx) => () => {
712
+ return ctx.getImageData(0, 0, ctx.width, ctx.height);
713
+ },
714
+ updatePixels: (ctx) => (pixels) => {
715
+ const imageData = new ImageData(
716
+ pixels instanceof Uint8ClampedArray ? pixels : new Uint8ClampedArray(pixels),
717
+ ctx.width,
718
+ ctx.height
719
+ );
720
+ ctx.putImageData(imageData, 0, 0);
721
+ },
722
+ readPixels: (ctx) => (x, y, w = 1, h = 1) => {
723
+ const imageData = ctx.getImageData(x, y, w, h);
724
+ return Array.from(imageData.data);
725
+ },
726
+ scaleTo: () => (originWidth, originHeight, destinationWidth, destinationHeight, cover = false) => {
727
+ const widthRatio = destinationWidth / originWidth;
728
+ const heightRatio = destinationHeight / originHeight;
729
+ return cover ? Math.max(widthRatio, heightRatio) : Math.min(widthRatio, heightRatio);
730
+ },
731
+ opacity: (ctx) => (value) => {
732
+ ctx.globalAlpha = ctx.constrain(value, 0, 1);
733
+ },
734
+ blend: (ctx) => (blend) => {
735
+ ctx.globalCompositeOperation = blend;
736
+ },
737
+ setCanvasOrigin: (ctx) => (type) => {
738
+ ctx.__canvasOrigin = type;
739
+ },
740
+ setImageOrigin: (ctx) => (type) => {
741
+ ctx.__imageOrigin = type;
742
+ },
743
+ setRectOrigin: (ctx) => (type) => {
744
+ ctx.__rectangleOrigin = type;
745
+ },
746
+ withConfig: (ctx) => (config) => {
747
+ Object.assign(ctx, config);
748
+ },
749
+ toBase64: (ctx) => (type = "image/png", quality) => {
750
+ const canvas = ctx.canvas;
751
+ return canvas.toDataURL(type, quality);
752
+ },
753
+ saveConfig: (ctx) => (from) => {
754
+ return Object.fromEntries(
755
+ CONFIG_PROPS.map((key) => [
756
+ key,
757
+ from?.[key] ?? ctx[key]
758
+ ])
759
+ );
760
+ },
761
+ restoreConfig: (ctx) => (config) => {
762
+ Object.assign(ctx, config);
763
+ },
764
+ resizeCanvas: (ctx) => (width, height) => {
765
+ if (ctx.__isMainContext)
766
+ return;
767
+ const config = ctx.saveConfig();
768
+ ctx.canvas.width = ctx.width = width;
769
+ ctx.canvas.height = ctx.height = height;
770
+ ctx.restoreConfig(config);
771
+ if (ctx.__canvasOrigin === "center") {
772
+ ctx.translate(ctx.width * 0.5, ctx.height * 0.5);
773
+ }
774
+ }
775
+ };
776
+
777
+ // src/useKlint.tsx
778
+ var DEFAULT_MOUSE_STATE = {
779
+ x: 0,
780
+ y: 0,
781
+ px: 0,
782
+ py: 0,
783
+ vx: 0,
784
+ vy: 0,
785
+ angle: 0,
786
+ isPressed: false,
787
+ isHover: false
788
+ };
789
+ var DEFAULT_SCROLL_STATE = {
790
+ distance: 0,
791
+ velocity: 0,
792
+ lastTime: 0
793
+ };
794
+ function useKlint() {
795
+ const contextRef = useRef2(null);
796
+ const mouseRef = useRef2(null);
797
+ const scrollRef = useRef2(null);
798
+ const useImage = () => {
799
+ const imagesRef = useRef2(/* @__PURE__ */ new Map());
800
+ const loadImage = useCallback2(
801
+ async (key, url) => {
802
+ return new Promise((resolve, reject) => {
803
+ const img = new Image();
804
+ img.onload = () => {
805
+ img.width = img.naturalWidth;
806
+ img.height = img.naturalHeight;
807
+ imagesRef.current.set(key, img);
808
+ resolve(img);
809
+ };
810
+ img.onerror = reject;
811
+ img.src = url;
812
+ });
813
+ },
814
+ []
815
+ );
816
+ const loadImages = useCallback2(
817
+ async (imageMap) => {
818
+ const promises = Object.entries(imageMap).map(
819
+ ([key, url]) => loadImage(key, url).then(
820
+ (img) => [key, img]
821
+ )
822
+ );
823
+ const results = await Promise.all(promises);
824
+ return new Map(results);
825
+ },
826
+ [loadImage]
827
+ );
828
+ const imagesProxy = useMemo(() => {
829
+ return new Proxy({}, {
830
+ get: (_, prop) => {
831
+ if (prop === "get") {
832
+ return (key) => imagesRef.current.get(key);
833
+ }
834
+ if (typeof prop === "string") {
835
+ return imagesRef.current.get(prop);
836
+ }
837
+ return void 0;
838
+ },
839
+ has: (_, prop) => {
840
+ if (typeof prop === "string") {
841
+ return imagesRef.current.has(prop);
842
+ }
843
+ return false;
844
+ }
845
+ });
846
+ }, []);
847
+ return {
848
+ images: imagesProxy,
849
+ loadImage,
850
+ loadImages,
851
+ getImage: useCallback2((key) => imagesRef.current.get(key), []),
852
+ hasImage: useCallback2((key) => imagesRef.current.has(key), []),
853
+ clearImages: useCallback2(() => imagesRef.current.clear(), [])
854
+ };
855
+ };
856
+ const useMouse = () => {
857
+ if (!mouseRef.current) {
858
+ mouseRef.current = { ...DEFAULT_MOUSE_STATE };
859
+ }
860
+ const clickCallbackRef = useRef2(null);
861
+ const mouseInCallbackRef = useRef2(null);
862
+ const mouseOutCallbackRef = useRef2(null);
863
+ const mouseDownCallbackRef = useRef2(null);
864
+ const mouseUpCallbackRef = useRef2(null);
865
+ useEffect2(() => {
866
+ if (!contextRef.current?.canvas)
867
+ return;
868
+ const canvas = contextRef.current.canvas;
869
+ const ctx = contextRef.current;
870
+ const updateMousePosition = (e) => {
871
+ const rect = canvas.getBoundingClientRect();
872
+ const dpr = window.devicePixelRatio || 1;
873
+ const origin = contextRef.current?.__canvasOrigin || "corner";
874
+ const x = origin === "center" ? (e.clientX - rect.left) * dpr - canvas.width / 2 : (e.clientX - rect.left) * dpr;
875
+ const y = origin === "center" ? (e.clientY - rect.top) * dpr - canvas.height / 2 : (e.clientY - rect.top) * dpr;
876
+ if (mouseRef.current) {
877
+ mouseRef.current.px = mouseRef.current.x;
878
+ mouseRef.current.py = mouseRef.current.y;
879
+ mouseRef.current.x = x;
880
+ mouseRef.current.y = y;
881
+ mouseRef.current.vx = x - mouseRef.current.px;
882
+ mouseRef.current.vy = y - mouseRef.current.py;
883
+ mouseRef.current.angle = Math.atan2(
884
+ mouseRef.current.vy,
885
+ mouseRef.current.vx
886
+ );
887
+ }
888
+ };
889
+ const handleMouseDown = (e) => {
890
+ if (mouseRef.current)
891
+ mouseRef.current.isPressed = true;
892
+ if (mouseDownCallbackRef.current)
893
+ mouseDownCallbackRef.current(ctx, e);
894
+ };
895
+ const handleMouseUp = (e) => {
896
+ if (mouseRef.current)
897
+ mouseRef.current.isPressed = false;
898
+ if (mouseUpCallbackRef.current)
899
+ mouseUpCallbackRef.current(ctx, e);
900
+ };
901
+ const handleMouseEnter = (e) => {
902
+ if (mouseRef.current)
903
+ mouseRef.current.isHover = true;
904
+ if (mouseInCallbackRef.current)
905
+ mouseInCallbackRef.current(ctx, e);
906
+ };
907
+ const handleMouseLeave = (e) => {
908
+ if (mouseRef.current)
909
+ mouseRef.current.isHover = false;
910
+ if (mouseOutCallbackRef.current)
911
+ mouseOutCallbackRef.current(ctx, e);
912
+ };
913
+ const handleClick = (e) => {
914
+ if (clickCallbackRef.current)
915
+ clickCallbackRef.current(ctx, e);
916
+ };
917
+ canvas.addEventListener("mousemove", updateMousePosition);
918
+ canvas.addEventListener("mousedown", handleMouseDown);
919
+ canvas.addEventListener("mouseup", handleMouseUp);
920
+ canvas.addEventListener("mouseenter", handleMouseEnter);
921
+ canvas.addEventListener("mouseleave", handleMouseLeave);
922
+ canvas.addEventListener("click", handleClick);
923
+ return () => {
924
+ canvas.removeEventListener("mousemove", updateMousePosition);
925
+ canvas.removeEventListener("mousedown", handleMouseDown);
926
+ canvas.removeEventListener("mouseup", handleMouseUp);
927
+ canvas.removeEventListener("mouseenter", handleMouseEnter);
928
+ canvas.removeEventListener("mouseleave", handleMouseLeave);
929
+ canvas.removeEventListener("click", handleClick);
930
+ };
931
+ });
932
+ return {
933
+ mouse: mouseRef.current,
934
+ onClick: (callback) => clickCallbackRef.current = callback,
935
+ onMouseIn: (callback) => mouseInCallbackRef.current = callback,
936
+ onMouseOut: (callback) => mouseOutCallbackRef.current = callback,
937
+ onMouseDown: (callback) => mouseDownCallbackRef.current = callback,
938
+ onMouseUp: (callback) => mouseUpCallbackRef.current = callback
939
+ };
940
+ };
941
+ const useScroll = () => {
942
+ if (!scrollRef.current) {
943
+ scrollRef.current = { ...DEFAULT_SCROLL_STATE };
944
+ }
945
+ const scrollCallbackRef = useRef2(null);
946
+ useEffect2(() => {
947
+ if (!contextRef.current?.canvas)
948
+ return;
949
+ const canvas = contextRef.current.canvas;
950
+ const ctx = contextRef.current;
951
+ const handleScroll = (e) => {
952
+ e.preventDefault();
953
+ if (!scrollRef.current)
954
+ return;
955
+ const currentTime = performance.now();
956
+ const deltaTime = currentTime - scrollRef.current.lastTime;
957
+ scrollRef.current.distance += e.deltaY;
958
+ scrollRef.current.velocity = deltaTime > 0 ? e.deltaY / deltaTime : 0;
959
+ scrollRef.current.lastTime = currentTime;
960
+ if (scrollCallbackRef.current) {
961
+ scrollCallbackRef.current(ctx, scrollRef.current, e);
962
+ }
963
+ };
964
+ canvas.addEventListener("wheel", handleScroll);
965
+ return () => canvas.removeEventListener("wheel", handleScroll);
966
+ });
967
+ return {
968
+ scroll: scrollRef.current,
969
+ onScroll: (callback) => scrollCallbackRef.current = callback
970
+ };
971
+ };
972
+ const useWindow = () => {
973
+ const resizeCallbackRef = useRef2(
974
+ null
975
+ );
976
+ const blurCallbackRef = useRef2(null);
977
+ const focusCallbackRef = useRef2(null);
978
+ const visibilityChangeCallbackRef = useRef2(null);
979
+ useEffect2(() => {
980
+ if (!contextRef.current)
981
+ return;
982
+ const ctx = contextRef.current;
983
+ const handleResize = () => {
984
+ if (resizeCallbackRef.current)
985
+ resizeCallbackRef.current(ctx);
986
+ };
987
+ const handleBlur = () => {
988
+ if (blurCallbackRef.current)
989
+ blurCallbackRef.current(ctx);
990
+ };
991
+ const handleFocus = () => {
992
+ if (focusCallbackRef.current)
993
+ focusCallbackRef.current(ctx);
994
+ };
995
+ const handleVisibilityChange = () => {
996
+ const isVisible = document.visibilityState === "visible";
997
+ if (visibilityChangeCallbackRef.current) {
998
+ visibilityChangeCallbackRef.current(ctx, isVisible);
999
+ }
1000
+ };
1001
+ window.addEventListener("resize", handleResize);
1002
+ window.addEventListener("blur", handleBlur);
1003
+ window.addEventListener("focus", handleFocus);
1004
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1005
+ return () => {
1006
+ window.removeEventListener("resize", handleResize);
1007
+ window.removeEventListener("blur", handleBlur);
1008
+ window.removeEventListener("focus", handleFocus);
1009
+ document.removeEventListener(
1010
+ "visibilitychange",
1011
+ handleVisibilityChange
1012
+ );
1013
+ };
1014
+ }, []);
1015
+ return {
1016
+ onResize: (callback) => resizeCallbackRef.current = callback,
1017
+ onBlur: (callback) => blurCallbackRef.current = callback,
1018
+ onFocus: (callback) => focusCallbackRef.current = callback,
1019
+ onVisibilityChange: (callback) => visibilityChangeCallbackRef.current = callback
1020
+ };
1021
+ };
1022
+ const buildKlintContext = (ctx, options) => {
1023
+ const context = ctx;
1024
+ context.__isMainContext = true;
1025
+ context.fps = 60;
1026
+ context.frame = 0;
1027
+ context.time = 0;
1028
+ context.deltaTime = 0;
1029
+ context.__imageOrigin = options.origin === "center" ? "center" : "corner";
1030
+ context.__rectangleOrigin = options.origin === "center" ? "center" : "corner";
1031
+ context.__canvasOrigin = options.origin === "center" ? "center" : "corner";
1032
+ context.__textFont = "sans-serif";
1033
+ context.__textWeight = "normal";
1034
+ context.__textStyle = "normal";
1035
+ context.__textSize = 72;
1036
+ context.__textAlignment = {
1037
+ horizontal: "left",
1038
+ vertical: "top"
1039
+ };
1040
+ context.__offscreens = /* @__PURE__ */ new Map();
1041
+ context.__isPlaying = true;
1042
+ context.__currentContext = context;
1043
+ Object.entries(KlintCoreFunctions).forEach(([name, fn]) => {
1044
+ context[name] = fn(context);
1045
+ });
1046
+ Object.entries(KlintFunctions).forEach(([name, fn]) => {
1047
+ context[name] = fn(context);
1048
+ });
1049
+ return context;
1050
+ };
1051
+ const initCoreContext = useCallback2(
1052
+ (canvas, options) => {
1053
+ if (!contextRef.current) {
1054
+ const ctx = canvas.getContext("2d", {
1055
+ alpha: options.alpha ?? true,
1056
+ willReadFrequently: options.willreadfrequently ?? true
1057
+ });
1058
+ if (!ctx)
1059
+ throw new Error("Failed to get canvas context");
1060
+ contextRef.current = buildKlintContext(ctx, options);
1061
+ }
1062
+ return contextRef.current;
1063
+ },
1064
+ []
1065
+ );
1066
+ const togglePlay = useCallback2((playing) => {
1067
+ if (!contextRef.current)
1068
+ return;
1069
+ if (playing !== void 0) {
1070
+ contextRef.current.__isPlaying = playing;
1071
+ } else {
1072
+ contextRef.current.__isPlaying = !contextRef.current.__isPlaying;
1073
+ }
1074
+ }, []);
1075
+ return {
1076
+ context: {
1077
+ context: contextRef.current,
1078
+ initCoreContext
1079
+ },
1080
+ useMouse,
1081
+ useScroll,
1082
+ useWindow,
1083
+ useImage,
1084
+ togglePlay
1085
+ };
1086
+ }
1087
+ var useProps = (props) => {
1088
+ const propsRef = useRef2(props);
1089
+ useEffect2(() => {
1090
+ propsRef.current = props;
1091
+ }, [props]);
1092
+ const get = useCallback2((key) => {
1093
+ return propsRef.current[key];
1094
+ }, []);
1095
+ const has = useCallback2((key) => {
1096
+ return key in propsRef.current;
1097
+ }, []);
1098
+ return {
1099
+ get,
1100
+ has,
1101
+ props: propsRef.current
1102
+ };
1103
+ };
1104
+ var useStorage = (initialProps = {}) => {
1105
+ const storeRef = useRef2(initialProps);
1106
+ const get = useCallback2((key) => {
1107
+ return storeRef.current[key];
1108
+ }, []);
1109
+ const set = useCallback2((key, value) => {
1110
+ storeRef.current[key] = value;
1111
+ }, []);
1112
+ const has = useCallback2((key) => {
1113
+ return key in storeRef.current;
1114
+ }, []);
1115
+ const remove = useCallback2((key) => {
1116
+ delete storeRef.current[key];
1117
+ }, []);
1118
+ return {
1119
+ get,
1120
+ set,
1121
+ has,
1122
+ remove,
1123
+ store: storeRef.current
1124
+ };
1125
+ };
1126
+ export {
1127
+ CONFIG_PROPS,
1128
+ EPSILON,
1129
+ Klint,
1130
+ KlintCoreFunctions,
1131
+ KlintFunctions,
1132
+ useKlint,
1133
+ useProps,
1134
+ useStorage
1135
+ };