Css Animations

Well, that was an exciting adventure.

Last two days I learnt about CSS animations and 3d css transformations. Quite an eye opener, I didn't realize how sophisticated some of these things had become.


http://codepen.io/shadowmint/pen/aHpwr

These days you can animate, transition and even catch animation events on the animations as they happen, abit with the caveat that every browser behaves differently and your CSS rapidly fills up with browser specific style content:

-webkit-transform: ...
-moz-transform: ...
-ms-transform: ...
-o-transform: ...
transform: ...

...and so forth.

Still, a couple of interesting things. First off, listening for css animations is quite easily handled by the magic of a bit of prefix in javascript:

var pfx = ["webkit", "moz", "MS", "o", ""];
function attach(element, type, cb) {
    for (var p = 0; p < pfx.length; p++) {
        if (!pfx[p]) type = type.toLowerCase();
        element.addEventListener(pfx[p] + type, cb, false);
    }
}

...and actually dealing with events is relatively; simple, you just need to look at the animationName to figure out which animation state is transitioning and apply the appropriate change. ...though to be entirely safe you should trigger a timeout at the same time, to support browser who don't play nicely.

The other surprise to me was that SASS (or SCSS rather) is actually fully featured enough to write a proper set of functions in. The transformations above come from the SCSS:

// Cog bits
$x: _unity();
$x: _multiply($x, _rotateX(15));
$x: _multiply($x, _translate(0, -55, 0));
$cog-rotate-1: $x;
$cog-rotate-2: _multiply($x, _rotateZ(45));
$cog-rotate-3: _multiply($x, _rotateZ(90));
$cog-rotate-4: _multiply($x, _rotateZ(135));
$cog-rotate-5: _multiply($x, _rotateZ(180));
$cog-rotate-6: _multiply($x, _rotateZ(225));
$cog-rotate-7: _multiply($x, _rotateZ(270));
$cog-rotate-8: _multiply($x, _rotateZ(315));

Where those magical _* operations are ones that I've written in sass itself to support various matrix transformations, as well and cos and sin using the Taylor series approximation.

Seems there are a few others online that do a similar thing, but none of them worked for me. Anyhow, basic maths:

$pi: 3.14159265359;

@function _exp($base, $exp) {
  $value: 1;
  @if $exp > 0 {
    @for $i from 0 through ($exp - 1) {
      $value: $value * $base;
    }
  }
  @return $value;
}

@function _fact($val) {
  $value: 1;
  @if $val > 0 {
    @for $i from 1 through $val {
      $value: $value * $i;
    }
  }
  @return $value;
}

@function _round($val) {
  @if $val < 0.0001 {
    @if $val > -0.0001 {
      $val: 0;
    }
  }
  @return $val;
}

/* Taylor cos approximation series */
@function _sin($angle) {
  $angle: $angle / 180 * $pi;
  $sin: 0;
  @for $n from 0 through 10 {
    $sin: $sin + _exp(-1, $n) * _exp($angle, (2 * $n) + 1) / _fact(($n * 2) + 1);
  }
  @return $sin;
}

/* Sin tests */
/*
@debug "sin(0) " + _sin(0);
@debug "sin(90) " + _sin(90);
@debug "sin(180) " + _sin(180);
@debug "sin(270) " + _sin(270);
*/

/* Taylor cos approximation series */
@function _cos($angle) {
  $angle: $angle / 180 * $pi;
  $cos: 0;
  @for $n from 0 through 10 {
    $cos: $cos + _exp(-1, $n) * _exp($angle, 2 * $n) / _fact($n * 2);
  }
  @return $cos;
}

/* Cos tests */
/*
@debug "cos(0) " + _cos(0);
@debug "cos(90) " + _cos(90);
@debug "cos(180) " + _cos(180);
@debug "cos(270) " + _cos(270);
*/

@function _tan($angle) {
  $angle: $angle / 180 * $pi;
  $tan: _sin($angle) / _cos($angle);
  @return $tan;
}

And then my truely hilarious matrix operations to top it off; its worth noting that if you use Compass you don't really need to go to this extreme... but, it is kind of nice to be able to do it all without any extra bits.

 /* Return a unity (no transform) matrix */
@function _unity() {
  @return (
    (1, 0, 0, 0),
    (0, 1, 0, 0),
    (0, 0, 1, 0),
    (0, 0, 0, 1)
  );
}

/* Convert a 4x4 matrix into a string */
@function _unify($matrix) {
  $rtn: '';
  @each $row in $matrix {
    @each $item in $row {
      @if $rtn == '' {
        $rtn: '' + _round($item);
      } @else {
        $rtn: $rtn + ', ' + _round($item);
      }
    }
  }
  @return unquote($rtn);
}

/* Stupid sass matrix multiplication bahahahahahahaa.... XD */
@function _multiply($m1, $m2) {
  $m1_j1: nth($m1, 1);
  $m1_j2: nth($m1, 2);
  $m1_j3: nth($m1, 3);
  $m1_j4: nth($m1, 4);
  $m2_j1: nth($m2, 1);
  $m2_j2: nth($m2, 2);
  $m2_j3: nth($m2, 3);
  $m2_j4: nth($m2, 4);
  $m1_i1_j1: nth($m1_j1, 1);
  $m1_i2_j1: nth($m1_j1, 2);
  $m1_i3_j1: nth($m1_j1, 3);
  $m1_i4_j1: nth($m1_j1, 4);
  $m1_i1_j2: nth($m1_j2, 1);
  $m1_i2_j2: nth($m1_j2, 2);
  $m1_i3_j2: nth($m1_j2, 3);
  $m1_i4_j2: nth($m1_j2, 4);
  $m1_i1_j3: nth($m1_j3, 1);
  $m1_i2_j3: nth($m1_j3, 2);
  $m1_i3_j3: nth($m1_j3, 3);
  $m1_i4_j3: nth($m1_j3, 4);
  $m1_i1_j4: nth($m1_j4, 1);
  $m1_i2_j4: nth($m1_j4, 2);
  $m1_i3_j4: nth($m1_j4, 3);
  $m1_i4_j4: nth($m1_j4, 4);
  $m2_i1_j1: nth($m2_j1, 1);
  $m2_i2_j1: nth($m2_j1, 2);
  $m2_i3_j1: nth($m2_j1, 3);
  $m2_i4_j1: nth($m2_j1, 4);
  $m2_i1_j2: nth($m2_j2, 1);
  $m2_i2_j2: nth($m2_j2, 2);
  $m2_i3_j2: nth($m2_j2, 3);
  $m2_i4_j2: nth($m2_j2, 4);
  $m2_i1_j3: nth($m2_j3, 1);
  $m2_i2_j3: nth($m2_j3, 2);
  $m2_i3_j3: nth($m2_j3, 3);
  $m2_i4_j3: nth($m2_j3, 4);
  $m2_i1_j4: nth($m2_j4, 1);
  $m2_i2_j4: nth($m2_j4, 2);
  $m2_i3_j4: nth($m2_j4, 3);
  $m2_i4_j4: nth($m2_j4, 4);
  @return (
    (
      $m1_i1_j1 * $m2_i1_j1 + $m1_i2_j1 * $m2_i1_j2 + $m1_i3_j1 * $m2_i1_j3 + $m1_i4_j1 * $m2_i1_j4,
      $m1_i1_j1 * $m2_i2_j1 + $m1_i2_j1 * $m2_i2_j2 + $m1_i3_j1 * $m2_i2_j3 + $m1_i4_j1 * $m2_i2_j4,
      $m1_i1_j1 * $m2_i3_j1 + $m1_i2_j1 * $m2_i3_j2 + $m1_i3_j1 * $m2_i3_j3 + $m1_i4_j1 * $m2_i3_j4,
      $m1_i1_j1 * $m2_i4_j1 + $m1_i2_j1 * $m2_i4_j2 + $m1_i3_j1 * $m2_i4_j3 + $m1_i4_j1 * $m2_i4_j4
    ),
    (
      $m1_i1_j2 * $m2_i1_j1 + $m1_i2_j2 * $m2_i1_j2 + $m1_i3_j2 * $m2_i1_j3 + $m1_i4_j2 * $m2_i1_j4,
      $m1_i1_j2 * $m2_i2_j1 + $m1_i2_j2 * $m2_i2_j2 + $m1_i3_j2 * $m2_i2_j3 + $m1_i4_j2 * $m2_i2_j4,
      $m1_i1_j2 * $m2_i3_j1 + $m1_i2_j2 * $m2_i3_j2 + $m1_i3_j2 * $m2_i3_j3 + $m1_i4_j2 * $m2_i3_j4,
      $m1_i1_j2 * $m2_i4_j1 + $m1_i2_j2 * $m2_i4_j2 + $m1_i3_j2 * $m2_i4_j3 + $m1_i4_j2 * $m2_i4_j4
    ),
    (
      $m1_i1_j3 * $m2_i1_j1 + $m1_i2_j3 * $m2_i1_j2 + $m1_i3_j3 * $m2_i1_j3 + $m1_i4_j3 * $m2_i1_j4,
      $m1_i1_j3 * $m2_i2_j1 + $m1_i2_j3 * $m2_i2_j2 + $m1_i3_j3 * $m2_i2_j3 + $m1_i4_j3 * $m2_i2_j4,
      $m1_i1_j3 * $m2_i3_j1 + $m1_i2_j3 * $m2_i3_j2 + $m1_i3_j3 * $m2_i3_j3 + $m1_i4_j3 * $m2_i3_j4,
      $m1_i1_j3 * $m2_i4_j1 + $m1_i2_j3 * $m2_i4_j2 + $m1_i3_j3 * $m2_i4_j3 + $m1_i4_j3 * $m2_i4_j4
    ),
    (
      $m1_i1_j4 * $m2_i1_j1 + $m1_i2_j4 * $m2_i1_j2 + $m1_i3_j4 * $m2_i1_j3 + $m1_i4_j4 * $m2_i1_j4,
      $m1_i1_j4 * $m2_i2_j1 + $m1_i2_j4 * $m2_i2_j2 + $m1_i3_j4 * $m2_i2_j3 + $m1_i4_j4 * $m2_i2_j4,
      $m1_i1_j4 * $m2_i3_j1 + $m1_i2_j4 * $m2_i3_j2 + $m1_i3_j4 * $m2_i3_j3 + $m1_i4_j4 * $m2_i3_j4,
      $m1_i1_j4 * $m2_i4_j1 + $m1_i2_j4 * $m2_i4_j2 + $m1_i3_j4 * $m2_i4_j3 + $m1_i4_j4 * $m2_i4_j4
    )
  );
}

Finally, mix it all up with the actual transformation matricies:

/* A scale matrix */
@function _scale($x, $y, $z) {
  @return (
    ($x, 0, 0, 0),
    (0, $y, 0, 0),
    (0, 0, $z, 0),
    (0, 0, 0, 1)
  );
}

/* Rotation matrix for x, y, z */
@function _rotateX($angle) {
  $angle: -$angle; // Match CSS transform
  @return (
    (1, 0, 0, 0),
    (0, _cos($angle), _sin(-$angle), 0),
    (0, _sin($angle), _cos( $angle), 0),
    (0, 0, 0, 1)
  );
}

@function _rotateY($angle) {
  $angle: -$angle; // Match CSS transform
  @return (
    (_cos($angle), 0, _sin($angle), 0),
    (0, 1, 0, 0),
    (_sin(-$angle), 0, _cos($angle), 0),
    (0, 0, 0, 1)
  );
}

@function _rotateZ($angle) {
  $angle: -$angle; // Match CSS transform
  @return (
    (_cos($angle), _sin(-$angle), 0, 0),
    (_sin($angle), _cos($angle), 0, 0),
    (0, 0, 1, 0),
    (0, 0, 0, 1)
  );
}

/* A translate matrix */
@function _translate($x, $y, $z) {
  @return (
    (1, 0, 0, 0),
    (0, 1, 0, 0),
    (0, 0, 1, 0),
    ($x, $y, $z, 1)
  );
}

...and you end up with a builder that lets you combine it all like so:

transform: scaleX(2) scaleY(2) scaleZ(2) translate3d(250px, 50px, 0) rotateZ(10deg) rotateY(15deg) rotateX(20deg);

Or:

$matrix3d: _unity();
$matrix3d: _multiply($matrix3d, _rotateX(20));
$matrix3d: _multiply($matrix3d, _rotateY(15));
$matrix3d: _multiply($matrix3d, _rotateZ(10));
$matrix3d: _multiply($matrix3d, _translate(50, 50, 0));
$matrix3d: _multiply($matrix3d, _scale(2, 2, 2));
$matrix3d: _unify($matrix3d);
trnasform: matrix3d($matrix);

Have a play with it on code pen if you like! http://codepen.io/shadowmint/pen/rImpz

Anyway, why would you really want to do that?

Well, actually it turns out that a single matrix3d() call is more performant and massively reduces the size of your final stylesheets.

Completely worth it? Hm... not entirely convinced yet either way myself, but for the time being, I'm pretty happy with the result.