Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a simple paused and progress with event to control transition animation. #102

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="transition_paused" href="#transition_paused">#</a> <i>transition</i>.<b>paused</b>([<i>value</i>]) [<>](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.


<a name="transition_progress" href="#transition_progress">#</a> <i>transition</i>.<b>progress</b>([<i>value</i>]) [<>](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.

<a name="transition_end" href="#transition_end">#</a> <i>transition</i>.<b>end</b>() · [Source](https://github.com/d3/d3-transition/blob/master/src/transition/end.js)
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/transition/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions src/transition/paused.js
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions src/transition/progress.js
Original file line number Diff line number Diff line change
@@ -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) {
snowyu marked this conversation as resolved.
Show resolved Hide resolved
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);
}
30 changes: 29 additions & 1 deletion src/transition/schedule.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
199 changes: 199 additions & 0 deletions test/transition/paused-test.js
Original file line number Diff line number Diff line change
@@ -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();
}
});