diff --git a/README.md b/README.md index bad872d..4b81a22 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,27 @@ Specifies a factory for the transition [easing function](https://github.com/d3/d ### Control Flow +The [paused](#transition_paused) and [progress](#transition_progress) of a transition is configurable in runtime. + +# transition.paused([value]) [<>](https://github.com/d3/d3-transition/blob/master/src/transition/paused.js "Source") + +To pause the transition animation, set the transition paused to `true`, or `false` to resume. The *value* may be specified either as a constant or a function. + +```js +transition.paused(true); +``` + +If a *value* is not specified, returns the current value of the paused for the first (non-null) element in the transition. This is generally useful only if you know that the transition contains exactly one element. + + +# transition.progress([value]) [<>](https://github.com/d3/d3-transition/blob/master/src/transition/progress.js "Source") + +The progress is a value between 0 (begin) to 1 (end). You can set or get the progress of the transition at any time. + +```js +transition.progress(0.5); +``` + For advanced usage, transitions provide methods for custom control flow. # transition.end() ยท [Source](https://github.com/d3/d3-transition/blob/master/src/transition/end.js) @@ -377,6 +398,7 @@ Returns a promise that resolves when every selected element finishes transitioni Adds or removes a *listener* to each selected element for the specified event *typenames*. The *typenames* is one of the following string event types: * `start` - when the transition starts. +* `progress` - notify when the transition progresses. * `end` - when the transition ends. * `interrupt` - when the transition is interrupted. * `cancel` - when the transition is cancelled. diff --git a/src/transition/index.js b/src/transition/index.js index 355be71..0fb4fcb 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -19,6 +19,8 @@ import transition_textTween from "./textTween.js"; import transition_transition from "./transition.js"; import transition_tween from "./tween.js"; import transition_end from "./end.js"; +import transition_paused from "./paused.js"; +import transition_progress from "./progress.js"; var id = 0; @@ -64,6 +66,8 @@ Transition.prototype = transition.prototype = { tween: transition_tween, delay: transition_delay, duration: transition_duration, + paused: transition_paused, + progress: transition_progress, ease: transition_ease, easeVarying: transition_easeVarying, end: transition_end, diff --git a/src/transition/paused.js b/src/transition/paused.js new file mode 100644 index 0000000..e4040ed --- /dev/null +++ b/src/transition/paused.js @@ -0,0 +1,23 @@ +import {get} from "./schedule.js"; + +function pausedFunction(id, value) { + return function() { + get(this, id).paused = Boolean(value.apply(this, arguments)); + }; +} + +function pausedConstant(id, value) { + return value = Boolean(value), function() { + get(this, id).paused = value; + }; +} + +export default function(value) { + var id = this._id; + + return arguments.length + ? this.each((typeof value === "function" + ? pausedFunction + : pausedConstant)(id, value)) + : get(this.node(), id).paused; +} diff --git a/src/transition/progress.js b/src/transition/progress.js new file mode 100644 index 0000000..4c3afbf --- /dev/null +++ b/src/transition/progress.js @@ -0,0 +1,28 @@ +import {get} from "./schedule.js"; + +function progressFunction(id, value) { + return function() { + get(this, id).progress = +value.apply(this, arguments); + }; +} + +function progressConstant(id, value) { + return value = +value, function() { + get(this, id).progress = value; + }; +} + +function abs(value) { + if (value < 0) value = -value; + return value; +} + +export default function(value) { + var id = this._id; + + return arguments.length + ? this.each((typeof value === "function" + ? progressFunction + : progressConstant)(id, value)) + : abs(get(this.node(), id).progress); +} diff --git a/src/transition/schedule.js b/src/transition/schedule.js index f4e88d7..290de8a 100644 --- a/src/transition/schedule.js +++ b/src/transition/schedule.js @@ -1,7 +1,7 @@ import {dispatch} from "d3-dispatch"; import {timer, timeout} from "d3-timer"; -var emptyOn = dispatch("start", "end", "cancel", "interrupt"); +var emptyOn = dispatch("start", "end", "cancel", "interrupt", "progress"); var emptyTween = []; export var CREATED = 0; @@ -127,7 +127,35 @@ function create(node, id, self) { tween.length = j + 1; } + function getProgress(elapsed) { + if (self.paused) { + if (self.progress >= 0) { + elapsed = self._lastprogress !== self.progress ? self.progress * self.duration : -1; + } else { + self.progress = elapsed / self.duration; + } + if (self._lastprogress !== self.progress) { + self.on.call("progress", node, node.__data__, self.index, self.group, self.progress); + self._lastprogress = self.progress; + } + } else if (self.progress >= 0) { + elapsed = elapsed - (self.progress * self.duration); + self.timer.restart(tick, 0, self.time + elapsed); + elapsed = self.progress = - (self.progress + 1e-10); + } else { + if (elapsed >= self.duration) { + self.progress = -1; + } else { + self.progress = - (elapsed / self.duration); + } + self.on.call("progress", node, node.__data__, self.index, self.group, -self.progress); + } + return elapsed; + } + function tick(elapsed) { + elapsed = getProgress(elapsed); + if (elapsed < 0) return; var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1), i = -1, n = tween.length; diff --git a/test/transition/paused-test.js b/test/transition/paused-test.js new file mode 100644 index 0000000..d419ea5 --- /dev/null +++ b/test/transition/paused-test.js @@ -0,0 +1,199 @@ +var tape = require("tape"), + jsdom = require("../jsdom"), + d3_ease = require("d3-ease"), + d3_timer = require("d3-timer"), + d3_interpolate = require("d3-interpolate"), + d3_selection = require("d3-selection"); + +require("../../"); + +tape("transition.paused(true) allows pause the transition animation", function(test) { + var root = jsdom().documentElement, + ease = d3_ease.easeCubic, + duration = 100, + interpolate = d3_interpolate.interpolateNumber(0, 100), + selection = d3_selection.select(root).attr("t", 0), + transition = selection.transition().duration(duration).attr("t", 100).on("end", ended); + var beginTime = d3_timer.now(); + + d3_timer.timeout(function(elapsed) { + transition.paused(true); + test.strictEqual(root.__transition[transition._id].paused, true); + test.strictEqual(transition.paused(), true); + test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(elapsed / duration))); + }, 50); + + d3_timer.timeout(function(elapsed) { + var progress = root.__transition[transition._id].progress; + test.strictEqual(transition.progress(), progress); + test.ok(progress >= 0.5); + test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress))); + transition.paused(false); + test.strictEqual(root.__transition[transition._id].paused, false); + test.strictEqual(transition.paused(), false); + }, 150); + + function ended() { + var t = d3_timer.now() - beginTime; + test.ok(t > 150); + test.end(); + } +}); + +tape("transition.progress() allows to get the progress of the transition animation", function(test) { + var root = jsdom().documentElement, + ease = d3_ease.easeCubic, + duration = 100, + interpolate = d3_interpolate.interpolateNumber(0, 100), + selection = d3_selection.select(root).attr("t", 0), + transition = selection.transition().duration(duration).attr("t", 100).on("end", ended); + var beginTime = d3_timer.now(); + var oldProgress; + + d3_timer.timeout(function(elapsed) { + // get the progress on runtime + var progress = -root.__transition[transition._id].progress; + test.strictEqual(transition.progress(), progress); + test.ok(progress >= 0.5); + test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress))); + transition.paused(true); + oldProgress = progress; + transition.progress(progress); + }, 50); + + d3_timer.timeout(function(elapsed) { + var progress = root.__transition[transition._id].progress; + test.strictEqual(transition.progress(), progress); + test.strictEqual(oldProgress, progress); + test.ok(progress >= 0.5); + test.strictEqual(Number(root.getAttribute("t")), interpolate(ease(progress))); + transition.paused(false); + }, 150); + + function ended() { + var t = d3_timer.now() - beginTime; + test.ok(t > 150); + test.strictEqual(transition.progress(), 1); + test.end(); + } +}); + +tape("transition.on(\"progress\", listener) event to notify animation progress", function(test) { + var root = jsdom().documentElement, + duration = 100, + selection = d3_selection.select(root).attr("t", 0), + transition = selection.transition().duration(duration).attr("t", 100) + .on("progress", onProgress) + .on("end", ended); + var beginTime = d3_timer.now(); + var oldProgress; + var lastProgress = 0; + + d3_timer.timeout(function(elapsed) { + // get the progress on runtime + var progress = -root.__transition[transition._id].progress; + test.strictEqual(transition.progress(), progress); + test.strictEqual(lastProgress, progress); + test.ok(progress >= 0.5); + transition.paused(true); + oldProgress = progress; + transition.progress(progress); + }, 50); + + d3_timer.timeout(function(elapsed) { + var progress = root.__transition[transition._id].progress; + test.strictEqual(transition.progress(), progress); + test.strictEqual(oldProgress, progress); + test.strictEqual(lastProgress, progress); + test.ok(progress >= 0.5); + transition.paused(false); + }, 150); + + function onProgress(data, index, grp, progress) { + test.ok(progress >= lastProgress, `${progress} >= ${lastProgress}`); + lastProgress = progress; + } + + function ended() { + var t = d3_timer.now() - beginTime; + test.ok(t > 150); + test.strictEqual(transition.progress(), 1); + test.strictEqual(lastProgress, 1); + test.end(); + } +}); + +tape("transition.on(\"progress\", listener) event should work on paused status", function(test) { + var root = jsdom().documentElement, + duration = 100, + selection = d3_selection.select(root).attr("t", 0), + transition = selection.transition().duration(duration).attr("t", 100).paused(true) + .on("progress", onProgress) + .on("end", ended); + var beginTime = d3_timer.now(); + var progresses = []; + + d3_timer.timeout(function(elapsed) { + test.ok(progresses.length); + test.strictEqual(transition.progress(), progresses[0]); + transition.progress(0.2); + }, 50); + + d3_timer.timeout(function(elapsed) { + test.ok(progresses.length === 2, `progresses.length(${progresses.length}) === 2`); + test.strictEqual(transition.progress(), progresses[1]); + transition.progress(1); + }, 100); + + function onProgress(data, index, grp, progress) { + progresses.push(progress); + } + + function ended() { + var t = d3_timer.now() - beginTime; + test.ok(t >= 100); + test.ok(t < 150); + test.ok(progresses.length === 3, `progresses.length(${progresses.length}) === 3`); + test.strictEqual(progresses[2], 1); + test.strictEqual(transition.progress(), 1); + test.end(); + } +}); + +tape("transition.progress(true) should pause the animation", function(test) { + var root = jsdom().documentElement, + duration = 2000, + selection = d3_selection.select(root).attr("t", 0), + transition = selection.transition().duration(duration).attr("t", 100) + .on("progress", onProgress) + .on("end", ended); + var needCheck = 0; + var lastProgress; + var lastProgress2; + + d3_timer.timeout(function(elapsed) { + transition.paused(true); + lastProgress = transition.progress(); + test.ok(lastProgress >= 0.25, lastProgress + " progress should >= 0.25"); + test.ok(lastProgress <= 0.35, lastProgress + " progress should <= 0.35"); + }, 600); + + d3_timer.timeout(function(elapsed) { + needCheck = 1; + transition.paused(false); + }, 2100); + + function onProgress(data, index, grp, progress) { + if (needCheck && needCheck <=2) { + if (needCheck === 2) lastProgress2 = progress; + test.ok(progress - lastProgress <= 0.1, "(progress - lastProgress) should <= 0.1 "); + needCheck++; + } + } + + function ended() { + test.ok(typeof lastProgress2 === 'number', 'already checked'); + test.strictEqual(transition.progress(), 1, 'progress should be end'); + test.end(); + } +});