create-definedmotion 0.1.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.
Files changed (98) hide show
  1. package/bin/index.js +3 -0
  2. package/package.json +37 -0
  3. package/src/cli.js +100 -0
  4. package/template/.editorconfig +9 -0
  5. package/template/.prettierignore +6 -0
  6. package/template/.prettierrc.yaml +10 -0
  7. package/template/_gitignore +10 -0
  8. package/template/build/entitlements.mac.plist +12 -0
  9. package/template/build/icon.icns +0 -0
  10. package/template/build/icon.ico +0 -0
  11. package/template/build/icon.png +0 -0
  12. package/template/electron-builder.yml +43 -0
  13. package/template/electron.vite.config.ts +50 -0
  14. package/template/eslint.config.mjs +24 -0
  15. package/template/package-lock.json +10299 -0
  16. package/template/package.json +64 -0
  17. package/template/resources/icon.png +0 -0
  18. package/template/src/assets/audio/fadeSound.mp3 +0 -0
  19. package/template/src/assets/audio/fadeSound2.mp3 +0 -0
  20. package/template/src/assets/audio/interstellar.mp3 +0 -0
  21. package/template/src/assets/audio/keyboard1.mp3 +0 -0
  22. package/template/src/assets/audio/keyboard2.mp3 +0 -0
  23. package/template/src/assets/audio/keyboard3.mp3 +0 -0
  24. package/template/src/assets/audio/tick_sound.mp3 +0 -0
  25. package/template/src/assets/base.css +67 -0
  26. package/template/src/assets/electron.svg +10 -0
  27. package/template/src/assets/fonts/Geo-Regular.woff +0 -0
  28. package/template/src/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 +0 -0
  29. package/template/src/assets/fonts/Montserrat-Medium.ttf +0 -0
  30. package/template/src/assets/fonts/Montserrat-Medium.woff +0 -0
  31. package/template/src/assets/fonts/Montserrat-VariableFont_wght.woff2 +0 -0
  32. package/template/src/assets/fonts/glitch.ttf +0 -0
  33. package/template/src/assets/hdri/indoor1.hdr +0 -0
  34. package/template/src/assets/hdri/metro1.hdr +0 -0
  35. package/template/src/assets/hdri/outdoor1.hdr +0 -0
  36. package/template/src/assets/hdri/photo-studio1.hdr +0 -0
  37. package/template/src/assets/hdri/photo-studio2.hdr +0 -0
  38. package/template/src/assets/hdri/photo-studio3.hdr +0 -0
  39. package/template/src/assets/objects/keyboardScene/ibm-keyboard.glb +0 -0
  40. package/template/src/assets/wavy-lines.svg +25 -0
  41. package/template/src/entry.ts +20 -0
  42. package/template/src/example_scenes/alternativesScene.ts +88 -0
  43. package/template/src/example_scenes/dependencyScene.ts +116 -0
  44. package/template/src/example_scenes/fourierMachineScene.ts +108 -0
  45. package/template/src/example_scenes/fourierSeriesScene.ts +678 -0
  46. package/template/src/example_scenes/keyboardScene.ts +447 -0
  47. package/template/src/example_scenes/surfaceScene.ts +88 -0
  48. package/template/src/example_scenes/tutorials/easy1.ts +59 -0
  49. package/template/src/example_scenes/tutorials/easy2.ts +141 -0
  50. package/template/src/example_scenes/tutorials/easy3.ts +133 -0
  51. package/template/src/example_scenes/tutorials/medium1.ts +154 -0
  52. package/template/src/example_scenes/vectorField.ts +209 -0
  53. package/template/src/example_scenes/visulizingFunctions.ts +246 -0
  54. package/template/src/main/index.ts +101 -0
  55. package/template/src/main/rendering.ts +219 -0
  56. package/template/src/main/storage.ts +35 -0
  57. package/template/src/preload/index.d.ts +8 -0
  58. package/template/src/preload/index.ts +36 -0
  59. package/template/src/renderer/index.html +17 -0
  60. package/template/src/renderer/src/App.svelte +130 -0
  61. package/template/src/renderer/src/app.css +24 -0
  62. package/template/src/renderer/src/env.d.ts +2 -0
  63. package/template/src/renderer/src/lib/animation/animations.ts +214 -0
  64. package/template/src/renderer/src/lib/animation/captureCanvas.ts +85 -0
  65. package/template/src/renderer/src/lib/animation/helpers.ts +7 -0
  66. package/template/src/renderer/src/lib/animation/interpolations.ts +155 -0
  67. package/template/src/renderer/src/lib/animation/protocols.ts +79 -0
  68. package/template/src/renderer/src/lib/audio/loader.ts +104 -0
  69. package/template/src/renderer/src/lib/fonts/Roboto_Regular.json +1 -0
  70. package/template/src/renderer/src/lib/fonts/montserrat-medium.json +1 -0
  71. package/template/src/renderer/src/lib/fonts/montserrat.json +1 -0
  72. package/template/src/renderer/src/lib/general/helpers.ts +77 -0
  73. package/template/src/renderer/src/lib/general/onDestory.ts +10 -0
  74. package/template/src/renderer/src/lib/mathHelpers/vectors.ts +18 -0
  75. package/template/src/renderer/src/lib/rendering/bumpMaps/noise.ts +84 -0
  76. package/template/src/renderer/src/lib/rendering/helpers.ts +35 -0
  77. package/template/src/renderer/src/lib/rendering/lighting3d.ts +387 -0
  78. package/template/src/renderer/src/lib/rendering/materials.ts +6 -0
  79. package/template/src/renderer/src/lib/rendering/objects/import.ts +148 -0
  80. package/template/src/renderer/src/lib/rendering/objects2d.ts +489 -0
  81. package/template/src/renderer/src/lib/rendering/objects3d.ts +89 -0
  82. package/template/src/renderer/src/lib/rendering/protocols.ts +21 -0
  83. package/template/src/renderer/src/lib/rendering/setup.ts +71 -0
  84. package/template/src/renderer/src/lib/rendering/svg/drawing.ts +213 -0
  85. package/template/src/renderer/src/lib/rendering/svg/parsing.ts +717 -0
  86. package/template/src/renderer/src/lib/rendering/svg/rastered.ts +42 -0
  87. package/template/src/renderer/src/lib/rendering/svgObjects.ts +1137 -0
  88. package/template/src/renderer/src/lib/scene/helpers.ts +89 -0
  89. package/template/src/renderer/src/lib/scene/sceneClass.ts +648 -0
  90. package/template/src/renderer/src/lib/shaders/background_gradient/frag.glsl +12 -0
  91. package/template/src/renderer/src/lib/shaders/background_gradient/vert.glsl +6 -0
  92. package/template/src/renderer/src/lib/shaders/hdri_blur/frag.glsl +45 -0
  93. package/template/src/renderer/src/lib/shaders/hdri_blur/vert.glsl +5 -0
  94. package/template/src/renderer/src/main.ts +9 -0
  95. package/template/svelte.config.mjs +7 -0
  96. package/template/tsconfig.json +4 -0
  97. package/template/tsconfig.node.json +10 -0
  98. package/template/tsconfig.web.json +32 -0
@@ -0,0 +1,717 @@
1
+ import { parse, RootNode, Node, ElementNode } from 'svg-parser'
2
+ import { Command, parseSVG as parseSVGPath } from 'svg-path-parser'
3
+
4
+ import {
5
+ fromObject,
6
+ applyToPoint,
7
+ compose,
8
+ Matrix,
9
+ translate,
10
+ scale,
11
+ rotate,
12
+ skew,
13
+ transform,
14
+ identity
15
+ } from 'transformation-matrix'
16
+ import { drawVectorizedNodes } from './drawing'
17
+
18
+ export const createSVGShape = (svg: string, width: number) => {
19
+ return drawVectorizedNodes(vectorizeSVGStructure(parseSVGString(svg)), width)
20
+ }
21
+
22
+ export const parseSVGString = (svg: string) => {
23
+ return parse(svg)
24
+ }
25
+
26
+ export interface VectorizedElementNode {
27
+ type: 'element'
28
+ tagName?: string | undefined
29
+ properties?: Record<string, string | number> | undefined
30
+ children: Array<VectorizedElementNode | string>
31
+ value?: string | undefined
32
+ metadata?: string | undefined
33
+ points: [number, number][]
34
+ subpathIndices: number[]
35
+ isClosed: boolean
36
+ }
37
+
38
+ export function isApproximatelyEqual2D(
39
+ a: [number, number],
40
+ b: [number, number],
41
+ tolerance = 1e-6
42
+ ): boolean {
43
+ return Math.abs(a[0] - b[0]) < tolerance && Math.abs(a[1] - b[1]) < tolerance
44
+ }
45
+
46
+ export function isApproximatelyEqual2d3d(
47
+ a: [number, number, number],
48
+ b: [number, number, number],
49
+ tolerance = 1e-6
50
+ ): boolean {
51
+ return Math.abs(a[0] - b[0]) < tolerance && Math.abs(a[1] - b[1]) < tolerance
52
+ }
53
+
54
+ // Placeholder function for points conversion
55
+ function generatePoints(
56
+ node: ElementNode,
57
+ idMap: Map<string, ElementNode>
58
+ ): {
59
+ points: [number, number][]
60
+ subpathIndices: number[]
61
+ } {
62
+ const tag = node.tagName.toLowerCase()
63
+ const props = node.properties as any
64
+
65
+ switch (tag) {
66
+ case 'line': {
67
+ const x1 = props.x1
68
+ const y1 = props.y1
69
+ const x2 = props.x2
70
+ const y2 = props.y2
71
+ return {
72
+ points: [
73
+ [x1, y1],
74
+ [x2, y2]
75
+ ],
76
+ subpathIndices: []
77
+ }
78
+ }
79
+
80
+ case 'polyline':
81
+ case 'polygon': {
82
+ const raw = props.points // e.g. "10,20 30,40 50,60"
83
+ const coords = raw
84
+ .trim()
85
+ .split(/[\s,]+/)
86
+ .map(Number) // [10, 20, 30, 40, 50, 60]
87
+ const points: [number, number][] = []
88
+ for (let i = 0; i < coords.length - 1; i += 2) {
89
+ points.push([coords[i], coords[i + 1]])
90
+ }
91
+ if (tag === 'polygon' && points.length > 0) {
92
+ points.push([...points[0]]) // Close the polygon
93
+ }
94
+ return { points, subpathIndices: [] }
95
+ }
96
+
97
+ case 'rect': {
98
+ const x = props.x
99
+ const y = props.y
100
+ const width = props.width
101
+ const height = props.height
102
+ const rx = props.rx || 0
103
+ const ry = props.ry || 0
104
+
105
+ if (rx === 0 && ry === 0) {
106
+ // Plain rectangle
107
+ return {
108
+ points: [
109
+ [x, y],
110
+ [x + width, y],
111
+ [x + width, y + height],
112
+ [x, y + height],
113
+ [x, y]
114
+ ],
115
+ subpathIndices: []
116
+ }
117
+ } else {
118
+ // Optional: approximate rounded rectangle
119
+ // You could add arc samples at each corner here
120
+ console.warn('Rounded rect approximation not implemented')
121
+ return { points: [], subpathIndices: [] }
122
+ }
123
+ }
124
+
125
+ case 'circle': {
126
+ const cx = props.cx
127
+ const cy = props.cy
128
+ const r = props.r
129
+ const samples = 100
130
+ return {
131
+ points: Array.from({ length: samples + 1 }, (_, i) => {
132
+ const theta = (2 * Math.PI * i) / samples
133
+ return [cx + r * Math.cos(theta), cy + r * Math.sin(theta)]
134
+ }),
135
+ subpathIndices: []
136
+ }
137
+ }
138
+
139
+ case 'ellipse': {
140
+ const cx = props.cx
141
+ const cy = props.cy
142
+ const rx = props.rx
143
+ const ry = props.ry
144
+ const samples = 32
145
+ return {
146
+ points: Array.from({ length: samples + 1 }, (_, i) => {
147
+ const theta = (2 * Math.PI * i) / samples
148
+ return [cx + rx * Math.cos(theta), cy + ry * Math.sin(theta)]
149
+ }),
150
+ subpathIndices: []
151
+ }
152
+ }
153
+
154
+ case 'path': {
155
+ const d = props.d
156
+ const pointsObject = pathToPoints(parseSVGPath(d)) // Your earlier logic
157
+
158
+ return pointsObject
159
+ }
160
+
161
+ default:
162
+ //console.warn(`Unsupported SVG tag: <${tag}>`)
163
+ return { points: [], subpathIndices: [] }
164
+ }
165
+ }
166
+
167
+ export function vectorizeSVGStructure(root: RootNode) {
168
+ const idMap = new Map<string, ElementNode>()
169
+
170
+ function buildIdMap(node: Node) {
171
+ if (typeof node === 'string' || node.type === 'text') return
172
+ if (node.properties?.id) {
173
+ idMap.set(node.properties.id as string, node)
174
+ }
175
+ for (const child of node.children) {
176
+ buildIdMap(child as Node)
177
+ }
178
+ }
179
+ buildIdMap(root as any)
180
+
181
+ function transformNode(
182
+ node: Node | string,
183
+ parentMatrix: Matrix,
184
+ parentStyles: Record<string, string | number> = {}
185
+ ): VectorizedElementNode | undefined {
186
+ if (typeof node === 'string' || node.type === 'text') return undefined
187
+
188
+ // Skip elements inside <defs> (they'll only be used via references)
189
+ if ((node as ElementNode).tagName?.toLowerCase() === 'defs') return undefined
190
+
191
+ const copyParentStyles = { ...parentStyles }
192
+ removeTransforms(copyParentStyles)
193
+
194
+ const props = node.properties ?? {}
195
+ const tag = (node as ElementNode).tagName?.toLowerCase()
196
+
197
+ //Link content to "use" elements
198
+ if (tag === 'use') {
199
+ const href: string = (props['xlink:href'] as string) || (props.href as string)
200
+ if (!href) return undefined
201
+
202
+ const id = href.startsWith('#') ? href.slice(1) : href
203
+ const referenced = idMap.get(id)
204
+ if (!referenced) return undefined
205
+
206
+ // Clone the defined element since the same definition can be used in multiple places and we don't want to affect the source
207
+ const clonedDef: ElementNode = {
208
+ ...referenced,
209
+ properties: {
210
+ ...referenced.properties, // Base styles from symbol
211
+ ...parentStyles, // Inherited from parent
212
+ ...props, // <use> overrides
213
+ // Explicitly remove processed attributes
214
+ transform: undefined,
215
+ x: undefined,
216
+ y: undefined
217
+ }
218
+ }
219
+
220
+ // Apply <use> transforms (x/y and transform attribute)
221
+ const x = (props.x as number) || 0
222
+ const y = (props.y as number) || 0
223
+ const useTransforms = parseSVGTransform((props.transform as string) || '')
224
+ if (x || y) useTransforms.push({ type: 'translate', params: { tx: x, ty: y } })
225
+
226
+ const useMatrix = compose(parentMatrix, applyTransforms(useTransforms))
227
+
228
+ // Process referenced element with combined transforms
229
+ const referencedNode = transformNode(clonedDef, useMatrix)
230
+ if (!referencedNode) return undefined
231
+
232
+ return {
233
+ ...clonedDef,
234
+ points: referencedNode.points,
235
+ subpathIndices: referencedNode.subpathIndices,
236
+ isClosed: referencedNode.isClosed,
237
+ children: [referencedNode]
238
+ }
239
+ }
240
+ const effectiveProps = { ...parentStyles, ...node.properties }
241
+ const transformStr = typeof props.transform === 'string' ? props.transform : ''
242
+ const localTransforms = parseSVGTransform(transformStr)
243
+ const localMatrix = applyTransforms(localTransforms)
244
+
245
+ // Combine parent and local transform
246
+ const combinedMatrix = compose(parentMatrix, localMatrix)
247
+
248
+ // Apply transform to points
249
+ const rawPoints = generatePoints(node as ElementNode, idMap)
250
+ const transformedPoints: [number, number][] = rawPoints.points.map(([x, y]) => {
251
+ const { x: tx, y: ty } = applyToPoint(combinedMatrix, { x, y })
252
+ return [tx, ty]
253
+ })
254
+
255
+ const vectorizedNode: VectorizedElementNode = {
256
+ ...node,
257
+ properties: effectiveProps,
258
+ points: transformedPoints,
259
+ subpathIndices: rawPoints.subpathIndices,
260
+ isClosed:
261
+ transformedPoints.length === 0
262
+ ? false
263
+ : isApproximatelyEqual2D(
264
+ transformedPoints[0],
265
+ transformedPoints[transformedPoints.length - 1]
266
+ )
267
+ }
268
+
269
+ // Recursively transform children
270
+ const transformedChildren: VectorizedElementNode[] = []
271
+ for (const child of node.children) {
272
+ const transformedChild = transformNode(child, combinedMatrix, effectiveProps)
273
+ if (transformedChild) transformedChildren.push(transformedChild)
274
+ }
275
+ vectorizedNode.children = transformedChildren
276
+
277
+ return vectorizedNode
278
+ }
279
+ const result: VectorizedElementNode[] = []
280
+
281
+ for (const child of root.children) {
282
+ const topNode = transformNode(child, identity(), {})
283
+ if (topNode) result.push(topNode)
284
+ }
285
+
286
+ return result
287
+ }
288
+
289
+ function removeTransforms(properties: any) {
290
+ delete properties.transform
291
+ delete properties.x
292
+ delete properties.y
293
+ return properties
294
+ }
295
+
296
+ function parseSVGTransform(string: string) {
297
+ const transforms: Array<{
298
+ type: 'translate' | 'matrix' | 'rotate' | 'skewX' | 'skewY' | 'scale'
299
+ params: Record<string, number>
300
+ }> = []
301
+
302
+ const matches = string.match(/(translate|matrix|rotate|skewX|skewY|scale)\([^)]*\)/g)
303
+
304
+ if (matches) {
305
+ for (const match of matches) {
306
+ const [type, values] = match.split('(')
307
+ const params = values
308
+ .replace(')', '')
309
+ .split(/[, \t\n]+/)
310
+ .map(parseFloat)
311
+
312
+ switch (type as any) {
313
+ case 'translate':
314
+ transforms.push({
315
+ type: 'translate',
316
+ params: {
317
+ tx: params[0] || 0,
318
+ ty: params[1] || 0
319
+ }
320
+ })
321
+ break
322
+
323
+ case 'scale':
324
+ transforms.push({
325
+ type: 'scale',
326
+ params: {
327
+ sx: params[0] || 1,
328
+ sy: params[1] || params[0] || 1
329
+ }
330
+ })
331
+ break
332
+
333
+ case 'rotate':
334
+ transforms.push({
335
+ type: 'rotate',
336
+ params: {
337
+ angle: params[0] || 0,
338
+ cx: params[1] || 0,
339
+ cy: params[2] || 0
340
+ }
341
+ })
342
+ break
343
+
344
+ case 'skewX':
345
+ transforms.push({
346
+ type: 'skewX',
347
+ params: { angle: params[0] || 0 }
348
+ })
349
+ break
350
+
351
+ case 'skewY':
352
+ transforms.push({
353
+ type: 'skewY',
354
+ params: { angle: params[0] || 0 }
355
+ })
356
+ break
357
+
358
+ case 'matrix':
359
+ transforms.push({
360
+ type: 'matrix',
361
+ params: {
362
+ a: params[0] || 0,
363
+ b: params[1] || 0,
364
+ c: params[2] || 0,
365
+ d: params[3] || 0,
366
+ e: params[4] || 0,
367
+ f: params[5] || 0
368
+ }
369
+ })
370
+ break
371
+ }
372
+ }
373
+ }
374
+
375
+ return transforms
376
+ }
377
+
378
+ function applyTransforms(transforms) {
379
+ if (transforms.length === 0) return identity()
380
+ return compose(
381
+ ...transforms.map((t) => {
382
+ switch (t.type) {
383
+ case 'translate':
384
+ return translate(t.params.tx, t.params.ty)
385
+ case 'rotate':
386
+ return rotate((t.params.angle * Math.PI) / 180, t.params.cx, t.params.cy)
387
+ case 'scale':
388
+ return scale(t.params.sx, t.params.sy)
389
+ case 'skewX':
390
+ return skew((t.params.angle * Math.PI) / 180, 0)
391
+ case 'skewY':
392
+ return skew(0, (t.params.angle * Math.PI) / 180)
393
+ case 'matrix':
394
+ // Explicit matrix parameter passing
395
+ return transform(t.params.a, t.params.b, t.params.c, t.params.d, t.params.e, t.params.f)
396
+ default:
397
+ return translate(0, 0)
398
+ }
399
+ })
400
+ )
401
+ }
402
+
403
+ /*
404
+ Example of commands from:
405
+
406
+ [ { code:'M', command:'moveto', x:3, y:7 },
407
+ { code:'L', command:'lineto', x:5, y:-6 },
408
+ { code:'L', command:'lineto', x:1, y:7 },
409
+ { code:'L', command:'lineto', x:100, y:-0.4 },
410
+ { code:'m', command:'moveto', relative:true, x:-10, y:10 },
411
+ { code:'l', command:'lineto', relative:true, x:10, y:0 },
412
+ { code:'V', command:'vertical lineto', y:27 },
413
+ { code:'V', command:'vertical lineto', y:89 },
414
+ { code:'H', command:'horizontal lineto', x:23 },
415
+ { code:'v', command:'vertical lineto', relative:true, y:10 },
416
+ { code:'h', command:'horizontal lineto', relative:true, x:10 },
417
+ { code:'C', command:'curveto', x1:33, y1:43, x2:38, y2:47, x:43, y:47 },
418
+ { code:'c', command:'curveto', relative:true, x1:0, y1:5, x2:5, y2:10, x:10, y:10 },
419
+ { code:'S', command:'smooth curveto', x2:63, y2:67, x:63, y:67 },
420
+ { code:'s', command:'smooth curveto', relative:true, x2:-10, y2:10, x:10, y:10 },
421
+ { code:'Q', command:'quadratic curveto', x1:50, y1:50, x:73, y:57 },
422
+ { code:'q', command:'quadratic curveto', relative:true, x1:20, y1:-5, x:0, y:-10 },
423
+ { code:'T', command:'smooth quadratic curveto', x:70, y:40 },
424
+ { code:'t', command:'smooth quadratic curveto', relative:true, x:0, y:-15 },
425
+ { code:'A', command:'elliptical arc', rx:5, ry:5, xAxisRotation:45, largeArc:true, sweep:false, x:40, y:20 },
426
+ { code:'a', command:'elliptical arc', relative:true, rx:5, ry:5, xAxisRotation:20, largeArc:false, sweep:true, x:-10, y:-10 },
427
+ { code:'Z', command:'closepath' } ]
428
+
429
+ */
430
+
431
+ export function pathToPoints(parsedPath: Command[]) {
432
+ const points: [number, number][] = []
433
+ const subpathIndices: number[] = []
434
+ let current: [number, number] = [0, 0]
435
+ let lastControl: [number, number] | null = null
436
+ let startPoint: [number, number] = [0, 0]
437
+
438
+ for (const cmd of parsedPath) {
439
+ switch (cmd.code) {
440
+ case 'M':
441
+ subpathIndices.push(points.length)
442
+ current = [cmd.x, cmd.y]
443
+ startPoint = current
444
+ points.push([current[0], current[1]])
445
+ lastControl = null
446
+ break
447
+
448
+ case 'm':
449
+ subpathIndices.push(points.length)
450
+ current = [current[0] + cmd.x, current[1] + cmd.y]
451
+ startPoint = current
452
+ points.push([current[0], current[1]])
453
+ lastControl = null
454
+ break
455
+
456
+ case 'L':
457
+ current = [cmd.x, cmd.y]
458
+ points.push([current[0], current[1]])
459
+ lastControl = null
460
+ break
461
+
462
+ case 'l':
463
+ current = [current[0] + cmd.x, current[1] + cmd.y]
464
+ points.push([current[0], current[1]])
465
+ lastControl = null
466
+ break
467
+
468
+ case 'H':
469
+ current = [cmd.x, current[1]]
470
+ points.push([current[0], current[1]])
471
+ lastControl = null
472
+ break
473
+
474
+ case 'h':
475
+ current = [current[0] + cmd.x, current[1]]
476
+ points.push([current[0], current[1]])
477
+ lastControl = null
478
+ break
479
+
480
+ case 'V':
481
+ current = [current[0], cmd.y]
482
+ points.push([current[0], current[1]])
483
+ lastControl = null
484
+ break
485
+
486
+ case 'v':
487
+ current = [current[0], current[1] + cmd.y]
488
+ points.push([current[0], current[1]])
489
+ lastControl = null
490
+ break
491
+
492
+ case 'C':
493
+ case 'c': {
494
+ const x1 = cmd.code === 'c' ? current[0] + cmd.x1 : cmd.x1
495
+ const y1 = cmd.code === 'c' ? current[1] + cmd.y1 : cmd.y1
496
+ const x2 = cmd.code === 'c' ? current[0] + cmd.x2 : cmd.x2
497
+ const y2 = cmd.code === 'c' ? current[1] + cmd.y2 : cmd.y2
498
+ const x = cmd.code === 'c' ? current[0] + cmd.x : cmd.x
499
+ const y = cmd.code === 'c' ? current[1] + cmd.y : cmd.y
500
+
501
+ const samples = sampleCubicBezier(current, [x1, y1], [x2, y2], [x, y], 10)
502
+ samples.slice(1).forEach((p) => points.push([p[0], p[1]]))
503
+ current = [x, y]
504
+ lastControl = [x2, y2]
505
+ break
506
+ }
507
+
508
+ case 'S':
509
+ case 's': {
510
+ const reflected = lastControl
511
+ ? [2 * current[0] - lastControl[0], 2 * current[1] - lastControl[1]]
512
+ : current
513
+ const x2 = cmd.code === 's' ? current[0] + cmd.x2 : cmd.x2
514
+ const y2 = cmd.code === 's' ? current[1] + cmd.y2 : cmd.y2
515
+ const x = cmd.code === 's' ? current[0] + cmd.x : cmd.x
516
+ const y = cmd.code === 's' ? current[1] + cmd.y : cmd.y
517
+
518
+ const samples = sampleCubicBezier(current, reflected, [x2, y2], [x, y], 10)
519
+ samples.slice(1).forEach((p) => points.push([p[0], p[1]]))
520
+ current = [x, y]
521
+ lastControl = [x2, y2]
522
+ break
523
+ }
524
+
525
+ case 'Q':
526
+ case 'q': {
527
+ const x1 = cmd.code === 'q' ? current[0] + cmd.x1 : cmd.x1
528
+ const y1 = cmd.code === 'q' ? current[1] + cmd.y1 : cmd.y1
529
+ const x = cmd.code === 'q' ? current[0] + cmd.x : cmd.x
530
+ const y = cmd.code === 'q' ? current[1] + cmd.y : cmd.y
531
+
532
+ const samples = sampleQuadraticBezier(current, [x1, y1], [x, y], 10)
533
+ samples.slice(1).forEach((p) => points.push([p[0], p[1]]))
534
+ current = [x, y]
535
+ lastControl = [x1, y1]
536
+ break
537
+ }
538
+
539
+ case 'T':
540
+ case 't': {
541
+ const reflected = lastControl
542
+ ? [2 * current[0] - lastControl[0], 2 * current[1] - lastControl[1]]
543
+ : current
544
+ const x = cmd.code === 't' ? current[0] + cmd.x : cmd.x
545
+ const y = cmd.code === 't' ? current[1] + cmd.y : cmd.y
546
+
547
+ const samples = sampleQuadraticBezier(current, reflected, [x, y], 10)
548
+ samples.slice(1).forEach((p) => points.push([p[0], p[1]]))
549
+ current = [x, y]
550
+ lastControl = reflected
551
+ break
552
+ }
553
+
554
+ case 'A':
555
+ case 'a': {
556
+ const x = cmd.code === 'a' ? current[0] + cmd.x : cmd.x
557
+ const y = cmd.code === 'a' ? current[1] + cmd.y : cmd.y
558
+
559
+ const arcPoints = arcToPoints(
560
+ current,
561
+ [x, y],
562
+ cmd.rx,
563
+ cmd.ry,
564
+ cmd.xAxisRotation,
565
+ cmd.largeArc,
566
+ cmd.sweep,
567
+ 10
568
+ )
569
+
570
+ arcPoints.slice(1).forEach((p) => points.push([p[0], p[1]]))
571
+ current = [x, y]
572
+ lastControl = null
573
+ break
574
+ }
575
+
576
+ case 'Z':
577
+ case 'z':
578
+ points.push([startPoint[0], startPoint[1]])
579
+ current = startPoint
580
+ lastControl = null
581
+ break
582
+
583
+ default:
584
+ console.warn(`Unhandled command: ${cmd}`)
585
+ break
586
+ }
587
+ }
588
+
589
+ subpathIndices.push(points.length)
590
+
591
+ return {
592
+ points,
593
+ subpathIndices
594
+ }
595
+ }
596
+
597
+ function sampleCubicBezier(
598
+ p0: [number, number],
599
+ p1: [number, number],
600
+ p2: [number, number],
601
+ p3: [number, number],
602
+ samples = 20
603
+ ): [number, number][] {
604
+ const result: [number, number][] = []
605
+ for (let i = 0; i <= samples; i++) {
606
+ const t = i / samples
607
+ const mt = 1 - t
608
+
609
+ const x = mt ** 3 * p0[0] + 3 * mt ** 2 * t * p1[0] + 3 * mt * t ** 2 * p2[0] + t ** 3 * p3[0]
610
+
611
+ const y = mt ** 3 * p0[1] + 3 * mt ** 2 * t * p1[1] + 3 * mt * t ** 2 * p2[1] + t ** 3 * p3[1]
612
+
613
+ result.push([x, y])
614
+ }
615
+ return result
616
+ }
617
+
618
+ function sampleQuadraticBezier(
619
+ p0: [number, number],
620
+ p1: [number, number],
621
+ p2: [number, number],
622
+ samples = 20
623
+ ) {
624
+ const result: [number, number][] = []
625
+ for (let i = 0; i <= samples; i++) {
626
+ const t = i / samples
627
+ const mt = 1 - t
628
+ const x = mt ** 2 * p0[0] + 2 * mt * t * p1[0] + t ** 2 * p2[0]
629
+ const y = mt ** 2 * p0[1] + 2 * mt * t * p1[1] + t ** 2 * p2[1]
630
+ result.push([x, y])
631
+ }
632
+ return result
633
+ }
634
+
635
+ function arcToPoints(
636
+ from: [number, number],
637
+ to: [number, number],
638
+ rx: number,
639
+ ry: number,
640
+ phi: number,
641
+ largeArc: boolean,
642
+ sweep: boolean,
643
+ samples: number = 20
644
+ ): [number, number][] {
645
+ const [x1, y1] = from
646
+ const [x2, y2] = to
647
+ const rad = (phi * Math.PI) / 180
648
+
649
+ // Step 1: Compute (x1', y1') in transformed space
650
+ const dx = (x1 - x2) / 2
651
+ const dy = (y1 - y2) / 2
652
+ const x1p = Math.cos(rad) * dx + Math.sin(rad) * dy
653
+ const y1p = -Math.sin(rad) * dx + Math.cos(rad) * dy
654
+
655
+ // Step 2: Correct radii if needed
656
+ const rx_sq = rx * rx
657
+ const ry_sq = ry * ry
658
+ const x1p_sq = x1p * x1p
659
+ const y1p_sq = y1p * y1p
660
+
661
+ let lambda = x1p_sq / rx_sq + y1p_sq / ry_sq
662
+ if (lambda > 1) {
663
+ const scale = Math.sqrt(lambda)
664
+ rx *= scale
665
+ ry *= scale
666
+ }
667
+
668
+ // Step 3: Compute center cx', cy' in transformed space
669
+ const sign = largeArc === sweep ? -1 : 1
670
+ const num = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq
671
+ const denom = rx_sq * y1p_sq + ry_sq * x1p_sq
672
+ const factor = sign * Math.sqrt(Math.max(0, num / denom))
673
+ const cxp = (factor * (rx * y1p)) / ry
674
+ const cyp = (factor * (-ry * x1p)) / rx
675
+
676
+ // Step 4: Transform back to original space
677
+ const cx = Math.cos(rad) * cxp - Math.sin(rad) * cyp + (x1 + x2) / 2
678
+ const cy = Math.sin(rad) * cxp + Math.cos(rad) * cyp + (y1 + y2) / 2
679
+
680
+ // Step 5: Calculate angles
681
+ function angle(u: [number, number], v: [number, number]) {
682
+ const dot = u[0] * v[0] + u[1] * v[1]
683
+ const len = Math.sqrt(u[0] ** 2 + u[1] ** 2) * Math.sqrt(v[0] ** 2 + v[1] ** 2)
684
+ const sign = u[0] * v[1] - u[1] * v[0] < 0 ? -1 : 1
685
+ return sign * Math.acos(Math.min(Math.max(dot / len, -1), 1))
686
+ }
687
+
688
+ const v1: [number, number] = [(x1p - cxp) / rx, (y1p - cyp) / ry]
689
+ const v2: [number, number] = [(-x1p - cxp) / rx, (-y1p - cyp) / ry]
690
+
691
+ let theta1 = angle([1, 0], v1)
692
+ let deltaTheta = angle(v1, v2)
693
+
694
+ if (!sweep && deltaTheta > 0) {
695
+ deltaTheta -= 2 * Math.PI
696
+ } else if (sweep && deltaTheta < 0) {
697
+ deltaTheta += 2 * Math.PI
698
+ }
699
+
700
+ // Step 6: Sample points along arc
701
+ const points: [number, number][] = []
702
+ for (let i = 0; i <= samples; i++) {
703
+ const t = i / samples
704
+ const theta = theta1 + deltaTheta * t
705
+ const cosTheta = Math.cos(theta)
706
+ const sinTheta = Math.sin(theta)
707
+
708
+ const xp = rx * cosTheta
709
+ const yp = ry * sinTheta
710
+
711
+ const x = Math.cos(rad) * xp - Math.sin(rad) * yp + cx
712
+ const y = Math.sin(rad) * xp + Math.cos(rad) * yp + cy
713
+ points.push([x, y])
714
+ }
715
+
716
+ return points
717
+ }