8
\$\begingroup\$

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() {
        checkEnd1(fn);
    }, 200);
}
function checkEnd2(fn) {
    (cdend) ? fn() : setTimeout(function() {
        checkEnd2(fn);
    }, 200);
}
function checkEnd3(fn) {
    (a3end) ? fn() : setTimeout(function() {
        checkEnd3(fn);
    }, 200);
}
function checkEnd4(fn) {
    (a4end) ? fn() : setTimeout(function() {
        checkEnd4(fn);
    }, 200);
}

jsFiddle demo

I've also noticed that the first animation isn't working as excepted. Out of curiousity, I tried it with jQuery's .animate() and it worked as excepted:

$('#box').animate({
    top: window.innerHeight - 120,
    left: window.innerWidth - 120
});

Note: please don't tell me to use jQuery or other libs, I want to use vanilla.


Bounty Edit

Currently, the animation goes diagonally for a while, then when it reaches the bottom, it continues to go sideways. If you tell me how to make it go diagonally all the time, I'll give you a bounty.

\$\endgroup\$
  • 3
    \$\begingroup\$ I see you're trying to use javascript. You should totally drop that and use jQuery. smirk \$\endgroup\$ – Ólafur Waage Mar 5 '11 at 19:28
  • 2
    \$\begingroup\$ @Ólafur No, I want to use vanilla JS. ;) \$\endgroup\$ – nyuszika7h Mar 5 '11 at 19:48
7
+100
\$\begingroup\$

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.

The triangle

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.

\$\endgroup\$
  • \$\begingroup\$ Thanks so much! I've subtracted 20 both from window.innerWidth - box.offsetWidth and window.innerHeight - box.offsetHeight, since the box's original position is 20, 20, and it works like a charm. You only did the first animation, but doing further animations in the callback is easy, I can do it myself. \$\endgroup\$ – nyuszika7h May 21 '11 at 12:51
5
\$\begingroup\$

You're repeating code blocks all over the place; you may want to abstract some out. For example, calling something like

function animatePxProperty(prop, limit, step, interval){
    if(!step) step = 1; //I miss optional arguments :(
    if(!interval) interval = 1;
    var anim = setInterval(
        function() {
            if(parseInt(prop) === parseInt(limit)) clearInterval(anim);
            prop = parseInt(prop)  + step + 'px';
        }, interval
    );
}

instead of providing inline anonymous functions to interval (I haven't tested the above, so tweakery may be required).


When making comparisons/assignments like

(box.style.left === (window.innerWidth - 120) + 'px') {

and

box.style.left = (
    +box.style.left.replace('px', '') + 1
) + 'px'

it's probably better to use parseInt instead of .replace. So

var left_val = parseInt(box.style.left);
...
(left_val === (window.innerWidth - 120)) {
...
box.style.left = left_val + 1 + 'px';

Indulge me, because I'm curious. If you can express exactly what you want with three lines of jQuery, why would you want to implement the same with 88 lines of convoluted side-effects?

\$\endgroup\$
  • 1
    \$\begingroup\$ I want to learn vanilla JS. Though I'm still thinking if I really want do to this. I may change my mind. Or not. I tried parseInt, but for some reason, it didn't work. \$\endgroup\$ – nyuszika7h Mar 7 '11 at 20:18
  • \$\begingroup\$ Doing it as a thought exercise is ok, but god help you if I find this in production code that I have to maintain. What do you mean by "didn't work"? I really can't help diagnose it without additional information other than to point you to its documentation. \$\endgroup\$ – Inaimathi Mar 7 '11 at 20:54
  • \$\begingroup\$ box.style.top = (parseInt(box.style.top) + 1) + 'px'; – this didn't work. jsfiddle.net/Nyuszika7H/stGcj/6. Though if I do var num = '120px'; num = parseInt(num) + 1 + 'px' in the console, it works; num becomes '121px'. \$\endgroup\$ – nyuszika7h Mar 8 '11 at 9:59
  • \$\begingroup\$ @Nyuszika7H - That is odd. I assumed box.style.top returned something like "100px", in which case parseInt(box.style.top) should give you 100. Try using an intermediate variable (t = parseInt(box.style.top) + 1; box.style.top = t+"px"), although I have to admit that's a bit of programming by superstition. \$\endgroup\$ – Inaimathi Mar 8 '11 at 13:58
  • \$\begingroup\$ , The elem.style object doesn't have the styles from external css. It only has values if you set them in the javascript, or inline on the element. \$\endgroup\$ – ICR May 21 '11 at 9:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.