While playing around with the Web Animation API it occurred to me that it might also be used for SVG line animations; I decided to apply it to an interactive roadtrip passing through three locations in my home country of New Zealand.
This interactive is also progressive on mobile, in the sense that smaller devices do not attempt to show the map and locations in their limited viewports, but only show the location details.
The SVG
I won’t go into tremendous depth on the SVG; you can see all the details on the associated CodePen.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 29.7 1061.9 1545.2" id="nzmap">
<g fill="teal">
<path d="M668.9 …"/>
</g>
<path id="ni" pathlength="240" d="M797.2…"/>
<path id="si" pathlength="1250" d="M797.2 …"/>
<a href="#">
<circle cx="801.2" cy="488.9" r="17.2" class="outline" />
<circle cx="801.2" cy="488.9" r="15" />
<text dx="818.382" dy="439.046">Rotorua</text>
<desc>
Known for it’s geothermal activity, geysers and mud pools,
the air of Rotorua smells constantly of sulphur.
</desc>
</a>
<a href="#">
<circle cx="105.3" cy="1343.8" r="17.2" class="outline" style="animation-delay: 1s;" />
<circle cx="105.3" cy="1343.8" r="15" />
<text dx="160.028" dy="1360.957">Fiordland</text>
<desc>
A variant spelling of the Scandinavian word for the deep valleys
plunging into the ocean and bays…
</desc>
</a>
<a href="#">
<circle cx="635.9" cy="628.1" r="17.2" class="outline"
style="animation-delay: 1.2s;" />
<circle cx="635.9" cy="628.1" r="15" />
<text dx="395.53" dy="644.244">Taranaki</text>
<desc>
Taranaki is located on the lower third of the west coast of the
North Island, and is named for the stratovolcano that is its central feature.
</desc>
</a>
</svg>
Several points from the SVG do need to be pointed out:
- The three islands are a single path, filled with a teal color from the surrounding group.
- The paths for the roadtrip elements are given different
id
values (representing “north island” and “south island”). - The paths also use the
pathLength
attribute to describe the total length of each path, discovered through experimentation. - Links in the SVG surround the circle locations and the associated text; the
<desc>
content in each is not visible, but will be used by the JavaScript later. - The two circles in each link are styled and animated with CSS; the outer circle has an
animation-delay
so that all the locations don’t pulse at the same time.
The HTML
The SVG is inline in the page, and surrounded with a <div>
; there is another <div>
for the location description and image:
<div id="map">
<svg …></svg>
<div id="description">
</div>
</div>
The CSS
The stylesheet sets up the animations that will be used:
@keyframes fadein {
to { opacity: 1; }
}
@keyframes lookit {
to {
stroke-width: 20px;
stroke: rgba(255, 255, 255, 0.2);
}
}
Then the CSS addresses relationship between the map and the description alongside it. Note that the elements inside the description are delayed when it has a class of active
, so that they appear one after the other when the script makes them visible:
#map svg {
margin-left: 2rem;
width: 33%;
height: auto;
display: inline-block;
}
#description {
display: inline-block;
background: rgba(0, 0, 0, 0.6);
color: #fff;
width: 50%;
margin: 1rem 3rem 0;
vertical-align: top;
transition: .8s cubic-bezier(.14, .39, .41, 1.29);
}
#description * { opacity: 0; }
#description p { margin-bottom: 2rem; }
#description.active * {
animation: fadein 3s forwards;
}
#description.active img {
animation-delay: .4s;
}
#description.active h1 {
animation-delay: 1s;
}
#description.active p {
animation-delay: 2s;
}
#description h1,
#description p {
margin-left: 2rem;
margin-right: 2rem;
}
#description img {
width: 100%;
}
Then finally the map, text and location circles; the circles have a custom easing curve for a “pulsing” motion:
#map text {
font-size: 48px;
font-weight: 400;
fill: #fff;
}
#map circle {
fill: #000;
}
#map circle.outline {
fill: none;
stroke-width: 5px;
stroke: #fff;
transition: .4s;
animation: lookit 1.8s infinite alternate cubic-bezier(.85, 0, .32, .83);
}
#map a:hover circle:not(.outline),
a:focus circle {
fill: #f00;
}
…and travel paths. Each of the paths have the same values for stroke-dasharray
and stroke-dashoffset
, meaning that they don’t have any visible stroke:
#ni, #si {
fill: none;
stroke: #f00;
stroke-width: 12px;
transition: .4s opacity;
}
#ni {
stroke-dasharray: 240;
stroke-dashoffset: 240;
}
#si {
stroke-dasharray: 1250;
stroke-dashoffset: 1250;
}
The JavaScript
If the user clicks anywhere on the SVG map, the script uses event bubbling to filter out registering anything other than a link. It then grabs the text associated with the clicked link, test that the user hasn’t clicked on the same link twice, and animates the appropriate travel path. Finally, the description
div is filled with the desc
content:
let prevLoc, locName;
nzmap.addEventListener("click", function(e) {
let loc = e.target.parentNode;
e.preventDefault();
let desc = loc.querySelector("desc").textContent;
if (loc.nodeName == "a") {
locName = loc.querySelector("text").textContent;
if (locName !== prevLoc) {
if (locName == "Taranaki") {
pathTravel(ni, ni.pathLength.baseVal);
}
if (locName == "Fiordland") {
if (prevLoc == "Taranaki") {
pathTravel(si, si.pathLength.baseVal);
} else {
pathTravel(ni, ni.pathLength.baseVal, "true");
}
}
description.classList.remove("active");
description.innerHTML = "";
prevLoc = locName;
description.insertAdjacentHTML("afterbegin", fillDesc(locName, desc));
description.classList.add("active");
}
}
});
That description is filled with a function that creates a srcset
image based on the name used in the <text>
element of the clicked link, together with the description (based on the <desc>
content).
function fillDesc(locName, desc) {
var imageURL = "",
fileName = locName.split("."),
longDesc = "<img src="+imageURL + locName.toLowerCase() +".jpg srcset=""+imageURL + fileName[0].toLowerCase() +".jpg 2x" alt>";
longDesc += "<h1>"+locName+"</h1>";
longDesc += "<p>"+desc+"</p>";
return longDesc;
}
To animate the travel paths I used the Web Animation API:
function pathTravel(travelPath, pathDist, extra) {
travelPath.animate([
{ strokeDashoffset: pathDist },
{ strokeDashoffset: "0" }
], {
duration: pathDist * 5,
fill: "forwards"
}).onfinish = function() {
if (extra) {
pathTravel(si, si.pathLength.baseVal);
}
extra = false;
}
}
In most cases, the growth of the travel path will be from a northern location to the current location, but in the case of Fiordland I needed the travel to involve both paths. To make this happen, I used the Web Animation API’s onfinish
method to test if that needed to happen; if it did, I called the function again to trace the route from Taranaki to Fiordland after the Rotorua-Fiordland path was complete.
Mobile Accomodations
No matter how much fun it is, some UIs just aren’t built for smaller screens: this example is one of them. To address that, I’ve used a matchMedia
check to see if the browser is under 750 pixels wide. If it is, I fill the description
div with the text and pictures of all the locations:
var screencheck = window.matchMedia("(max-width: 750px)");
window.addEventListener("load", function() {
if (screencheck.matches) {
let locs = nzmap.getElementsByTagName("text"),
descs = nzmap.getElementsByTagName("desc");
for (let i = 0; i < locs.length; i++) {
description.insertAdjacentHTML("afterbegin", "<div>"+fillDesc(locs[i].textContent, descs[i].textContent)+"</div>");
}
}
});
…and in my CSS, use an @media
query combined with flexbox to display the result, while hiding the map:
@media all and (max-width: 790px) {
#nzmap {
display: none;
}
#description {
width: 100%;
margin: 0;
}
#description * {
opacity: 1;
}
#description > div {
flex: 1;
}
}
Conclusion
This interface was originally designed to support a video roadtrip using WebVTT chapters; I’ll eventually get to that, but hope that you’ll find this version sufficiently interesting in the meantime.
Photographs by Reinis Traidas, Kathrin & Stefan Marks, and Jocelyn Kinghorn, licensed under Creative Commons
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/zKBVPm