diff options
-rw-r--r-- | dirty.html | 57 | ||||
-rw-r--r-- | playgrounds/matrix/drag.js | 76 | ||||
-rw-r--r-- | src/controller.js | 85 | ||||
-rw-r--r-- | src/morph.js | 21 | ||||
-rw-r--r-- | src/runner.js | 107 | ||||
-rw-r--r-- | src/timeline.js | 7 | ||||
-rw-r--r-- | useCases.md | 47 |
7 files changed, 235 insertions, 165 deletions
@@ -75,37 +75,38 @@ function getColor(t) { // } // } -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> diff --git a/playgrounds/matrix/drag.js b/playgrounds/matrix/drag.js index 143699d..2dd6cac 100644 --- a/playgrounds/matrix/drag.js +++ b/playgrounds/matrix/drag.js @@ -1,51 +1,43 @@ +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, { @@ -53,7 +45,7 @@ SVG.extend(SVG.Element, { let sx, sy - reactToDrag(this, (e, x, y)=> { + reactToDrag(this, (e, x, y) => { this.transform({ origin: [sx, sy], @@ -64,15 +56,13 @@ SVG.extend(SVG.Element, { 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 }, }) diff --git a/src/controller.js b/src/controller.js index 064c4f2..37cdfec 100644 --- a/src/controller.js +++ b/src/controller.js @@ -9,22 +9,16 @@ 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) { +function makeSetterGetter (k) { + return function (v) { + if (v == null) return this[v] + this[k] = v + return this + } +} - }, - }, +SVG.Stepper = SVG.invent ({ + create: function () {}, }) /*** @@ -51,7 +45,7 @@ SVG.Ease = SVG.invent ({ return from + (to - from) * this.ease(pos) }, - isComplete: function (dt, c) { + done: function (dt, c) { return false }, }, @@ -90,13 +84,8 @@ SVG.Controller = SVG.invent ({ 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 }, }, }) @@ -136,6 +125,53 @@ SVG.Spring = function spring(duration, overshoot) { }) } +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 @@ -147,6 +183,7 @@ SVG.PID = function (P, I, D, antiwindup) { 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 @@ -160,4 +197,4 @@ SVG.PID = function (P, I, D, antiwindup) { return current + (P * p + I * i + D * d) }) -} +}*/ diff --git a/src/morph.js b/src/morph.js index 8bb6fbd..87f82e0 100644 --- a/src/morph.js +++ b/src/morph.js @@ -37,14 +37,6 @@ SVG.Morphable = SVG.invent({ // 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 }, @@ -98,10 +90,13 @@ SVG.Morphable = SVG.invent({ 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) { @@ -114,7 +109,7 @@ SVG.Morphable = SVG.invent({ 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) }) ) ) diff --git a/src/runner.js b/src/runner.js index ebdd93e..eecb950 100644 --- a/src/runner.js +++ b/src/runner.js @@ -13,16 +13,18 @@ SVG.Runner = SVG.invent({ 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 @@ -31,7 +33,7 @@ SVG.Runner = SVG.invent({ 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 @@ -140,12 +142,14 @@ SVG.Runner = SVG.invent({ */ 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 }, @@ -169,52 +173,45 @@ SVG.Runner = SVG.invent({ 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) }, @@ -270,21 +267,30 @@ SVG.Runner = SVG.invent({ */ // 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 @@ -298,22 +304,17 @@ SVG.Runner = SVG.invent({ // 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 } @@ -350,20 +351,20 @@ SVG.extend(SVG.Runner, { 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 @@ -414,7 +415,7 @@ SVG.extend(SVG.Runner, { return this.queue(function() {}, function (pos) { this.pushRightTransform(new Matrix(morpher.at(pos))) - return morpher.isComplete() + return morpher.done() }, this._isDeclarative) } @@ -456,7 +457,7 @@ SVG.extend(SVG.Runner, { this.pushRightTransform(matrix) } - return morpher.isComplete() + return morpher.done() }, this._isDeclarative) return this @@ -494,11 +495,11 @@ SVG.extend(SVG.Runner, { 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 }, @@ -513,11 +514,11 @@ SVG.extend(SVG.Runner, { 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 }, @@ -532,7 +533,7 @@ SVG.extend(SVG.Runner, { // Animatable center y-axis cy: function (y) { - return this._queueNumber('cy', x) + return this._queueNumber('cy', y) }, // Add animatable move diff --git a/src/timeline.js b/src/timeline.js index 9ed6b74..be8d52e 100644 --- a/src/timeline.js +++ b/src/timeline.js @@ -248,13 +248,13 @@ SVG.Timeline = SVG.invent({ 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 } @@ -288,7 +288,6 @@ SVG.Timeline = SVG.invent({ }, }, - // These methods will be added to all SVG.Element objects parent: SVG.Element, construct: { diff --git a/useCases.md b/useCases.md index 2285a82..a15d511 100644 --- a/useCases.md +++ b/useCases.md @@ -249,3 +249,50 @@ let timeline = new SVG.Timeline() .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()... + +``` |