jquery.transformlist

jquery.transformlist is a Javascript plugin for jQuery to add support for styling and correctly animating CSS3 transforms that make use of multiple transform functions.

jquery.transformlist is not intended to provide animation between arbitrary transform styles - it doesn't provide the matrix decomposition required for that task. Instead it aims to assist you in creating a transform list of several transform functions that are composited for a specific task, and then provides you with means to use jQuery.animate() between different sets of arguments for those transform functions.

That's a little unclear, so imagine you set up a "camera" transform list allowing you to adjust the X, Y, and Z of the focal point; and then the rotation around, elevation above or below, and distance from the focal point.

jquery.transformlist then lets you animate between any different set of those values, using all the power and features of jQuery.animate().

To take it a step further, you could create a transform list to style and position a scene, a transform list for positioning of objects/elements within that scene, and finally a transform list for the camera view of the scene. You could then animate individual objects within the scene using the positioning transform list, while independently animating the camera view of the scene.

You can find more on my GitHub page for jquery.transformlist.

Example

OX
OO
X
Preset Positions
Manual Settings
focalX px
focalY px
focalHeight px
cameraElevation deg
cameraRotation deg
cameraDistance px
Duration ms

The HTML

The HTML is just a viewport div and the board, just a table with some Os and Xs:

<div class="viewport">
  <div class="board">
    <table class="oxo">
      <tr><td>O</td><td>X</td><td></td></tr>
      <tr><td>O</td><td>O</td><td></td></tr>
      <tr><td></td><td></td><td>X</td></tr>
    </table>
  </div>
</div>

The CSS

For brevity I've used prefixfree, but I'd recommend you manually include the vendor prefixes for perspective and transform-style so that you don't get the onload perspective pop seen here.

<style type="text/css">
.viewport {
    perspective: 1000px;
    transform-style: preserve-3d;
    overflow: hidden;
    border: 2px #999 solid; width: 400px; height: 400px;
}

.viewport .board {
    transform-style: preserve-3d;
    width: 400px; height: 400px; background-color: #8a8;
}

.viewport .oxo {
    width: 400px; height: 400px; border-collapse:collapse;
}

.viewport .oxo TD {
    text-align: center; vertical-align: middle;
    font-size: 80px; font-weight: bold;
    border: 6px #88a solid;
}
</style>

perspective configures how sharp the perspective is by controlling the distance to the vanishing point.

transform-type ensures that the 3D frame of reference for the element is passed onto its children. In our case this means the board will transform relative to the viewport, with the same perspective settings.

The rest of the CSS is just making things look pretty.

The Javascript

First you need to define a named transform list:

$.TransformList('camera')
    .addTransform('cameraDistance',     'translateZ', '-250px')
    .addTransform('cameraElevation',    'rotateX',    '-25deg')
    .addTransform('cameraRotation',     'rotateY',    '30deg')
    .addTransform('rotateToHorizontal', 'rotateX',    '90deg')
    .addTransform('focalX',             'translateX', '0px')
    .addTransform('focalY',             'translateY', '0px')
    .addTransform('focalHeight',        'translateZ', '0px');

This creates a transform list named camera composed of 7 named transform functions, each applying a property of our camera positioning via a CSS3 transform function.

(There only actually needs to be 6, but I've fudged things a little because I'm combining camera positioning and the board positioning into a single operation for this example.)

Each named transform function will be composited in the order you added them, and will use the defaults supplied at this stage if you don't supply arguments later.

var board = $('#example .board');
board.css('transformlist', 'camera');

This step just sets the initial board position using jQuery's css() method.

Rather than access the transform CSS3 property directly, we use a faux property transformlist and pass it an anonymous object containing the name and properties of the transform list we want to use.

Since we just want the defaults, we don't define any properties beyond the name.

$('#example_preset_1').on('click', function (e) {
        board.stop(true)
            .animate({
                // Just reset to the defaults.
                transformlist: 'camera'
            }, 2000, 'linear');
    });

You can also apply the transformlist faux-property within animation, and this just sets up an onclick handler to animate back to the initial state. Clicking on it won't do much unless you've moved away from the initial state, of course, so let's do that next.

$('#example_preset_2').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: -133,
                    focalY: 133,
                    cameraRotation: 360,
                    cameraElevation: -40
                }
            }, 2000, 'linear');
    });

This just sets up another button but that animates to a different position, this one moves the camera focus up to the centre of the top-right board square, orbits the camera horizontally around the focal point to a square-on angle 360 degrees from the untransformed position and steepens the camera angle to 40 degrees. Easy, yeah?

We use jQuery's stop(true) to interrupt any existing animation so that we start the new animation immediately from the current position. You can also use stop(true, true) to skip to the end, or leave out the stop() entirely if you want to queue the animations up. Basically it works like any other jQuery animation.

$('#example_preset_3').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: 0,
                    focalY: -133,
                    cameraRotation: 1000,
                    cameraElevation: -45,
                    cameraDistance: -2000
                }
            }, 3000, 'linear');
    });

Another preset button, this one moving to another focal point and zooming way back, also does the animation over 3 seconds instead of 2.

$('#example_preset_4').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: -133,
                    focalY: -133,
                    cameraRotation: 1600,
                    cameraElevation: -700
                }
            }, 10000, 'linear');
    });

This preset just does a bunch of crazy flips and stuff.

$('#example_preset_5').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    cameraRotation: 0,
                    cameraElevation: -90,
                    cameraDistance: 0
                }
            }, 2000, 'linear');
    });

This one just transforms the camera to a position engineered to undo the effects of the board's 90 degree flip to horizontal and make it appear to be untransformed.

$('#example_preset_6').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: 133,
                    focalY: 133,
                    cameraRotation: 15,
                    cameraElevation: -45,
                    cameraDistance: 500
                }
            }, 250, 'linear');
    });

And the final preset does a "crash zoom" into the top-left board square.

This actually zooms a little too close for Firefox, causing it to cull the X from the bottom-right board square - it doesn't restore it to view until the browser is idle after the next animation away from that position completes. I think it's the result of clipping behind the camera from the combination of cameraDistance and the perspective settings, so that's probably something to try to avoid when animating.

var form = $('#example_controls').get(0);

function getCameraFromForm() {
    return {
        transformlist:   'camera',
        focalX:          -$(form.focalX).val() + 'px',
        focalY:          $(form.focalY).val() + 'px',
        focalHeight:     -$(form.focalHeight).val() + 'px',
        cameraElevation: -$(form.cameraElevation).val() + 'deg',
        cameraRotation:  $(form.cameraRotation).val() + 'deg',
        cameraDistance:  -$(form.cameraDistance).val() + 'px'
        };
}

$(form).submit(function (e) {
        board.stop(true)
            .animate({ transformlist: getCameraFromForm() },
                parseFloat($(form.duration).val()), 'linear');
        return false;
    });

This just reads the camera details from the custom settings form, does some sign-flips to convert from human-readable form and applies them as an animation.

The Full Source

The full source, including some wrapping boilerplate to make it play nice in this page is below.

<table>
<tr><td>
<div class="viewport">
  <div class="board">
    <table class="oxo">
      <tr><td>O</td><td>X</td><td></td></tr>
      <tr><td>O</td><td>O</td><td></td></tr>
      <tr><td></td><td></td><td>X</td></tr>
    </table>
  </div>
</div>
</td><td>
<div class="controls">
<form id="example_controls">
<table>
  <tr class="heading"><td colspan="2">Preset Positions</td></tr>
  <tr class="presets">
    <td><input type="button" value="Initial" id="example_preset_1" /></td>
    <td><input type="button" value="Top Right" id="example_preset_2" /></td>
  </tr>
  <tr class="presets">
    <td><input type="button" value="Far Out" id="example_preset_3" /></td>
    <td><input type="button" value="Crazy Town" id="example_preset_4" /></td>
  </tr>
  <tr class="presets">
    <td><input type="button" value="Square" id="example_preset_5" /></td>
    <td><input type="button" value="Crash Zoom" id="example_preset_6" /></td>
  </tr>
  <tr class="heading"><td colspan="2">Manual Settings</td></tr>
  <tr><td>focalX</td><td><input type="text" name="focalX" value="0" size="6" /> px</td></tr>
  <tr><td>focalY</td><td><input type="text" name="focalY" value="0" size="6" /> px</td></tr>
  <tr><td>focalHeight</td><td><input type="text" name="focalHeight" value="0" size="6" /> px</td></tr>
  <tr><td>cameraElevation</td><td><input type="text" name="cameraElevation" value="25" size="6" /> deg</td></tr>
  <tr><td>cameraRotation</td><td><input type="text" name="cameraRotation" value="30" size="6" /> deg</td></tr>
  <tr><td>cameraDistance</td><td><input type="text" name="cameraDistance" value="250" size="6" /> px</td></tr>
  <tr><td>Duration</td><td><input type="text" name="duration" value="1000" size="6" /> ms</td></tr>
  <tr>
    <td colspan="2" align="center"><input type="submit" value="Animate" /></td>
  </tr>
</table>
</form>
</div>
</td></tr>
</table>

<style type="text/css">
.viewport {
    perspective: 1000px;
    transform-style: preserve-3d;
    overflow: hidden;
    border: 2px #999 solid; width: 400px; height: 400px;
}

.viewport .board {
    transform-style: preserve-3d;
    width: 400px; height: 400px; background-color: #8a8;
}

.viewport .oxo {
    width: 400px; height: 400px; border-collapse:collapse;
}

.viewport .oxo TD {
    text-align: center; vertical-align: middle;
    font-size: 80px; font-weight: bold;
    border: 6px #88a solid;
}
</style>

<style type="text/css">
.controls {
  font-size: small;
}

.controls .heading {
  font-size: 9px;
  line-height: 1em;
}

.controls .heading TD {
  border-bottom: 1px #999 solid;
}

.controls .presets TD {
    text-align: center;
    vertical-align: middle;
}
</style>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script>
jQuery.noConflict();
</script>
<script src="/js/external/jquery.transformlist"></script>
<script src="/js/external/prefixfree/0.0/prefixfree"></script>
<script src="/js/external/jquery.transformlist"></script>
<script>
(function (window, $, undefined) {
"use strict";

$.TransformList('camera')
    .addTransform('cameraDistance',     'translateZ', '-250px')
    .addTransform('cameraElevation',    'rotateX',    '-25deg')
    .addTransform('cameraRotation',     'rotateY',    '30deg')
    .addTransform('rotateToHorizontal', 'rotateX',    '90deg')
    .addTransform('focalX',             'translateX', '0px')
    .addTransform('focalY',             'translateY', '0px')
    .addTransform('focalHeight',        'translateZ', '0px');

var board = $('#example .board');
board.css('transformlist', 'camera');

$('#example_preset_1').on('click', function (e) {
        board.stop(true)
            .animate({
                // Just reset to the defaults.
                transformlist: 'camera'
            }, 2000, 'linear');
    });
$('#example_preset_2').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: -133,
                    focalY: 133,
                    cameraRotation: 360,
                    cameraElevation: -40
                }
            }, 2000, 'linear');
    });
$('#example_preset_3').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: 0,
                    focalY: -133,
                    cameraRotation: 1000,
                    cameraElevation: -45,
                    cameraDistance: -2000
                }
            }, 3000, 'linear');
    });
$('#example_preset_4').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: -133,
                    focalY: -133,
                    cameraRotation: 1600,
                    cameraElevation: -700
                }
            }, 10000, 'linear');
    });
$('#example_preset_5').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    cameraRotation: 0,
                    cameraElevation: -90,
                    cameraDistance: 0
                }
            }, 2000, 'linear');
    });
$('#example_preset_6').on('click', function (e) {
        board.stop(true)
            .animate({
                transformlist: {
                    transformlist: 'camera',
                    focalX: 133,
                    focalY: 133,
                    cameraRotation: 15,
                    cameraElevation: -45,
                    cameraDistance: 500
                }
            }, 250, 'linear');
    });

var form = $('#example_controls').get(0);

function getCameraFromForm() {
    return {
        transformlist:   'camera',
        focalX:          -$(form.focalX).val() + 'px',
        focalY:          $(form.focalY).val() + 'px',
        focalHeight:     -$(form.focalHeight).val() + 'px',
        cameraElevation: -$(form.cameraElevation).val() + 'deg',
        cameraRotation:  $(form.cameraRotation).val() + 'deg',
        cameraDistance:  -$(form.cameraDistance).val() + 'px'
        };
}

$(form).submit(function (e) {
        board.stop(true)
            .animate({ transformlist: getCameraFromForm() },
                parseFloat($(form.duration).val()), 'linear');
        return false;
    });

})(window, jQuery);
</script>

Recent blog entries for jquery.transformlist

I don't appear to have written any blog entries about this project yet.

© 2009-2013 Sam Graham, unless otherwise noted. All rights reserved.