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.
- package/bin/index.js +3 -0
- package/package.json +37 -0
- package/src/cli.js +100 -0
- package/template/.editorconfig +9 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc.yaml +10 -0
- package/template/_gitignore +10 -0
- package/template/build/entitlements.mac.plist +12 -0
- package/template/build/icon.icns +0 -0
- package/template/build/icon.ico +0 -0
- package/template/build/icon.png +0 -0
- package/template/electron-builder.yml +43 -0
- package/template/electron.vite.config.ts +50 -0
- package/template/eslint.config.mjs +24 -0
- package/template/package-lock.json +10299 -0
- package/template/package.json +64 -0
- package/template/resources/icon.png +0 -0
- package/template/src/assets/audio/fadeSound.mp3 +0 -0
- package/template/src/assets/audio/fadeSound2.mp3 +0 -0
- package/template/src/assets/audio/interstellar.mp3 +0 -0
- package/template/src/assets/audio/keyboard1.mp3 +0 -0
- package/template/src/assets/audio/keyboard2.mp3 +0 -0
- package/template/src/assets/audio/keyboard3.mp3 +0 -0
- package/template/src/assets/audio/tick_sound.mp3 +0 -0
- package/template/src/assets/base.css +67 -0
- package/template/src/assets/electron.svg +10 -0
- package/template/src/assets/fonts/Geo-Regular.woff +0 -0
- package/template/src/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 +0 -0
- package/template/src/assets/fonts/Montserrat-Medium.ttf +0 -0
- package/template/src/assets/fonts/Montserrat-Medium.woff +0 -0
- package/template/src/assets/fonts/Montserrat-VariableFont_wght.woff2 +0 -0
- package/template/src/assets/fonts/glitch.ttf +0 -0
- package/template/src/assets/hdri/indoor1.hdr +0 -0
- package/template/src/assets/hdri/metro1.hdr +0 -0
- package/template/src/assets/hdri/outdoor1.hdr +0 -0
- package/template/src/assets/hdri/photo-studio1.hdr +0 -0
- package/template/src/assets/hdri/photo-studio2.hdr +0 -0
- package/template/src/assets/hdri/photo-studio3.hdr +0 -0
- package/template/src/assets/objects/keyboardScene/ibm-keyboard.glb +0 -0
- package/template/src/assets/wavy-lines.svg +25 -0
- package/template/src/entry.ts +20 -0
- package/template/src/example_scenes/alternativesScene.ts +88 -0
- package/template/src/example_scenes/dependencyScene.ts +116 -0
- package/template/src/example_scenes/fourierMachineScene.ts +108 -0
- package/template/src/example_scenes/fourierSeriesScene.ts +678 -0
- package/template/src/example_scenes/keyboardScene.ts +447 -0
- package/template/src/example_scenes/surfaceScene.ts +88 -0
- package/template/src/example_scenes/tutorials/easy1.ts +59 -0
- package/template/src/example_scenes/tutorials/easy2.ts +141 -0
- package/template/src/example_scenes/tutorials/easy3.ts +133 -0
- package/template/src/example_scenes/tutorials/medium1.ts +154 -0
- package/template/src/example_scenes/vectorField.ts +209 -0
- package/template/src/example_scenes/visulizingFunctions.ts +246 -0
- package/template/src/main/index.ts +101 -0
- package/template/src/main/rendering.ts +219 -0
- package/template/src/main/storage.ts +35 -0
- package/template/src/preload/index.d.ts +8 -0
- package/template/src/preload/index.ts +36 -0
- package/template/src/renderer/index.html +17 -0
- package/template/src/renderer/src/App.svelte +130 -0
- package/template/src/renderer/src/app.css +24 -0
- package/template/src/renderer/src/env.d.ts +2 -0
- package/template/src/renderer/src/lib/animation/animations.ts +214 -0
- package/template/src/renderer/src/lib/animation/captureCanvas.ts +85 -0
- package/template/src/renderer/src/lib/animation/helpers.ts +7 -0
- package/template/src/renderer/src/lib/animation/interpolations.ts +155 -0
- package/template/src/renderer/src/lib/animation/protocols.ts +79 -0
- package/template/src/renderer/src/lib/audio/loader.ts +104 -0
- package/template/src/renderer/src/lib/fonts/Roboto_Regular.json +1 -0
- package/template/src/renderer/src/lib/fonts/montserrat-medium.json +1 -0
- package/template/src/renderer/src/lib/fonts/montserrat.json +1 -0
- package/template/src/renderer/src/lib/general/helpers.ts +77 -0
- package/template/src/renderer/src/lib/general/onDestory.ts +10 -0
- package/template/src/renderer/src/lib/mathHelpers/vectors.ts +18 -0
- package/template/src/renderer/src/lib/rendering/bumpMaps/noise.ts +84 -0
- package/template/src/renderer/src/lib/rendering/helpers.ts +35 -0
- package/template/src/renderer/src/lib/rendering/lighting3d.ts +387 -0
- package/template/src/renderer/src/lib/rendering/materials.ts +6 -0
- package/template/src/renderer/src/lib/rendering/objects/import.ts +148 -0
- package/template/src/renderer/src/lib/rendering/objects2d.ts +489 -0
- package/template/src/renderer/src/lib/rendering/objects3d.ts +89 -0
- package/template/src/renderer/src/lib/rendering/protocols.ts +21 -0
- package/template/src/renderer/src/lib/rendering/setup.ts +71 -0
- package/template/src/renderer/src/lib/rendering/svg/drawing.ts +213 -0
- package/template/src/renderer/src/lib/rendering/svg/parsing.ts +717 -0
- package/template/src/renderer/src/lib/rendering/svg/rastered.ts +42 -0
- package/template/src/renderer/src/lib/rendering/svgObjects.ts +1137 -0
- package/template/src/renderer/src/lib/scene/helpers.ts +89 -0
- package/template/src/renderer/src/lib/scene/sceneClass.ts +648 -0
- package/template/src/renderer/src/lib/shaders/background_gradient/frag.glsl +12 -0
- package/template/src/renderer/src/lib/shaders/background_gradient/vert.glsl +6 -0
- package/template/src/renderer/src/lib/shaders/hdri_blur/frag.glsl +45 -0
- package/template/src/renderer/src/lib/shaders/hdri_blur/vert.glsl +5 -0
- package/template/src/renderer/src/main.ts +9 -0
- package/template/svelte.config.mjs +7 -0
- package/template/tsconfig.json +4 -0
- package/template/tsconfig.node.json +10 -0
- 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
|
+
}
|