299 lines
7.3 KiB
TypeScript
299 lines
7.3 KiB
TypeScript
import {
|
|
createContext,
|
|
createRenderEffect,
|
|
createSignal,
|
|
untrack,
|
|
useContext,
|
|
type Accessor,
|
|
type Signal,
|
|
} from "solid-js";
|
|
|
|
export type HeroSource = {
|
|
[key: string | symbol | number]: HTMLElement | undefined;
|
|
};
|
|
|
|
const HeroSourceContext = createContext<Signal<HeroSource>>(
|
|
/* __@PURE__ */ undefined,
|
|
);
|
|
|
|
export const HeroSourceProvider = HeroSourceContext.Provider;
|
|
|
|
export function useHeroSource() {
|
|
return useContext(HeroSourceContext);
|
|
}
|
|
|
|
/**
|
|
* Use hero value for the {@link key}.
|
|
*
|
|
* 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.
|
|
*/
|
|
export function useHeroSignal(
|
|
key: string | symbol | number,
|
|
): Signal<HTMLElement | undefined> {
|
|
const source = useHeroSource();
|
|
if (source) {
|
|
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 {
|
|
console.debug("no hero source");
|
|
return [() => undefined, () => undefined];
|
|
}
|
|
}
|
|
|
|
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 speed = transitionSpeedForEnter(window.innerHeight);
|
|
|
|
const durationX = Math.floor(height / speed);
|
|
const durationY = Math.floor(width / speed);
|
|
|
|
// 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.5)", opacity: 0, height: "0px", offset: 0 },
|
|
{
|
|
transform: `scaleX(${minDuration === durationX ? "1" : centerOffset / 2 + 0.5})`,
|
|
height: `${(minDuration === durationY ? 1 : centerOffset) * height}px`,
|
|
offset: centerOffset,
|
|
},
|
|
{ transform: "scaleX(1)", height: `${height}px`, opacity: 1, 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 speed = transitionSpeedForLeave(window.innerWidth);
|
|
|
|
const duration = Math.floor(Math.max(width / speed, height / speed));
|
|
|
|
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;
|
|
}
|
|
|
|
// Contribution to the animation speed:
|
|
// - the screen size: mobiles should have longer transition,
|
|
// the transition time should be longer as the travelling distance longer,
|
|
// but it's not linear. The larger screen should have higher velocity,
|
|
// to avoid the transition is too long.
|
|
// As the screen larger, on desktops, the transition should be simpler and
|
|
// signficantly faster.
|
|
// On much smaller screens, like wearables, the transition should be shorter
|
|
// than on mobiles.
|
|
// - Animation complexity: On mobile:
|
|
// - large, complex, full-screen transitions may have longer durations, over 375ms
|
|
// - entering screen over 225ms
|
|
// - leaving screen over 195ms
|
|
|
|
function transitionSpeedForEnter(innerWidth: number) {
|
|
if (innerWidth < 300) {
|
|
return 2.4;
|
|
} else if (innerWidth < 560) {
|
|
return 1.6;
|
|
} else if (innerWidth < 1200) {
|
|
return 2.4;
|
|
} else {
|
|
return 2.55;
|
|
}
|
|
}
|
|
|
|
function transitionSpeedForLeave(innerWidth: number) {
|
|
if (innerWidth < 300) {
|
|
return 2.8;
|
|
} else if (innerWidth < 560) {
|
|
return 1.96;
|
|
} else if (innerWidth < 1200) {
|
|
return 2.8;
|
|
} else {
|
|
return 2.55;
|
|
}
|
|
}
|
|
|
|
export function animateSlideInFromRight(
|
|
root: HTMLElement,
|
|
options?: Omit<KeyframeAnimationOptions, "duration">,
|
|
) {
|
|
const { left } = root.getBoundingClientRect();
|
|
const { innerWidth } = window;
|
|
|
|
const oldOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = "hidden";
|
|
|
|
const distance = Math.abs(left - innerWidth);
|
|
const duration = Math.floor(distance / transitionSpeedForEnter(innerWidth));
|
|
|
|
const opts = Object.assign({ duration }, options);
|
|
|
|
const animation = root.animate(
|
|
{
|
|
left: [`${innerWidth}px`, `${left}px`],
|
|
},
|
|
opts,
|
|
);
|
|
|
|
const restore = () => {
|
|
document.body.style.overflow = oldOverflow;
|
|
};
|
|
|
|
animation.addEventListener("cancel", restore);
|
|
animation.addEventListener("finish", restore);
|
|
|
|
return animation;
|
|
}
|
|
|
|
export function animateSlideOutToRight(
|
|
root: HTMLElement,
|
|
options?: Omit<KeyframeAnimationOptions, "duration">,
|
|
) {
|
|
const { left } = root.getBoundingClientRect();
|
|
const { innerWidth } = window;
|
|
|
|
const oldOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = "hidden";
|
|
|
|
const distance = Math.abs(left - innerWidth);
|
|
const duration = Math.floor(distance / transitionSpeedForLeave(innerWidth));
|
|
|
|
const opts = Object.assign({ duration }, options);
|
|
|
|
const animation = root.animate(
|
|
{
|
|
left: [`${left}px`, `${innerWidth}px`],
|
|
},
|
|
opts,
|
|
);
|
|
|
|
const restore = () => {
|
|
document.body.style.overflow = oldOverflow;
|
|
};
|
|
|
|
animation.addEventListener("cancel", restore);
|
|
animation.addEventListener("finish", restore);
|
|
|
|
return animation;
|
|
}
|