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>( /* __@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 { const source = useHeroSource(); if (source) { const [get, set] = createSignal(); 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, ) { 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, ) { 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, ) { 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, ) { 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; }