Skip to content

Commit

Permalink
Ensure cursor pauses correctly during each action.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmacarthur committed Jul 13, 2022
1 parent be00e9f commit ad463f6
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ describe("animation already exists", () => {

describe("animation has already been canceled", () => {
it("does not cancel it or preserve current time.", () => {
let mockAnimation = addMockAnimation(cursor, {
playState: "idle",
});
cursor.getAnimations = () => [];

const setCursorAnimationSpy = jest
.spyOn(setCursorAnimation, "default")
.mockImplementation(() => {
Expand All @@ -68,7 +67,6 @@ describe("animation has already been canceled", () => {
});

expect(setCursorAnimationSpy).toHaveBeenCalledTimes(1);
expect(mockAnimation.cancel).not.toHaveBeenCalled();
expect(result.currentTime).toEqual(undefined);
});
});
Expand Down
91 changes: 91 additions & 0 deletions packages/typeit/__tests__/helpers/setCursorAnimation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import setCursorAnimation from "../../src/helpers/setCursorAnimation";
import * as beforePaint from "../../src/helpers/beforePaint";

let cursor;
let mockAnimate;
let mockPause;
let mockPlay;
let beforePaintSpy;

beforeEach(() => {
setHTML`<span class='ti-cursor' data-ti-animation-id="123abc">|</span>`;

cursor = document.querySelector(".ti-cursor");

beforePaintSpy = jest
.spyOn(beforePaint, "default")
.mockImplementation((cb) => {
return cb();
});
mockPause = jest.fn();
mockPlay = jest.fn();
mockAnimate = jest.fn(() => {
return {
pause: mockPause,
play: mockPlay,
};
});

cursor.animate = mockAnimate;
});

describe("setting correct options", () => {
it("sets correct defaults", () => {
setCursorAnimation({ cursor });

expect(mockAnimate).toBeCalledTimes(1);
expect(mockAnimate).toBeCalledWith(
[{ opacity: 0 }, { opacity: 0 }, { opacity: 1 }],
{
iterations: Infinity,
easing: "steps(2, start)",
fill: "forwards",
}
);
});

it("takes custom options", () => {
setCursorAnimation({
cursor,
frames: [{ height: "10px" }, { height: "50px" }],
timingOptions: {
iterations: 3,
easing: "linear",
fill: "backwards",
},
});

expect(mockAnimate).toBeCalledTimes(1);
expect(mockAnimate).toBeCalledWith(
[
{
height: "10px",
},
{
height: "50px",
},
],
{
iterations: 3,
easing: "linear",
fill: "backwards",
}
);
});
});

describe("initial behavior", () => {
it("pauses and then plays after repaint", () => {
let animation = setCursorAnimation({ cursor });

expect(mockPause).toHaveBeenCalledTimes(1);
expect(mockPlay).toHaveBeenCalledTimes(1);
expect(beforePaintSpy).toHaveBeenCalledTimes(2);
});

it("sets correct animation ID", () => {
let animation = setCursorAnimation({ cursor });

expect(animation.id).toEqual("123abc");
});
});
26 changes: 13 additions & 13 deletions packages/typeit/__tests__/setup.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
beforeEach(() => {
global.HTMLElement.prototype.animate = () => {
return {};
};
global.HTMLElement.prototype.getAnimations = () => [
{
effect: {
getComputedTiming: () => {
return {};
},
getKeyframes: () => [],
let animation = {
pause: () => {},
play: () => {},
effect: {
getComputedTiming: () => {
return {};
},
cancel: () => {},
currentTime: 0,
getKeyframes: () => [],
},
];
cancel: () => {},
currentTime: 0,
};

global.HTMLElement.prototype.animate = () => animation;
global.HTMLElement.prototype.getAnimations = () => [animation];
});

global.setHTML = (html, shouldReturn = false) => {
Expand Down
23 changes: 12 additions & 11 deletions packages/typeit/src/helpers/fireItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ let fireItem = async ({
}

let fire = async (): Promise<{
animation: Animation;
timingOptions: object;
frames: AnimationKeyFrame[];
}> => {
// An animation is only registered on the cursor when it's made visible.
// If the cursor has been disabled, there won't be one here.
let animation = getAnimationFromElement(cursor);
let timingOptions: object, frames: AnimationKeyFrame[];
let timingOptions: object = {};
let frames: AnimationKeyFrame[] = null;

if (animation) {
timingOptions = cursor
Expand All @@ -70,7 +70,9 @@ let fireItem = async ({
}

await wait(async () => {
// Pause the cursor while stuff is happening.
// If it's a qualified queue item, pause the cursor at the
// beginning of the item's execution by destroying the aniatmion.
// Immediately after completing, the animation will be recreated (with a delay).
if (animation && queueItem.shouldPauseCursor()) {
animation.cancel();
}
Expand All @@ -80,17 +82,16 @@ let fireItem = async ({
});
}, queueItem.delay);

return { animation, frames, timingOptions };
return { frames, timingOptions };
};

let { animation, frames, timingOptions } = await fire();
let { frames, timingOptions } = await fire();

animation &&
rebuildCursorAnimation({
cursor,
frames,
timingOptions,
});
await rebuildCursorAnimation({
cursor,
frames,
timingOptions,
});

return index;
};
Expand Down
3 changes: 2 additions & 1 deletion packages/typeit/src/helpers/rebuildCursorAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { El } from "../types";
import beforePaint from "./beforePaint";
import getAnimationFromElement from "./getAnimationFromElement";
import setCursorAnimation from "./setCursorAnimation";

Expand Down Expand Up @@ -26,7 +27,7 @@ let rebuildCursorAnimation = ({

// An existing animation is actively running...
// so carry over the timing properties we care about.
if (animation && animation.playState !== "idle") {
if (animation) {
timingOptions.delay = animation.effect.getComputedTiming().delay;

// This needs to be set later, since there's no way to pass
Expand Down
20 changes: 15 additions & 5 deletions packages/typeit/src/helpers/setCursorAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { El } from "../types";
import beforePaint from "./beforePaint";

const DEFAULT_TIMING_OPTIONS: Partial<AnimationEffectTiming> = {
iterations: Infinity,
easing: "steps(2, start)",
fill: "forwards",
};

const DEFAULT_FRAMES: AnimationKeyFrame[] = [
{ opacity: 0 },
{ opacity: 0 },
{ opacity: 1 },
];
const DEFAULT_FRAMES: AnimationKeyFrame[] = [0, 0, 1].map((n) => {
return { opacity: n };
});

/**
* Create and return an animation for the cursor.
Expand All @@ -29,8 +28,19 @@ let setCursorAnimation = ({
...timingOptions,
});

animation.pause();

animation.id = cursor.dataset.tiAnimationId;

// Kicking back the animation until after the next repaint
// prevents odd freezing issues when a new animation is
// generated in place of an older one.
beforePaint(() => {
beforePaint(() => {
animation.play();
});
});

return animation;
};

Expand Down

0 comments on commit ad463f6

Please sign in to comment.