• Stars
    star
    153
  • Rank 243,368 (Top 5 %)
  • Language
    JavaScript
  • Created almost 8 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🎞️ A library for handing animations combining page position and time

transform-when Build Status

A library for handing animations combining page position and time, written at SamKnows.

For a full demo at 60fps, see: samknows.com (archive.org), or for a simpler demo, check out this article I wrote.

Installation

$ npm install --save transform-when

Features

  • Blurs the line the between reactive and time-based animations, allowing you to combine variables such as page position, time, and user actions.
  • Uses a number of techniques to ensure extremely high performance: both on desktop and on mobile.
    • Uses pure functions to intelligently know when a property is going to change without having to call the transform function first.
    • Calculates every value to set, and then sets them all in one go, effectively making layout thrashing impossible.
    • Stores property values and compares changes against the old values to ensure that it actually is a change before setting it: sort of like a virtual DOM, but without a virtual DOM.
    • Uses requestAnimationFrame to ensure that it is only ran when necessary.
  • It is powerful. You can make complicated animations with this library.
  • Because it is low-level and doesn't contain any knowledge of the stuff it is animating (that bit is left up to you), it's extremely lightweight: minified and gzipped, the whole library is under 10KB
  • Works with both HTML elements and SVG elements.
  • Tested in IE11+.

Usage

const Transformer = require('transform-when');

new Transformer([
  {
    el: document.querySelector('.my-element'),
    styles: [
      ['opacity', function (y) {
        if (y > 600) {
          return 0;
        }
        
        return 1;
      }]
    ]
  }
]);

The above code sets up a fairly simple transformer: it sets the opacity of the element to 0 if window.scrollY reaches more than 600, and back to 1 if the user scrolls back up above 600px again.

In addition to styles, transform-when can animate attrs, and transforms. transforms is a helper function, and will set the transform style on HTML elements and the transform property on SVG elements.

Let's take a look at a longer example that uses all three:

const Transformer = require('transform-when');

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    styles: [
      ['opacity', function (y) {
        // This function animates the opacity from 1 to 0 between 500px and 600px:
        // we'll explore it some more later.
        return Transformer.transform([500, 600], [1, 0], y);
      }]
    ],
    attrs: [
      ['class', function (y) {
        return 'my-element' + (y > 500 && y < 600 ? ' animating' : '');
      }]
    ],
    transforms: [
      ['scale', function (y) {
        return Transformer.transform([500, 600], [1, 0.5], y);
      }]
    ]
  }
]);

That code would take the element (or elements) matching .my-element, and then when the user scrolls between 500px and 600px, it would animate the opacity from 1 to 0, animate the scale from 1 to 0.5, and apply the animating class.

Terminology

  • The Transformer function takes either a transform object or an array of them.
  • Each transform object should have an el property containing an element (or NodeList) and some properties to animate: styles, attrs, and transforms.
  • Properties use transform functions to calculate what values should be changed. Transform functions should be pure functions (without side effects), and request only the arguments requested so they can be heavily optimised.

That's it.

Transform function arguments (smart arguments)

The transform functions above only have one argument, y, but if you were to change that to x or i, you would get a different number. This is because transform-when uses the arguments to detect when a property needs to be changed before actually calling the transform function: if the only argument is y and the y position of the page hasn't changed since the function was last called, then it doesn't bother to call the transform function.

This is what makes transform-when so performant - but it means that transform functions should be pure as much as possible (if you want something to be random, don't worry - read the section on i below).

There are (currently) four different arguments you can request: y, x, i and actions.

Scroll position (x and y)

The x and y values are simply window.scrollX and window.scrollY (or the IE equivalents)—how far down or along the page the user has scrolled.

Time (i)

i starts at 0, and increases by 1 for each frame—effectively, it's the frame number. This is useful for animating by time.

If you want the actual time or a duration, you can calculate that yourself using Date.now().

If you want an impure transform function—say, you want to change it a bit randomly—request the i argument and the transform function will be called every time.

User actions (actions)

Sometimes you want a break in the normal animation: say, if a user clicks on something, or if a certain position on the page is reached. transform-when has a concept of actions: these can be triggered, and then play for a given amount of time.

You trigger them using the .trigger() method, and they're passed in using an actions argument:

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    transforms: [
      ['rotate', function (actions) {
        // actions === { spin: x } where x is a number between 0 and 1
        
        if (actions.spin) {
          return 360 * actions.spin;
        }
        
        return 0;
      }, 'deg']
    ]
  }
]);

transforms.trigger('spin', 2000);

Multiple actions can be triggered at the same time.

The .trigger() function returns a promise which resolves when the action completes. It uses native promises, and will return undefined when window.Promise is undefined.

Custom variables

It's possible to add your own variables.

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    styles: [
      ['opacity', function (myCustomVariable) {
      
      }]
    ]
  }
]);

transforms.addVariable('myCustomVariable', function () {
  // Return what you want `myCustomVariable` to equal
});

The transform function is still only called when the variable is changed - except for the way it is generated, custom variables are treated exactly the same as scroll position, time and user actions.

Minifiers

Minifiers will, by default, break transform-when if they rename variables. The way around this is to wrap the function in an array saying what variables you need:

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    transforms: [
      ['rotate', ['actions', function (actions) {
        if (actions.spin) {
          return 360 * actions.spin;
        }
        
        return 0;
      }], 'deg']
    ]
  }
]);

The minifier won't touch the string, and transform-when will look at that instead.

this

In a transform function, this refers to the transform object. This allows you to store stuff like scales on the transform object:

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    colorScale: chroma.scale(['red', 'blue']).domain([500, 600]),
    styles: [
      ['color', function (y) {
        return this.colorScale(y);
      }]
    ]
  }
]);

Types of properties

There are three types of properties, styles, attrs and transforms. The first two are both pretty simple: they just set styles and attributes of an element. Be careful animating attributes and styles that aren't the opacity: they are more expensive to animate than transforms and opacity, and might make your animation jerky.

Each takes an array of three things: the property (style or attribute) to animate, the transform functions, and optionally the unit to use - it's better to let transform-when handle adding the unit, because it will also round the number for you.

Let's take a look at an example:

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    styles: [
      ['padding', function (y) {
        return Transformer.transform([500, 600], [20, 50], y);
      }, 'px']
    ],
    attrs: [
      ['class', function (y) {
        return 'my-element' + (y > 500 && y < 600 ? ' animating' : '');
      }]
    ],
  }
]);

That animates the padding of an element from 20px to 50px, and adds the animating class.

Transforms are a little trickier.

Animating transforms

CSS or SVG transforms are all set on one property. For example, a CSS transform could be scaleY(0.5) translate(10px 20px) and an SVG transform could be scale(1 0.5) translate(10 20). Transforms are the reason for the slightly strange syntax using arrays for properties, not objects: order is important. Translating an element then scaling it is pretty different to scaling it and then translating it.

transform-when looks at the array, turning each property into part of the transform attribute (for SVG) or style (for HTML elements).

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    transforms: [
      ['scale', function (y) {
        return Transformer.transform([500, 600], [1, 1.5], y);
      }],
      ['translateX', function (y) {
        return Transformer.transform([500, 600], [0, 50], y);
      }, 'px']
    ]
  }
]);

That would return scale(1) translateX(0px) when the y position of the page is 500px, scale(1.5) translateX(50px) when the y position of the page is 600px, and transition between the two.

Because the library doesn't have any knowledge of the properties it is animating, remember to specify units when required for CSS transforms, and don't try to use scaleY on an SVG!

Animating multiple properties at once

Sometimes it's necessary to animate multiple properties at the same time with the same value—for example, for CSS vendor prefixes. It isn't necessary to specify two different properties with the same transform functions (and it would be pretty inefficient, too): you can just specify the property as an array:

const transformer = new Transformer([
  {
    el: mock,
    styles: [
      [['clip-path', 'webkit-clip-path'], function (i) {
        return 'circle(50px at 0% 100px)';
      }]
    ]
  }
]);

Transform helpers

transform-when provides a couple functions to help with animating values between two different points: Transformer.transform(), and Transformer.transformObj(). If you're familiar with d3, Transformer.transform() work pretty similar to d3's scale functions.

Both functions map a domain to a range: for example, if you want to animate the scale of an element from 1 to 2 between the y positions of 500px and 600px, you could do it like this:

const scale = (x) => (2 - 1) * (y - 500) / (600 - 500) + 1;

That gets complicated. Instead, you can use one of the helpers:

Transformer.transform([500, 600], [1, 2], y);

Transformer.transform()

A simple scale function with three arguments, domain, range, and value. Takes the value and converts it into a new number.

Transformer.transform([400, 600], [1, 0], 400); // 1
Transformer.transform([400, 600], [1, 0], 500); // 0.5
Transformer.transform([400, 600], [1, 0], 600); // 0

If only given two arguments, it'll return a function that can be called with the final value, but there is no performance advantage to doing this:

const myTransform = Transformer.transform([400, 600], [1, 0]);

myTransform(400); // 1
myTransform(500); // 0.5
myTransform(600); // 0

Transformer.transformObj()

A slightly more complicated, more powerful version of the previous function. It takes an object with input values and output values to allow scales with multiple stages:

const myTransform = Transformer.transformObj({
  400: 1,
  600: 0,
  1000: 0,
  1200: 1
});

myTransform(0); // 1
myTransform(400); // 1
myTransform(500); // 0.5
myTransform(600); // 0

If the y position of the page were passed in and the result used as an opacity, the above code would make the element start visible, then fade it out between 400px and 600px, then fade it back in again between 1000px and 1200px.

This function also takes two more arguments, loopBy and easing.

loopBy

This argument allows you to specify a point after which the animation should repeat itself. For example, if you want to animate the scale from 0.5 to 1 and back again over time, you could do this:

const scaleTransform = Transformer.transformObj({
  0: 0.5,
  30: 1
}, 60);

scaleTransform(i); // Animates from 0.5 to 1 and back repeatedly as i increases
easing

Transformer.transformObj() has basic support for easings. You can either pass in the name of the easing—you can find the built in ones here—or you can pass in you own easing function.

Unlike standard easing functions, they're given one argument and return one number: both percentages (number between 0 and 1).

For example, a quadratic ease in (easeInQuad) looks like this:

const easeInQuad = (x) => x * x;

Pull requests adding other easings very welcome!

visible & setVisible

Transform objects also accept another property, visible. This should be two numbers where when the y position of the page is outside of these values, the element will not be animated. This helps ensure that if you have a lot of elements on the page, the ones that aren't being displayed aren't wasting resources.

const transforms = new Transformer([
  {
    el: document.querySelector('.my-element'),
    visible: [0, 600],
    styles: [
      ['opacity', function (y) {
        return Transformer.transform([500, 600], [1, 0], y);
      }]
    ]
  }
]);

You can also set the property on everything at once using the setVisible() method:

transforms.setVisible([500, 600]);

Pausing and cancelling an animation

It's possible to stop and start the animation using the stop() and start() methods. Stopping the animation will leave the currently animated properties exactly where they are, and stop i from increasing. Starting it again will resume things from where they were when the animation was stopped.

The following will pause the animation for a second:

transforms.stop();

setTimeout(function () {
  transforms.start();
}, 1000);

There's also a reset() method for when you want to stop an animation and restore the transform and element displays to what they were to start off with (styles and attributes will be left as they were). This is useful if you need to reinitialise the animate when the window is resized:

let transforms;

function init() {
  if (transforms) {
    transforms.reset();
  }
  
  transforms = new Transformer([ ... ]);
}

init();
window.addEventListener('resize', debounce(init));

Changing the element to get the scroll position from

By default, transform-when gets the scroll positions from the window, but this isn't always what you want. To change it, just change the scrollElement property to contain a selector for the element you want to get the scroll position of instead:

transforms.scrollElement = '.my-scroll-element';

Configuring how i increases

The default behaviour of i is to increase by 1 on each frame, up to a maximum of 60 times. On most monitors, this just means that i will be the number of the frame, because most monitors don't go above 60fps. On monitors that are capable of a higher fps such as gaming monitors, however, this means that i won't necessarily be a whole number. If the monitor runs at 120fps, i will increase by about 0.5 120 times a second.

This is configurable! There are three options, belowOptimal and aboveOptimal, each of which can be set to "count" (to increase by 1 each frame) or "time" (to increase so that i increases by 60 per second). By default, belowOptimal is set to "count" and aboveOptimal is set to "time".

You may want to change belowOptimal to "time". You probably don't want to change aboveOptimal to "count".

transforms.iIncrease.belowOptimal = 'time';

You can also configure the optimal FPS. By default it's 60, but you can change it:

transforms.iIncrease.optimalFps = 120;

Happy animating :)

🎉

License

Released under the MIT license.

More Repositories

1

futurelink

🔮 Predicts when a link is about to be clicked
JavaScript
147
star
2

tryregex

📖 An interactive regex tutorial
HTML
103
star
3

vue-test

🏁 DEPRECATED: Component testing for Vue.js
JavaScript
90
star
4

regex-tuesday

📆 The repo for the regex tuesday challenges
JavaScript
87
star
5

gulp-w3cjs

🚦 w3cjs wrapper for gulp to validate your HTML
JavaScript
56
star
6

rev-del

💇 Delete old fingerprinted files
JavaScript
35
star
7

vue-futurelink

Preload links about to be clicked with futurelink.
Vue
34
star
8

path.js

A library for morphing between SVG paths
JavaScript
20
star
9

animation-talk-demo

Vue
19
star
10

if_changed

📥 Run commands on git pull
Shell
17
star
11

sketchbook

🎨
TypeScript
15
star
12

when-scroll

📜 A library for firing callbacks when the user scrolls
JavaScript
14
star
13

find-node-modules

⬆️ Return an array of all parent node_modules directories
JavaScript
13
star
14

timerizerJS

⏰ A library for working with relative times in JS.
JavaScript
11
star
15

covid-visualisations

Companion repository for my "Building an animated COVID-19 tracker using Vue.js" article.
Vue
10
star
16

learning-from-jquery

📘 The code repository for Learning from jQuery
JavaScript
10
star
17

dotfiles

ℹ️ My dotfiles
Perl
8
star
18

phpBB-Mobile

😴 An old, unmaintained modification for phpBB to make it mobile friendly
PHP
8
star
19

macr.ae

😄 My website!
Vue
7
star
20

whtevr

💁 The lazy loading library that just doesn't care.
JavaScript
6
star
21

phpbb-ideas_old

An idea center for phpBB (based on WordPress idea center).
PHP
6
star
22

Hex

An IRC bot for the x10hosting IRC, written in Node.js
JavaScript
6
star
23

PHPCheck

A testing library for PHP loosely based on Haskell's QuickCheck library.
PHP
6
star
24

gulp-contains

✋ Throws an error or calls a callback if a given string is found in a file.
JavaScript
5
star
25

phpbbmobile-style

Style to be used with phpBB Mobile
JavaScript
5
star
26

obj-to-attrs

➡️ Turns a JavaScript object into a string containing HTML attributes.
JavaScript
4
star
27

sudoku-creator

An algorithm to create a solvable Sudoku puzzle.
JavaScript
4
star
28

lynx.io

The source for the new lynx.io website
JavaScript
4
star
29

MatrixJS

A matrix library for JavaScript and Node.js.
JavaScript
3
star
30

perf-test

Vue
3
star
31

baconjs

A dead JavaScript framework I wrote ages ago.
JavaScript
3
star
32

url-shortener

A simple URL shortener library
PHP
3
star
33

lynx-framework

A framework built by the admin of lynxphp, Callum Macrae
PHP
3
star
34

afk

💤 JavaScript library to handle user going inactive
JavaScript
3
star
35

touch-screen

📱 Detects touch screens, in theory.
JavaScript
3
star
36

after-delay

⌛ Basically setTimeout on steroids
JavaScript
3
star
37

js-irc

An IRC client written in Node.js and JavaScript
JavaScript
3
star
38

climb-trump-tower

Climb Trump Tower
JavaScript
3
star
39

phpbb-vagrant

phpBB… on Vagrant
2
star
40

charity-massage

💆 A massage scheduling app to raising money for charity.
JavaScript
2
star
41

vinyl-fs-fake

😏 Extends vinyl-fs to be testable
JavaScript
2
star
42

tresjs-block-game

A Minecraft-like environment powered by TresJS
TypeScript
2
star
43

find-node-modules-down

⬇️ Find child node_modules directories
JavaScript
2
star
44

art

🎨 experiments!
JavaScript
2
star
45

c-algos

A collection of algos I have implemented in C.
C
2
star
46

codepen-game

🎮 A game you can play in codepen!
TypeScript
2
star
47

plist

Parses iTunes library and tells the user how long they have listened to iTunes for
PHP
2
star
48

cypress-select-value-issue

Vue
2
star
49

baconjs-django-template

A Django template language parser for JavaScript
JavaScript
2
star
50

socialSharesBookmarklet

A bookmarklet to show you how many times something has been shared.
JavaScript
2
star
51

news-script

A simple news script written in PHP.
JavaScript
2
star
52

toggle

🔄 A tiny toggle library
JavaScript
2
star
53

lang

🌎 An tiny language library
JavaScript
2
star
54

lastfmbot

An IRC bot for last.fm .np commands
JavaScript
2
star
55

js-bot

An IRC bot that executes any JavaScript that is sent to it and outputs the result back into the channel.
JavaScript
2
star
56

webpack-inline-loader-error

See stackoverflow for detailed description of problem!
JavaScript
2
star
57

intentiothemes.com

The source code for Intentio website
JavaScript
2
star
58

andro

A dark theme for an evil amount of editors.
2
star
59

vue-photo-album

TypeScript
2
star
60

js-labs

A set of simple experimental apps written in JavaScript.
JavaScript
2
star
61

nums

1️⃣ 2️⃣ 3️⃣ nums(3, 6) => [3, 4, 5, 6]
JavaScript
2
star
62

colour.js

A colour manipulation library for JavaScript
JavaScript
2
star
63

radial-elevation

JavaScript
1
star
64

css-test

An experiment to see how far CSS3 can be pushed
PHP
1
star
65

jacob.reviews

😈 Reviews of jacob
JavaScript
1
star
66

960

Very simple 960 grid system library
1
star
67

gulp-cssnano

Minify CSS with cssnano.
JavaScript
1
star
68

sketchbook-2

TypeScript
1
star
69

jscheck_prettify

This project takes JSCheck and handles reports, adding a nice interface on the reports.
JavaScript
1
star
70

notunhelpful

An IRC bot to op people and stuff
JavaScript
1
star
71

Akismet

An Akismet library written in object orientated PHP
PHP
1
star
72

parsesearch

JavaScript
1
star
73

typescript-bug

JavaScript
1
star
74

callum.reviews

😰 Reviews of Callum.
JavaScript
1
star
75

about-me

😎 ☺️
JavaScript
1
star
76

Graph

A simple graphing library for JS
JavaScript
1
star
77

numbers.py

Converts numbers in word form (such as "two hundred and thirty-seven") to proper integers (237).
Python
1
star
78

where-was-callum

🔎🌍 a lil experiment
JavaScript
1
star
79

Zirck

JavaScript
1
star
80

webrtc-net

🕸️ WIP WIP. WebRTC powered network using a gossip algorithm to propagate messages.
JavaScript
1
star
81

JSColor

🔴 🔵 JSColor + Bower
JavaScript
1
star
82

Count-thing

My first C project: Attempts to prove that the fact that there are 500,000,000 "1"s in the first 500,000,000 positive integers isn't remarkable.
C
1
star