Animating a pirate treasure map path
Animating an SVG line is nothing to write home about. It has already been described in great detail many, multiple, different times. We make the line use dashes, configure it so that there is a single dash that spans the entire line, and then we animate the offset of that dash. Done.
But what if we want to animate a line that itself is dashed? Like a dashed line in a treasure map?
As it turns out, we can animate it in pretty much the same way! But instead of animating the dashed line, we mask that dashed line with an identical line, and then we animate that line. So, step by step:
-
Define the line
path
inside adefs
element so that it’s not drawn. -
Draw the line by defining a
use
element whosexlink:href
attribute points to thepath
defined in the previous step. -
Create a mask with an identical line by using another
use
element. The mask must be white (like in Photoshop) and have the samestroke
properties (stroke-width
,stroke-linecap
,stroke-miterlimit
, etc) as the drawn line so that it can cover it perfectly. -
Make the mask line use dashes, make it dashed with a single dash that spans its total length, and then animate the dash offset. The usual stuff.
Finally, we can change the transition timing function to achieve different results. Using the steps transition function with the same amount of steps as the number of line dashes results in a nice effect.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body {
height: 100%;
margin: 0;
}
svg {
width: 100%;
height: 100%;
display: block;
}
.controls {
position: absolute;
top: 1em;
left: 1em;
}
#line {
fill: none;
stroke-width: 4;
}
#animated-line {
stroke: currentColor;
}
#animated-line-mask {
stroke: white;
}
</style>
<body>
<form class="controls">
<label><button type="button" value="continuous-line">Continuous line</button></label>
<label><button type="button" value="continuous-dashes">Continuous dashes</button></label>
<label><button type="button" value="dash-by-dash">Dash by dash</button></label>
</form>
<svg viewBox="0 0 560 98">
<defs>
<path id="line" d="M108 10S-8 30 21 67c41 44 66-71 161-2 95 70 181-58 122-57-58 0-34 76 60 76 95 0 54-72 113-71s69 70 69 70" />
<mask id="animated-line-mask">
<use xlink:href="#line" />
</mask>
</defs>
<use xlink:href="#line" id="animated-line" mask="url(#animated-line-mask)" />
</svg>
<script type="text/javascript">
"use strict";
var line = document.querySelector("#line");
var animatedLine = document.querySelector("#animated-line");
var animatedLineMask = document.querySelector("#animated-line-mask use");
var lineLength = line.getTotalLength();
var numDashes = 14;
function handleClick(event) {
reset();
switch(event.target.value) {
case "continuous-line":
animate(animatedLine, "ease");
break;
case "continuous-dashes":
makeLineDashed();
animate(animatedLineMask, "linear");
break;
case "dash-by-dash":
makeLineDashed();
animate(animatedLineMask, `steps(${numDashes}, jump-start)`);
break;
}
}
function animate(el, transitionFn) {
el.style.strokeDashoffset = lineLength;
el.style.strokeDasharray = lineLength;
setTimeout(() => {
el.style["transition-duration"] = "3s";
el.style["transition-property"] = "stroke-dashoffset";
el.style["transition-timing-function"] = transitionFn;
el.style.strokeDashoffset = 0;
}, 10);
}
function makeLineDashed() {
animatedLine.style.strokeDasharray = lineLength / (numDashes * 2 - 1);
}
function reset() {
animatedLine.removeAttribute("style");
animatedLineMask.removeAttribute("style");
animatedLine.style["transition-property"] = "none";
animatedLineMask.style["transition-property"] = "none";
}
document.querySelector(".controls").addEventListener("click", handleClick);
</script>
</body>