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 "./BottomSheet.css";
|
||||||
import { useHeroSignal } from "../platform/anim";
|
import { useHeroSignal } from "../platform/anim";
|
||||||
import material from "./material.module.css";
|
import material from "./material.module.css";
|
||||||
|
import { ANIM_CURVE_ACELERATION, ANIM_CURVE_DECELERATION } from "./theme";
|
||||||
|
import { animateSlideInFromRight, animateSlideOutToRight } from "../platform/anim";
|
||||||
|
|
||||||
export type BottomSheetProps = {
|
export type BottomSheetProps = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
|
@ -39,7 +41,7 @@ function composeAnimationFrame(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOVE_SPEED = 1200;
|
const MOVE_SPEED = 1600;
|
||||||
|
|
||||||
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
let element: HTMLDialogElement;
|
let element: HTMLDialogElement;
|
||||||
|
@ -87,11 +89,16 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const animation = props.bottomUp
|
const onAnimationEnd = () => {
|
||||||
|
element.classList.remove("animated")
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
element.classList.add("animated")
|
||||||
|
animation = props.bottomUp
|
||||||
? animateSlideInFromBottom(element, true)
|
? animateSlideInFromBottom(element, true)
|
||||||
: animateSlideInFromRight(element, true);
|
: animateSlideOutToRight(element, { easing: ANIM_CURVE_ACELERATION });
|
||||||
animation.addEventListener("finish", onClose);
|
animation.addEventListener("finish", onAnimationEnd);
|
||||||
animation.addEventListener("cancel", onClose);
|
animation.addEventListener("cancel", onAnimationEnd);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,35 +116,14 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
} else if (props.bottomUp) {
|
} else if (props.bottomUp) {
|
||||||
animateSlideInFromBottom(element);
|
animateSlideInFromBottom(element);
|
||||||
} else if (window.innerWidth <= 560) {
|
} else if (window.innerWidth <= 560) {
|
||||||
animateSlideInFromRight(element);
|
element.classList.add("animated")
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = () => {
|
const onAnimationEnd = () => {
|
||||||
element.classList.remove("animated");
|
element.classList.remove("animated")
|
||||||
document.body.style.overflow = oldOverflow;
|
}
|
||||||
animation = undefined;
|
animation = animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION });
|
||||||
};
|
animation.addEventListener("finish", onAnimationEnd)
|
||||||
animation.addEventListener("cancel", onAnimationEnd);
|
animation.addEventListener("cancel", onAnimationEnd)
|
||||||
animation.addEventListener("finish", onAnimationEnd);
|
}
|
||||||
return animation;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateSlideInFromBottom = (
|
const animateSlideInFromBottom = (
|
||||||
|
|
|
@ -195,3 +195,105 @@ export function animateShrinkToTopRight(
|
||||||
|
|
||||||
return animation;
|
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…
Reference in a new issue