Skip to content

Commit

Permalink
Support dynamic keyframe
Browse files Browse the repository at this point in the history
  • Loading branch information
inokawa committed Nov 19, 2022
1 parent a426cef commit e8ec7c9
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 193 deletions.
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export type {
TypedKeyframe,
TypedEasing,
GetKeyframeFunction,
AnimationOptions,
AnimatableCSSProperties,
PlayOptions,
ReverseOptions,
} from "./waapi";
5 changes: 5 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Expand<T> = T extends object
? T extends infer O
? { [K in keyof O]: O[K] }
: never
: T;
44 changes: 25 additions & 19 deletions src/core/waapi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CSSProperties } from "react";
import { getKeys, getStyle, noop, uniqBy } from "./utils";
import { getKeys, noop, uniqBy } from "./utils";

export type AnimatableCSSProperties = Omit<
CSSProperties,
Expand All @@ -18,6 +18,10 @@ export type TypedEasing = NonNullable<
Exclude<CSSProperties["animationTimingFunction"], CSSProperties["all"]>
>;

export type GetKeyframeFunction<Args = void> = Args extends void
? (prev: CSSStyleDeclaration) => TypedKeyframe[]
: (prev: CSSStyleDeclaration, args: Args) => TypedKeyframe[];

export interface AnimationOptions
extends Omit<KeyframeEffectOptions, "easing"> {
easing?: TypedEasing;
Expand Down Expand Up @@ -56,6 +60,8 @@ export const createAnimation = (

export type PlayOptions = { reset?: boolean };

export type ReverseOptions = {};

export const _play = (animation: Animation, opts: PlayOptions = {}) => {
if (opts.reset) {
animation.currentTime = 0;
Expand All @@ -77,24 +83,24 @@ export const _pause = (animation: Animation | undefined) => {
if (!animation) return;
animation.pause();
};
export const _persist = (
animation: Animation | undefined,
el: Element,
keyframes: TypedKeyframe[]
) => {
if (!animation) return;
// https://www.w3.org/TR/web-animations-1/#fill-behavior
if (animation.commitStyles) {
animation.commitStyles();
} else {
// Fallback for commitStyles
const computedStyle = getStyle(el);
getKeyframeKeys(keyframes).forEach((k) => {
((el as HTMLElement).style as any)[k] = (computedStyle as any)[k];
});
}
animation.cancel();
};
// export const _persist = (
// animation: Animation | undefined,
// el: Element,
// keyframes: TypedKeyframe[]
// ) => {
// if (!animation) return;
// // https://www.w3.org/TR/web-animations-1/#fill-behavior
// if (animation.commitStyles) {
// animation.commitStyles();
// } else {
// // Fallback for commitStyles
// const computedStyle = getStyle(el);
// getKeyframeKeys(keyframes).forEach((k) => {
// ((el as HTMLElement).style as any)[k] = (computedStyle as any)[k];
// });
// }
// animation.cancel();
// };
export const _setTime = (animation: Animation | undefined, time: number) => {
if (!animation) return;
animation.currentTime = time;
Expand Down
15 changes: 4 additions & 11 deletions src/react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
export { useAnimation } from "./useAnimation";
export type { AnimationHandle } from "./useAnimation";
export { useAnimationController } from "./useAnimationController";
export type { AnimationController } from "./useAnimationController";
export { useAnimationFunction } from "./useAnimationFunction";
export type {
AnimationFunctionHandle,
AnimationFunction,
ComputedTimingContext,
} from "./useAnimationFunction";
export { useTransitionAnimation } from "./useTransitionAnimation";
export * from "./useAnimation";
export * from "./useAnimationController";
export * from "./useAnimationFunction";
export * from "./useTransitionAnimation";
201 changes: 105 additions & 96 deletions src/react/hooks/useAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import type { Expand } from "../../core/types";
import {
assign,
getStyle,
Expand All @@ -9,7 +10,9 @@ import {
import {
AnimationOptions,
createAnimation,
GetKeyframeFunction,
PlayOptions,
ReverseOptions,
TypedKeyframe,
_cancel,
_end,
Expand All @@ -22,119 +25,125 @@ import {
} from "../../core/waapi";
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";

export type AnimationHandle = {
type PlayArgs<Args = void> = Args extends void
? [PlayOptions?]
: [Expand<PlayOptions & (Args extends void ? {} : { args: Args })>];

type ReverseArgs<Args = void> = Args extends void
? [ReverseOptions?]
: [Expand<ReverseOptions & (Args extends void ? {} : { args: Args })>];

export type AnimationHandle<Args = void> = {
(ref: Element | null): void;
play: (opts?: PlayOptions) => AnimationHandle;
reverse: () => AnimationHandle;
cancel: () => AnimationHandle;
finish: () => AnimationHandle;
pause: () => AnimationHandle;
setTime: (time: number) => AnimationHandle;
play: (...opts: PlayArgs<Args>) => AnimationHandle<Args>;
reverse: (...opts: ReverseArgs<Args>) => AnimationHandle<Args>;
cancel: () => AnimationHandle<Args>;
finish: () => AnimationHandle<Args>;
pause: () => AnimationHandle<Args>;
setTime: (time: number) => AnimationHandle<Args>;
setPlaybackRate: (
rate: number | ((prevRate: number) => number)
) => AnimationHandle;
end: () => Promise<AnimationHandle>;
) => AnimationHandle<Args>;
end: () => Promise<AnimationHandle<Args>>;
};

export const useAnimation = (
keyframe:
| TypedKeyframe
| TypedKeyframe[]
| ((prev: CSSStyleDeclaration) => TypedKeyframe[]),
export const useAnimation = <Args = void>(
keyframe: TypedKeyframe | TypedKeyframe[] | GetKeyframeFunction<Args>,
options?: AnimationOptions
): AnimationHandle => {
): AnimationHandle<Args> => {
const keyframeRef = useRef(keyframe);
const optionsRef = useRef(options);

const [animation, cleanup] = useState<[AnimationHandle, () => void]>(() => {
let target: Element | null = null;
const [animation, cleanup] = useState<[AnimationHandle<Args>, () => void]>(
() => {
let target: Element | null = null;

const getTarget = () => target;
const getKeyframes = () => {
if (typeof keyframeRef.current === "function") {
return keyframeRef.current(getStyle(getTarget()!));
}
return toArray(keyframeRef.current);
};
const getOptions = () => optionsRef.current;
const getTarget = () => target;
const getKeyframes = (args: Args) => {
if (typeof keyframeRef.current === "function") {
return keyframeRef.current(getStyle(getTarget()!), args);
}
return toArray(keyframeRef.current);
};

let cache:
| [
animation: Animation,
el: Element,
keyframes: TypedKeyframe[],
options: AnimationOptions | undefined
]
| undefined;
const initAnimation = (): Animation => {
const keyframes = getKeyframes();
const options = getOptions();
const el = getTarget()!;
if (cache) {
const [prevAnimation, prevEl, prevKeyframes, prevOptions] = cache;
if (
el === prevEl &&
isSameObjectArray(keyframes, prevKeyframes) &&
isSameObject(options, prevOptions)
) {
return prevAnimation;
let cache:
| [
animation: Animation,
el: Element,
keyframes: TypedKeyframe[],
options: AnimationOptions | undefined
]
| undefined;
const initAnimation = (opts: { args?: Args } = {}): Animation => {
const keyframes = getKeyframes(opts.args!);
const options = optionsRef.current;
const el = getTarget()!;
if (cache) {
const [prevAnimation, prevEl, prevKeyframes, prevOptions] = cache;
if (
el === prevEl &&
isSameObjectArray(keyframes, prevKeyframes) &&
isSameObject(options, prevOptions)
) {
return prevAnimation;
}
prevAnimation.cancel();
}
prevAnimation.cancel();
}
const animation = createAnimation(el, keyframes as Keyframe[], options);
cache = [animation, el, keyframes, options];
return animation;
};
const getAnimation = () => cache?.[0];
const animation = createAnimation(el, keyframes as Keyframe[], options);
cache = [animation, el, keyframes, options];
return animation;
};
const getAnimation = () => cache?.[0];

const cancel = () => {
_cancel(getAnimation());
};
const cancel = () => {
_cancel(getAnimation());
};

const externalHandle: AnimationHandle = assign(
(ref: Element | null) => {
target = ref;
},
{
play: (opts?: PlayOptions) => {
_play(initAnimation(), opts);
return externalHandle;
const externalHandle: AnimationHandle<Args> = assign(
(ref: Element | null) => {
target = ref;
},
reverse: () => {
_reverse(initAnimation());
return externalHandle;
},
cancel: () => {
{
play: (...opts: PlayArgs<Args>) => {
_play(initAnimation(opts[0] as { args?: Args }), opts[0]);
return externalHandle;
},
reverse: (...opts: ReverseArgs<Args>) => {
_reverse(initAnimation(opts[0]));
return externalHandle;
},
cancel: () => {
cancel();
return externalHandle;
},
finish: () => {
_finish(getAnimation());
return externalHandle;
},
pause: () => {
_pause(getAnimation());
return externalHandle;
},
setTime: (time: number) => {
_setTime(getAnimation(), time);
return externalHandle;
},
setPlaybackRate: (rate: number | ((prevRate: number) => number)) => {
_setRate(getAnimation(), rate);
return externalHandle;
},
end: () => _end(getAnimation()).then(() => externalHandle),
}
);

return [
externalHandle,
() => {
cancel();
return externalHandle;
},
finish: () => {
_finish(getAnimation());
return externalHandle;
},
pause: () => {
_pause(getAnimation());
return externalHandle;
},
setTime: (time: number) => {
_setTime(getAnimation(), time);
return externalHandle;
},
setPlaybackRate: (rate: number | ((prevRate: number) => number)) => {
_setRate(getAnimation(), rate);
return externalHandle;
},
end: () => _end(getAnimation()).then(() => externalHandle),
}
);

return [
externalHandle,
() => {
cancel();
},
];
})[0];
];
}
)[0];

useIsomorphicLayoutEffect(() => {
keyframeRef.current = keyframe;
Expand Down
Loading

0 comments on commit e8ec7c9

Please sign in to comment.