BottomSheet: move slides animations to platform
This commit is contained in:
		
							parent
							
								
									10b517bceb
								
							
						
					
					
						commit
						b61012f12b
					
				
					 2 changed files with 121 additions and 33 deletions
				
			
		| 
						 | 
				
			
			@ -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<BottomSheetProps> = (props) => {
 | 
			
		||||
  let element: HTMLDialogElement;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,11 +89,16 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (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<BottomSheetProps> = (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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue