Landscape illustrations are well-suited for parallax animation, typified by the excellent example of the site for Firewatch. These examples tend to move in vertical space, and usually inefficiently at that, forced to translate massive alpha-masked PNG files in layers.

It occurred to me that for certain applications SVG might make a very good alternative: not only would the vector layers of the illustration be small and light, but they could also be recoloured and moved individually, as shown in the demo above.

Building the Mesas

The landscape is built as a series of closed paths, with the exception of the sun, which is a simple <circle> element:

<svg id="arizona" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 279">
    <circle id="sun" fill="#FFF7EB" cx="655" cy="128" r="41.5"/>
    <path fill="hsl(32, 89%, 75%)" d="M750 ... "/>
    <path fill="hsl(31, 74%, 71%)" d="M745 ..." />

The paths are built “back to front”: the most distant mesa layer is defined first, followed by the next closest, and so on.

Sky & Shade

Rather than creating a <rect> element for the sky, the <svg> element is filled with color using :

#arizona { 
    background: hsl(47, 100%, 86%);
}

The repeated use of HSL - inline for the SVG layers and via a stylesheet for the SVG itself - is key to the effect. By altering the luminosity component of each element in response to a scroll event, we can provide the impression of a sunrise or sunset.

The tricky part is extracting the HSL from each element. To achieve this, I used a regular expression in JavaScript, defined while gathering other data about the SVG element:

var regex = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/,
arizona = document.getElementById("arizona"),
mesaLayers = arizona.querySelectorAll("path"),
SVGoffsettop = arizona.getBoundingClientRect().top,
vertHeight = arizona.getBoundingClientRect().height,
sun = document.getElementById("sun");

mesaLayers gathers all the paths together. The window scroll is handled inside a function:

function scrollHandler() {
    if (window.scrollY < vertHeight) {
        Array.prototype.forEach.call(mesaLayers, function(layer) { 
        var layerFill = layer.getAttribute("fill"),
        vertRoll = Math.abs(window.scrollY - vertHeight) / vertHeight;
        hslComponents = layerFill.match(regex).slice(1),
        newLum = parseFloat(hslComponents[2]) * vertRoll;
        layer.style.fill = "hsl(" + hslComponents[0] +", " + hslComponents[1] + "%, " +  newLum + "%)";
        arizona.style.background = "hsl(48, " + 100 * vertRoll + "%, " + 88 * vertRoll + "%)";
        sun.style.transform = "translate3d(0," + window.scrollY / 10 + "px, 0)";
    })
}

In order, this function:

  1. Extracts the fill of each path
  2. Determines the amount that the window has been scrolled
  3. Extracts the HSL of each path
  4. Alters the luminosity component of the path fill in relation to the amount of scroll
  5. Reassembles the HSL color for the path, including the new luminosity component, and applies it as a style.
  6. Changes the background of the SVG, again in relation to how much the page has been scrolled.
  7. Moves the sun element down using translate3d, to create a sunset.

The function is called using requestAnimationFrame for improved performance:

window.onscroll = function() {
    window.requestAnimationFrame(scrollHandler);
}

Woah There, Buckaroo

Tying element changes to scrolling can be problematic: if the relationship is too close, changes may happen too quickly to be appreciated. The easiest way to slow things down is to introduce a governing factor. The code changes to something like this:

if (window.scrollY < SVGoffsettop * 4) {
	  Array.prototype.forEach.call(mesaLayers, function(layer) { 
		  var layerFill = layer.getAttribute("fill"),
		  vertRoll = Math.abs((window.scrollY / 4 ) - SVGoffsettop) / SVGoffsettop;
…

Conclusion & Possible Improvements

Normally an effect like this would use position: fixed on the SVG element to keep it in place while the rest of the page is scrolled; in this case, I’ve removed that so that the demo can be integrated with the rest of the page, but the CodePen version has it in place. A parallax effect could be introduced along with the scroll, but I decided to concentrate on just the sunset aspect for this article.

The two remaining inefficient aspects of the script are:

  1. the repeated extraction and reassembly of the HSL components; it would probably be better to create these as properties for each element that were retained in the script, rather than being recalculated each time
  2. the hard-coded colors of the sky background in the script; gaining the colors from the CSS would be better, as the script could then respond to changes in the presentation, rather than having to duplicate changes.

I’ll explore these changes and much more in future articles.

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/oLxKGv