Skip to content

Commit

Permalink
feat: pauseAnimation and resumeAnimation (chanind#156)
Browse files Browse the repository at this point in the history
* feat: adding pauseAnimation and resumeAnimation

* fixing linting
  • Loading branch information
chanind authored Jan 12, 2020
1 parent 56e6906 commit ed7fe1b
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/HanziWriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ HanziWriter.prototype.loopCharacterAnimation = function(options = {}) {
));
};

HanziWriter.prototype.pauseAnimation = function() {
return this._withData(() => this._renderState.pauseAll());
};

HanziWriter.prototype.resumeAnimation = function() {
return this._withData(() => this._renderState.resumeAll());
};

HanziWriter.prototype.showOutline = function(options = {}) {
this._options.showOutline = true;
return this._withData(() => (
Expand Down
47 changes: 41 additions & 6 deletions src/Mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ function Mutation(scope, valuesOrCallable, options = {}) {
this._valuesOrCallable = valuesOrCallable;
this._duration = options.duration || 0;
this._force = options.force;
this._pausedDuration = 0;
this._tickBound = this._tick.bind(this);
this._startPauseTime = null;
}


Expand All @@ -65,8 +67,22 @@ Mutation.prototype.run = function(renderState) {
});
};

Mutation.prototype.pause = function() {
if (this._startPauseTime !== null) return;
if (this._frameHandle) cancelAnimationFrame(this._frameHandle);
this._startPauseTime = performanceNow();
};

Mutation.prototype.resume = function() {
if (this._startPauseTime === null) return;
this._frameHandle = requestAnimationFrame(this._tickBound);
this._pausedDuration += performanceNow() - this._startPauseTime;
this._startPauseTime = null;
};

Mutation.prototype._tick = function(timing) {
const progress = Math.min(1, (timing - this._startTime) / this._duration);
if (this._startPauseTime !== null) return;
const progress = Math.min(1, (timing - this._startTime - this._pausedDuration) / this._duration);
if (progress === 1) {
this._renderState.updateState(this._values);
this._frameHandle = null;
Expand Down Expand Up @@ -97,27 +113,46 @@ Mutation.prototype.cancel = function(renderState) {
}
};

// ------ Mutation.Pause Class --------
// ------ Mutation.Delay Class --------

function Pause(duration) {
function Delay(duration) {
this._duration = duration;
this._startTime = null;
this._paused = false;
}

Pause.prototype.run = function() {
Delay.prototype.pause = function() {
if (this._paused) return;
// to pause, clear the timeout and rewrite this._duration with whatever time is remaining
const elapsedDelay = performanceNow() - this._startTime;
this._duration = Math.max(0, this._duration - elapsedDelay);
clearTimeout(this._timeout);
this._paused = true;
};

Delay.prototype.resume = function() {
if (!this._paused) return;
this._startTime = performanceNow();
this._timeout = setTimeout(() => this.cancel(), this._duration);
this._paused = false;
};

Delay.prototype.run = function() {
const timeoutPromise = new Promise((resolve) => {
this._resolve = resolve;
});
this._startTime = performanceNow();
this._timeout = setTimeout(() => this.cancel(), this._duration);
return timeoutPromise;
};

Pause.prototype.cancel = function() {
Delay.prototype.cancel = function() {
clearTimeout(this._timeout);
if (this._resolve) this._resolve();
this._resolve = false;
};

Mutation.Pause = Pause;
Mutation.Delay = Delay;

// -------------------------------------

Expand Down
14 changes: 14 additions & 0 deletions src/RenderState.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ RenderState.prototype._run = function(mutationChain) {
});
};

RenderState.prototype._getActiveMutations = function() {
return this._mutationChains.map(chain => {
return chain._mutations[chain._index];
});
};

RenderState.prototype.pauseAll = function() {
this._getActiveMutations().forEach(mutation => mutation.pause());
};

RenderState.prototype.resumeAll = function() {
this._getActiveMutations().forEach(mutation => mutation.resume());
};

RenderState.prototype.cancelMutations = function(scopes) {
this._mutationChains.forEach(chain => {
chain._scopes.forEach(chainScope => {
Expand Down
61 changes: 61 additions & 0 deletions src/__tests__/HanziWriter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,67 @@ describe('HanziWriter', () => {
});
});

describe('pauseAnimation and resumeAnimation', () => {
it('pauses and resumes the currently running animations', async () => {
document.body.innerHTML = '<div id="target"></div>';
const writer = new HanziWriter('target', '人', { showCharacter: true, charDataLoader });
await writer._withDataPromise;

let isResolved = false;
let resolvedVal;
const onComplete = jest.fn();

clock.tick(50);
await resolvePromises();

writer.animateStroke(1, { onComplete }).then(result => {
isResolved = true;
resolvedVal = result;
});

await resolvePromises();
expect(writer._renderState.state.character.main.strokes[1].displayPortion).toBe(0);

clock.tick(50);
await resolvePromises();

const pausedDisplayPortion = writer._renderState.state.character.main.strokes[1].displayPortion;
expect(pausedDisplayPortion).toBeGreaterThan(0);
expect(pausedDisplayPortion).toBeLessThan(1);
expect(isResolved).toBe(false);

writer.pauseAnimation();
await resolvePromises();

clock.tick(2000);
await resolvePromises();

expect(isResolved).toBe(false);
expect(writer._renderState.state.character.main.strokes[1].displayPortion).toBe(pausedDisplayPortion);

writer.resumeAnimation();
await resolvePromises();

clock.tick(50);
await resolvePromises();

const newDisplayPortion = writer._renderState.state.character.main.strokes[1].displayPortion;
expect(newDisplayPortion).not.toBe(pausedDisplayPortion);
expect(newDisplayPortion).toBeGreaterThan(0);
expect(newDisplayPortion).toBeLessThan(1);
expect(isResolved).toBe(false);

clock.tick(2000);
await resolvePromises();

expect(writer._renderState.state.character.main.strokes[1].displayPortion).toBe(1);
expect(isResolved).toBe(true);
expect(resolvedVal).toEqual({ canceled: false });
expect(onComplete).toHaveBeenCalledTimes(1);
expect(onComplete).toHaveBeenCalledWith({ canceled: false });
});
});

describe('highlightStroke', () => {
it('highlights a single stroke', async () => {
document.body.innerHTML = '<div id="target"></div>';
Expand Down
94 changes: 94 additions & 0 deletions src/__tests__/Mutation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,57 @@ describe('Mutation', () => {
expect(isResolved).toBe(true);
});

it('can pause and resume during the tween', async () => {
const renderState = {
state: {a: {b: 10 } },
updateState: jest.fn(),
};

const mut = new Mutation('a.b', 20, { duration: 50 });
let isResolved = false;

mut.run(renderState).then(() => {
isResolved = true;
});

await Promise.resolve();
expect(isResolved).toBe(false);
expect(renderState.updateState).not.toHaveBeenCalled();

clock.tick(45);
const partialTweenVals = renderState.updateState.mock.calls.map(call => call[0].a.b);

expect(isResolved).toBe(false);
expect(renderState.updateState).toHaveBeenCalled();
renderState.updateState.mock.calls.forEach(mockCall => {
expect(mockCall[0].a.b).toBeGreaterThan(10);
expect(mockCall[0].a.b).toBeLessThan(20);
});

mut.pause();
await Promise.resolve();

clock.tick(1000);
await Promise.resolve();

expect(partialTweenVals.length).toBe(renderState.updateState.mock.calls.length);
expect(isResolved).toBe(false);
expect(renderState.updateState).toHaveBeenCalled();
renderState.updateState.mock.calls.forEach(mockCall => {
expect(mockCall[0].a.b).toBeGreaterThan(10);
expect(mockCall[0].a.b).toBeLessThan(20);
});

mut.resume();
await Promise.resolve();

clock.tick(25);
expect(renderState.updateState).toHaveBeenLastCalledWith({ a: { b: 20 } });

await Promise.resolve();
expect(isResolved).toBe(true);
});

it('updates state on cancel if force: true', async () => {
const renderState = {
state: {a: {b: 7 } },
Expand All @@ -101,3 +152,46 @@ describe('Mutation', () => {
expect(renderState.updateState).not.toHaveBeenCalled();
});
});

describe('Mutation.Delay', () => {
it('can pause and resume during the delay', async () => {

const delay = new Mutation.Delay(1000);
let isResolved = false;

delay.run().then(() => {
isResolved = true;
});

await Promise.resolve();
expect(isResolved).toBe(false);

clock.tick(200);
await Promise.resolve();

expect(isResolved).toBe(false);

delay.pause();
await Promise.resolve();

clock.tick(2000);
await Promise.resolve();

expect(isResolved).toBe(false);

delay.resume();
await Promise.resolve();

expect(isResolved).toBe(false);

clock.tick(500);
await Promise.resolve();

expect(isResolved).toBe(false);

clock.tick(500);
await Promise.resolve();

expect(isResolved).toBe(true);
});
});
4 changes: 2 additions & 2 deletions src/__tests__/RenderState-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('RenderState', () => {
renderState.run([
new Mutation('character.main.opacity', 0.3),
new Mutation('character.main.opacity', 0.9, { duration: 50 }),
new Mutation.Pause(100),
new Mutation.Delay(100),
new Mutation('character.main.opacity', 0, { duration: 50 }),
]).then(result => {
isResolved = true;
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('RenderState', () => {
renderState.run([
new Mutation('character.main.opacity', 0.3),
new Mutation('character.main.opacity', 0.9, { duration: 50 }),
new Mutation.Pause(100),
new Mutation.Delay(100),
new Mutation('character.main.opacity', 0, { duration: 50 }),
]).then(result => {
isResolved = true;
Expand Down
4 changes: 2 additions & 2 deletions src/characterActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ const animateCharacter = (charName, character, fadeDuration, speed, delayBetween
strokes: objRepeat({ opacity: 0 }, character.strokes.length),
}, { force: true }));
character.strokes.forEach((stroke, i) => {
if (i > 0) mutations.push(new Mutation.Pause(delayBetweenStrokes));
if (i > 0) mutations.push(new Mutation.Delay(delayBetweenStrokes));
mutations = mutations.concat(animateStroke(charName, stroke, speed));
});
return mutations;
};

const animateCharacterLoop = (charName, character, fadeDuration, speed, delayBetweenStrokes, delayBetweenLoops) => {
const mutations = animateCharacter(charName, character, fadeDuration, speed, delayBetweenStrokes);
mutations.push(new Mutation.Pause(delayBetweenLoops));
mutations.push(new Mutation.Delay(delayBetweenLoops));
return mutations;
};

Expand Down

0 comments on commit ed7fe1b

Please sign in to comment.