While playing around with the it occurred to me that it might also be used for 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 ; 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