Browse Source

Should be the final redesign

tags/3.0.0
Saivan 6 years ago
parent
commit
71df781bd0
7 changed files with 800 additions and 436 deletions
  1. 1
    0
      dirty.html
  2. 126
    0
      src/controller.js
  3. 1
    1
      src/default.js
  4. 10
    9
      src/morph.js
  5. 553
    90
      src/runner.js
  6. 26
    336
      src/timeline.js
  7. 83
    0
      useCases.md

+ 1
- 0
dirty.html View File

@@ -8,6 +8,7 @@
<script type="text/javascript" src="src/morph.js"></script>
<script type="text/javascript" src="src/runner.js"></script>
<script type="text/javascript" src="src/timeline.js"></script>
<script type="text/javascript" src="src/controller.js"></script>
</head>
<body style="background-color: #c7c7ff">


+ 126
- 0
src/controller.js View File

@@ -0,0 +1,126 @@

// c = {
// finished: Whether or not we are finished
// }

/***
Base Class
==========
The base stepper class that will be
***/

SVG.Stepper = SVG.Invent ({

create: function (fn) {

},

extend: {

step: function (current, target, dt, c) {

},

isComplete: function (dt, c) {

},
},
})

/***
Easing Functions
================
***/

SVG.Ease = SVG.Invent ({

inherit: SVG.Stepper,

create: function (fn) {
SVG.Stepper.call(this, fn)
},

extend: {

step: function (current, target, dt, c) {

},

isComplete: function (dt, c) {

},
},
})

SVG.easing = {
'-': function (pos) { return pos },
'<>': function (pos) { return -Math.cos(pos * Math.PI) / 2 + 0.5 },
'>': function (pos) { return Math.sin(pos * Math.PI / 2) },
'<': function (pos) { return -Math.cos(pos * Math.PI / 2) + 1 },
bezier: function (t0, x0, t1, x1) {
return function (t) {
// TODO: FINISH
}
},
}


/***
Controller Types
================
***/

SVG.Controller = SVG.Invent ({

inherit: SVG.Stepper,

create: function (fn) {
SVG.Stepper.call(this, fn)
},

extend: {

step: function (current, target, dt, c) {

},

isComplete: function (dt, c) {

},
},
})

SVG.Spring = function spring(duration, overshoot) {

// Apply the default parameters
duration = duration || 500
overshoot = overshoot || 15

// Calculate the PID natural response
var eps = 1e-10
var os = overshoot / 100 + eps
var zeta = -Math.log(os) / Math.sqrt(Math.PI ** 2 + Math.log(os) ** 2)
var wn = 4 / (zeta * duration / 1000)

// Calculate the Spring values
var D = 2 * zeta * wn
var K = wn * wn

// Return the acceleration required
return SVG.Controller(
function (current, target, dt, c) {

if(dt == Infinity) return target

// Get the parameters
var error = target - current
var lastError = c.error || 0
var velocity = (error - c.error) / dt

// Apply the control to get the new position and store it
var control = -D * velocity - K * error
var newPosition = current + control
c.error = error
return newPosition
})
}

+ 1
- 1
src/default.js View File

@@ -5,7 +5,7 @@ SVG.defaults = {

// Default animation values
timeline: {
duration: 600,
duration: 400,
ease: '>',
delay: 0,
},

+ 10
- 9
src/morph.js View File

@@ -1,8 +1,8 @@

SVG.Morphable = SVG.invent({
create: function (controller) {
// FIXME: the default controller does not know about easing
this._controller = controller || function (from, to, pos) {
create: function (stepper) {
// FIXME: the default stepper does not know about easing
this._stepper = stepper || function (from, to, pos) {
if(typeof from !== 'number') {
return pos < 1 ? from : to
}
@@ -44,7 +44,7 @@ SVG.Morphable = SVG.invent({

// non standard morphing
/*if(type instanceof SVG.Morphable.NonMorphable) {
this._controller = function (from, to, pos) {
this._stepper = function (from, to, pos) {
return pos < 1 ? from : to
}
}*/
@@ -90,21 +90,22 @@ SVG.Morphable = SVG.invent({
return result
},

controller: function (controller) {
this._controller = controller
stepper: function (stepper) {
this._stepper = stepper
},


at: function (pos) {
var _this = this

// for(var i = 0, len = this._from.length; i < len; ++i) {
// arr.push(this.controller(this._from[i], this._to[i]))
// arr.push(this.stepper(this._from[i], this._to[i]))
// }

return this._type.prototype.fromArray(
this.modifier(
this._from.map(function (i, index) {
return _this._controller(i, _this._to[index], pos, _this.context[i])
return _this._stepper.step(i, _this._to[index], pos, _this.context[i])
})
)
)
@@ -253,7 +254,7 @@ SVG.extend(SVG.MorphableTypes, {
/// el.animate()
/// .fill('#00f')
/// ---->> timeline.fill
/// val = new Morphable().to('#0ff').controller(controller)
/// val = new Morphable().to('#0ff').stepper(stepper)
/// func init() {
/// val.from(el.fill())
/// }

+ 553
- 90
src/runner.js View File

@@ -1,126 +1,589 @@

// TODO: Remove the timeline from the runner
// TODO: Pass in the starting time as a parameter
function Runner (start, duration) {

// We store a reference to the function to run and the timeline to use
this.transforms = []
this.functions = []
this.done = false

// We copy the current values from the timeline because they can change
this._duration = duration || null // if null, declarative methods used
this._timeline = timeline
this._last = 0

// Store the state of the runner
this._time = -start || 0
this._enabled = true
this.tags = {}
SVG.easing = {
'-': function (pos) { return pos },
'<>': function (pos) { return -Math.cos(pos * Math.PI) / 2 + 0.5 },
'>': function (pos) { return Math.sin(pos * Math.PI / 2) },
'<': function (pos) { return -Math.cos(pos * Math.PI / 2) + 1 }
}

// The runner gets the time from the timeline
Runner.prototype = {
SVG.Runner = SVG.Invent({

add: function (initFn, runFn, alwaysInitialise) {
this.functions.push({
alwaysInitialise: alwaysInitialise || false,
initialiser: initFn || SVG.void,
runner: runFn || SVG.void,
finished: false,
})
return this
parent: SVG.Element,

create: function (options) {

// Declare all of the variables
this._element = null
this._functions = []
this.done = false

// Work out the stepper and the duration
this._stepper = typeof options === 'function' && (options)
this._duration = typeof options === 'number' && options

// We copy the current values from the timeline because they can change
this._ease = SVG.defaults.timeline.ease
this._morphers = {}

// Store the state of the runner
this._enabled = true
this._time = 0
this._last = 0
this.tags = {}
},

construct: {

animate: function (duration, delay, when) {

// Initialise the default parameters
var times = 0
var swing = false
var waits = []

// If we have an object, unpack the values
if (typeof duration == 'object') {
delay = duration.delay || 0
when = duration.when || 'now'
duration = duration.duration || 1000
swing = duration.swing || false
times = duration.times || 0
waits = duration.waits || []
}

// Construct a new runner and setup its looping behaviour
var runner = new SVG.Runner(duration)
.loop(times, swing, waits)
.element(this)

// Attach this animation to a timeline
this.timeline().schedule(runner, delay, when)
return runner
},

loop: function (duration, times, swing) {

// If we have an object, unpack the values
if (typeof duration == 'object') {
duration.times = duration.times || Infinity
} else {
duration = {
duration: duration,
times: times || Infinity,
swing: swing
}
}
return this.animate(duration)
},

delay: function (by, when) {
return this.animate(0, by, when)
},
},

extend: {

/*
Runner Definitions
==================
These methods help us define the runtime behaviour of the Runner or they
help us make new runners from the current runner
*/

element: function (element) {
this._element = element
return this
},

animate: function () {
if(this._element) {
return this._element.animate.apply(this._element, arguments)
}
// TODO: throw an error if there is no element
},

loop: function () {
if(this._element) {
return this._element.loop.apply(this._element, arguments)
}
// TODO: throw an error
},

delay: function () {
if(this._element) {
return this._element.delay.apply(this._element, arguments)
}
// TODO: throw an error
},

/*
Basic Functionality
===================
These methods allow us to attach basic functions to the runner directly
*/

queue: function (initFn, runFn, alwaysInitialise) {
this._functions.push({
alwaysInitialise: alwaysInitialise || false,
initialiser: initFn || SVG.void,
runner: runFn || SVG.void,
finished: false,
})
return this
},

during: function (runFn) {
return this.queue(null, runFn, false)
},

/*
Runner animation methods
========================
Controls how the animation plays
*/

time: function (time) {
let dt = time - this._time
this.step(dt)
return this
},

step: function (dt) {

// If there is no duration, we are in declarative mode and dt has to be
// positive always, so if its negative, we ignore it.
if ( this._stepper && dt < 0 ) return false

// Increment the time and read out the parameters
var duration = this._duration
var time = this._time
this._time += dt || 16

// Work out if we are in range to run the function
var timeInside = 0 <= time && time <= duration
var position = time / duration
var finished = time >= duration

// If we are on the rising edge, initialise everything, otherwise,
// initialise only what needs to be initialised on the rising edge
var justStarted = this._last <= 0 && time >= 0
var justFinished = this._last <= duration && finished
this._initialise(justStarted)
this._last = time

// If we haven't started yet or we are over the time, just exit
if(!timeInside && !justFinished) return finished

// Run the runner and store the last time it was run
finished = this._run(
duration === null ? dt // No duration, declarative
: finished ? 1 // If completed, provide 1
: position // If running,
) || finished

// Work out if we are finished
return finished
},

finish: function () {
return this.step(Infinity)
},

// Sets the time to the end time and makes the time advance backwards
reverse: function () {

},

// Changes the animation easing function
ease: function (fn) {

},

enable: function (enabled) {
this._enabled = enabled
return this
},

/*
Runner Management
=================
Functions that are used to help index the runner
*/

tag: function (name) {
// Act as a getter to get all of the tags on this object
if (name == null) return Object.keys(this.tags)

// Add all of the tags to the object directly
name = Array.isArray(name) ? name : [name]
for(var i = name.length; i--;) {
this.tags[name[i]] = true
}
return this
},

untag: function (name) {

},

/*
Private Methods
===============
Methods that shouldn't be used externally
*/

// Save a morpher to the morpher list so that we can retarget it later
_saveMorpher: function (method, morpher) {
this._morphers[method] = morpher
},

// Try to set the target for a morpher if the morpher exists, otherwise
// do nothing and return false
_tryRetarget: function (method, target) {
return this._morphers[method] && this._morphers[method].to(target)
},

// Run each initialise function in the runner if required
_initialise: function (all) {
for (var i = 0, len = this._functions.length; i < len ; ++i) {

// Get the current initialiser
var current = this._functions[i]

// Determine whether we need to initialise
var always = current.alwaysInitialise
var running = !current.finished
if ((always || all) && running) {
current.initialiser.call(this._element)
}
}
},

// Run each run function for the position given
_run: function (position) {

// Run all of the _functions directly
var allfinished = false
for (var i = 0, len = this._functions.length; i < len ; ++i) {

// Get the current function to run
var current = this._functions[i]

// Run the function if its not finished, we keep track of the finished
// flag for the sake of declarative _functions
current.finished = current.finished
|| (current.runner.call(this._element, position) === true)

allfinished = allfinished && current.finished
}

// We report when all of the constructors are finished
return allfinished
},
},
})

tag: function (name) {
if (name == null) return Object.keys(this.tags)
// Extend the attribute methods separately to avoid cluttering the main
// Timeline class above
SVG.extend(SVG.Runner, {

name = Array.isArray(name) ? name : [name]

for(var i = name.length; i--;) {
this.tags[name[i]] = true
attr: function (a, v) {
return this.styleAttr('attr', a, v)
},

// Add animatable styles
css: function (s, v) {
return this.styleAttr('css', s, v)
},

styleAttr (type, name, val) {
// apply attributes individually
if (typeof name === 'object') {
for (var key in val) {
this.styleAttr(type, key, val[key])
}
}

var morpher = new Morphable(this._stepper).to(val)

this.queue(
function () {
morpher = morpher.from(this[type](name))
},
function () {
this[type](name, morpher.at(pos))
return morpher.isComplete()
}
)

return this
},

enable: function (activated) {
this._enabled = activated
zoom: function (level, point) {
var morpher = new Morphable(this._stepper).to(new SVG.Number(level))

this.queue(function() {
morpher = morpher.from(this.zoom())
}, function (pos) {
this.zoom(morpher.at(pos), point)
return morpher.isComplete()
})

return this
},

/**
** absolute transformations
**/

// M v -----|-----(D M v = I v)------|-----> T v
//
// 1. define the final state (T) and decompose it (once) t = [tx, ty, the, lam, sy, sx]
// 2. on every frame: pull the current state of all previous transforms (M - m can change)
// and then write this as m = [tx0, ty0, the0, lam0, sy0, sx0]
// 3. Find the interpolated matrix I(pos) = m + pos * (t - m)
// - Note I(0) = M
// - Note I(1) = T
// 4. Now you get the delta matrix as a result: D = I * inv(M)

transform: function (transforms, relative, affine) {
affine = transforms.affine || affine || !!transform.a
relative = transforms.relative || relative || false

var morpher

/**
The default of relative is false
affine defaults to true if transformations are used and to false when a matrix is given

We end up with 4 possibilities:
false, false: absolute direct matrix morph with SVG.Matrix
true, false: relative direct matrix morph with SVG.Marix or relative whatever was passed transformation with ObjectBag

false, true: absolute affine transformation with SVG.TransformBag
true, true: relative whatever was passed transformation with ObjectBag
**/


// if we have a relative transformation and its not a matrix
// we morph all parameters directly with the ObjectBag
// the following cases are covered here:
// - true, false with ObjectBag
// - true, true with ObjectBag
if(relative && transforms.a == null) {
morpher = SVG.Morphable.ObjectBag(formatTransforms({}))
.to(formatTransforms(transforms))
.stepper(this._stepper)

return this.queue(function() {}, function (pos) {
this.pushRightTransform(new Matrix(morpher.at(pos)))
})
}


// what is left is affine morphing for SVG.Matrix and absolute transformations with TransformBag
// also non affine direct and relative morhing with SVG.Matrix
// the following cases are covered here:
// - false, true with SVG.Matrix
// - false, true with SVG.TransformBag
// - true, false with SVG.Matrix
// - false, false with SVG.Matrix

// 1. define the final state (T) and decompose it (once) t = [tx, ty, the, lam, sy, sx]
var morpher = (transforms.a && !affine)
? new SVG.Matrix().to(transforms)
: new SVG.Morphable.TransformBag().to(transforms)

morpher.stepper(this._stepper)

// create identity Matrix for relative not affine Matrix transformation
morpher.from()

this.queue(function() {}, function (pos) {

// 2. on every frame: pull the current state of all previous transforms (M - m can change)
var curr = this.currentTransform()
if(!relative) morpher.from(curr)

// 3. Find the interpolated matrix I(pos) = m + pos * (t - m)
// - Note I(0) = M
// - Note I(1) = T
var matrix = morpher.at(pos)

if(!relative) {
// 4. Now you get the delta matrix as a result: D = I * inv(M)
var delta = matrix.multiply(curr.inverse())
this.pushLeftTransform(delta)
} else {
this.pushRightTransform(matrix)
}

return morpher.isComplete()
})

return this
},

time: function (time) {
let dt = time - this._time
this.step(dt)
return this
// Animatable x-axis
x: function (x, relative) {
return this._queueNumber('x', x)
},

step: function (dt) {
// Animatable y-axis
y: function (y) {
return this._queueNumber('y', y)
},

// Increment the time and read out the parameters
var duration = this._duration
var time = this._time
this._time += dt || 16
dx: function (x) {
return this._queueNumberDelta('dx', x)
},

// Work out if we are in range to run the function
var timeInside = 0 <= time && time <= duration
var position = time / duration
var finished = time >= duration
dy: function (y) {
return this._queueNumberDelta('dy', y)
},

// If we are on the rising edge, initialise everything, otherwise,
// initialise only what needs to be initialised on the rising edge
var justStarted = this._last <= 0 && time >= 0
var justFinished = this._last <= duration && finished
this._initialise(justStarted)
this._last = time
_queueNumberDelta: function (method, to) {
to = new SVG.Number(to)

// If we haven't started yet or we are over the time, just exit
if(!timeInside && !justFinished) return finished
// Try to change the target if we have this method already registerd
if (this._tryRetargetDelta(method, to)) return this

// Run the runner and store the last time it was run
this._run(
duration === null ? dt // No duration, declarative
: finished ? 1 // If completed, provide 1
: position // If running,
)
// Make a morpher and queue the animation
var morpher = new SVG.Morphable(this._stepper).to(to)
this.queue(function () {
var from = this[method]()
morpher.from(from)
morpher.to(from + x)
}, function (pos) {
this[method](morpher.at(pos))
return morpher.isComplete()
}, this._declarative)

// Work out if we are finished
return finished
// Register the morpher so that if it is changed again, we can retarget it
this._saveMorpher(method, morpher)
return this
},

// Initialise the runner when we are ready
_initialise: function (all) {
for (var i = 0, len = this.functions.length; i < len ; ++i) {
_queueObject: function (method, to) {

// Get the current initialiser
var current = this.functions[i]
// Try to change the target if we have this method already registerd
if (this._tryRetarget(method, to)) return this

// Determine whether we need to initialise
var always = current.alwaysInitialise
var running = !current.finished
if ((always || all) && running) {
current.initialiser()
}
}
// Make a morpher and queue the animation
var morpher = new SVG.Morphable(this._stepper).to(to)
this.queue(function () {
morpher.from(this[method]())
}, function (pos) {
this[method](morpher.at(pos))
return morpher.isComplete()
}, this._declarative)

// Register the morpher so that if it is changed again, we can retarget it
this._saveMorpher(method, morpher)
return this
},

_queueNumber: function (method, value) {
return this._queueObject(method, new SVG.Number(value))
},

// Animatable center x-axis
cx: function (x) {
return this._queueNumber('cx', x)
},

_run: function (position) {
// Animatable center y-axis
cy: function (y) {
return this._queueNumber('cy', x)
},

// Run all of the functions directly
var allfinished = false
for (var i = 0, len = this.functions.length; i < len ; ++i) {
// Add animatable move
move: function (x, y) {
return this.x(x).y(y)
},

// Get the current function to run
var current = this.functions[i]
// Add animatable center
center: function (x, y) {
return this.cx(x).cy(y)
},

// Run the function if its not finished, we keep track of the finished
// flag for the sake of declarative functions
current.finished = current.finished
|| (current.runner(position) === true)
allfinished = allfinished && current.finished
// Add animatable size
size: function (width, height) {
// animate bbox based size for all other elements
var box

if (!width || !height) {
box = this._element.bbox()
}

// We report when all of the constructors are finished
return allfinished
if (!width) {
width = box.width / box.height * height
}

if (!height) {
height = box.height / box.width * width
}

return this
.width(width)
.height(height)
},
}

// Add animatable width
width: function (width) {
return this._queueNumber('width', width)
},

// Add animatable height
height: function (height) {
return this._queueNumber('height', height)
},

// Add animatable plot
plot: function (a, b, c, d) {
// Lines can be plotted with 4 arguments
if (arguments.length === 4) {
return this.plot([a, b, c, d])
}

return this._queueObject('plot', new this._element.morphArray(a))

/*var morpher = this._element.morphArray().to(a)

this.queue(function () {
morpher.from(this._element.array())
}, function (pos) {
this._element.plot(morpher.at(pos))
})

return this*/
},

// Add leading method
leading: function (value) {
return this._queueNumber('leading', value)
},

// Add animatable viewbox
viewbox: function (x, y, width, height) {
return this._queueObject('viewbox', new SVG.Box(x, y, width, height))
},

update: function (o) {
if (typeof o !== 'object') {
return this.update({
offset: arguments[0],
color: arguments[1],
opacity: arguments[2]
})
}

if (o.opacity != null) this.attr('stop-opacity', o.opacity)
if (o.color != null) this.attr('stop-color', o.color)
if (o.offset != null) this.attr('offset', o.offset)


return this
}
})

+ 26
- 336
src/timeline.js View File

@@ -19,7 +19,6 @@ SVG.Timeline = SVG.invent({

// Store the timing variables
this._startTime = 0
this._lastPaused = null
this._duration = 0
this._ease = SVG.defaults.timeline.ease
this._speed = 1.0
@@ -35,7 +34,6 @@ SVG.Timeline = SVG.invent({
this._baseTransform = null
this._nextFrame = null
this._paused = false
this._runner = null
this._runners = []
this._time = 0
},
@@ -128,6 +126,20 @@ SVG.Timeline = SVG.invent({
}
}

schedule () {
// Work out when to start the animation
if ( when == null || when === 'last' || when === 'relative' ) {
// Take the last time and increment

} else if (when === 'absolute' || when === 'start' ) {

} else if (when === '' ) {

} else {
// TODO: Throw error
}
}

play () {

// Now make sure we are not paused and continue the animation
@@ -139,7 +151,6 @@ SVG.Timeline = SVG.invent({
pause () {

//
this._lastPaused = time.now()
this._nextFrame = null
this._paused = true
return this
@@ -233,6 +244,12 @@ SVG.Timeline = SVG.invent({
// this._runners.splice(i--, 1)
}

// TODO: Collapse transformations in transformationBag into one
// transformation directly
//
// Timeline has
// timeline.transformationBag

// Get the next animation frame to keep the simulation going
if (runnersLeft)
this._nextFrame = SVG.Animator.frame(this._step.bind(this))
@@ -254,6 +271,12 @@ SVG.Timeline = SVG.invent({

// These methods will be added to all SVG.Element objects
construct: {

timeline: function () {
this.timeline = (this.timeline || new SVG.Timeline(this))
return this.timeline
},

animate: function(o, delay, now) {

// Get the current timeline or construct a new one
@@ -281,338 +304,5 @@ SVG.Timeline = SVG.invent({
this.timeline._swing = o.swing || false
return this.timeline
}

/*

*/
timeline: function() {
return {
play: ()=> {}
pause: ()=> {}
persist: ()=> {}
seek: ()=> {}
stop: ()=> {}
}
}
}
})


// Extend the attribute methods separately to avoid cluttering the main
// Timeline class above
SVG.extend(SVG.Timeline, {


attr: function (a, v) {
return this.styleAttr('attr', a, v)
},

// Add animatable styles
css: function (s, v) {
return this.styleAttr('css', s, v)
},

styleAttr (type, name, val) {
// apply attributes individually
if (typeof name === 'object') {
for (var key in val) {
this.styleAttr(type, key, val[key])
}
}
2
var morpher = new Morphable(this._controller).to(val)

this.queue(
function () {
morpher = morpher.from(element[type](name))
},
function () {
this.element[type](name, morpher.at(pos))
}
)

return this
},

zoom: function (level, point) {
var morpher = new Morphable(this._controller).to(new SVG.Number(level))
var el = this.target()

this.queue(function() {
morpher = morpher.from(element.zoom())
}, function (pos) {
el.zoom(morpher.at(pos), point)
})

return this
},

/**
** absolute transformations
**/

// M v -----|-----(D M v = I v)------|-----> T v
//
// 1. define the final state (T) and decompose it (once) t = [tx, ty, the, lam, sy, sx]
// 2. on every frame: pull the current state of all previous transforms (M - m can change)
// and then write this as m = [tx0, ty0, the0, lam0, sy0, sx0]
// 3. Find the interpolated matrix I(pos) = m + pos * (t - m)
// - Note I(0) = M
// - Note I(1) = T
// 4. Now you get the delta matrix as a result: D = I * inv(M)

transform: function (transforms, relative, affine) {
affine = transforms.affine || affine || !!transform.a
relative = transforms.relative || relative || false

var morpher

/**
The default of relative is false
affine defaults to true if transformations are used and to false when a matrix is given

We end up with 4 possibilities:
false, false: absolute direct matrix morph with SVG.Matrix
true, false: relative direct matrix morph with SVG.Marix or relative whatever was passed transformation with ObjectBag

false, true: absolute affine transformation with SVG.TransformBag
true, true: relative whatever was passed transformation with ObjectBag
**/


// if we have a relative transformation and its not a matrix
// we morph all parameters directly with the ObjectBag
// the following cases are covered here:
// - true, false with ObjectBag
// - true, true with ObjectBag
if(relative && transforms.a == null) {
morpher = SVG.Morphable.ObjectBag(formatTransforms({}))
.to(formatTransforms(transforms))
.controller(this.controller)

return this.queue(function() {}, function (pos) {
this.pushRightTransform(new Matrix(morpher.at(pos)))
})
}


// what is left is affine morphing for SVG.Matrix and absolute transformations with TransformBag
// also non affine direct and relative morhing with SVG.Matrix
// the following cases are covered here:
// - false, true with SVG.Matrix
// - false, true with SVG.TransformBag
// - true, false with SVG.Matrix
// - false, false with SVG.Matrix

// 1. define the final state (T) and decompose it (once) t = [tx, ty, the, lam, sy, sx]
var morpher = (transforms.a && !affine)
? new SVG.Matrix().to(transforms)
: new SVG.Morphable.TransformBag().to(transforms)

morpher.controller(this.controller)

// create identity Matrix for relative not affine Matrix transformation
morpher.from()

this.queue(function() {}, function (pos) {

// 2. on every frame: pull the current state of all previous transforms (M - m can change)
var curr = this.currentTransform()
if(!relative) morpher.from(curr)

// 3. Find the interpolated matrix I(pos) = m + pos * (t - m)
// - Note I(0) = M
// - Note I(1) = T
var matrix = morpher.at(pos)

if(!relative) {
// 4. Now you get the delta matrix as a result: D = I * inv(M)
var delta = matrix.multiply(curr.inverse())
this.pushLeftTransform(delta)
} else {
this.pushRightTransform(matrix)
}
})

return this
},

// Animatable x-axis
x: function (x, relative) {
var morpher = new SVG.Morphable(this._controller)
.to(new SVG.Number(x))

/*
if (this.target() instanceof SVG.G) {
this.transform({x: x}, relative)
return this
}
*/

this.queue(function () {
var from = this._element.x()
morpher.from(from)
if(relative) morpher.to(from + x)
}, function (pos) {
this._element.x(morpher.at(pos))
}, function (newTarget) {
morpher.to(newTarget)
})

return this
},

// Animatable y-axis
y: function (y, relative) {
var morpher = new SVG.Morphable(this._controller)
.to(new SVG.Number(y))

/*
if (this.target() instanceof SVG.G) {
this.transform({y: y}, relative)
return this
}
*/

this.queue(function () {
var from = this._element.y()
morpher.from(from)
if(relative) morpher.to(from + y)
}, function (pos) {
this._element.y(morpher.at(pos))
})

return this
},

_queueObject: function (method, to) {
var morpher = new SVG.Morphable(this._controller).to(to)

this.queue(function () {
morpher.from(this._element[method]())
}, function (pos) {
this._element[method](morpher.at(pos))
})

return this
},

_queueNumber: function (method, value) {
return this._queueObject(method, new SVG.Number(value))
},

// Animatable center x-axis
cx: function (x) {
return this._queueNumber('cx', x)
},

// Animatable center y-axis
cy: function (y) {
return this._queueNumber('cy', x)
},

// Add animatable move
move: function (x, y) {
return this.x(x).y(y)
},

// Add animatable center
center: function (x, y) {
return this.cx(x).cy(y)
},

// Add animatable size
size: function (width, height) {
// animate bbox based size for all other elements
var box

if (!width || !height) {
box = this._element().bbox()
}

if (!width) {
width = box.width / box.height * height
}

if (!height) {
height = box.height / box.width * width
}

return this
.width(width)
.height(height)
},

// Add animatable width
width: function (width) {
return this._queueNumber('width', width)
},

// Add animatable height
height: function (height) {
return this._queueNumber('height', height)
},

// Add animatable plot
plot: function (a, b, c, d) {
// Lines can be plotted with 4 arguments
if (arguments.length === 4) {
return this.plot([a, b, c, d])
}

return this._queueObject('plot', new this._element.morphArray(a))

/*var morpher = this._element.morphArray().to(a)

this.queue(function () {
morpher.from(this._element.array())
}, function (pos) {
this._element.plot(morpher.at(pos))
})

return this*/
},

// Add leading method
leading: function (value) {
return this._element.leading
? this._queueNumber('leading', value)
: this
},

// Add animatable viewbox
viewbox: function (x, y, width, height) {
if (this._element instanceof SVG.Container) {
this._queueObject('viewbox', new SVG.Box(x, y, width, height))

/*var morpher = new SVG.Box().to(x, y, width, height)

this.queue(function () {
morpher.from(this._element.viewbox())
}, function (pos) {
this._element.viewbox(morpher.at(pos))
})

return this*/
}

return this
},
update: function (o) {
if (this._element instanceof SVG.Stop) {
if (typeof o !== 'object') {
return this.update({
offset: arguments[0],
color: arguments[1],
opacity: arguments[2]
})
}

if (o.opacity != null) this.attr('stop-opacity', o.opacity)
if (o.color != null) this.attr('stop-color', o.color)
if (o.offset != null) this.attr('offset', o.offset)
}

return this
}
})

+ 83
- 0
useCases.md View File

@@ -166,3 +166,86 @@ SVG.on(document, 'mousemove', (ev) => {
})

```


## Springy Mouse Chaser

Pretend we gave the user a springy controller that basically springs to a
target in 300ms for example. They might be constantly changing the target with:

```js

el.animate(Spring(500), 200)
.tag('declarative')
.persist()
.move(10, 10)

el.animate('declarative')
.move(300, 200)



SVG.on(document, 'mousemove', function (ev) {

el.animate(springy, 200)
.tag('declarative')
.move(ev.pageX, ev.pageY)

})

```


# Repeated Animations

The user might want to duplicate an animation and have it rerun a few times

```js

// User makes two copies of an animation
let animA = el.animate(300, 300, 'now')...(animation)...
let animB = animA.clone() // Deep copy

// Now let the user attach and reschedule their animations
el.timeline()
.schedule(animA, 500, 'absolute')
.schedule(animB, 2000, 'absolute')

```

Then the user can loop the timeline, by changing its play mode

```js
el.timeline()
.loop(times, swing, waits)
```


# Advanced Animations

The user can create their own runners and then attach it to the timeline
themselves if they like.

```js

// They declare their animation
let rotation = () => new SVG.Runner().rotate(500)

// They attach an element, and schedule the runner
let leftAnimation = rotation().element(leftSquare).reverse()

// They might want to animate another
let rightAnimation = rotation().element(rightSquare)

// They can schedule these two runners to a master element
timelineElement.timeline()
.schedule(leftAnimation, 300, 'absolute')
.schedule(rightAnimation, 500, 'now')
.schedule(rightAnimation, 300, 'end')

// Or they can schedule it to a timeline as well
let timeline = new SVG.Timeline()
.schedule(leftAnimation, 300, 'absolute')
.schedule(rightAnimation, 500, 'now')

```

Loading…
Cancel
Save