Skip to content

Instantly share code, notes, and snippets.

@snuffyDev
Created August 25, 2023 23:31
Show Gist options
  • Save snuffyDev/760aeba718593caa51be7e81e9726034 to your computer and use it in GitHub Desktop.
Save snuffyDev/760aeba718593caa51be7e81e9726034 to your computer and use it in GitHub Desktop.
import { EasingFactory, EasingFunction, linear } from "./easing";
import { MotionPath, PathTweenOptions } from "./motionPath";
import type { KeyframeWithTransform } from "./types";
import { skipFirstInvocation } from "./utils/function";
import { is } from "./utils/is";
// @ts-expect-error idk what broke but smth did
export interface TweenOptions extends KeyframeEffectOptions {
composite?: CompositeOperation;
delay?: number;
direction?: "normal" | "reverse" | "alternate" | "alternate-reverse";
duration: number;
fill?: "none" | "forwards" | "backwards" | "both";
iterations?: number | "infinite";
playbackRate?: number;
easing?: string | EasingFactory;
}
export type CompositeOperation =
| "replace"
| "add"
| "accumulate"
| "auto"
| "none";
/**
* Helper function for getting the CSS easing function from an easing factory or from a string
*
* If the easing is a string, it's assumed to be a CSS easing function and is returned as-is.
* @internal
* @param easing Easing function or string
* @returns CSS easing function (string)
*/
const getEasingFunction = (easing: string | EasingFactory): string => {
if (is<EasingFactory>(easing, (thing) => typeof thing === "object")) {
return easing.css;
}
return easing;
};
const createKeyframeEffect = (
element: Element,
keyframes: KeyframeWithTransform[],
options: TweenOptions,
): KeyframeEffect => {
const effect = new KeyframeEffect(
element,
keyframes as Keyframe[],
options as KeyframeAnimationOptions,
);
return effect;
};
const tick = () =>
new Promise<number>((resolve) => requestAnimationFrame(resolve));
const pathTween = (
element: Element,
keyframes: KeyframeWithTransform[],
options: PathTweenOptions,
) => {
// TODO: use this somehow
let easing: string | EasingFunction = "linear";
if (options.easing) {
const easingFactory = options.easing;
if (typeof easingFactory === "object") {
options.easing = easingFactory.css;
easing = easingFactory.calc;
} else if (typeof easingFactory === "string") {
easing = easingFactory;
} else {
easing = "linear";
}
}
const motionPath = new MotionPath(element as HTMLElement, options.path, {
...options,
});
motionPath.anchor = options.anchor ? options.anchor : "auto";
const frames = motionPath.build(
keyframes,
typeof options.easing === "object" ? options.easing : linear,
);
const effect = createKeyframeEffect(element, frames, {
...options,
easing: options.easing ? getEasingFunction(options.easing) : "linear",
});
const animation = new Animation(effect);
const createResizeHandler = skipFirstInvocation(async () => {
await tick();
const newFrames = motionPath.build(keyframes, options.easing);
effect.setKeyframes(newFrames as Keyframe[]);
const currentTime = animation.currentTime;
const playState = animation.playState;
if (playState === "running") {
animation.pause();
}
animation.effect = effect;
if (playState === "running") {
animation.currentTime = currentTime;
animation.play();
}
});
return {
get config() {
return options;
},
play: () => {
animation.play();
},
pause: () => {
animation.pause();
},
cancel: () => {
animation.cancel();
},
finish: () => {
animation.finish();
},
finished: () => {
return animation.finished.finally(() => {
createResizeHandler();
return Promise.resolve(animation);
});
},
onResize: () => {
return createResizeHandler();
},
};
};
export const tween = (
element: Element,
keyframes: KeyframeWithTransform[],
options: TweenOptions | PathTweenOptions,
) => {
if ("path" in options) {
return pathTween(element, keyframes, options);
}
const effect = createKeyframeEffect(element, keyframes, options);
const animation = new Animation(effect, document.timeline);
return {
get config() {
return options;
},
play: () => {
animation.play();
},
pause: () => {
animation.pause();
},
cancel: () => {
animation.cancel();
},
finish: () => {
animation.finish();
},
finished: () => {
return animation.finished;
},
onResize: () => {
return;
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment