SVG.js

Animating

SVG.js animation has three parts:

  • An element owns a Timeline, which provides the clock.
  • element.animate() creates and schedules a Runner on that timeline.
  • Calls such as move(), fill(), and transform() add work to the runner.

For most animations you only need animate() and the familiar element methods:

const rect = draw.rect(100, 100)

rect.animate(1000).move(200, 100).fill('#f06')

animate() returns the runner, not the element. Further calls therefore configure the animation rather than changing the element immediately.

animate()

returns SVG.Runner

element.animate(duration, delay, when)
  • duration is the animation duration in milliseconds. The default is 400.
  • delay adds milliseconds before the scheduled start. The default is 0.
  • when controls where the runner is placed on the timeline. The default is after.
rect.animate(2000, 1000, 'now').attr({ fill: '#f03' })

The first argument can also be an options object:

rect.animate({
  duration: 2000,
  delay: 1000,
  when: 'now',
  times: 5,
  swing: true,
  wait: 200
}).fill('#f03')

times, swing, and wait have the same meaning as the arguments to runner.loop().

Scheduling with when

Value Start time
after or last After the end of the most recently scheduled runner. If the timeline is empty, start at its current time.
with-last At the start of the most recently scheduled runner. If the timeline is empty, start at its current time.
now At the timeline's current time, plus delay.
absolute or start At the absolute timeline time given by delay.
relative Move this runner's previous start time by delay when rescheduling it.

All scheduling modes operate on the runner's timeline. Use after for a
sequence, with-last or now for parallel work, and absolute when
coordinating several elements on a shared timeline.

const first = rect.animate(600).move(100, 0)
const second = rect.animate(600, 0, 'with-last').fill('#f06')

// Both runners use `rect`'s timeline, so they start together.

Different elements have separate timelines by default. To coordinate runners
across elements, assign the same timeline to both elements as shown under
Orchestrating animations.

Chaining animations and delays

Call animate() on a runner to schedule another runner with the same element and timeline:

rect
  .animate(500)
  .fill('#f03')
  .animate(500)
  .move(100, 100)

delay() creates a zero-duration runner and is useful between chained animations:

rect
  .animate(500)
  .fill('#f03')
  .delay(200)
  .animate(500)
  .move(100, 100)

The equivalent form puts the delay on the next animation:

rect.animate(500).fill('#f03').animate(500, 200).move(100, 100)

SVG.Runner

A runner describes one animation segment. It stores the target element, the values to animate, easing or controller behavior, looping options, and its position on a timeline.

Runners support animatable versions of common element setters, including attr(), css(), fill(), stroke(), move(), center(), size(), plot(), transform(), rotate(), scale(), translate(), font(), viewbox(), and zoom().

const runner = rect.animate(1000)

runner.move(200, 100)
runner.rotate(90)
runner.fill({ color: '#f06', opacity: 0.5 })

The starting values are read when the runner first executes. This lets a queued animation begin from the state left by an earlier runner.

Animating transforms

Runner.transform() has two interpolation modes and can apply its target
absolutely or relatively:

runner.transform(transforms, relative, affine)

An absolute transform is the default. It animates the element's complete local
transform to the target, replacing the previous transform at the endpoint:

rect.animate(1000).transform({ rotate: 90, scale: 2 })

Pass true as the second argument to compose the animated transform with the
element's transform instead. The familiar transform helpers are relative too:

rect.animate(1000).transform({ translate: [100, 0] }, true)

// Equivalent relative transform helpers
rect.animate(1000).translate(100, 0).rotate(90).scale(2)

For a relative transform, the baseline is read when the runner starts, not when
it is created. A delayed or chained runner therefore composes with the state
left by preceding animation.

Transform objects use affine interpolation by default. SVG.js decomposes the
start and target into translation, scale, shear, rotation, and origin values;
an absolute rotation follows the shortest path, while a relative rotation uses
the requested angle as its delta. A matrix target instead interpolates its six
components directly:

const target = new SVG.Matrix().rotate(90)

rect.animate(1000).transform(target) // matrix components

Direct matrix interpolation can introduce skew or scale between endpoints.
Choose affine interpolation when the motion should look like a rotation or
scale by passing true as the third argument:

rect.animate(1000).transform(target, false, true)

If an affine transform cannot be decomposed, SVG.js falls back to finite matrix
interpolation.

Relative transforms from parallel runners compose in scheduling order. An
absolute transform replaces relative transforms that precede it while their
times overlap, so avoid overlapping absolute transform runners unless that
replacement is intentional. Timeline seeking preserves this composition in
both directions.

Creating a runner directly

Normally element.animate() creates, binds, and schedules the runner. For manual control you can construct one directly:

const runner = new SVG.Runner(1000)
  .element(rect)
  .move(100, 100)

// Manual stepping does not require a timeline.
runner.step(250)

// Automatic playback does.
const timeline = new SVG.Timeline()
runner.schedule(timeline)
timeline.play()

Binding and scheduling

runner.element()              // get the bound element
runner.element(rect)          // bind an element
runner.timeline()             // get the bound timeline
runner.timeline(timeline)     // bind a timeline
runner.schedule(200, 'now')   // use the already-bound timeline
runner.schedule(timeline, 200, 'now')
runner.unschedule()

Calling schedule() without a bound or explicit timeline throws an error.

Playback position

The following methods are getters without an argument and fluent setters with an argument:

runner.time()          // elapsed runner time in milliseconds
runner.time(250)

runner.position()      // current position within a loop, from 0 to 1
runner.position(0.5)

runner.progress()      // progress across all loops and waits, from 0 to 1
runner.progress(0.5)

runner.loops()         // completed loops, including a fractional current loop
runner.loops(2.5)

position() describes the current loop and accounts for swing and reverse direction. progress() describes the runner's complete duration, including every loop and wait period.

Other playback methods are:

runner.duration()       // total duration, including loops and waits
runner.step(16)         // advance manually by 16 ms; negative values go backwards
runner.reset()          // return to time 0
runner.finish()         // apply the finished state
runner.reverse()        // toggle direction
runner.reverse(true)    // backwards
runner.reverse(false)   // forwards
runner.active(false)    // skip this runner while its timeline advances
runner.active(true)

For a controller-based runner, finish() settles its queued actions at their
targets without assigning an infinite runner time.

Looping

runner.loop(times, swing, wait)
  • times is the total number of iterations.
  • swing alternates direction on every iteration.
  • wait is the delay in milliseconds between iterations.
rect.animate(500).move(100, 0).loop(4, true, 100)

Call loop() without arguments, or pass true, to loop forever:

rect.animate(500).move(100, 0).loop(true, true)

Callbacks and events

runner.during(function (position) {
  // Called on every step. `this` is the runner.
})

runner.after(function (event) {
  // Called when the runner reaches its finished state.
})

Runners are event targets. They fire start, step, and finished events, so on() and off() can be used when the convenience methods are not enough:

runner.on('start', function () {})
runner.on('step', function () {})
runner.on('finished', function () {})

For timed runners, start and finished describe forward progress across the
runner's boundaries. start fires when playback moves forward across the start,
and finished fires when it moves forward across the end. Reversing across
either boundary still produces step updates, but does not fire a mirrored
start or finished event. Controller completion is described under
Controllers.

For a non-animating side effect that must also reverse correctly, derive its state from the position supplied to during() (or to a queued run callback):

runner.during(function (position) {
  const state = position === 0 ? 'before'
    : position === 1 ? 'after'
    : 'active'

  rect.attr('data-animation-state', state)
})

queue(initialise, run) is the low-level extension point behind animated methods. The initialiser runs when the runner starts; the run callback executes on every step. Prefer during() unless you need separate setup and step callbacks.

Persistence

Completed timed runners are normally removed from their timeline to release
references. Controller runners persist by default so they can be retargeted.
Persistence keeps a timed runner available for seeking or reversing after it
has finished, or changes that controller default:

runner.persist()       // get the current setting
runner.persist(1000)   // retain for 1000 ms after it ends
runner.persist(true)   // retain indefinitely

Set persistence before scheduling the runner. A runner setting overrides the timeline's default persistence.

Text positioning with amove()

amove(x, y) animates text using its anchor and baseline coordinates. Use move(x, y) when positioning by the visual bounding box instead.

text.animate(500).amove(100, 50)

Easing

The default easing is ease-out (>). Change it with runner.ease():

rect.animate(1000).ease('<>').move(200, 0)

The built-in string values are:

  • <>: ease in and out
  • >: ease out
  • <: ease in
  • -: linear

You can also pass a function, or create one with SVG.easing.bezier() or SVG.easing.steps():

runner.ease(function (position) {
  return position * position
})

runner.ease(SVG.easing.bezier(0.42, 0, 0.58, 1))
runner.ease(SVG.easing.steps(5, 'jump-end'))

For more easing equations, see the svg.easing.js plugin.

Controllers

An easing function maps a known duration to a position. A controller instead computes the next value from the elapsed time and decides when it has converged. SVG.js includes SVG.Spring and SVG.PID.

const spring = new SVG.Spring(500, 10) // settle time, overshoot percentage
rect.animate(spring).move(200, 200)

const pid = new SVG.PID(0.1, 0.01, 0) // proportional, integral, derivative
circle.animate(pid).move(300, 200)

Controller parameters can be read or changed:

spring.duration(700).overshoot(5)
pid.p(0.2).i(0.01).d(0.001).windup(500)

Because a controller determines its own completion time, controller-based runners cannot be reliably placed in duration-based sequences or played in reverse. They are especially useful for interactive animation because an existing target can be changed while the runner is active:

const follow = rect.animate(new SVG.Spring()).move(100, 100)

draw.on('pointermove', function (event) {
  const point = this.point(event.clientX, event.clientY)
  follow.move(point.x, point.y)
})

Calling the same animated setter again retargets an absolute controller
animation from its current state. Absolute transform() animations also update
their origin when retargeted. Relative controller transforms do not retarget;
each call adds another transform action to the runner.

A controller runner fires finished whenever all of its actions converge. If
it is retargeted afterward, it becomes active again and fires finished again
when it reaches the new target. This also means an after() callback can run
more than once on a retargeted controller.

SVG.Timeline

A timeline supplies time to runners and controls them as a group. Every element creates a timeline lazily when element.timeline() or element.animate() is first called.

const timeline = rect.timeline()       // get or create the element's timeline
rect.timeline(sharedTimeline)          // assign another timeline

Playback controls

timeline.play()          // continue from the current time
timeline.pause()         // pause at the current time
timeline.stop()          // seek to 0 and pause
timeline.finish()        // settle all runners at their targets and pause
timeline.active()        // true while a frame is scheduled

Time, seeking, speed, and direction

time() uses an absolute timeline time. seek() moves by a relative amount:

timeline.time()          // get the current time in milliseconds
timeline.time(1000)      // go to absolute time 1000
timeline.seek(250)       // move forward by 250 ms
timeline.seek(-100)      // move backward by 100 ms

Changing speed affects all scheduled runners:

timeline.speed()         // get the speed multiplier
timeline.speed(2)        // twice as fast
timeline.speed(0.5)      // half speed
timeline.speed(-1)       // backwards at normal speed

reverse() toggles the sign of the current speed. Passing a boolean sets the direction explicitly:

rect.animate(3000).move(100, 100)

// Seek to the end before starting backwards from the final state.
rect.timeline().seek(3000).reverse()

timeline.reverse(true)   // backwards
timeline.reverse(false)  // forwards

Scheduling and inspection

timeline.schedule(runner, delay, when)
timeline.unschedule(runner)

The scheduling values are the same as for animate(). Calling schedule() without a runner returns a snapshot of the schedule:

for (const item of timeline.schedule()) {
  console.log(item.start, item.duration, item.end, item.runner)
}

getEndTime() returns the end time of the runner that was scheduled most recently. This is the point used when scheduling the next after runner; it is not necessarily the latest end time among every runner on the timeline.

const nextStart = timeline.getEndTime()

Persistence and cleanup

Timeline persistence is the default used by runners that do not set their own value:

timeline.persist()       // get the current default
timeline.persist(1000)   // retain completed runners for another 1000 ms
timeline.persist(true)   // retain completed runners indefinitely

terminate() stops frame scheduling, removes every runner, resets time, speed, persistence, and direction, and releases the references held by the timeline. Use it when the entire animation group is no longer needed:

timeline.terminate()

Unlike stop(), termination removes the schedule rather than merely rewinding it.

Custom time source

A timeline normally reads performance.now(). Pass a function to the constructor or use source() to provide another millisecond clock:

let clock = 0
const timeline = new SVG.Timeline(() => clock)

timeline.source()             // get the current time-source function
timeline.source(() => clock)  // replace it
timeline.play()

clock += 16

Custom clocks are an advanced feature. The source must be monotonic and return milliseconds.

Orchestrating animations

Assign one timeline to several elements to control them together:

const timeline = new SVG.Timeline()
const rect = draw.rect(100, 100).timeline(timeline)
const circle = draw.circle(100).timeline(timeline)

rect.animate(600, 0, 'absolute').move(300, 100)
circle.animate(400, 200, 'absolute').move(300, 250)

timeline.play()

Here the rectangle starts at timeline time 0; the circle starts at 200. Pausing, seeking, changing speed, or reversing the shared timeline affects both runners.

For relative sequences, keep the timeline shared and use the scheduling modes instead of calculating every start time:

rect.animate(500).move(100, 0)
circle.animate(500, 0, 'with-last').move(100, 0)
rect.animate(300, 0, 'after').fill('#f06')
Fork me on GitHub