body {
display: flex;
align-items: center;
justify-content: center;
}
svg {
width: 100%;
height: 100%;
}
path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}:root {
--stroke: #000000;
}
@media (prefers-color-scheme: dark) {
:root {
--stroke: #898989;
}
}Draw a path
<svg width="320" height="160" viewBox="-120 -60 240 120" xmlns="http://www.w3.org/2000/svg">
<path id="path" d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"/>
</svg>#path {
stroke: var(--stroke);
stroke-width: 7;
}Animate the path using stroke dasharray and dashoffset :
const animate = pth => {
const len = pth.getTotalLength();
const dsh = len + 1;
pth.style.strokeDasharray = dsh + " " + dsh;
pth.style.strokeDashoffset = len;
const loop = dir => {
pth.animate(
[
{ strokeDashoffset: dir * len },
{ strokeDashoffset: (1 - dir) * (1 - len) }
],
{
duration: 3000,
easing: "ease-in-out",
fill: "both",
}
).finished.then(() => loop(1 - dir))
};
loop(1);
};animate(document.querySelector("#path"));Dashed line animation requires a mask with same stroke-width :
<svg width="320" height="160" viewBox="-120 -60 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="masked">
<path
id="mask"
d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"/>
</mask>
</defs>
<path
id="path"
stroke-dasharray="0 15.02"
mask="url(#masked)"
d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"/>
</svg>#mask {
stroke: #FFFFFF;
stroke-width: 7;
}animate(document.querySelector("#mask"));Add some style :
<svg width="320" height="160" viewBox="-120 -60 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur1"/>
<feFlood result="color1"/>
<feComposite in="color1" in2="blur1" operator="in" result="shadow1"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur2"/>
<feFlood result="color2"/>
<feComposite in="color2" in2="blur2" operator="in" result="shadow2"/>
<feMerge>
<feMergeNode in="shadow2"/>
<feMergeNode in="shadow1"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<path id="path" d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"
filter="url(#glow)"
/>
</svg>:root {
--stroke: #00adff;
}
#glow feFlood {
flood-color: color-mix(in srgb, var(--stroke) 70%, white 20%)
}/*
Same glow effect with a simple CSS filter...
Not showing on iOs, how surprising :)
*/
:root {
--stroke: #00adff;
--shadow: color-mix(in srgb, var(--stroke) 70%, white 30%);
}
#path {
stroke: var(--stroke);
filter: drop-shadow(0 0 2px var(--shadow)) drop-shadow(0 0 4px var(--shadow));
}Dash animation mask cuts off glow effect :
<svg width="320" height="160" viewBox="-120 -60 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur1"/>
<feFlood result="color1"/>
<feComposite in="color1" in2="blur1" operator="in" result="shadow1"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur2"/>
<feFlood result="color2"/>
<feComposite in="color2" in2="blur2" operator="in" result="shadow2"/>
<feMerge>
<feMergeNode in="shadow2"/>
<feMergeNode in="shadow1"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<mask id="masked" x="-50%" y="-50%" width="200%" height="200%">
<path
id="mask"
d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"/>
</mask>
</defs>
<path id="path" d="M-100 0c0-40 60-60 100 0s100 40 100 0S40-60 0 0s-100 40-100 0"
stroke-dasharray="0 15.02"
mask="url(#masked)"
filter="url(#glow)"
/>
</svg>Increasing mask stroke-width value messes up path crossings :
#mask {
stroke-width: 28px;
}Remove the mask and calculate dashes one by one :
// TODO ADD DIR VARIABLE 1 or -1 and LOOP FOREVER
const path = document.querySelector("#path");
const len = path.getTotalLength();
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
const dash = 15.02;
const loops = Math.floor(len / dash);
const times = Math.round(8000 / loops);
let count = 0;
const loop = setInterval(() => {
path.style.strokeDashoffset = 0;
path.style.strokeDasharray = `0 ${dash} `.repeat(count)
+ " 0 " + (len - count * dash);
if(++count === loops)
clearInterval(loop);
}, times);Introduce precise calculations, frame loop, and promise :
const dashDraw = (path, dashSize, gapSize, duration) =>
new Promise(done => {
const segment = dashSize + gapSize;
const len = path.getTotalLength();
path.style.strokeDasharray = `0 ${len}`;
let startTime = 0;
const animate = currentTime => {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const drawn = Math.min((elapsed / duration) * len, len);
if(drawn >= len) {
path.style.strokeDasharray = `${dashSize} ${gapSize}`;
return done();
}
const segments = Math.floor(drawn / segment);
const remainder = drawn % segment;
const dashPart = Math.min(remainder, dashSize);
const gapPart = Math.max(0, remainder - dashSize);
path.style.strokeDasharray =
`${dashSize} ${gapSize} `.repeat(segments) +
`${dashPart} ${gapPart} 0 ${len}`;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
});dashDraw(path, 0, 15.02, 10000)
.then(
() => {
// the end
}
);A nice line from this old article
<svg xmlns="http://www.w3.org/2000/svg" height="160" width="640" viewBox="0 0 640 160">
<defs>
<filter id="shadow">
<feDropShadow dx="3" dy="4" stdDeviation="2" flood-color="rgba(0, 0, 0, .5)"/>
</filter>
</defs>
<path id="path"
d="M92.4 45.9c-25-7.74-56.6 4.8-60.4 24.3-3.73 19.6 21.6 35 39.6 37.6 42.8 6.2 72.9-53.4 116-58.9 65-18.2 191 101 215 28.8 5-16.7-7-49.1-34-44-34 11.5-31 46.5-14 69.3 9.38 12.6 24.2 20.6 39.8 22.9 91.4 9.05 102-98.9 176-86.7 18.8 3.81 33 17.3 36.7 34.6 2.01 10.2.124 21.1-5.18 30.1"
filter="url(#shadow)"
/>
</svg>body {
background: #858585;
height: 100%;
}
svg {
width: 100%;
height: 100%;
}
path {
stroke: #00adff;
stroke-width: 3px !important;
stroke-linecap: butt;
stroke-linejoin: miter;
}dashDraw(path, 10, 5, 5000)
.then(
() => {
// the end
}
);