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.

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 = circle.animate(600, 0, 'with-last').move(100, 0)

// `first` and `second` start together on their respective timelines.

For precise coordination 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.

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)

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 () {})

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.

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 runners are normally removed from their timeline to release references. Persistence keeps one available for seeking or reversing after it has finished:

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)
})

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()        // apply all end values 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