@shopify/klint 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,3 @@
1
- import "../chunk-3RG5ZIWI.js";
2
-
3
1
  // src/plugins/FontParser.tsx
4
2
  var BinaryReader = class {
5
3
  constructor() {
@@ -21,14 +19,6 @@ var BinaryReader = class {
21
19
  const num = this.readShort(data, o);
22
20
  return num / 16384;
23
21
  }
24
- readInt(buff, p) {
25
- const a = this.t.uint8;
26
- a[0] = buff[p + 3];
27
- a[1] = buff[p + 2];
28
- a[2] = buff[p + 1];
29
- a[3] = buff[p];
30
- return this.t.int32[0];
31
- }
32
22
  readInt8(buff, p) {
33
23
  const a = this.t.uint8;
34
24
  a[0] = buff[p];
@@ -57,19 +47,11 @@ var BinaryReader = class {
57
47
  a[0] = buff[p + 3];
58
48
  return this.t.uint32[0];
59
49
  }
60
- readUint64(buff, p) {
61
- return this.readUint(buff, p) * (4294967295 + 1) + this.readUint(buff, p + 4);
62
- }
63
50
  readASCII(buff, p, l) {
64
51
  let s = "";
65
52
  for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
66
53
  return s;
67
54
  }
68
- readBytes(buff, p, l) {
69
- const arr = [];
70
- for (let i = 0; i < l; i++) arr.push(buff[p + i]);
71
- return arr;
72
- }
73
55
  };
74
56
  var bin = new BinaryReader();
75
57
  function findTable(data, tab, foff = 0) {
@@ -77,7 +59,6 @@ function findTable(data, tab, foff = 0) {
77
59
  let offset = foff + 12;
78
60
  for (let i = 0; i < numTables; i++) {
79
61
  const tag = bin.readASCII(data, offset, 4);
80
- const checkSum = bin.readUint(data, offset + 4);
81
62
  const toffset = bin.readUint(data, offset + 8);
82
63
  const length = bin.readUint(data, offset + 12);
83
64
  if (tag === tab) return [toffset, length];
@@ -88,105 +69,48 @@ function findTable(data, tab, foff = 0) {
88
69
  var Tables = {
89
70
  head: {
90
71
  parseTab(data, offset, length) {
91
- const obj = {};
92
- const tableVersion = bin.readFixed(data, offset);
93
- offset += 4;
94
- obj.fontRevision = bin.readFixed(data, offset);
95
- offset += 4;
96
- const checkSumAdjustment = bin.readUint(data, offset);
97
- offset += 4;
98
- const magicNumber = bin.readUint(data, offset);
99
- offset += 4;
100
- obj.flags = bin.readUshort(data, offset);
101
- offset += 2;
102
- obj.unitsPerEm = bin.readUshort(data, offset);
103
- offset += 2;
104
- obj.created = bin.readUint64(data, offset);
105
- offset += 8;
106
- obj.modified = bin.readUint64(data, offset);
107
- offset += 8;
108
- obj.xMin = bin.readShort(data, offset);
109
- offset += 2;
110
- obj.yMin = bin.readShort(data, offset);
111
- offset += 2;
112
- obj.xMax = bin.readShort(data, offset);
113
- offset += 2;
114
- obj.yMax = bin.readShort(data, offset);
115
- offset += 2;
116
- obj.macStyle = bin.readUshort(data, offset);
117
- offset += 2;
118
- obj.lowestRecPPEM = bin.readUshort(data, offset);
119
- offset += 2;
120
- obj.fontDirectionHint = bin.readShort(data, offset);
121
- offset += 2;
122
- obj.indexToLocFormat = bin.readShort(data, offset);
123
- offset += 2;
124
- obj.glyphDataFormat = bin.readShort(data, offset);
125
- offset += 2;
126
- return obj;
72
+ return {
73
+ flags: bin.readUshort(data, offset + 16),
74
+ unitsPerEm: bin.readUshort(data, offset + 18),
75
+ xMin: bin.readShort(data, offset + 36),
76
+ yMin: bin.readShort(data, offset + 38),
77
+ xMax: bin.readShort(data, offset + 40),
78
+ yMax: bin.readShort(data, offset + 42),
79
+ indexToLocFormat: bin.readShort(data, offset + 50)
80
+ };
127
81
  }
128
82
  },
129
83
  maxp: {
130
84
  parseTab(data, offset, length) {
131
- const obj = {};
132
- const ver = bin.readUint(data, offset);
133
- offset += 4;
134
- obj.numGlyphs = bin.readUshort(data, offset);
135
- offset += 2;
136
- return obj;
85
+ return { numGlyphs: bin.readUshort(data, offset + 4) };
137
86
  }
138
87
  },
139
88
  hhea: {
140
89
  parseTab(data, offset, length) {
141
- const obj = {};
142
- const tableVersion = bin.readFixed(data, offset);
143
- offset += 4;
144
- const keys = [
145
- "ascender",
146
- "descender",
147
- "lineGap",
148
- "advanceWidthMax",
149
- "minLeftSideBearing",
150
- "minRightSideBearing",
151
- "xMaxExtent",
152
- "caretSlopeRise",
153
- "caretSlopeRun",
154
- "caretOffset",
155
- "res0",
156
- "res1",
157
- "res2",
158
- "res3",
159
- "metricDataFormat",
160
- "numberOfHMetrics"
161
- ];
162
- for (let i = 0; i < keys.length; i++) {
163
- const key = keys[i];
164
- const func = key === "advanceWidthMax" || key === "numberOfHMetrics" ? bin.readUshort : bin.readShort;
165
- obj[key] = func.call(bin, data, offset + i * 2);
166
- }
167
- return obj;
90
+ return {
91
+ ascender: bin.readShort(data, offset + 4),
92
+ descender: bin.readShort(data, offset + 6),
93
+ lineGap: bin.readShort(data, offset + 8),
94
+ numberOfHMetrics: bin.readUshort(data, offset + 34)
95
+ };
168
96
  }
169
97
  },
170
98
  hmtx: {
171
99
  parseTab(data, offset, length, font) {
172
100
  const aWidth = [];
173
- const lsBearing = [];
174
101
  const nG = font.maxp.numGlyphs;
175
102
  const nH = font.hhea.numberOfHMetrics;
176
- let aw = 0, lsb = 0, i = 0;
103
+ let aw = 0, i = 0;
177
104
  while (i < nH) {
178
105
  aw = bin.readUshort(data, offset + (i << 2));
179
- lsb = bin.readShort(data, offset + (i << 2) + 2);
180
106
  aWidth.push(aw);
181
- lsBearing.push(lsb);
182
107
  i++;
183
108
  }
184
109
  while (i < nG) {
185
110
  aWidth.push(aw);
186
- lsBearing.push(lsb);
187
111
  i++;
188
112
  }
189
- return { aWidth, lsBearing };
113
+ return { aWidth };
190
114
  }
191
115
  },
192
116
  loca: {
@@ -246,9 +170,6 @@ var Tables = {
246
170
  readFormat0(data, offset, map) {
247
171
  let pleft = -1;
248
172
  const nPairs = bin.readUshort(data, offset);
249
- const searchRange = bin.readUshort(data, offset + 2);
250
- const entrySelector = bin.readUshort(data, offset + 4);
251
- const rangeShift = bin.readUshort(data, offset + 6);
252
173
  offset += 8;
253
174
  for (let j = 0; j < nPairs; j++) {
254
175
  const left = bin.readUshort(data, offset);
@@ -305,25 +226,16 @@ var cmap = {
305
226
  return obj;
306
227
  },
307
228
  parse0(data, offset, obj) {
308
- const startOffset = offset;
309
- const format = bin.readUshort(data, offset);
310
- offset += 2;
311
- const length = bin.readUshort(data, offset);
312
- offset += 2;
313
- const language = bin.readUshort(data, offset);
314
- offset += 2;
229
+ offset += 6;
315
230
  obj.map = [];
316
231
  for (let i = 0; i < 256; i++) obj.map.push(data[offset + i]);
317
232
  return obj;
318
233
  },
319
234
  parse4(data, offset, obj) {
320
235
  const startOffset = offset;
321
- const format = bin.readUshort(data, offset);
322
236
  offset += 2;
323
237
  const length = bin.readUshort(data, offset);
324
- offset += 2;
325
- const language = bin.readUshort(data, offset);
326
- offset += 2;
238
+ offset += 4;
327
239
  const segCountX2 = bin.readUshort(data, offset);
328
240
  offset += 2;
329
241
  const segCount = segCountX2 >>> 1;
@@ -353,13 +265,7 @@ var cmap = {
353
265
  return obj;
354
266
  },
355
267
  parse6(data, offset, obj) {
356
- const startOffset = offset;
357
- const format = bin.readUshort(data, offset);
358
- offset += 2;
359
- const length = bin.readUshort(data, offset);
360
- offset += 2;
361
- const language = bin.readUshort(data, offset);
362
- offset += 2;
268
+ offset += 6;
363
269
  obj.firstCode = bin.readUshort(data, offset);
364
270
  offset += 2;
365
271
  obj.entryCount = bin.readUshort(data, offset);
@@ -368,12 +274,7 @@ var cmap = {
368
274
  return obj;
369
275
  },
370
276
  parse12(data, offset, obj) {
371
- const startOffset = offset;
372
- offset += 4;
373
- const length = bin.readUint(data, offset);
374
- offset += 4;
375
- const language = bin.readUint(data, offset);
376
- offset += 4;
277
+ offset += 12;
377
278
  const nGroups = bin.readUint(data, offset) * 3;
378
279
  offset += 4;
379
280
  obj.groups = new Uint32Array(nGroups);
@@ -417,8 +318,7 @@ var glyf = {
417
318
  }
418
319
  const instructionLength = bin.readUshort(data, off);
419
320
  off += 2;
420
- if (data.length - off < instructionLength) return null;
421
- gl.instructions = bin.readBytes(data, off, instructionLength);
321
+ if (off + instructionLength > data.length) return null;
422
322
  off += instructionLength;
423
323
  const crdnum = gl.endPts[gl.noc - 1] + 1;
424
324
  gl.flags = [];
@@ -1563,54 +1463,26 @@ var FontParser = class {
1563
1463
  },
1564
1464
  samplePathPoints(glyphPath, sampling) {
1565
1465
  const rawContours = this.parseContours(glyphPath);
1566
- const contoursWithLength = rawContours.map(
1567
- (contour, index) => ({
1568
- contour,
1569
- originalIndex: index,
1570
- length: this.calculateContourLength(contour)
1571
- })
1466
+ const prepared = rawContours.map(
1467
+ (contour) => this.buildSegments(contour)
1572
1468
  );
1573
- contoursWithLength.sort((a, b) => b.length - a.length);
1469
+ prepared.sort((a, b) => b.totalLength - a.totalLength);
1574
1470
  const allPoints = [];
1575
- for (let i = 0; i < contoursWithLength.length; i++) {
1576
- const { contour } = contoursWithLength[i];
1577
- const contourPoints = this.sampleContour(contour, sampling, i);
1578
- allPoints.push(...contourPoints);
1579
- }
1580
- return allPoints;
1581
- },
1582
- // Calculate total length of a contour
1583
- calculateContourLength(contour) {
1584
- let totalLength = 0;
1585
- let currentX = contour.startX;
1586
- let currentY = contour.startY;
1587
- for (const seg of contour.segments) {
1588
- if (seg.cmd === "L") {
1589
- const endX = seg.coords[0];
1590
- const endY = seg.coords[1];
1591
- totalLength += Math.sqrt(
1592
- (endX - currentX) ** 2 + (endY - currentY) ** 2
1593
- );
1594
- currentX = endX;
1595
- currentY = endY;
1596
- } else if (seg.cmd === "Q") {
1597
- const controlX = seg.coords[0];
1598
- const controlY = seg.coords[1];
1599
- const endX = seg.coords[2];
1600
- const endY = seg.coords[3];
1601
- totalLength += this.approximateQuadraticLength(
1602
- currentX,
1603
- currentY,
1604
- controlX,
1605
- controlY,
1606
- endX,
1607
- endY
1608
- );
1609
- currentX = endX;
1610
- currentY = endY;
1471
+ for (let i = 0; i < prepared.length; i++) {
1472
+ const { segments, totalLength } = prepared[i];
1473
+ const targetPoints = Math.max(
1474
+ 5,
1475
+ Math.floor(totalLength * sampling * 0.1)
1476
+ );
1477
+ for (let j = 0; j <= targetPoints; j++) {
1478
+ const targetLength = j / targetPoints * totalLength;
1479
+ const pt = this.getPointAtLengthInContour(segments, targetLength);
1480
+ if (pt) {
1481
+ allPoints.push({ x: pt.x, y: pt.y, contour: i });
1482
+ }
1611
1483
  }
1612
1484
  }
1613
- return totalLength;
1485
+ return allPoints;
1614
1486
  },
1615
1487
  // Parse glyph path into separate contours
1616
1488
  parseContours(glyphPath) {
@@ -1652,8 +1524,8 @@ var FontParser = class {
1652
1524
  }
1653
1525
  return contours;
1654
1526
  },
1655
- // Sample a single contour uniformly
1656
- sampleContour(contour, sampling, contourIndex) {
1527
+ // Build segments from a contour with lengths pre-calculated
1528
+ buildSegments(contour) {
1657
1529
  const segments = [];
1658
1530
  let currentX = contour.startX;
1659
1531
  let currentY = contour.startY;
@@ -1662,9 +1534,7 @@ var FontParser = class {
1662
1534
  if (seg.cmd === "L") {
1663
1535
  const endX = seg.coords[0];
1664
1536
  const endY = seg.coords[1];
1665
- const length = Math.sqrt(
1666
- (endX - currentX) ** 2 + (endY - currentY) ** 2
1667
- );
1537
+ const length = Math.hypot(endX - currentX, endY - currentY);
1668
1538
  segments.push({
1669
1539
  type: "L",
1670
1540
  startX: currentX,
@@ -1682,14 +1552,9 @@ var FontParser = class {
1682
1552
  const controlY = seg.coords[1];
1683
1553
  const endX = seg.coords[2];
1684
1554
  const endY = seg.coords[3];
1685
- const length = this.approximateQuadraticLength(
1686
- currentX,
1687
- currentY,
1688
- controlX,
1689
- controlY,
1690
- endX,
1691
- endY
1692
- );
1555
+ const chord = Math.hypot(endX - currentX, endY - currentY);
1556
+ const ctrl = Math.hypot(controlX - currentX, controlY - currentY) + Math.hypot(endX - controlX, endY - controlY);
1557
+ const length = (chord + ctrl) / 2;
1693
1558
  segments.push({
1694
1559
  type: "Q",
1695
1560
  startX: currentX,
@@ -1706,26 +1571,7 @@ var FontParser = class {
1706
1571
  currentY = endY;
1707
1572
  }
1708
1573
  }
1709
- const points = [];
1710
- const targetPoints = Math.max(
1711
- 5,
1712
- Math.floor(totalLength * sampling * 0.1)
1713
- );
1714
- for (let i = 0; i <= targetPoints; i++) {
1715
- const targetLength = i / targetPoints * totalLength;
1716
- const pointData = this.getPointAtLengthInContour(
1717
- segments,
1718
- targetLength
1719
- );
1720
- if (pointData) {
1721
- points.push({
1722
- x: pointData.x,
1723
- y: pointData.y,
1724
- contour: contourIndex
1725
- });
1726
- }
1727
- }
1728
- return points;
1574
+ return { segments, totalLength };
1729
1575
  },
1730
1576
  // Get point at length within a single contour
1731
1577
  getPointAtLengthInContour(segments, targetLength) {
@@ -1747,12 +1593,6 @@ var FontParser = class {
1747
1593
  }
1748
1594
  }
1749
1595
  return null;
1750
- },
1751
- // Helper function to approximate quadratic curve length
1752
- approximateQuadraticLength(x0, y0, x1, y1, x2, y2) {
1753
- const chordLength = Math.sqrt((x2 - x0) ** 2 + (y2 - y0) ** 2);
1754
- const controlLength = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) + Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
1755
- return (chordLength + controlLength) / 2;
1756
1596
  }
1757
1597
  };
1758
1598
  }
@@ -1920,264 +1760,6 @@ var CatmullRom = class {
1920
1760
  }
1921
1761
  };
1922
1762
 
1923
- // src/plugins/Things.tsx
1924
- var Things = class {
1925
- /**
1926
- * Configure the plugin
1927
- */
1928
- static configure(config) {
1929
- this.config = { ...this.config, ...config };
1930
- }
1931
- /**
1932
- * Create a new thing
1933
- */
1934
- static create(options = {}) {
1935
- if (this.things.size >= (this.config.maxThings || 1e3)) {
1936
- throw new Error(
1937
- `Maximum number of things (${this.config.maxThings}) reached`
1938
- );
1939
- }
1940
- const thing = {
1941
- id: options.id || `thing_${this.idCounter++}`,
1942
- x: options.x || 0,
1943
- y: options.y || 0,
1944
- width: options.width || this.config.defaultSize || 50,
1945
- height: options.height || this.config.defaultSize || 50,
1946
- rotation: options.rotation || 0,
1947
- scale: options.scale || 1,
1948
- color: options.color || this.config.defaultColor || "#ffffff",
1949
- data: options.data || {}
1950
- };
1951
- this.things.set(thing.id, thing);
1952
- return thing;
1953
- }
1954
- /**
1955
- * Get a thing by ID
1956
- */
1957
- static get(id) {
1958
- return this.things.get(id);
1959
- }
1960
- /**
1961
- * Get all things
1962
- */
1963
- static getAll() {
1964
- return Array.from(this.things.values());
1965
- }
1966
- /**
1967
- * Update a thing's properties
1968
- */
1969
- static update(id, updates) {
1970
- const thing = this.things.get(id);
1971
- if (thing) {
1972
- Object.assign(thing, updates);
1973
- }
1974
- }
1975
- /**
1976
- * Remove a thing
1977
- */
1978
- static remove(id) {
1979
- return this.things.delete(id);
1980
- }
1981
- /**
1982
- * Clear all things
1983
- */
1984
- static clear() {
1985
- this.things.clear();
1986
- this.idCounter = 0;
1987
- }
1988
- /**
1989
- * Move a thing
1990
- */
1991
- static move(id, dx, dy) {
1992
- const thing = this.things.get(id);
1993
- if (thing) {
1994
- thing.x += dx;
1995
- thing.y += dy;
1996
- }
1997
- }
1998
- /**
1999
- * Rotate a thing
2000
- */
2001
- static rotate(id, angle) {
2002
- const thing = this.things.get(id);
2003
- if (thing) {
2004
- thing.rotation += angle;
2005
- }
2006
- }
2007
- /**
2008
- * Scale a thing
2009
- */
2010
- static scale(id, factor) {
2011
- const thing = this.things.get(id);
2012
- if (thing) {
2013
- thing.scale *= factor;
2014
- }
2015
- }
2016
- /**
2017
- * Find things within a radius
2018
- */
2019
- static findNear(x, y, radius) {
2020
- const near = [];
2021
- const radiusSq = radius * radius;
2022
- this.things.forEach((thing) => {
2023
- const dx = thing.x - x;
2024
- const dy = thing.y - y;
2025
- if (dx * dx + dy * dy <= radiusSq) {
2026
- near.push(thing);
2027
- }
2028
- });
2029
- return near;
2030
- }
2031
- /**
2032
- * Find things that overlap with a rectangle
2033
- */
2034
- static findInRect(x, y, width, height) {
2035
- const found = [];
2036
- this.things.forEach((thing) => {
2037
- const halfW = thing.width * thing.scale / 2;
2038
- const halfH = thing.height * thing.scale / 2;
2039
- if (thing.x + halfW >= x && thing.x - halfW <= x + width && thing.y + halfH >= y && thing.y - halfH <= y + height) {
2040
- found.push(thing);
2041
- }
2042
- });
2043
- return found;
2044
- }
2045
- /**
2046
- * Apply a function to all things
2047
- */
2048
- static forEach(fn) {
2049
- this.things.forEach(fn);
2050
- }
2051
- /**
2052
- * Map things to a new array
2053
- */
2054
- static map(fn) {
2055
- return Array.from(this.things.values()).map(fn);
2056
- }
2057
- /**
2058
- * Filter things
2059
- */
2060
- static filter(fn) {
2061
- return Array.from(this.things.values()).filter(fn);
2062
- }
2063
- /**
2064
- * Sort things by a property or function
2065
- */
2066
- static sort(fn) {
2067
- return Array.from(this.things.values()).sort(fn);
2068
- }
2069
- /**
2070
- * Draw all things
2071
- */
2072
- static draw(ctx, options) {
2073
- const thingsToDraw = options?.filter ? this.filter(options.filter) : this.getAll();
2074
- thingsToDraw.forEach((thing) => {
2075
- if (options?.customDraw) {
2076
- options.customDraw(ctx, thing);
2077
- } else {
2078
- this.drawThing(ctx, thing);
2079
- }
2080
- });
2081
- }
2082
- /**
2083
- * Default drawing method for a thing
2084
- */
2085
- static drawThing(ctx, thing) {
2086
- ctx.save();
2087
- ctx.translate(thing.x, thing.y);
2088
- ctx.rotate(thing.rotation);
2089
- ctx.scale(thing.scale, thing.scale);
2090
- ctx.fillStyle = thing.color;
2091
- ctx.fillRect(
2092
- -thing.width / 2,
2093
- -thing.height / 2,
2094
- thing.width,
2095
- thing.height
2096
- );
2097
- ctx.restore();
2098
- }
2099
- /**
2100
- * Animate things with a simple physics update
2101
- */
2102
- static animatePhysics(deltaTime, options) {
2103
- const gravity = options?.gravity || 0;
2104
- const friction = options?.friction || 0.99;
2105
- const bounds = options?.bounds;
2106
- this.things.forEach((thing) => {
2107
- if (!thing.data.vx) thing.data.vx = 0;
2108
- if (!thing.data.vy) thing.data.vy = 0;
2109
- thing.data.vy += gravity;
2110
- thing.data.vx *= friction;
2111
- thing.data.vy *= friction;
2112
- thing.x += thing.data.vx;
2113
- thing.y += thing.data.vy;
2114
- if (bounds) {
2115
- const halfW = thing.width * thing.scale / 2;
2116
- const halfH = thing.height * thing.scale / 2;
2117
- if (thing.x - halfW < bounds.x) {
2118
- thing.x = bounds.x + halfW;
2119
- thing.data.vx *= -0.8;
2120
- }
2121
- if (thing.x + halfW > bounds.x + bounds.width) {
2122
- thing.x = bounds.x + bounds.width - halfW;
2123
- thing.data.vx *= -0.8;
2124
- }
2125
- if (thing.y - halfH < bounds.y) {
2126
- thing.y = bounds.y + halfH;
2127
- thing.data.vy *= -0.8;
2128
- }
2129
- if (thing.y + halfH > bounds.y + bounds.height) {
2130
- thing.y = bounds.y + bounds.height - halfH;
2131
- thing.data.vy *= -0.8;
2132
- }
2133
- }
2134
- });
2135
- }
2136
- /**
2137
- * Get the count of things
2138
- */
2139
- static count() {
2140
- return this.things.size;
2141
- }
2142
- /**
2143
- * Check if a thing exists
2144
- */
2145
- static has(id) {
2146
- return this.things.has(id);
2147
- }
2148
- /**
2149
- * Utility: Get distance between two things
2150
- */
2151
- static distance(id1, id2) {
2152
- const thing1 = this.things.get(id1);
2153
- const thing2 = this.things.get(id2);
2154
- if (!thing1 || !thing2) return Infinity;
2155
- const dx = thing2.x - thing1.x;
2156
- const dy = thing2.y - thing1.y;
2157
- return Math.sqrt(dx * dx + dy * dy);
2158
- }
2159
- /**
2160
- * Utility: Check collision between two things
2161
- */
2162
- static collides(id1, id2) {
2163
- const thing1 = this.things.get(id1);
2164
- const thing2 = this.things.get(id2);
2165
- if (!thing1 || !thing2) return false;
2166
- const halfW1 = thing1.width * thing1.scale / 2;
2167
- const halfH1 = thing1.height * thing1.scale / 2;
2168
- const halfW2 = thing2.width * thing2.scale / 2;
2169
- const halfH2 = thing2.height * thing2.scale / 2;
2170
- return Math.abs(thing1.x - thing2.x) < halfW1 + halfW2 && Math.abs(thing1.y - thing2.y) < halfH1 + halfH2;
2171
- }
2172
- };
2173
- Things.things = /* @__PURE__ */ new Map();
2174
- Things.config = {
2175
- maxThings: 1e3,
2176
- defaultSize: 50,
2177
- defaultColor: "#ffffff"
2178
- };
2179
- Things.idCounter = 0;
2180
-
2181
1763
  // src/plugins/Sprites.tsx
2182
1764
  var Sprites = class {
2183
1765
  /**
@@ -2480,10 +2062,555 @@ var SpriteAnimation = class {
2480
2062
  return this.playing;
2481
2063
  }
2482
2064
  };
2065
+
2066
+ // src/plugins/MatterPhysics.tsx
2067
+ var _MatterPhysics = class _MatterPhysics {
2068
+ /**
2069
+ * Dynamically load matter-js and initialize the engine.
2070
+ * Tries a local `import("matter-js")` first — if that fails (package not installed),
2071
+ * falls back to loading from CDN via a script tag.
2072
+ * @param config - Engine configuration
2073
+ * @returns Promise that resolves when ready
2074
+ *
2075
+ * @example
2076
+ * ```tsx
2077
+ * // In preload or setup:
2078
+ * await MatterPhysics.load({ gravity: { x: 0, y: 1 } });
2079
+ * ```
2080
+ */
2081
+ static async load(config = {}) {
2082
+ if (this._loaded && this.engine) return;
2083
+ try {
2084
+ const matterModule = await import("matter-js");
2085
+ const Matter = matterModule.default || matterModule;
2086
+ this.init(Matter, config);
2087
+ this._loaded = true;
2088
+ return;
2089
+ } catch {
2090
+ }
2091
+ try {
2092
+ const Matter = await this._loadFromCDN();
2093
+ this.init(Matter, config);
2094
+ this._loaded = true;
2095
+ } catch (e) {
2096
+ throw new Error(
2097
+ `MatterPhysics: failed to load matter-js. Install it with "npm install matter-js" or check your network connection for CDN fallback (${this.MATTER_CDN}).`
2098
+ );
2099
+ }
2100
+ }
2101
+ /**
2102
+ * Load Matter.js from CDN by injecting a script tag.
2103
+ * Resolves with the global `Matter` object.
2104
+ */
2105
+ static _loadFromCDN() {
2106
+ return new Promise((resolve, reject) => {
2107
+ if (typeof globalThis.Matter !== "undefined") {
2108
+ resolve(globalThis.Matter);
2109
+ return;
2110
+ }
2111
+ const script = document.createElement("script");
2112
+ script.src = this.MATTER_CDN;
2113
+ script.async = true;
2114
+ script.onload = () => {
2115
+ if (typeof globalThis.Matter !== "undefined") {
2116
+ resolve(globalThis.Matter);
2117
+ } else {
2118
+ reject(new Error("Matter global not found after script load"));
2119
+ }
2120
+ };
2121
+ script.onerror = () => {
2122
+ reject(new Error(`Failed to load script from ${this.MATTER_CDN}`));
2123
+ };
2124
+ document.head.appendChild(script);
2125
+ });
2126
+ }
2127
+ /**
2128
+ * Check if MatterPhysics has been loaded and initialized.
2129
+ */
2130
+ static get isLoaded() {
2131
+ return this._loaded && this.engine !== null;
2132
+ }
2133
+ /**
2134
+ * Initialize the physics engine with a Matter.js module reference.
2135
+ * Prefer `load()` for dynamic import. Use this if you want to pass the module directly.
2136
+ * @param matterModule - The Matter.js module (import Matter from 'matter-js')
2137
+ * @param config - Engine configuration
2138
+ */
2139
+ static init(matterModule, config = {}) {
2140
+ this.Matter = matterModule;
2141
+ this._loaded = true;
2142
+ this.engine = matterModule.Engine.create({
2143
+ enableSleeping: config.enableSleeping ?? false,
2144
+ constraintIterations: config.constraintIterations ?? 2,
2145
+ positionIterations: config.positionIterations ?? 6,
2146
+ velocityIterations: config.velocityIterations ?? 4
2147
+ });
2148
+ this.world = this.engine.world;
2149
+ if (config.gravity) {
2150
+ this.world.gravity.x = config.gravity.x;
2151
+ this.world.gravity.y = config.gravity.y;
2152
+ if (config.gravity.scale !== void 0) {
2153
+ this.world.gravity.scale = config.gravity.scale;
2154
+ }
2155
+ }
2156
+ this.bodies.clear();
2157
+ this.constraints.clear();
2158
+ this.idCounter = 0;
2159
+ this.constraintIdCounter = 0;
2160
+ }
2161
+ static ensureInit() {
2162
+ if (!this.Matter || !this.engine) {
2163
+ throw new Error(
2164
+ "MatterPhysics not initialized. Call MatterPhysics.init(Matter) first."
2165
+ );
2166
+ }
2167
+ }
2168
+ static generateId(prefix = "body") {
2169
+ return `${prefix}_${this.idCounter++}`;
2170
+ }
2171
+ /**
2172
+ * Add a rectangle body.
2173
+ * @param x - Center X
2174
+ * @param y - Center Y
2175
+ * @param width - Width
2176
+ * @param height - Height
2177
+ * @param options - Body options
2178
+ * @returns Body reference with id
2179
+ */
2180
+ static addRect(x, y, width, height, options = {}) {
2181
+ this.ensureInit();
2182
+ const id = options.id || this.generateId("rect");
2183
+ const body = this.Matter.Bodies.rectangle(x, y, width, height, options);
2184
+ this.Matter.Composite.add(this.world, body);
2185
+ const ref = { id, body, label: options.label };
2186
+ this.bodies.set(id, ref);
2187
+ return ref;
2188
+ }
2189
+ /**
2190
+ * Add a circle body.
2191
+ * @param x - Center X
2192
+ * @param y - Center Y
2193
+ * @param radius - Radius
2194
+ * @param options - Body options
2195
+ * @returns Body reference with id
2196
+ */
2197
+ static addCircle(x, y, radius, options = {}) {
2198
+ this.ensureInit();
2199
+ const id = options.id || this.generateId("circle");
2200
+ const body = this.Matter.Bodies.circle(x, y, radius, options);
2201
+ this.Matter.Composite.add(this.world, body);
2202
+ const ref = { id, body, label: options.label };
2203
+ this.bodies.set(id, ref);
2204
+ return ref;
2205
+ }
2206
+ /**
2207
+ * Add a polygon body.
2208
+ * @param x - Center X
2209
+ * @param y - Center Y
2210
+ * @param sides - Number of sides
2211
+ * @param radius - Radius
2212
+ * @param options - Body options
2213
+ * @returns Body reference with id
2214
+ */
2215
+ static addPolygon(x, y, sides, radius, options = {}) {
2216
+ this.ensureInit();
2217
+ const id = options.id || this.generateId("polygon");
2218
+ const body = this.Matter.Bodies.polygon(x, y, sides, radius, options);
2219
+ this.Matter.Composite.add(this.world, body);
2220
+ const ref = { id, body, label: options.label };
2221
+ this.bodies.set(id, ref);
2222
+ return ref;
2223
+ }
2224
+ /**
2225
+ * Add a body from custom vertices.
2226
+ * @param x - Center X
2227
+ * @param y - Center Y
2228
+ * @param vertices - Array of {x, y} points
2229
+ * @param options - Body options
2230
+ * @returns Body reference with id
2231
+ */
2232
+ static addFromVertices(x, y, vertices, options = {}) {
2233
+ this.ensureInit();
2234
+ const id = options.id || this.generateId("custom");
2235
+ const body = this.Matter.Bodies.fromVertices(x, y, vertices, options);
2236
+ this.Matter.Composite.add(this.world, body);
2237
+ const ref = { id, body, label: options.label };
2238
+ this.bodies.set(id, ref);
2239
+ return ref;
2240
+ }
2241
+ /**
2242
+ * Add a constraint between two bodies.
2243
+ * @param bodyIdA - First body ID
2244
+ * @param bodyIdB - Second body ID
2245
+ * @param options - Constraint options
2246
+ * @returns Constraint ID
2247
+ */
2248
+ static addConstraint(bodyIdA, bodyIdB, options = {}) {
2249
+ this.ensureInit();
2250
+ const refA = this.bodies.get(bodyIdA);
2251
+ const refB = this.bodies.get(bodyIdB);
2252
+ if (!refA || !refB) {
2253
+ throw new Error(`Body not found: ${!refA ? bodyIdA : bodyIdB}`);
2254
+ }
2255
+ const id = options.id || `constraint_${this.constraintIdCounter++}`;
2256
+ const constraint = this.Matter.Constraint.create({
2257
+ bodyA: refA.body,
2258
+ bodyB: refB.body,
2259
+ ...options
2260
+ });
2261
+ this.Matter.Composite.add(this.world, constraint);
2262
+ this.constraints.set(id, constraint);
2263
+ return id;
2264
+ }
2265
+ /**
2266
+ * Add a constraint anchored to a world point.
2267
+ * @param bodyId - Body ID
2268
+ * @param worldPoint - World anchor point {x, y}
2269
+ * @param options - Constraint options
2270
+ * @returns Constraint ID
2271
+ */
2272
+ static addWorldConstraint(bodyId, worldPoint, options = {}) {
2273
+ this.ensureInit();
2274
+ const ref = this.bodies.get(bodyId);
2275
+ if (!ref) throw new Error(`Body not found: ${bodyId}`);
2276
+ const id = options.id || `constraint_${this.constraintIdCounter++}`;
2277
+ const constraint = this.Matter.Constraint.create({
2278
+ bodyA: ref.body,
2279
+ pointB: worldPoint,
2280
+ ...options
2281
+ });
2282
+ this.Matter.Composite.add(this.world, constraint);
2283
+ this.constraints.set(id, constraint);
2284
+ return id;
2285
+ }
2286
+ /**
2287
+ * Apply force to a body.
2288
+ * @param bodyId - Body ID
2289
+ * @param force - Force vector {x, y}
2290
+ */
2291
+ static applyForce(bodyId, force) {
2292
+ this.ensureInit();
2293
+ const ref = this.bodies.get(bodyId);
2294
+ if (!ref) return;
2295
+ this.Matter.Body.applyForce(ref.body, ref.body.position, force);
2296
+ }
2297
+ /**
2298
+ * Set velocity of a body.
2299
+ * @param bodyId - Body ID
2300
+ * @param velocity - Velocity vector {x, y}
2301
+ */
2302
+ static setVelocity(bodyId, velocity) {
2303
+ this.ensureInit();
2304
+ const ref = this.bodies.get(bodyId);
2305
+ if (!ref) return;
2306
+ this.Matter.Body.setVelocity(ref.body, velocity);
2307
+ }
2308
+ /**
2309
+ * Set position of a body.
2310
+ * @param bodyId - Body ID
2311
+ * @param position - Position {x, y}
2312
+ */
2313
+ static setPosition(bodyId, position) {
2314
+ this.ensureInit();
2315
+ const ref = this.bodies.get(bodyId);
2316
+ if (!ref) return;
2317
+ this.Matter.Body.setPosition(ref.body, position);
2318
+ }
2319
+ /**
2320
+ * Set gravity.
2321
+ * @param x - Horizontal gravity
2322
+ * @param y - Vertical gravity
2323
+ */
2324
+ static setGravity(x, y) {
2325
+ this.ensureInit();
2326
+ this.world.gravity.x = x;
2327
+ this.world.gravity.y = y;
2328
+ }
2329
+ /**
2330
+ * Get a body reference by ID.
2331
+ */
2332
+ static getBody(id) {
2333
+ return this.bodies.get(id);
2334
+ }
2335
+ /**
2336
+ * Get all body references.
2337
+ */
2338
+ static getAllBodies() {
2339
+ return Array.from(this.bodies.values());
2340
+ }
2341
+ /**
2342
+ * Remove a body.
2343
+ * @param id - Body ID
2344
+ */
2345
+ static removeBody(id) {
2346
+ this.ensureInit();
2347
+ const ref = this.bodies.get(id);
2348
+ if (!ref) return false;
2349
+ this.Matter.Composite.remove(this.world, ref.body);
2350
+ return this.bodies.delete(id);
2351
+ }
2352
+ /**
2353
+ * Remove a constraint.
2354
+ * @param id - Constraint ID
2355
+ */
2356
+ static removeConstraint(id) {
2357
+ this.ensureInit();
2358
+ const constraint = this.constraints.get(id);
2359
+ if (!constraint) return false;
2360
+ this.Matter.Composite.remove(this.world, constraint);
2361
+ return this.constraints.delete(id);
2362
+ }
2363
+ /**
2364
+ * Step the physics engine.
2365
+ * @param deltaTime - Time step in milliseconds
2366
+ */
2367
+ static update(deltaTime) {
2368
+ this.ensureInit();
2369
+ this.Matter.Engine.update(this.engine, deltaTime);
2370
+ }
2371
+ /**
2372
+ * Debug draw all bodies and constraints using Klint drawing primitives.
2373
+ * @param ctx - Klint context
2374
+ * @param options - Drawing options
2375
+ */
2376
+ static draw(ctx, options = {}) {
2377
+ const {
2378
+ showBodies = true,
2379
+ showConstraints = true,
2380
+ showBounds = false,
2381
+ bodyStroke = "#ffffff",
2382
+ bodyFill = "transparent",
2383
+ staticFill = "#666666",
2384
+ constraintStroke = "#44ff44",
2385
+ lineWidth = 1
2386
+ } = options;
2387
+ ctx.save();
2388
+ ctx.strokeWidth(lineWidth);
2389
+ if (showBodies) {
2390
+ this.bodies.forEach((ref) => {
2391
+ const body = ref.body;
2392
+ const vertices = body.vertices;
2393
+ ctx.fillStyle = body.isStatic ? staticFill : bodyFill;
2394
+ ctx.strokeStyle = bodyStroke;
2395
+ ctx.beginShape();
2396
+ for (let i = 0; i < vertices.length; i++) {
2397
+ ctx.vertex(vertices[i].x, vertices[i].y);
2398
+ }
2399
+ ctx.endShape(true);
2400
+ if (showBounds) {
2401
+ const { min, max } = body.bounds;
2402
+ ctx.noFill();
2403
+ ctx.strokeStyle = "#ff444444";
2404
+ ctx.setRectOrigin("corner");
2405
+ ctx.rectangle(min.x, min.y, max.x - min.x, max.y - min.y);
2406
+ }
2407
+ });
2408
+ }
2409
+ if (showConstraints) {
2410
+ ctx.strokeStyle = constraintStroke;
2411
+ this.constraints.forEach((constraint) => {
2412
+ const { bodyA, bodyB, pointA, pointB } = constraint;
2413
+ const startX = bodyA ? bodyA.position.x + (pointA?.x || 0) : pointA?.x || 0;
2414
+ const startY = bodyA ? bodyA.position.y + (pointA?.y || 0) : pointA?.y || 0;
2415
+ const endX = bodyB ? bodyB.position.x + (pointB?.x || 0) : pointB?.x || 0;
2416
+ const endY = bodyB ? bodyB.position.y + (pointB?.y || 0) : pointB?.y || 0;
2417
+ ctx.line(startX, startY, endX, endY);
2418
+ });
2419
+ }
2420
+ ctx.restore();
2421
+ }
2422
+ /**
2423
+ * Iterate over all bodies with their position and angle.
2424
+ * Convenience method for syncing physics bodies to visual objects.
2425
+ * @param fn - Callback receiving body data
2426
+ */
2427
+ static forEach(fn) {
2428
+ this.bodies.forEach((ref) => {
2429
+ fn({
2430
+ id: ref.id,
2431
+ x: ref.body.position.x,
2432
+ y: ref.body.position.y,
2433
+ angle: ref.body.angle,
2434
+ velocity: ref.body.velocity,
2435
+ body: ref.body,
2436
+ label: ref.label
2437
+ });
2438
+ });
2439
+ }
2440
+ /**
2441
+ * Register a collision callback.
2442
+ * @param event - Event type: 'collisionStart', 'collisionActive', 'collisionEnd'
2443
+ * @param callback - Callback receiving collision pairs
2444
+ */
2445
+ static onCollision(event, callback) {
2446
+ this.ensureInit();
2447
+ this.Matter.Events.on(this.engine, event, (e) => {
2448
+ callback(e.pairs);
2449
+ });
2450
+ }
2451
+ /**
2452
+ * Get the raw Matter.js engine for advanced usage.
2453
+ */
2454
+ static getEngine() {
2455
+ return this.engine;
2456
+ }
2457
+ /**
2458
+ * Get the raw Matter.js world for advanced usage.
2459
+ */
2460
+ static getWorld() {
2461
+ return this.world;
2462
+ }
2463
+ /**
2464
+ * Clear all bodies, constraints, and reset the engine.
2465
+ */
2466
+ static clear() {
2467
+ if (this.world) {
2468
+ this.Matter?.Composite.clear(this.world, false);
2469
+ }
2470
+ this.bodies.clear();
2471
+ this.constraints.clear();
2472
+ this.idCounter = 0;
2473
+ this.constraintIdCounter = 0;
2474
+ }
2475
+ /**
2476
+ * Destroy the engine entirely.
2477
+ */
2478
+ static destroy() {
2479
+ this.clear();
2480
+ if (this.engine) {
2481
+ this.Matter?.Engine.clear(this.engine);
2482
+ }
2483
+ this.engine = null;
2484
+ this.world = null;
2485
+ this.Matter = null;
2486
+ this._loaded = false;
2487
+ }
2488
+ };
2489
+ _MatterPhysics.Matter = null;
2490
+ _MatterPhysics.engine = null;
2491
+ _MatterPhysics.world = null;
2492
+ _MatterPhysics.bodies = /* @__PURE__ */ new Map();
2493
+ _MatterPhysics.constraints = /* @__PURE__ */ new Map();
2494
+ _MatterPhysics.idCounter = 0;
2495
+ _MatterPhysics.constraintIdCounter = 0;
2496
+ _MatterPhysics._loaded = false;
2497
+ /** Matter.js version loaded from CDN when not installed locally */
2498
+ _MatterPhysics.MATTER_VERSION = "0.20.0";
2499
+ /** CDN URL template — version is interpolated */
2500
+ _MatterPhysics.MATTER_CDN = `https://cdnjs.cloudflare.com/ajax/libs/matter-js/${_MatterPhysics.MATTER_VERSION}/matter.min.js`;
2501
+ var MatterPhysics = _MatterPhysics;
2502
+
2503
+ // src/plugins/Projector.tsx
2504
+ var RAD_TO_DEG = 180 / Math.PI;
2505
+ var Projector = class {
2506
+ constructor(options) {
2507
+ this.perspective = options?.perspective ?? 2;
2508
+ this.radius = options?.radius ?? 1;
2509
+ }
2510
+ /**
2511
+ * Build a DOMMatrix from a transform callback.
2512
+ * The callback receives a Transform3D object — call rotateX, rotateY, etc.
2513
+ * in the order you want them applied.
2514
+ */
2515
+ buildMatrix(transforms) {
2516
+ const m = new DOMMatrix();
2517
+ if (!transforms) return m;
2518
+ transforms({
2519
+ rotateX: (r) => m.rotateAxisAngleSelf(1, 0, 0, r * RAD_TO_DEG),
2520
+ rotateY: (r) => m.rotateAxisAngleSelf(0, 1, 0, r * RAD_TO_DEG),
2521
+ rotateZ: (r) => m.rotateAxisAngleSelf(0, 0, 1, r * RAD_TO_DEG),
2522
+ translate: (x, y, z) => m.translateSelf(x, y, z),
2523
+ scale: (x, y, z) => m.scaleSelf(x, y ?? x, z ?? x)
2524
+ });
2525
+ return m;
2526
+ }
2527
+ /**
2528
+ * Apply perspective projection to a transformed 3D point.
2529
+ * Returns 2D coordinates + depth + scale factor.
2530
+ */
2531
+ projectPoint(m, point) {
2532
+ const p = m.transformPoint(new DOMPoint(point.x, point.y, point.z, 1));
2533
+ const d = this.perspective;
2534
+ const f = d === 0 ? 1 : 1 / (d - p.z);
2535
+ const r = this.radius;
2536
+ return {
2537
+ x: p.x * f * r,
2538
+ y: p.y * f * r,
2539
+ z: p.z,
2540
+ scale: Math.abs(f)
2541
+ };
2542
+ }
2543
+ /**
2544
+ * Project a single 3D point.
2545
+ *
2546
+ * @param point - The 3D point to project.
2547
+ * @param transforms - Optional callback to apply 3D transforms before projection.
2548
+ * @returns Projected 2D point with depth and scale info.
2549
+ *
2550
+ * @example
2551
+ * ```tsx
2552
+ * const p = projector.project({ x: 0, y: 1, z: 0 }, (t) => {
2553
+ * t.rotateY(angle);
2554
+ * });
2555
+ * K.circle(p.x, p.y, 8 * p.scale);
2556
+ * ```
2557
+ */
2558
+ project(point, transforms) {
2559
+ return this.projectPoint(this.buildMatrix(transforms), point);
2560
+ }
2561
+ /**
2562
+ * Project an array of 3D points with the same transform.
2563
+ * The transform matrix is built once and reused for all points.
2564
+ *
2565
+ * @param points - Array of 3D points.
2566
+ * @param transforms - Optional transform callback (applied identically to all points).
2567
+ * @returns Array of projected points (same order as input).
2568
+ *
2569
+ * @example
2570
+ * ```tsx
2571
+ * const projected = projector.projectAll(cubeVertices, (t) => {
2572
+ * t.rotateX(K.time);
2573
+ * t.rotateY(K.time * 0.7);
2574
+ * });
2575
+ * for (const p of projected) {
2576
+ * K.circle(p.x, p.y, 6 * p.scale);
2577
+ * }
2578
+ * ```
2579
+ */
2580
+ projectAll(points, transforms) {
2581
+ const m = this.buildMatrix(transforms);
2582
+ return points.map((pt) => this.projectPoint(m, pt));
2583
+ }
2584
+ /**
2585
+ * Project and depth-sort an array of 3D points (back-to-front).
2586
+ * Each result includes `index` — the original index in the input array,
2587
+ * so you can map back to colors, labels, etc.
2588
+ *
2589
+ * @param points - Array of 3D points.
2590
+ * @param transforms - Optional transform callback.
2591
+ * @returns Depth-sorted array of projected points with original indices.
2592
+ *
2593
+ * @example
2594
+ * ```tsx
2595
+ * const sorted = projector.projectSorted(points, (t) => {
2596
+ * t.rotateX(K.time);
2597
+ * });
2598
+ * for (const p of sorted) {
2599
+ * K.fillColor(colors[p.index]);
2600
+ * K.circle(p.x, p.y, 10 * p.scale);
2601
+ * }
2602
+ * ```
2603
+ */
2604
+ projectSorted(points, transforms) {
2605
+ const m = this.buildMatrix(transforms);
2606
+ return points.map((pt, i) => ({ ...this.projectPoint(m, pt), index: i })).sort((a, b) => a.z - b.z);
2607
+ }
2608
+ };
2483
2609
  export {
2484
2610
  CatmullRom,
2485
2611
  Delaunay,
2486
2612
  FontParser,
2487
- Sprites,
2488
- Things
2613
+ MatterPhysics,
2614
+ Projector,
2615
+ Sprites
2489
2616
  };