diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 130a7a2..2ebed75 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -11,6 +11,15 @@ import { import "./BottomSheet.css"; import { useHeroSignal } from "../platform/anim"; import material from "./material.module.css"; +import { + ANIM_CURVE_ACELERATION, + ANIM_CURVE_DECELERATION, + ANIM_CURVE_STD, +} from "./theme"; +import { + animateSlideInFromRight, + animateSlideOutToRight, +} from "../platform/anim"; export type BottomSheetProps = { open?: boolean; @@ -39,7 +48,7 @@ function composeAnimationFrame( }; } -const MOVE_SPEED = 1200; +const MOVE_SPEED = 1600; const BottomSheet: ParentComponent = (props) => { let element: HTMLDialogElement; @@ -87,11 +96,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 +123,18 @@ 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, @@ -176,13 +171,19 @@ const BottomSheet: ParentComponent = (props) => { element: HTMLElement, reserve?: boolean, ) => { - const easing = "cubic-bezier(0.4, 0, 0.2, 1)"; + const easing = ANIM_CURVE_STD; element.classList.add("animated"); - const distance = Math.sqrt( + // distance_lt = (|top_start - top_end|^2 + |left_end - left_end|^2)^(-2) + const distancelt = Math.sqrt( Math.pow(Math.abs(startRect.top - endRect.top), 2) + - Math.pow(Math.abs(startRect.left - startRect.top), 2), + Math.pow(Math.abs(startRect.left - endRect.left), 2), ); - const duration = (distance / MOVE_SPEED) * 1000; + const distancerb = Math.sqrt( + Math.pow(Math.abs(startRect.bottom - endRect.bottom), 2) + + Math.pow(Math.abs(startRect.right - endRect.right), 2), + ); + const distance = distancelt + distancerb; + const duration = distance / 1.6; animation = element.animate( [ composeAnimationFrame(startRect, { transform: "none" }), diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx index 04b8d86..a80a423 100644 --- a/src/material/Menu.tsx +++ b/src/material/Menu.tsx @@ -13,10 +13,12 @@ import { animateShrinkToTopRight, } from "../platform/anim"; +type Anchor = Pick + type Props = { open?: boolean; onClose?: JSX.EventHandlerUnion; - anchor: () => DOMRect; + anchor: () => Anchor; }; function px(n?: number) { diff --git a/src/platform/anim.ts b/src/platform/anim.ts index 3dd0ead..dfffc87 100644 --- a/src/platform/anim.ts +++ b/src/platform/anim.ts @@ -49,7 +49,7 @@ export function useHeroSignal( return [get, set]; } else { - console.debug("no hero source") + console.debug("no hero source"); return [() => undefined, () => undefined]; } } @@ -85,7 +85,6 @@ export function animateRollOutFromTop( return animation; } - export function animateRollInFromBottom( root: HTMLElement, options?: Omit, @@ -126,8 +125,10 @@ export function animateGrowFromTopRight( const { width, height } = root.getBoundingClientRect(); - const durationX = Math.floor((height / 1600) * 1000); - const durationY = Math.floor((width / 1600) * 1000); + 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)% @@ -137,20 +138,19 @@ export function animateGrowFromTopRight( const centerOffset = minDuration / maxDuration; const keyframes = [ - { transform: "scaleX(0)", opacity: 0, height: "0px", offset: 0 }, + { transform: "scaleX(0.5)", opacity: 0, height: "0px", offset: 0 }, { - transform: `scaleX(${minDuration === durationX ? "1" : centerOffset})`, + transform: `scaleX(${minDuration === durationX ? "1" : centerOffset / 2 + 0.5})`, height: `${(minDuration === durationY ? 1 : centerOffset) * height}px`, offset: centerOffset, - opacity: 1, }, - { transform: "scaleX(1)", height: `${height}px`, offset: 1 }, + { transform: "scaleX(1)", height: `${height}px`, opacity: 1, offset: 1 }, ]; - const animation = root.animate( - keyframes, - { ...options, duration: maxDuration }, - ); + const animation = root.animate(keyframes, { + ...options, + duration: maxDuration, + }); const restore = () => { root.style.transformOrigin = transformOrigin; @@ -173,9 +173,9 @@ export function animateShrinkToTopRight( const { width, height } = root.getBoundingClientRect(); - const duration = Math.floor( - Math.max((width / 1600) * 1000, (height / 1600) * 1000), - ); + const speed = transitionSpeedForLeave(window.innerWidth); + + const duration = Math.floor(Math.max(width / speed, height / speed)); const animation = root.animate( { @@ -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; +} diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index 9015e8b..4becc5b 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -26,10 +26,14 @@ import { Close, Edit, ExpandMore, + Group, MoreVert, OpenInBrowser, + PersonOff, + PlaylistAdd, Send, Share, + Translate, Verified, } from "@suid/icons-material"; import { Title } from "../material/typography"; @@ -90,6 +94,10 @@ const Profile: Component = () => { console.error(err); }); + const isCurrentSessionProfile = () => { + return session().account?.inf?.url === profile()?.url; + }; + const [recentTootFilter, setRecentTootFilter] = createSignal({ pinned: true, boost: false, @@ -261,18 +269,48 @@ const Profile: Component = () => { document.getElementById(menuButId)!.getBoundingClientRect() } > + + + + + Subscribe... + + } + > + + + + + Edit... + + + - + - Edit... + Subscribers + + + + + + Blocklist + + + + + + Translate Name and Bio... - - Mention {profile()?.displayName || ""}... + Mention in... { - + 0}> >(props: Props) { ); }; + let anchor: { left: number; top: number; right: number }; + + const onClick = (event: MouseEvent) => { + anchor = { + left: event.clientX, + right: event.clientX, + top: event.clientY, + }; + setOpen(true); + }; + return ( <> - - - document.getElementById(buttonId)!.getBoundingClientRect() - } - > + anchor}> {(item, idx) => ( <>