tutu/src/platform/anim.ts

198 lines
4.6 KiB
TypeScript
Raw Normal View History

import {
createContext,
createRenderEffect,
createSignal,
untrack,
useContext,
type Accessor,
type Signal,
} from "solid-js";
2024-07-14 20:28:44 +08:00
2024-08-05 15:33:00 +08:00
export type HeroSource = {
2024-10-09 17:33:27 +08:00
[key: string | symbol | number]: HTMLElement | undefined;
2024-08-05 15:33:00 +08:00
};
2024-07-14 20:28:44 +08:00
const HeroSourceContext = createContext<Signal<HeroSource>>(
/* __@PURE__ */ undefined,
);
2024-07-14 20:28:44 +08:00
2024-08-05 15:33:00 +08:00
export const HeroSourceProvider = HeroSourceContext.Provider;
2024-07-14 20:28:44 +08:00
2024-10-29 15:22:25 +08:00
export function useHeroSource() {
2024-08-05 15:33:00 +08:00
return useContext(HeroSourceContext);
2024-07-14 20:28:44 +08:00
}
2024-08-12 21:55:26 +08:00
/**
* Use hero value for the {@link key}.
2024-10-29 15:22:25 +08:00
*
* Note: the setter here won't set the value of the hero source.
* To set hero source, use {@link useHeroSource} and set the corresponding key.
2024-08-12 21:55:26 +08:00
*/
export function useHeroSignal(
key: string | symbol | number,
2024-10-09 17:33:27 +08:00
): Signal<HTMLElement | undefined> {
const source = useHeroSource();
if (source) {
2024-10-09 17:33:27 +08:00
const [get, set] = createSignal<HTMLElement>();
createRenderEffect(() => {
const value = source[0]();
if (value[key]) {
set(value[key]);
source[1]((x) => {
const cpy = Object.assign({}, x);
delete cpy[key];
return cpy;
});
}
});
return [get, set];
} else {
2024-10-29 15:22:25 +08:00
console.debug("no hero source")
return [() => undefined, () => undefined];
}
}
2024-11-02 16:45:18 +08:00
export function animateRollOutFromTop(
root: HTMLElement,
options?: Omit<KeyframeAnimationOptions, "duration">,
) {
const overflow = root.style.overflow;
root.style.overflow = "hidden";
const { height } = root.getBoundingClientRect();
const opts = Object.assign(
{
duration: Math.floor((height / 1600) * 1000),
},
options,
);
const animation = root.animate(
{
height: ["0px", `${height}px`],
},
opts,
);
const restore = () => (root.style.overflow = overflow);
animation.addEventListener("finish", restore);
animation.addEventListener("cancel", restore);
return animation;
}
export function animateRollInFromBottom(
root: HTMLElement,
options?: Omit<KeyframeAnimationOptions, "duration">,
) {
const overflow = root.style.overflow;
root.style.overflow = "hidden";
const { height } = root.getBoundingClientRect();
const opts = Object.assign(
{
duration: Math.floor((height / 1600) * 1000),
},
options,
);
const animation = root.animate(
{
height: [`${height}px`, "0px"],
},
opts,
);
const restore = () => (root.style.overflow = overflow);
animation.addEventListener("finish", restore);
animation.addEventListener("cancel", restore);
return animation;
}
export function animateGrowFromTopRight(
root: HTMLElement,
options?: KeyframeAnimationOptions,
) {
const transformOrigin = root.style.transformOrigin;
root.style.transformOrigin = "top right";
const { width, height } = root.getBoundingClientRect();
const durationX = Math.floor((height / 1600) * 1000);
const durationY = Math.floor((width / 1600) * 1000);
// finds the offset for the center frame,
// it will stops at the (minDuration / maxDuration)%
const minDuration = Math.min(durationX, durationY);
const maxDuration = Math.max(durationX, durationY);
const centerOffset = minDuration / maxDuration;
const keyframes = [
{ transform: "scaleX(0)", opacity: 0, height: "0px", offset: 0 },
{
transform: `scaleX(${minDuration === durationX ? "1" : centerOffset})`,
height: `${(minDuration === durationY ? 1 : centerOffset) * height}px`,
offset: centerOffset,
opacity: 1,
},
{ transform: "scaleX(1)", height: `${height}px`, offset: 1 },
];
const animation = root.animate(
keyframes,
{ ...options, duration: maxDuration },
);
const restore = () => {
root.style.transformOrigin = transformOrigin;
};
animation.addEventListener("cancel", restore);
animation.addEventListener("finish", restore);
return animation;
}
export function animateShrinkToTopRight(
root: HTMLElement,
options?: KeyframeAnimationOptions,
) {
const overflow = root.style.overflow;
root.style.overflow = "hidden";
const transformOrigin = root.style.transformOrigin;
root.style.transformOrigin = "top right";
const { width, height } = root.getBoundingClientRect();
const duration = Math.floor(
Math.max((width / 1600) * 1000, (height / 1600) * 1000),
);
const animation = root.animate(
{
transform: ["scale(1)", "scale(0.5)"],
opacity: [1, 0],
},
{ ...options, duration },
);
const restore = () => {
root.style.overflow = overflow;
root.style.transformOrigin = transformOrigin;
};
animation.addEventListener("cancel", restore);
animation.addEventListener("finish", restore);
return animation;
}