Harry Parton

Animating a Virtual Journey

Fieldwork has recently been working with Sustrans to create a new look & feel and new and improved features for their sustainable travel challenge platform. As part of the work I created the Virtual Journey.

The idea of the Virtual Journey is that as you complete journeys in the main app you progress along the virtual landscape, following the trail of a trip from Land’s End to John O’Groats.

Scrolling Along

Getting started with a large project can be difficult, in the end I decided to crack on with a fairly major piece of the functionality, it needs to scroll (well it sorta swipes but you get the idea). This proved to be a bit of a challenge.

This was originally done by just using overflow-y:scroll but then I realised that not many people have a fancy schmancy mouse with horizontal scrolling, the other option was to hijack the vertical scroll and make that scroll horizontally across the image, but I found that this made it annoying to navigate around the rest of the page. So I settled for something else, draggable scrolling for desktop and overflow:scroll for mobiles/tablets (where drag to scroll is default behaviour).

The other bit of scrolling action was centring the viewport on the progress indicator.

var mapAnimated = false;
$(window).scroll(function(){
    // Is it visible and has it not been animated yet ?
    if ($(".backdrop").visible(true) && !mapAnimated){
        // Our progress indicator.
        var where = $('.where-wrapper');
        // How far is it to the left ?
        var scrollTo = where.position().left;
        // How wide is it ?
        var whereOffset = 38; //Background size of the pin (can't just do .width as this includes the text and offsets it too much)
        // Half of the current screensize.
        var offset = $(document).width() / 2;
        // Scroll to the left to the progress indicator - half the screensize + indicator size to center.
        $('.backdrop').animate({'scrollLeft': scrollTo - offset + whereOffset}, 2000);
        // Set var to true and stop it animating on every scroll.
        mapAnimated = true;
    }
});

Plotting the Pins

The information and location for the ‘Points of Interest’ pins are stored in a JSON array.

var pageData = {
    "points": [{
        "title": "Lands End along NCN3 towards Truro",
        "x": 80,
        "y": 1,
        "content": "<img src=\"\/img\/virtualjourney\/content\/Step-1-Lands-End.png\" height=\"173px\" \/><p>We head off on our Virtual Journey along NCN 3 from Lands End heading in the direction of John O\u2019Groats.<\/p>"
    }, {
        "title": "Eden Project NCN 3",
        "x": 71,
        "y": 4,
        "content": "<img src=\"\/img\/virtualjourney\/content\/Step-2-Eden-Project.png\" height=\"188px\" \/><p>NCN 3 takes us past the Domes of the Eden Project. From here we\u2019re skipping North to the Tarka Trail<\/p>"
    }],
    "progress": 5
}

The pins are then plotted on the map using the x and y co-ordinates and given a data attribute based on their number in the array.

var output="";

for (var i in pageData.points ) {
    output += "<a href='#' data-content-id='" + i + "' class='pins' style='top:" + pageData.points[i].x + "%; left:" + pageData.points[i].y + "%; '></a>";
}

$(".pins-container").append(output);

When you click on the pin an information box appears with content relative to that pin. This works by getting the data attribute value from that pin and then using that to query the JSON for the correct information, here is the whole script.

$('.pins-container').on('click', '.pins',  function(e) {
    e.preventDefault();
    // Get the content-id data attribute of a pin and then find the
    // JSON content relative to that pin
    var contentId = $(this).data('contentId');
    var infoContent = pageData.points[contentId].content;
    var infoTitle = pageData.points[contentId].title;

    if ($('.closing-container').css('max-height') > '1px') {
        // Put the content into a hidden div
        hiddenContent.html(infoContent);
        placeTitle.html(infoTitle);

        // Fade out the old content
        placeContent.fadeOut(200);
        setTimeout(function () {
            // Replace the content
            placeContent.html(hiddenContent.html());
            // Fade new content in
            placeContent.fadeIn(200);
        }, 300);
    } else {
        hiddenContent.html(infoContent);
        // swap out the title
        placeTitle.html(infoTitle);

        var newheight = hiddenContent.outerHeight();
        placeInfo.css('height', newheight);
        placeContent.html(hiddenContent.html());

        $('.location-info').addClass('active');
    }
});

Breathing some life into the journey with some animation

The Virtual Journey was working, but I felt I could do better with the user experience. A touch of animation can help draw the user into the area of the screen you want them to focus on and let them know there’s something to interact with, as well as adding a sense of fun.

It took a bit of time to find an animation for the pins, that both suited the context and wasn’t too flashy. This is especially important as there are lots of elements using it and you see it on every load.

In the end I decided on a simple pop up and a little shake using scale and transforms.

pins

Here are the keyframes.

@keyframes popup {
    0% {
        transform: rotate(0deg) scaleY(0.1);
        visibility: visible;
    }
    20% {
        transform: rotate(-2deg) scaleY(1.05);
    }
    35% {
        transform: rotate(2deg) scaleY(1);
    }
    50% {
        transform: rotate(-2deg);
    }
    65% {
        transform: rotate(1deg);
    }
    80% {
        transform: rotate(-1deg);
    }
    100% {
        transform: rotate(0deg);
        visibility: visible;
    }
}

I Had some issues with transform:scale(0) in safari as it for some reason doesn’t accept 0 as a number in scale, it has to be 0% even though it normally accepts unit-less numbers. I set it to 0.1 as it wasn’t noticeable in the short space of the animation. (One more strange quirk to add to the long list)

I also wanted to stagger them animating in so it wasn’t too much movement at once and created a feeling of progression along the map. we did this by adding a delay that increases by 0.1s for each pin. Instead of writing this one hundred times, we just made a simple Sass loop.

// Animation delays on pins.
@for $i from 1 through 100{
    .pins:nth-child(#{$i}){
       animation-delay: (#{1400 + $i * 100}ms);
    }
}

Unfortunately this code is part of a private repo and the site is no longer live so your just going to have to trust me that it was cool.