@atom.io/template-preact-svg-editor 0.1.0 → 0.1.1
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/CHANGELOG.md +14 -0
- package/COLLABORATION.md +132 -0
- package/README.md +43 -10
- package/eslint.config.ts +1 -1
- package/package.json +30 -16
- package/src/BezierPlayground.tsx +179 -320
- package/src/preact-logo.ts +155 -0
- package/src/svg-contour.test.ts +89 -0
- package/src/svg-contour.ts +579 -0
- package/src/svg-convergence.test.ts +264 -0
- package/src/svg-convergence.ts +566 -0
- package/src/svg-domain-realtime.test.ts +369 -0
- package/src/svg-domain.test.ts +618 -0
- package/src/svg-domain.ts +939 -0
- package/src/svg-editor-state.test.ts +401 -0
- package/src/svg-editor-state.ts +1102 -0
- package/vite.config.ts +1 -0
package/src/BezierPlayground.tsx
CHANGED
|
@@ -1,92 +1,75 @@
|
|
|
1
|
+
import { useAtomicRef, useO } from "@atom.io/react"
|
|
1
2
|
import type { Loadable } from "atom.io"
|
|
2
|
-
import {
|
|
3
|
-
atom,
|
|
4
|
-
atomFamily,
|
|
5
|
-
getState,
|
|
6
|
-
resetState,
|
|
7
|
-
runTransaction,
|
|
8
|
-
selectorFamily,
|
|
9
|
-
setState,
|
|
10
|
-
transaction,
|
|
11
|
-
} from "atom.io"
|
|
12
|
-
import { useAtomicRef, useO } from "atom.io/react"
|
|
3
|
+
import { atom, getState, setState } from "atom.io"
|
|
13
4
|
import type { PointerEventHandler, TargetedPointerEvent, VNode } from "preact"
|
|
14
5
|
import { useCallback, useEffect, useRef } from "preact/hooks"
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
import { parsePreactLogo } from "./preact-logo.ts"
|
|
8
|
+
import { materializeSvgOrder } from "./svg-convergence.ts"
|
|
9
|
+
import {
|
|
10
|
+
activeDragAtom,
|
|
11
|
+
beginSvgDrag,
|
|
12
|
+
createSvgGestureClock,
|
|
13
|
+
finishSvgDrag,
|
|
14
|
+
pathDrawSelectors,
|
|
15
|
+
pathOrderAtom,
|
|
16
|
+
pointerCaptureAtom,
|
|
17
|
+
type PointXY,
|
|
18
|
+
previewSvgDrag,
|
|
19
|
+
projectedEdgeSelectors,
|
|
20
|
+
projectedNodeSelectors,
|
|
21
|
+
replaceSvgDrawing,
|
|
22
|
+
subpathOrderAtoms,
|
|
23
|
+
type SvgDragTarget,
|
|
24
|
+
svgElementAtom,
|
|
25
|
+
} from "./svg-editor-state.ts"
|
|
18
26
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
default: [],
|
|
22
|
-
})
|
|
23
|
-
const subpathKeysAtoms = atomFamily<string[], string>({
|
|
24
|
-
key: `subpathKeys`,
|
|
25
|
-
default: [],
|
|
26
|
-
})
|
|
27
|
-
const nodeAtoms = atomFamily<PointXY | null, string>({
|
|
28
|
-
key: `node`,
|
|
29
|
-
default: null,
|
|
30
|
-
})
|
|
31
|
-
const edgeAtoms = atomFamily<EdgeXY | boolean, string>({
|
|
32
|
-
key: `edge`,
|
|
33
|
-
default: true,
|
|
34
|
-
})
|
|
35
|
-
const pathDrawSelectors = selectorFamily<string, string>({
|
|
36
|
-
key: `pathDraw`,
|
|
37
|
-
get:
|
|
38
|
-
(pathKey) =>
|
|
39
|
-
({ get }) => {
|
|
40
|
-
const subpathKeys = get(subpathKeysAtoms, pathKey)
|
|
41
|
-
return subpathKeys
|
|
42
|
-
.map((subpathKey, idx) => {
|
|
43
|
-
const node = get(nodeAtoms, subpathKey)
|
|
44
|
-
const edge = get(edgeAtoms, subpathKey)
|
|
27
|
+
const WIDTH = 256
|
|
28
|
+
const HEIGHT = 296
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (idx === 0) {
|
|
50
|
-
return `M ${node.x} ${node.y}`
|
|
51
|
-
}
|
|
52
|
-
if (edge === false) {
|
|
53
|
-
return `M ${node.x} ${node.y}`
|
|
54
|
-
}
|
|
55
|
-
if (edge === true) {
|
|
56
|
-
return `L ${node.x} ${node.y}`
|
|
57
|
-
}
|
|
58
|
-
if (`c` in edge) {
|
|
59
|
-
return `C ${edge.c.x} ${edge.c.y} ${edge.s.x} ${edge.s.y} ${node.x} ${node.y}`
|
|
60
|
-
}
|
|
61
|
-
return `S ${edge.s.x} ${edge.s.y} ${node.x} ${node.y}`
|
|
62
|
-
})
|
|
63
|
-
.join(` `)
|
|
64
|
-
},
|
|
65
|
-
})
|
|
30
|
+
function clamp(number: number, minimum: number, maximum: number): number {
|
|
31
|
+
return Math.max(minimum, Math.min(maximum, number))
|
|
32
|
+
}
|
|
66
33
|
|
|
67
|
-
function
|
|
68
|
-
|
|
34
|
+
function logicalPoint(evt: TargetedPointerEvent<SVGSVGElement>): PointXY | null {
|
|
35
|
+
const svg = getState(svgElementAtom)
|
|
36
|
+
if (svg === null) return null
|
|
37
|
+
const point = svg.createSVGPoint()
|
|
38
|
+
point.x = evt.clientX
|
|
39
|
+
point.y = evt.clientY
|
|
40
|
+
const inverse = svg.getScreenCTM()?.inverse()
|
|
41
|
+
if (inverse === undefined) return null
|
|
42
|
+
const transformed = point.matrixTransform(inverse)
|
|
43
|
+
return {
|
|
44
|
+
x: clamp(transformed.x, -185, WIDTH + 185),
|
|
45
|
+
y: clamp(transformed.y, -10, HEIGHT + 10),
|
|
46
|
+
}
|
|
69
47
|
}
|
|
70
48
|
|
|
71
|
-
function
|
|
49
|
+
function ControlHandle({
|
|
72
50
|
at,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
51
|
+
control,
|
|
52
|
+
origin,
|
|
53
|
+
startDrag,
|
|
54
|
+
subpathId,
|
|
76
55
|
}: {
|
|
77
|
-
at: PointXY
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
readonly at: PointXY
|
|
57
|
+
readonly control: `c` | `s`
|
|
58
|
+
readonly origin: PointXY | null
|
|
59
|
+
readonly startDrag: (
|
|
60
|
+
event: TargetedPointerEvent<SVGCircleElement>,
|
|
61
|
+
target: SvgDragTarget,
|
|
62
|
+
point: PointXY,
|
|
63
|
+
) => void
|
|
64
|
+
readonly subpathId: string
|
|
65
|
+
}): VNode | null {
|
|
66
|
+
if (origin === null) return null
|
|
67
|
+
return (
|
|
85
68
|
<>
|
|
86
69
|
<line
|
|
87
70
|
class="bezier"
|
|
88
|
-
x1={
|
|
89
|
-
y1={
|
|
71
|
+
x1={origin.x}
|
|
72
|
+
y1={origin.y}
|
|
90
73
|
x2={at.x}
|
|
91
74
|
y2={at.y}
|
|
92
75
|
stroke="#777"
|
|
@@ -107,12 +90,8 @@ function Bezier({
|
|
|
107
90
|
cx={at.x}
|
|
108
91
|
cy={at.y}
|
|
109
92
|
r={6}
|
|
110
|
-
onPointerDown={(
|
|
111
|
-
|
|
112
|
-
setState(currentlyDraggingAtom, {
|
|
113
|
-
key: subpathKey,
|
|
114
|
-
by: !maybeNode ? `c` : `s`,
|
|
115
|
-
})
|
|
93
|
+
onPointerDown={(event) => {
|
|
94
|
+
startDrag(event, { control, kind: `edge-control`, subpathId }, at)
|
|
116
95
|
}}
|
|
117
96
|
/>
|
|
118
97
|
</>
|
|
@@ -120,35 +99,49 @@ function Bezier({
|
|
|
120
99
|
}
|
|
121
100
|
|
|
122
101
|
function Node({
|
|
123
|
-
|
|
124
|
-
|
|
102
|
+
previousSubpathId,
|
|
103
|
+
startDrag,
|
|
104
|
+
subpathId,
|
|
125
105
|
}: {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
106
|
+
readonly previousSubpathId: string | null
|
|
107
|
+
readonly startDrag: (
|
|
108
|
+
event: TargetedPointerEvent<SVGCircleElement>,
|
|
109
|
+
target: SvgDragTarget,
|
|
110
|
+
point: PointXY,
|
|
111
|
+
) => void
|
|
112
|
+
readonly subpathId: string
|
|
113
|
+
}): VNode | null {
|
|
114
|
+
const node = useO(projectedNodeSelectors, subpathId)
|
|
115
|
+
const edge = useO(projectedEdgeSelectors, subpathId)
|
|
116
|
+
const previousNode = useO(
|
|
117
|
+
projectedNodeSelectors,
|
|
118
|
+
previousSubpathId ?? subpathId,
|
|
119
|
+
)
|
|
120
|
+
if (node === null || edge === null || edge.kind === `close`) return null
|
|
121
|
+
return (
|
|
132
122
|
<>
|
|
133
|
-
{
|
|
134
|
-
<rect class="node" x={node.x - 3} y={node.y - 3} width={6} height={6} />
|
|
135
|
-
) : (
|
|
123
|
+
{edge.kind === `cubic` ? (
|
|
136
124
|
<>
|
|
137
|
-
<
|
|
125
|
+
<ControlHandle
|
|
138
126
|
at={edge.s}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
127
|
+
control="s"
|
|
128
|
+
origin={node}
|
|
129
|
+
startDrag={startDrag}
|
|
130
|
+
subpathId={subpathId}
|
|
142
131
|
/>
|
|
143
|
-
{edge.c ? (
|
|
144
|
-
<
|
|
132
|
+
{edge.c === undefined ? null : (
|
|
133
|
+
<ControlHandle
|
|
145
134
|
at={edge.c}
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
control="c"
|
|
136
|
+
origin={previousNode}
|
|
137
|
+
startDrag={startDrag}
|
|
138
|
+
subpathId={subpathId}
|
|
148
139
|
/>
|
|
149
|
-
)
|
|
140
|
+
)}
|
|
150
141
|
<circle class="node" cx={node.x} cy={node.y} r={3} />
|
|
151
142
|
</>
|
|
143
|
+
) : (
|
|
144
|
+
<rect class="node" x={node.x - 3} y={node.y - 3} width={6} height={6} />
|
|
152
145
|
)}
|
|
153
146
|
<circle
|
|
154
147
|
class="node-draggable"
|
|
@@ -156,254 +149,114 @@ function Node({
|
|
|
156
149
|
cx={node.x}
|
|
157
150
|
cy={node.y}
|
|
158
151
|
r={10}
|
|
159
|
-
onPointerDown={(
|
|
160
|
-
|
|
161
|
-
setState(currentlyDraggingAtom, { key: subpathKey })
|
|
152
|
+
onPointerDown={(event) => {
|
|
153
|
+
startDrag(event, { kind: `node`, subpathId }, node)
|
|
162
154
|
}}
|
|
163
155
|
/>
|
|
164
156
|
</>
|
|
165
157
|
)
|
|
166
158
|
}
|
|
167
159
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
key={spk}
|
|
183
|
-
/>
|
|
184
|
-
))}
|
|
185
|
-
</>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function PathsDemo() {
|
|
190
|
-
const pathKeys = useO(pathKeysAtom)
|
|
160
|
+
function Path({
|
|
161
|
+
pathId,
|
|
162
|
+
startDrag,
|
|
163
|
+
}: {
|
|
164
|
+
readonly pathId: string
|
|
165
|
+
readonly startDrag: (
|
|
166
|
+
event: TargetedPointerEvent<SVGCircleElement>,
|
|
167
|
+
target: SvgDragTarget,
|
|
168
|
+
point: PointXY,
|
|
169
|
+
) => void
|
|
170
|
+
}): VNode {
|
|
171
|
+
const draw = useO(pathDrawSelectors, pathId)
|
|
172
|
+
const subpathOrder = useO(subpathOrderAtoms, pathId)
|
|
173
|
+
const subpathIds = materializeSvgOrder(subpathOrder).map(({ value }) => value)
|
|
191
174
|
return (
|
|
192
175
|
<>
|
|
193
|
-
{
|
|
194
|
-
|
|
176
|
+
<path d={draw} class="path" style={{ pointerEvents: `none` }} />
|
|
177
|
+
{subpathIds.toReversed().map((subpathId, reverseIndex) => {
|
|
178
|
+
const index = subpathIds.length - reverseIndex - 1
|
|
179
|
+
return (
|
|
180
|
+
<Node
|
|
181
|
+
key={subpathId}
|
|
182
|
+
previousSubpathId={subpathIds[index - 1] ?? null}
|
|
183
|
+
startDrag={startDrag}
|
|
184
|
+
subpathId={subpathId}
|
|
185
|
+
/>
|
|
186
|
+
)
|
|
195
187
|
})}
|
|
196
188
|
</>
|
|
197
189
|
)
|
|
198
190
|
}
|
|
199
191
|
|
|
200
|
-
const svgRefAtom = atom<SVGSVGElement | null>({
|
|
201
|
-
key: `svgRef`,
|
|
202
|
-
default: null,
|
|
203
|
-
})
|
|
204
|
-
const currentlyDraggingAtom = atom<{ key: string; by?: `c` | `s` } | null>({
|
|
205
|
-
key: `currentlyDragging`,
|
|
206
|
-
default: null,
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
function onPointerMove(evt: TargetedPointerEvent<SVGSVGElement>): void {
|
|
210
|
-
evt.preventDefault()
|
|
211
|
-
const { key: currentlyDragging, by: draggingBy } =
|
|
212
|
-
getState(currentlyDraggingAtom) ?? {}
|
|
213
|
-
const svg = getState(svgRefAtom)
|
|
214
|
-
if (!svg || !currentlyDragging) {
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
const pt = svg.createSVGPoint()
|
|
218
|
-
pt.x = evt.clientX
|
|
219
|
-
pt.y = evt.clientY
|
|
220
|
-
const ctm = svg.getScreenCTM()?.inverse()
|
|
221
|
-
const { x, y } = pt.matrixTransform(ctm)
|
|
222
|
-
|
|
223
|
-
switch (draggingBy) {
|
|
224
|
-
case undefined:
|
|
225
|
-
setState(nodeAtoms, currentlyDragging, {
|
|
226
|
-
x: clamp(x, -185, WIDTH + 185),
|
|
227
|
-
y: clamp(y, -10, HEIGHT + 10),
|
|
228
|
-
})
|
|
229
|
-
break
|
|
230
|
-
case `s`:
|
|
231
|
-
setState(edgeAtoms, currentlyDragging, (prev) => ({
|
|
232
|
-
...(prev as EdgeXY),
|
|
233
|
-
s: { x: clamp(x, -185, WIDTH + 185), y: clamp(y, -10, HEIGHT + 10) },
|
|
234
|
-
}))
|
|
235
|
-
break
|
|
236
|
-
case `c`:
|
|
237
|
-
setState(edgeAtoms, currentlyDragging, (prev) => ({
|
|
238
|
-
...(prev as EdgeXY),
|
|
239
|
-
c: { x: clamp(x, -185, WIDTH + 185), y: clamp(y, -10, HEIGHT + 10) },
|
|
240
|
-
}))
|
|
241
|
-
break
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const CODES = [`m`, `M`, `l`, `L`, `c`, `C`, `v`, `V`, `z`, `Z`] as const
|
|
246
|
-
|
|
247
192
|
const preactLogoAtom = atom<Loadable<string>>({
|
|
248
193
|
key: `preactLogo`,
|
|
249
|
-
default: () => fetch(`preact.svg`).then((
|
|
194
|
+
default: () => fetch(`preact.svg`).then((response) => response.text()),
|
|
250
195
|
})
|
|
251
196
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const shapes = logo
|
|
262
|
-
.split(`\n`)
|
|
263
|
-
.filter((line) => line.startsWith(`\t<path`))
|
|
264
|
-
.map((line) => {
|
|
265
|
-
const raw = line.split(`d="`)[1].slice(0, -9)
|
|
266
|
-
|
|
267
|
-
type Letter = (typeof CODES)[number]
|
|
268
|
-
let letter: Letter | undefined
|
|
269
|
-
let number = ``
|
|
270
|
-
let numbers: number[] = []
|
|
271
|
-
|
|
272
|
-
const instructions: { letter: Letter; numbers: number[] }[] = []
|
|
273
|
-
for (const c of raw) {
|
|
274
|
-
if (CODES.includes(c as Letter)) {
|
|
275
|
-
if (number) {
|
|
276
|
-
numbers.push(Number.parseFloat(number))
|
|
277
|
-
number = ``
|
|
278
|
-
}
|
|
279
|
-
if (letter) {
|
|
280
|
-
instructions.push({ letter, numbers })
|
|
281
|
-
}
|
|
282
|
-
letter = c as Letter
|
|
283
|
-
numbers = []
|
|
284
|
-
continue
|
|
285
|
-
}
|
|
286
|
-
if (c === ` `) {
|
|
287
|
-
numbers.push(Number.parseFloat(number))
|
|
288
|
-
number = ``
|
|
289
|
-
continue
|
|
290
|
-
}
|
|
291
|
-
if (c === `-` && number) {
|
|
292
|
-
numbers.push(Number.parseFloat(number))
|
|
293
|
-
number = `-`
|
|
294
|
-
continue
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
number += c
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let prev: PointXY = { x: 0, y: 0 }
|
|
301
|
-
const edgeNodes = instructions.map<{
|
|
302
|
-
node: PointXY | null
|
|
303
|
-
edge: boolean | { c?: PointXY; s: PointXY }
|
|
304
|
-
}>(({ letter: l, numbers: ns }) => {
|
|
305
|
-
let node: PointXY | null
|
|
306
|
-
let edge: boolean | { c?: PointXY; s: PointXY }
|
|
307
|
-
switch (l) {
|
|
308
|
-
case `m`:
|
|
309
|
-
node = { x: prev.x + ns[0], y: prev.y + ns[1] }
|
|
310
|
-
edge = false
|
|
311
|
-
break
|
|
312
|
-
case `M`:
|
|
313
|
-
node = { x: ns[0], y: ns[1] }
|
|
314
|
-
edge = false
|
|
315
|
-
break
|
|
316
|
-
case `l`:
|
|
317
|
-
node = { x: prev.x + ns[0], y: prev.y + ns[1] }
|
|
318
|
-
edge = true
|
|
319
|
-
break
|
|
320
|
-
case `L`:
|
|
321
|
-
node = { x: ns[0], y: ns[1] }
|
|
322
|
-
edge = true
|
|
323
|
-
break
|
|
324
|
-
case `c`:
|
|
325
|
-
node = { x: prev.x + ns[4], y: prev.y + ns[5] }
|
|
326
|
-
edge = {
|
|
327
|
-
c: { x: prev.x + ns[0], y: prev.y + ns[1] },
|
|
328
|
-
s: { x: prev.x + ns[2], y: prev.y + ns[3] },
|
|
329
|
-
}
|
|
330
|
-
break
|
|
331
|
-
case `C`:
|
|
332
|
-
node = { x: ns[4], y: ns[5] }
|
|
333
|
-
edge = {
|
|
334
|
-
c: { x: ns[0], y: ns[1] },
|
|
335
|
-
s: { x: ns[2], y: ns[3] },
|
|
336
|
-
}
|
|
337
|
-
break
|
|
338
|
-
case `v`:
|
|
339
|
-
node = { x: prev.x, y: prev.y + ns[0] }
|
|
340
|
-
edge = true
|
|
341
|
-
break
|
|
342
|
-
case `V`:
|
|
343
|
-
node = { x: prev.x, y: ns[0] }
|
|
344
|
-
edge = true
|
|
345
|
-
break
|
|
346
|
-
case `z`:
|
|
347
|
-
case `Z`:
|
|
348
|
-
node = null
|
|
349
|
-
edge = true
|
|
350
|
-
}
|
|
351
|
-
if (node) {
|
|
352
|
-
prev = node
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return { node, edge }
|
|
356
|
-
})
|
|
197
|
+
export default function BezierPlayground(): VNode {
|
|
198
|
+
const svgRef = useAtomicRef(svgElementAtom, useRef)
|
|
199
|
+
const pathOrder = useO(pathOrderAtom)
|
|
200
|
+
const gestureClock = useRef(
|
|
201
|
+
createSvgGestureClock({
|
|
202
|
+
actor: `local-designer`,
|
|
203
|
+
session: globalThis.crypto.randomUUID(),
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
357
206
|
|
|
358
|
-
|
|
207
|
+
const startDrag = useCallback(
|
|
208
|
+
(
|
|
209
|
+
event: TargetedPointerEvent<SVGCircleElement>,
|
|
210
|
+
target: SvgDragTarget,
|
|
211
|
+
point: PointXY,
|
|
212
|
+
) => {
|
|
213
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
214
|
+
beginSvgDrag({
|
|
215
|
+
element: event.currentTarget,
|
|
216
|
+
gesture: gestureClock.current.begin(),
|
|
217
|
+
point,
|
|
218
|
+
pointerId: event.pointerId,
|
|
219
|
+
target,
|
|
359
220
|
})
|
|
221
|
+
},
|
|
222
|
+
[],
|
|
223
|
+
)
|
|
360
224
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
{ length: numberOfNodes },
|
|
376
|
-
(_, nodeNum) => `subpath${jj + nodeNum}`,
|
|
377
|
-
),
|
|
378
|
-
)
|
|
379
|
-
setState(pathKeysAtom, (prev) => [...prev, `path${i}`])
|
|
380
|
-
i++
|
|
225
|
+
const onPointerMove: PointerEventHandler<SVGSVGElement> = useCallback(
|
|
226
|
+
(event) => {
|
|
227
|
+
if (getState(activeDragAtom) === null) return
|
|
228
|
+
event.preventDefault()
|
|
229
|
+
const point = logicalPoint(event)
|
|
230
|
+
if (point !== null) previewSvgDrag(point)
|
|
231
|
+
},
|
|
232
|
+
[],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const finishPointer = useCallback((commit: boolean) => {
|
|
236
|
+
const capture = getState(pointerCaptureAtom)
|
|
237
|
+
if (capture?.element.hasPointerCapture(capture.pointerId) === true) {
|
|
238
|
+
capture.element.releasePointerCapture(capture.pointerId)
|
|
381
239
|
}
|
|
382
|
-
|
|
383
|
-
})
|
|
384
|
-
const reset = runTransaction(resetTransaction)
|
|
240
|
+
finishSvgDrag({ commit })
|
|
241
|
+
}, [])
|
|
385
242
|
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
const svgRef = useAtomicRef(svgRefAtom, useRef)
|
|
390
|
-
const onPointerUp: PointerEventHandler<SVGSVGElement> = useCallback((evt) => {
|
|
391
|
-
evt.currentTarget.releasePointerCapture(evt.pointerId)
|
|
392
|
-
setState(currentlyDraggingAtom, null)
|
|
243
|
+
const reset = useCallback(async () => {
|
|
244
|
+
const svg = await getState(preactLogoAtom)
|
|
245
|
+
replaceSvgDrawing(parsePreactLogo(svg), gestureClock.current.begin())
|
|
393
246
|
}, [])
|
|
394
247
|
|
|
395
|
-
useEffect(() => void reset(), [])
|
|
248
|
+
useEffect(() => void reset(), [reset])
|
|
396
249
|
|
|
397
250
|
return (
|
|
398
251
|
<div
|
|
399
252
|
style={{
|
|
253
|
+
alignItems: `center`,
|
|
400
254
|
display: `flex`,
|
|
401
255
|
flexFlow: `column`,
|
|
402
|
-
position: `relative`,
|
|
403
|
-
overflow: `hidden`,
|
|
404
256
|
maxWidth: `1280px`,
|
|
257
|
+
overflow: `hidden`,
|
|
258
|
+
position: `relative`,
|
|
405
259
|
width: `100vw`,
|
|
406
|
-
alignItems: `center`,
|
|
407
260
|
}}
|
|
408
261
|
>
|
|
409
262
|
<svg
|
|
@@ -412,8 +265,12 @@ export default function BezierPlayground(): VNode {
|
|
|
412
265
|
width={1000}
|
|
413
266
|
height={500}
|
|
414
267
|
onPointerMove={onPointerMove}
|
|
415
|
-
onPointerUp={
|
|
416
|
-
|
|
268
|
+
onPointerUp={() => {
|
|
269
|
+
finishPointer(true)
|
|
270
|
+
}}
|
|
271
|
+
onPointerCancel={() => {
|
|
272
|
+
finishPointer(false)
|
|
273
|
+
}}
|
|
417
274
|
>
|
|
418
275
|
<title>Bezier Playground</title>
|
|
419
276
|
<defs>
|
|
@@ -429,7 +286,9 @@ export default function BezierPlayground(): VNode {
|
|
|
429
286
|
height={HEIGHT + 20}
|
|
430
287
|
fill="url(#grid)"
|
|
431
288
|
/>
|
|
432
|
-
|
|
289
|
+
{materializeSvgOrder(pathOrder).map(({ value: pathId }) => (
|
|
290
|
+
<Path pathId={pathId} startDrag={startDrag} key={pathId} />
|
|
291
|
+
))}
|
|
433
292
|
</svg>
|
|
434
293
|
<button type="button" class="flat" onClick={reset}>
|
|
435
294
|
Reset
|