danielwertheim

danielwertheim


notes from a passionate developer

Developer that lives by the mantra "code is meant to be shared".

Share


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

CSS3 transitions and JavaScript queues without jQuery

Daniel WertheimDaniel Wertheim

Recently I needed to replace a jQuery animation and instead use CSS3 transitions to be able to use the hardware accelerations and get a smoother animation. I still had to use JavaScript for some calculations that were applied to properties being transitioned. The animation was divided in different steps and each step ran synchronously and issued a jQuery animate call, waiting for one to complete before kicking of the next one. This article will show a simplified solution on how you can use simple JavaScript arrays, holding steps, where each step knows how to advance to the next step and using simple timeouts to “pause” to execution without freezing the UI and to let the CSS-transitions to finish before advancing. And by doing recursive calls, you can get this to loop on-and-on-and-on, without doing any JavaScript intervals etc.

The sample is really simplified and you could rework it and instead make use of key-frames etc. But the idea is to let three different boxes “walk” diagonal. Again, oversimplified but if you have more dynamic needs where calculations are a big part, you could evolve this sample.

First lets look at the markup, since that is the least exciting part.

<div class="transform-me one"></div>  
<div class="transform-me two"></div>  
<div class="transform-me three"></div>  

Three simple boxes. Which has the following styling (just sticking to webkit browsers here to make the sample easier)

.transform-me {
    position: absolute;
    width: 100px;
    height: 100px;
    top: 0;
    left: 0;
    -webkit-transform: translate3d(0, 0, 0);
    -webkit-transition: top 0.5s linear, left 0.5s linear;
}

.transform-me.one {
    left: 0;
    background-color: aqua;
}

.transform-me.two {
    left: 125px;
    background-color: green;
}

.transform-me.three {
    left: 250px;
    background-color: yellow;
}

Nothing complicated here. The transition will apply to top and left changes, and both directions will transition for 0.5s linearly.

Now, the first part of the JavaScript, the consuming part.

//The idea is to let the box traverse diagonal from
//top-left to bottom - right
var effect = function (el, index) {  
  var initialTop = parseInt(el.getAttribute('data-offset-top')),
      initialLeft = parseInt(el.getAttribute('data-offset-left')),
      step = ((index + 1) * 10);

      el.setAttribute('style',
        'top: ' + (initialTop + step) + 'px;' +
        'left: ' + (initialLeft + step) + 'px;');
};

//Extract data attributes needed in the effect above
var initializeElement = function (el) {  
    el.setAttribute('data-offset-top', el.offsetTop);
    el.setAttribute('data-offset-left', el.offsetLeft);
};

//Start of three Steppers to see different behaviours
//in relation to number of steps, tickMs & pauseFactor
new Stepper('transform-me one', 1, 500, 1, effect)  
  .forEachElement(initializeElement)
  .start();
new Stepper('transform-me two', 2, 1000, 0.5, effect)  
  .forEachElement(initializeElement)
  .start();
new Stepper('transform-me three', 3, 1500, 1/3, effect)  
  .forEachElement(initializeElement)
  .start();
var Stepper = function (cssClassSelector, numOfSteps, tickMs, pauseFactor, effect) {  
  var self = this,
    els = document.getElementsByClassName(cssClassSelector),
    isStarted = false,
    applyEffect = function (stepIndex) {
      self.forEachElement(function (el) {
        effect(el, stepIndex);
      });
    },
    oncomplete = function () {
      self.forEachElement(function (el) {
        el.setAttribute('style', '');
      });
      setTimeout(function () {
        run();
      }, tickMs * pauseFactor);
    },
    tick = function (step) {
      applyEffect(step.index);
      setTimeout(function () {
        var next = step.next();
        if (next)
          tick(next);
        else
          oncomplete();
      }, tickMs);
    },
    run = function () {
      var steps = [];

      for (var i = 0; i < numOfSteps; i++) {
        steps.push({
          index: i,
          next: function() {
            return steps.shift();
          }
        });
      }

      tick(steps.shift());
    };

  this.forEachElement = function (cb) {
    for(var i = 0, m = els.length; i < m; i++) {
      cb(els[i]);
    }
    return this;
  };

  this.start = function () {
    if (isStarted) {
      return;
    }

    isStarted = true;
    run();
  };
};

Some quick words. The start function is basically a one time operation (isStarted prevents more calls), which invokes the run function which builds the “queue” of steps, and using a clojure we get a function in the step that lets us acquire the next step in the queue. Basically it only walks “one way” and then “completes” the path by removing any styles and pauses, so that it will transition back to the initial state (see oncomplete).

That’s it.

//Daniel

Developer that lives by the mantra "code is meant to be shared".

Comments