// }
// }
-var randPoint = (x = 50, y = 50) => [
- Math.random() * 100 - 50 + x,
- Math.random() * 100 - 50 + y
-]
-
-var poly = SVG('<polygon>').plot([
- randPoint(),
- randPoint(),
- randPoint(),
- randPoint(),
- randPoint()
-]).attr({fill: 'none', stroke: 'black'}).addTo('svg')
-var polyAni = poly.animate(SVG.PID(null, 0))
-
-SVG.on(document, 'click', function (e) {
- polyAni.plot([
- randPoint(e.pageX-50, e.pageY-50),
- randPoint(e.pageX+50, e.pageY-50),
- randPoint(e.pageX+50, e.pageY),
- randPoint(e.pageX+50, e.pageY+50),
- randPoint(e.pageX-50, e.pageY+50)
- ])
-})
-/*
-var mover = SVG('rect').clone().show()
-var anim = mover.animate(SVG.PID()).move(500, 500)
+// var randPoint = (x = 50, y = 50) => [
+// Math.random() * 100 - 50 + x,
+// Math.random() * 100 - 50 + y
+// ]
+//
+// var poly = SVG('<polygon>').plot([
+// randPoint(),
+// randPoint(),
+// randPoint(),
+// randPoint(),
+// randPoint()
+// ]).attr({fill: 'none', stroke: 'black'}).addTo('svg')
+// var polyAni = poly.animate(new SVG.PID(null, 0))
+//
+// SVG.on(document, 'click', function (e) {
+// polyAni.plot([
+// randPoint(e.pageX-50, e.pageY-50),
+// randPoint(e.pageX+50, e.pageY-50),
+// randPoint(e.pageX+50, e.pageY),
+// randPoint(e.pageX+50, e.pageY+50),
+// randPoint(e.pageX-50, e.pageY+50)
+// ])
+// })
+
+var mover = SVG('<ellipse>').size(50, 50).center(100, 100).addTo('svg')
+var anim = mover.animate(SVG.PID(null, null, null, false)).move(500, 500)
SVG.on(document, 'mousemove', function (e) {
//mover.animate(SVG.PID()).move(e.pageX, e.pageY)
- anim.move(e.pageX, e.pageY)
-})*/
+ var p = mover.point(e.pageX, e.pageY)
+ anim.center(p.x, p.y)
+})
</script>
+function reactToDrag(element, onDrag, beforeDrag) {
-function reactToDrag (element, onDrag, beforeDrag) {
+ let xStart, yStart
+ let startDrag = event => {
- let xStart, yStart
+ // Avoid the default events
+ event.preventDefault()
- let startDrag = event=> {
+ // Store the position where the drag started
+ xStart = event.pageX
+ yStart = event.pageY
- // Avoid the default events
- event.preventDefault()
-
- // Store the position where the drag started
- xStart = event.pageX
- yStart = event.pageY
-
- // Fire the start drag event
- if (beforeDrag) {
- var {x, y} = parent.point(event.pageX, event.pageY)
- beforeDrag(event, x, y)
- }
-
- // Register events to react to dragging
- SVG.on(window, 'mousemove.drag', reactDrag)
- SVG.on(window, 'touchmove.drag', reactDrag)
-
- // Register the events required to finish dragging
- SVG.on(window, 'mouseup.drag', stopDrag)
- SVG.on(window, 'touchend.drag', stopDrag)
+ // Fire the start drag event
+ if (beforeDrag) {
+ var { x, y } = parent.point(event.pageX, event.pageY)
+ beforeDrag(event, x, y)
}
- let reactDrag = event=> {
+ // Register events to react to dragging and drag ends
+ SVG.on(window, ['mousemove.drag', 'touchmove.drag'], reactDrag)
+ SVG.on(window, ['mouseup.drag', 'touchend.drag'], stopDrag)
+ }
- // Convert screen coordinates to svg coordinates and use them
- var {x, y} = parent.point(event.pageX, event.pageY)
- if (onDrag)
- onDrag(event, x, y)
- }
+ let reactDrag = event => {
- let stopDrag = event=> {
- SVG.off(window, 'mousemove.drag')
- SVG.off(window, 'touchmove.drag')
- SVG.off(window, 'mouseup.drag')
- SVG.off(window, 'touchend.drag')
- }
+ // Convert screen coordinates to svg coordinates and use them
+ var { x, y } = parent.point(event.pageX, event.pageY)
+ if (onDrag)
+ onDrag(event, x, y)
+ }
+
+ let stopDrag = event => {
+ SVG.off(window, ['mousemove.drag', 'touchmove.drag'])
+ SVG.off(window, ['mouseup.drag', 'touchend.drag'])
+ }
- // Bind the drag tracker to this element directly
- let parent = element.doc()
- let point = new SVG.Point()
- element.mousedown(startDrag).touchstart(startDrag)
+ // Bind the drag tracker to this element directly
+ let parent = element.doc()
+ let point = new SVG.Point()
+ element.mousedown(startDrag).touchstart(startDrag)
}
SVG.extend(SVG.Element, {
let sx, sy
- reactToDrag(this, (e, x, y)=> {
+ reactToDrag(this, (e, x, y) => {
this.transform({
origin: [sx, sy],
after(this, x, y)
}
- }, (e, x, y)=> {
+ }, (e, x, y) => {
var toAbsolute = new SVG.Matrix(this).inverse()
var p = new SVG.Point(x, y).transform(toAbsolute)
sx = p.x
sy = p.y
-
})
-
return this
},
})
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) {
+function makeSetterGetter (k) {
+ return function (v) {
+ if (v == null) return this[v]
+ this[k] = v
+ return this
+ }
+}
- },
- },
+SVG.Stepper = SVG.invent ({
+ create: function () {},
})
/***
return from + (to - from) * this.ease(pos)
},
- isComplete: function (dt, c) {
+ done: function (dt, c) {
return false
},
},
return this.stepper(current, target, dt, c)
},
- isComplete: function (dt, c) {
- return false
- var result = false
- for(var i = c.length; i--;) {
- result = result || (Math.abs(c[i].error) < 0.01)
- }
- return result
+ done: function (c) {
+ return c.done
},
},
})
})
}
+SVG.PID = SVG.invent ({
+ inherit: SVG.Controller,
+
+ create: function (p, i, d, windup) {
+ if(!(this instanceof SVG.PID))
+ return new SVG.PID(p, i, d, windup)
+ SVG.Controller.call(this)
+
+ p = p == null ? 0.1 : p
+ i = i == null ? 0.01 : i
+ d = d == null ? 0 : d
+ windup = windup == null ? 1000 : windup
+ this.p(p).i(i).d(d).windup(windup)
+ },
+
+ extend: {
+ step: function (current, target, dt, c) {
+
+ c.done = dt == Infinity
+
+ if(dt == Infinity) return target
+ if(dt == 0) return current
+
+ var p = target - current
+ var i = (c.integral || 0) + p * dt
+ var d = (p - (c.error || 0)) / dt
+ var windup = this.windup
+
+ // antiwindup
+ if(windup !== false)
+ i = Math.max(-windup, Math.min(i, windup))
+
+ c.error = p
+ c.integral = i
+
+ c.done = Math.abs(p) < 0.001
+
+ return current + (this.P * p + this.I * i + this.D * d)
+ },
+
+ windup: makeSetterGetter('windup'),
+ p: makeSetterGetter('P'),
+ i: makeSetterGetter('I'),
+ d: makeSetterGetter('D'),
+ }
+})
+/*
SVG.PID = function (P, I, D, antiwindup) {
P = P == null ? 0.1 : P
I = I == null ? 0.01 : I
function (current, target, dt, c) {
if(dt == Infinity) return target
+ if(dt == 0) return current
var p = target - current
var i = (c.integral || 0) + p * dt
return current + (P * p + I * i + D * d)
})
-}
+}*/
// setter
this._type = type
-
- // non standard morphing
- /*if(type instanceof SVG.Morphable.NonMorphable) {
- this._stepper = function (from, to, pos) {
- return pos < 1 ? from : to
- }
- }*/
-
return this
},
this._stepper = stepper
},
- // FIXME: we can call this._stepper.isComplete directly
- // no need for this wrapper here
- isComplete: function () {
- return this._stepper && this._stepper.isComplete(null, this._context)
+ done: function () {
+ var complete = this._context
+ .map(this._stepper.done)
+ .reduce(function (last, curr) {
+ return last && curr
+ }, true)
+ return complete
},
at: function (pos) {
return this._type.prototype.fromArray(
this.modifier(
this._from.map(function (i, index) {
- return _this._stepper.step(i, _this._to[index], pos, _this._context[index])
+ return _this._stepper.step(i, _this._to[index], pos, _this._context[index], _this._context)
})
)
)
create: function (options) {
// ensure a default value
- options = options || SVG.defaults.timeline.duration
+ options = options == null
+ ? SVG.defaults.timeline.duration
+ : options
// ensure that we get a controller
options = typeof options === 'function'
- ? new SVG.Controller(options) :
- options
+ ? new SVG.Controller(options)
+ : options
// Declare all of the variables
this._element = null
- this._functions = []
+ this._queue = []
this.done = false
// Work out the stepper and the duration
this._stepper = this._isDeclarative ? options : new SVG.Ease()
// We copy the current values from the timeline because they can change
- this._morphers = {}
+ this._history = {}
// Store the state of the runner
this.enabled = true
*/
queue: function (initFn, runFn, alwaysInitialise) {
- this._functions.push({
+ this._queue.push({
alwaysInitialise: alwaysInitialise || false,
initialiser: initFn || SVG.void,
runner: runFn || SVG.void,
finished: false,
})
+ this.timeline()._continue()
+ this._element.timeline()._continue()
return this
},
step: function (dt) {
- // FIXME: It makes more sense to have this in the timeline
- // because the user should still ne able to step a runner
- // even if disabled
- // Don't bother running when not enabled
- if(!this.enabled) return false
-
// 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._isDeclarative && dt < 0) return false
// Increment the time and read out the parameters
- var duration = this._duration
- this._time += dt || 16 // FIXME: step(0) is valid but will get changed to 16 here
+ var duration = this._duration || Infinity
+ this._time += isFinite(dt) ? dt : 16
var time = this._time
// Work out if we are in range to run the function
var timeInside = 0 <= time && time <= duration
var position = time / duration
- var finished = !this._isDeclarative && time >= duration // TODO: clean this up. finished returns true even for declarative if we do not check for it explicitly
+ 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(!this._isDeclarative && !timeInside && !justFinished) return finished // TODO: same as above
+ if(!timeInside && !justFinished) return finished
// Run the runner and store the last time it was run
- finished = this._run(
- this._isDeclarative ? dt // No duration, declarative
- : finished ? 1 // If completed, provide 1
- : position // If running,
- ) || finished
-
- // FIXME: for the sake of conformity this method should return this
- // we can then add a functon isFinished to see if a runner is finished
- // Work out if we are finished
- return finished
+ var runnersFinished = this._run(
+ this._isDeclarative ? dt
+ : finished ? 1
+ : position
+ )
+ finished = (this._isDeclarative && runnersFinished)
+ || (!this._isDeclarative && finished)
+
+ // Set whether this runner is complete or not
+ this.done = finished
+ return this
},
finish: function () {
- // FIXME: this is wrong as long as step returns a boolean
return this.step(Infinity)
},
*/
// Save a morpher to the morpher list so that we can retarget it later
- _saveMorpher: function (method, morpher) {
- this._morphers[method] = morpher
+ _remember: function (method, morpher) {
+ this._history[method] = {
+ morpher: morpher,
+ caller: this._queue[this._queue.length - 1],
+ }
},
// 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)
+ if(this._history[method]) {
+ this._history[method].morpher.to(target)
+ this._history[method].caller.finished = false
+ this.timeline()._continue()
+ return true
+ }
+ return false
},
// Run each initialise function in the runner if required
_initialise: function (all) {
- for (var i = 0, len = this._functions.length; i < len ; ++i) {
+ for (var i = 0, len = this._queue.length; i < len ; ++i) {
// Get the current initialiser
- var current = this._functions[i]
+ var current = this._queue[i]
// Determine whether we need to initialise
var always = current.alwaysInitialise
// Run each run function for the position given
_run: function (position) {
- // TODO: review this one
- // Make sure to keep runner running when no functions where added yet
- if(!this._functions.length) return false
-
- // Run all of the _functions directly
- var allfinished = false
- for (var i = 0, len = this._functions.length; i < len ; ++i) {
+ // Run all of the _queue directly
+ var allfinished = true
+ for (var i = 0, len = this._queue.length; i < len ; ++i) {
// Get the current function to run
- var current = this._functions[i]
+ var current = this._queue[i]
// Run the function if its not finished, we keep track of the finished
- // flag for the sake of declarative _functions
+ // flag for the sake of declarative _queue
current.finished = current.finished
|| (current.runner.call(this._element, position) === true)
-
allfinished = allfinished && current.finished
}
morpher = morpher.from(this[type](name))
}, function () {
this[type](name, morpher.at(pos))
- return morpher.isComplete()
+ return morpher.done()
}, this._isDeclarative)
return this
},
zoom: function (level, point) {
- var morpher = new Morphable(this._stepper).to(new SVG.Number(level))
+ 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 morpher.done()
}, this._isDeclarative)
return this
return this.queue(function() {}, function (pos) {
this.pushRightTransform(new Matrix(morpher.at(pos)))
- return morpher.isComplete()
+ return morpher.done()
}, this._isDeclarative)
}
this.pushRightTransform(matrix)
}
- return morpher.isComplete()
+ return morpher.done()
}, this._isDeclarative)
return this
morpher.to(from + x)
}, function (pos) {
this[method](morpher.at(pos))
- return morpher.isComplete()
+ return morpher.done()
}, this._isDeclarative)
// Register the morpher so that if it is changed again, we can retarget it
- this._saveMorpher(method, morpher)
+ this._remember(method, morpher)
return this
},
morpher.from(this[method]())
}, function (pos) {
this[method](morpher.at(pos))
- return morpher.isComplete()
+ return morpher.done()
}, this._isDeclarative)
// Register the morpher so that if it is changed again, we can retarget it
- this._saveMorpher(method, morpher)
+ this._remember(method, morpher)
return this
},
// Animatable center y-axis
cy: function (y) {
- return this._queueNumber('cy', x)
+ return this._queueNumber('cy', y)
},
// Add animatable move
var runnersLeft = false
for (var i = 0; i < this._runners.length ; i++) {
- // Get and run the current runner and figure out if its done running
+ // Get and run the current runner and ignore it if its inactive
var runner = this._runners[i]
-
- var finished = runner.step(dt)
+ if(!runner.active()) continue
// If this runner is still going, signal that we need another animation
// frame, otherwise, remove the completed runner
+ var finished = runner.step(dt).done
if (!finished) {
runnersLeft = true
}
},
},
-
// These methods will be added to all SVG.Element objects
parent: SVG.Element,
construct: {
.schedule(rightAnimation, 500, 'now')
```
+
+
+# Modifying Controller Parameters
+
+Some user might want to change the speed of a controller, or how the controller
+works in the middle of an animation. For example, they might do:
+
+```js
+
+var pid = PID(30, 20, 40)
+let animation = el.animate(pid).move(.., ..)
+
+
+// Some time later, the user slides a slider, and they can do:
+slider1.onSlide( v => pid.p(v) )
+
+```
+
+
+# Bidirectional Scheduling **(TODO)**
+
+We would like to schedule a runner to a timeline, or to do the opposite
+
+```js
+
+// If we have a runner and a timeline
+let timeline = new Timeline()...
+let runner = new Runner()...
+
+// Since the user can schedule a runner onto a timeline
+timeline.schedule(runner, ...rest)
+
+// It should be possible to do the opposite
+runner.schedule(timeline, ...rest)
+
+// It could be Implemented like this
+runner.schedule = (t, duration, delay, now) {
+ this._timeline.remove(this) // Should work even if its not scheduled
+ t.schedule(this, duration, delay, now)
+ return this
+}
+
+// The benefit would be that they could call animate afterwards: eg:
+runner.schedule(timeline, ...rest)
+ .animate()...
+
+```