]> source.dussan.org Git - svg.js.git/commitdiff
Fixed all of the low hanging problems so declarative works
authorSaivan <savian@me.com>
Fri, 25 May 2018 14:01:33 +0000 (00:01 +1000)
committerSaivan <savian@me.com>
Fri, 25 May 2018 14:01:33 +0000 (00:01 +1000)
dirty.html
playgrounds/matrix/drag.js
src/controller.js
src/morph.js
src/runner.js
src/timeline.js
useCases.md

index 6893e4248cc98d31b4e2dea419cfd9918c3be6f8..25ee5c3c55cf463ce23792489ff2205af01470a4 100644 (file)
@@ -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>
 
index 143699d578cb3c12e8d1bf765a11ca77f071af81..2dd6cac534d28e647e25fa03b214c5fb2d63e53b 100644 (file)
@@ -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
   },
 })
index 064c4f25d23ad032c3c8588292d1c444efbad916..37cdfec0106d37501c34085e907e5f2b77d7b0dc 100644 (file)
@@ -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)
   })
-}
+}*/
index 8bb6fbdcf8c822dcee4bbdf3a65eb3d75e7bf8f6..87f82e02f4ca3e4cf813fa478cc0342b0833efbc 100644 (file)
@@ -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)
           })
         )
       )
index ebdd93e6d818f7c8e2ec8751c144c2a5b90cfc89..eecb950b0cc67fcada57b271b5a9d9c9e299166b 100644 (file)
@@ -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
index 9ed6b74c8f605c23ae88c997e05a52ea39a04651..be8d52e777f1045926333c8186ae6ec962ee7b3b 100644 (file)
@@ -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: {
index 2285a822313e62d1c2c988520adceae9437a32be..a15d51168416af14e12bc851ec1518664fb219d1 100644 (file)
@@ -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()...
+
+```