HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Animations with vanilla JS

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
animationsvanillawith

Problem

I've used jQuery for a long time, and now I'm going to do some animations with vanilla JS. Here's my code so far:

```
var box = document.getElementById('box'),
a1end,
a2end,
cdend,
a3end,
a4end,
animate1 = setInterval(function() {
if (box.style.top === (window.innerHeight - 120) + 'px') {
clearInterval(animate1);
a1end = true;
}
box.style.top = (
+box.style.top.replace('px', '') + 1
) + 'px';
}, 1),
animate2 = setInterval(function() {
if (box.style.left === (window.innerWidth - 120) + 'px') {
clearInterval(animate2);
a2end = true;
}
box.style.left = (
+box.style.left.replace('px', '') + 1
) + 'px';
}, 1);

checkEnd1(function() {
box.style.background = 'greenyellow';
box.innerText = 'Next stage starts in 5 seconds';
var i = 4,
cd = setInterval(function() {
if (!i) {
clearInterval(cd);
cdend = true;
}
box.innerText = 'Next stage starts in ' + i + ' seconds';
i--;
}, 1000);

});
checkEnd2(function() {
box.style.background = 'white';
box.innerText = '';
var animate3 = setInterval(function() {
if (box.style.left === '20px') {
clearInterval(animate3);
a3end = true;
}
box.style.left = (
+box.style.left.replace('px', '') - 1
) + 'px';
}, 1);
});
checkEnd3(function() {
var animate4 = setInterval(function() {
if (box.style.top === '20px') {
clearInterval(animate4);
a4end = true;
}
box.style.top = (
+box.style.top.replace('px', '') - 1
) + 'px';
}, 1);
});
checkEnd4(function() {
box.style.background = 'lightcyan';
box.innerText = 'Animation complete';
});

function checkEnd1(fn) {
(a1end && a2end) ? fn() : setTimeout(function() {

Solution

Rather than use your code as a base, I chose to rewrite from scratch to incorporate a couple of ideas to get it to move diagonally properly, as well as hopefully make some cleaner code.

var speed = 2;

function animateTo(dom_elem, x, y, finishedCallback) {
    var pos = {
        x: dom_elem.offsetLeft,
        y: dom_elem.offsetTop
    };

    function animate() {    
        var xToTarget = x - pos.x;
        var yToTarget = y - pos.y;

        xToTarget = Math.max(0, xToTarget);
        yToTarget = Math.max(0, yToTarget);

        if (xToTarget == 0 && yToTarget == 0) {
            finishedCallback();
        }

        var scale = speed/Math.sqrt(Math.pow(xToTarget, 2) + Math.pow(yToTarget, 2));

        var delta_x = xToTarget * scale;
        var delta_y = yToTarget * scale;

        pos.x += delta_x;
        pos.y += delta_y;

        pos.x = Math.min(x, pos.x);
        pos.y = Math.min(y, pos.y);

        dom_elem.style.left = pos.x + 'px';
        dom_elem.style.top = pos.y + 'px';

        setTimeout(animate, 1);
    }

    setTimeout(animate, 1);
}

var box = document.getElementById('box');
animateTo(box, window.innerWidth - box.offsetWidth, window.innerHeight - box.offsetHeight, function() {
    alert('done');
});


Firstly, I have abstracted it into the idea of animating a dom element to a particular position. You can then chain together the animations needed using the finishedCallback.

The trick to getting it to animate diagonally properly, instead of hitting the bottom and sliding along, is to move along x and y by different amounts depending on the ratio needed to reach the target instead of always incrementing by 1.

Imagine the current position of the box as one point on the hypotenuse of a triangle, and the destination as the other end of the hypotenuse. The distance along the x-axis forms one of the other edges, and the distance on the y-axis the final edge.

The hypotenuse shows us the line we want the box to travel along. But we don't want to travel there all in one go, we want to move along it slowly. If we take the triangle we have formed, and scale it down the angle of the hypotenuse is still the same, meaning if we keep moving in that direction we will eventually reach the end point, but the distance on the x and y axis are much smaller.

These smaller x and y distances (or deltas) are what we want to add to our position instead of 1.

If we were trying to animate from A to B, the red line shows how you would move if you always added 1. The dotted lines show the scaled triangles.

Math.sqrt(Math.pow(xToTarget, 2) + Math.pow(yToTarget, 2)) calculates the length of the hypotenuse in our triangle from current position to the target. We then use speed/length to create the scale we multiply the x and y lengths by. In this way the box will always move the equivalent of speed pixels diagonally towards the target.

I chose to keep track of the x and y position manually instead of reading it back from the dom element because I think it makes the code cleaner, and I think it should be faster.

Code Snippets

var speed = 2;

function animateTo(dom_elem, x, y, finishedCallback) {
    var pos = {
        x: dom_elem.offsetLeft,
        y: dom_elem.offsetTop
    };

    function animate() {    
        var xToTarget = x - pos.x;
        var yToTarget = y - pos.y;

        xToTarget = Math.max(0, xToTarget);
        yToTarget = Math.max(0, yToTarget);

        if (xToTarget == 0 && yToTarget == 0) {
            finishedCallback();
        }

        var scale = speed/Math.sqrt(Math.pow(xToTarget, 2) + Math.pow(yToTarget, 2));

        var delta_x = xToTarget * scale;
        var delta_y = yToTarget * scale;

        pos.x += delta_x;
        pos.y += delta_y;

        pos.x = Math.min(x, pos.x);
        pos.y = Math.min(y, pos.y);

        dom_elem.style.left = pos.x + 'px';
        dom_elem.style.top = pos.y + 'px';

        setTimeout(animate, 1);
    }

    setTimeout(animate, 1);
}

var box = document.getElementById('box');
animateTo(box, window.innerWidth - box.offsetWidth, window.innerHeight - box.offsetHeight, function() {
    alert('done');
});

Context

StackExchange Code Review Q#1171, answer score: 7

Revisions (0)

No revisions yet.