diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 130a7a2..1258c81 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -11,6 +11,8 @@ import { import "./BottomSheet.css"; import { useHeroSignal } from "../platform/anim"; import material from "./material.module.css"; +import { ANIM_CURVE_ACELERATION, ANIM_CURVE_DECELERATION } from "./theme"; +import { animateSlideInFromRight, animateSlideOutToRight } from "../platform/anim"; export type BottomSheetProps = { open?: boolean; @@ -39,7 +41,7 @@ function composeAnimationFrame( }; } -const MOVE_SPEED = 1200; +const MOVE_SPEED = 1600; const BottomSheet: ParentComponent = (props) => { let element: HTMLDialogElement; @@ -87,11 +89,16 @@ const BottomSheet: ParentComponent = (props) => { onClose(); return; } - const animation = props.bottomUp + const onAnimationEnd = () => { + element.classList.remove("animated") + onClose() + } + element.classList.add("animated") + animation = props.bottomUp ? animateSlideInFromBottom(element, true) - : animateSlideInFromRight(element, true); - animation.addEventListener("finish", onClose); - animation.addEventListener("cancel", onClose); + : animateSlideOutToRight(element, { easing: ANIM_CURVE_ACELERATION }); + animation.addEventListener("finish", onAnimationEnd); + animation.addEventListener("cancel", onAnimationEnd); } }; @@ -109,37 +116,16 @@ const BottomSheet: ParentComponent = (props) => { } else if (props.bottomUp) { animateSlideInFromBottom(element); } else if (window.innerWidth <= 560) { - animateSlideInFromRight(element); + element.classList.add("animated") + const onAnimationEnd = () => { + element.classList.remove("animated") + } + animation = animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION }); + animation.addEventListener("finish", onAnimationEnd) + animation.addEventListener("cancel", onAnimationEnd) } }; - const animateSlideInFromRight = (element: HTMLElement, reserve?: boolean) => { - const rect = element.getBoundingClientRect(); - const easing = "cubic-bezier(0.4, 0, 0.2, 1)"; - element.classList.add("animated"); - const oldOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - const distance = Math.abs(rect.left - window.innerWidth); - const duration = (distance / MOVE_SPEED) * 1000; - - animation = element.animate( - { - left: reserve - ? [`${rect.left}px`, `${window.innerWidth}px`] - : [`${window.innerWidth}px`, `${rect.left}px`], - }, - { easing, duration }, - ); - const onAnimationEnd = () => { - element.classList.remove("animated"); - document.body.style.overflow = oldOverflow; - animation = undefined; - }; - animation.addEventListener("cancel", onAnimationEnd); - animation.addEventListener("finish", onAnimationEnd); - return animation; - }; - const animateSlideInFromBottom = ( element: HTMLElement, reserve?: boolean, diff --git a/src/platform/anim.ts b/src/platform/anim.ts index 3dd0ead..3369e09 100644 --- a/src/platform/anim.ts +++ b/src/platform/anim.ts @@ -195,3 +195,105 @@ export function animateShrinkToTopRight( 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; +}