1
1
import { computed , type Ref } from "vue" ;
2
2
import roughjs from "roughjs" ;
3
+ import { splitPath } from "./split-path" ;
3
4
4
5
type RoughSVG = ReturnType < typeof roughjs . svg > ;
5
6
7
+ const DEFAULT_ANIMATION_DURATION = 800 ; // Same as https://github.com/rough-stuff/rough-notation/blob/668ba82ac89c903d6f59c9351b9b85855da9882c/src/model.ts#L3C14-L3C47
8
+
6
9
const createArrowHeadSvg = (
7
10
roughSvg : RoughSVG ,
8
11
lineLength : number ,
@@ -18,11 +21,18 @@ const createArrowHeadSvg = (
18
21
19
22
const g = document . createElementNS ( "http://www.w3.org/2000/svg" , "g" ) ;
20
23
24
+ function addAllChildren ( anotherGroup : SVGGElement ) {
25
+ // `for (... of anotherGroup.children)` doesn't work well: the second child and the latter will be discarded somehow.
26
+ for ( const child of Array . from ( anotherGroup . children ) ) {
27
+ g . appendChild ( child ) ;
28
+ }
29
+ }
30
+
21
31
if ( type === "line" ) {
22
- g . appendChild ( roughSvg . line ( x1 , y1 , 0 , 0 , options ) ) ;
23
- g . appendChild ( roughSvg . line ( x2 , y2 , 0 , 0 , options ) ) ;
32
+ addAllChildren ( roughSvg . line ( x1 , y1 , 0 , 0 , options ) ) ;
33
+ addAllChildren ( roughSvg . line ( x2 , y2 , 0 , 0 , options ) ) ;
24
34
} else if ( type === "polygon" ) {
25
- g . appendChild (
35
+ addAllChildren (
26
36
roughSvg . polygon (
27
37
[
28
38
[ x1 , y1 ] ,
@@ -53,6 +63,12 @@ export function useRoughArrow(props: {
53
63
seed ?: number ;
54
64
twoWay : boolean ;
55
65
centerPositionParam : number ;
66
+ animation ?: {
67
+ duration ?: number ;
68
+ delay ?: number ;
69
+ } ;
70
+ strokeAnimationKeyframeName : string ;
71
+ fillAnimationKeyframeName : string ;
56
72
} ) {
57
73
const {
58
74
point1 : point1Ref ,
@@ -64,6 +80,9 @@ export function useRoughArrow(props: {
64
80
seed,
65
81
twoWay,
66
82
centerPositionParam,
83
+ animation,
84
+ strokeAnimationKeyframeName,
85
+ fillAnimationKeyframeName,
67
86
} = props ;
68
87
const baseOptions = {
69
88
// We don't support the `bowing` param because it's not so effective for arc.
@@ -105,7 +124,7 @@ export function useRoughArrow(props: {
105
124
const angle =
106
125
Math . atan2 ( point2 . y - point1 . y , point2 . x - point1 . x ) - Math . PI / 2 ;
107
126
return {
108
- svg,
127
+ svgPath : svg . getElementsByTagName ( "path" ) [ 0 ] ,
109
128
angle1 : angle ,
110
129
angle2 : angle ,
111
130
lineLength : Math . hypot ( point2 . x - point1 . x , point2 . y - point1 . y ) ,
@@ -191,7 +210,7 @@ export function useRoughArrow(props: {
191
210
} ;
192
211
193
212
return {
194
- svg,
213
+ svgPath : svg . getElementsByTagName ( "path" ) [ 0 ] ,
195
214
angle1,
196
215
angle2,
197
216
lineLength : R * ( endAngle - startAngle ) ,
@@ -241,7 +260,7 @@ export function useRoughArrow(props: {
241
260
`translate(${ point2Ref . value . x } ,${ point2Ref . value . y } ) rotate(${ ( arcData . value . angle2 * 180 ) / Math . PI + ( centerPositionParam >= 0 ? 90 : - 90 ) } )` ,
242
261
) ;
243
262
if ( ! twoWay ) {
244
- return { arrowHeadForwardSvg, arrowHeadBackwardSvg : null } ;
263
+ return { arrowHeadForwardSvg, arrowHeadBackwardSvg : null , lineLength } ;
245
264
}
246
265
247
266
const arrowHeadBackwardSvg = createArrowHeadSvg (
@@ -254,7 +273,7 @@ export function useRoughArrow(props: {
254
273
"transform" ,
255
274
`translate(${ point1Ref . value . x } ,${ point1Ref . value . y } ) rotate(${ ( arcData . value . angle1 * 180 ) / Math . PI + ( centerPositionParam >= 0 ? - 90 : 90 ) } )` ,
256
275
) ;
257
- return { arrowHeadBackwardSvg, arrowHeadForwardSvg } ;
276
+ return { arrowHeadBackwardSvg, arrowHeadForwardSvg, lineLength } ;
258
277
} ) ;
259
278
260
279
const arrowSvg = computed ( ( ) => {
@@ -264,17 +283,100 @@ export function useRoughArrow(props: {
264
283
return null ;
265
284
}
266
285
267
- g . appendChild ( arcData . value . svg ) ;
268
-
286
+ const arcPath = arcData . value . svgPath ;
269
287
const arrowHeadBackwardSvg = arrowHeadData . value . arrowHeadBackwardSvg ;
270
288
const arrowHeadForwardSvg = arrowHeadData . value . arrowHeadForwardSvg ;
271
289
290
+ // RoughSVG.arc() may generate <path> element whose `d` attribute contains multiple segments like `M... M...`.
291
+ // Such paths don't be animated as expected, so we split them into multiple <path> elements that only contain `d` with only one `M`
292
+ // and animate them individually.
293
+ const splitPaths = splitPath ( arcPath ) ;
294
+ splitPaths . forEach ( ( path ) => g . appendChild ( path ) ) ;
272
295
g . appendChild ( arrowHeadForwardSvg ) ;
273
-
274
296
if ( arrowHeadBackwardSvg ) {
275
297
g . appendChild ( arrowHeadBackwardSvg ) ;
276
298
}
277
299
300
+ if ( animation ) {
301
+ interface AnimationSegment {
302
+ length : number ;
303
+ strokedPaths : SVGPathElement [ ] ;
304
+ filledPaths : SVGPathElement [ ] ;
305
+ }
306
+ const segments : AnimationSegment [ ] = [ ] ;
307
+
308
+ segments . push ( {
309
+ length : arcData . value . lineLength ,
310
+ strokedPaths : splitPaths ,
311
+ filledPaths : [ ] ,
312
+ } ) ;
313
+
314
+ function getArrowHeadAnimationSegment (
315
+ arrowHeadG : SVGGElement ,
316
+ length : number ,
317
+ ) : AnimationSegment {
318
+ const strokedPaths : SVGPathElement [ ] = [ ] ;
319
+ const filledPaths : SVGPathElement [ ] = [ ] ;
320
+ arrowHeadG . childNodes . forEach ( ( child ) => {
321
+ if ( child instanceof SVGPathElement ) {
322
+ const stroke = child . getAttribute ( "stroke" ) ;
323
+ const fill = child . getAttribute ( "fill" ) ;
324
+ if ( stroke && stroke !== "none" ) {
325
+ strokedPaths . push ( child ) ;
326
+ } else if ( fill && fill !== "none" ) {
327
+ filledPaths . push ( child ) ;
328
+ }
329
+ }
330
+ } ) ;
331
+ return {
332
+ strokedPaths : strokedPaths ,
333
+ filledPaths : filledPaths ,
334
+ length,
335
+ } ;
336
+ }
337
+
338
+ segments . push (
339
+ getArrowHeadAnimationSegment (
340
+ arrowHeadForwardSvg ,
341
+ arrowHeadData . value . lineLength * 2 ,
342
+ ) ,
343
+ ) ;
344
+ if ( arrowHeadBackwardSvg ) {
345
+ segments . push (
346
+ getArrowHeadAnimationSegment (
347
+ arrowHeadBackwardSvg ,
348
+ arrowHeadData . value . lineLength * 2 ,
349
+ ) ,
350
+ ) ;
351
+ }
352
+
353
+ const totalLength = segments
354
+ . map ( ( s ) => s . length )
355
+ . reduce ( ( a , b ) => a + b , 0 ) ;
356
+
357
+ const { duration = DEFAULT_ANIMATION_DURATION , delay = 0 } = animation ;
358
+ let currentDelay = delay ;
359
+ // Animation impl inspired by https://github.com/rough-stuff/rough-notation/blob/668ba82ac89c903d6f59c9351b9b85855da9882c/src/render.ts#L222-L235
360
+ for ( const segment of segments ) {
361
+ const segmentDuration = ( segment . length / totalLength ) * duration ;
362
+ const pathDuration = segmentDuration / segment . strokedPaths . length ;
363
+ segment . strokedPaths . forEach ( ( path , index ) => {
364
+ const pathDelay =
365
+ currentDelay +
366
+ ( index / segment . strokedPaths . length ) * segmentDuration ;
367
+ path . style . animation = `${ strokeAnimationKeyframeName } ${ pathDuration } ms ease-out ${ pathDelay } ms forwards` ;
368
+ path . style . strokeDashoffset = `${ segment . length } ` ;
369
+ path . style . strokeDasharray = `${ segment . length } ` ;
370
+ path . style . visibility = "hidden" ;
371
+ } ) ;
372
+ currentDelay += segmentDuration ;
373
+ segment . filledPaths . forEach ( ( path ) => {
374
+ path . style . animation = `${ fillAnimationKeyframeName } ${ segmentDuration } ms ease-out ${ currentDelay } ms forwards` ;
375
+ path . style . visibility = "hidden" ;
376
+ } ) ;
377
+ }
378
+ }
379
+
278
380
return g . innerHTML ;
279
381
} ) ;
280
382
0 commit comments