diff --git a/src/platform/StackedRouter.css b/src/platform/StackedRouter.css index 5dae005..6b912c1 100644 --- a/src/platform/StackedRouter.css +++ b/src/platform/StackedRouter.css @@ -24,12 +24,8 @@ dialog.StackedPage { background: var(--tutu-color-surface); box-shadow: var(--tutu-shadow-e16); - @media (min-width: 560px) { - & { - left: 50%; - transform: translateX(-50%); - } - } + margin-left: auto; + margin-right: auto; @media (max-width: 560px) { & { @@ -50,4 +46,12 @@ dialog.StackedPage { &::backdrop { background: none; } + + &.animating { + overflow: hidden; + + * { + overflow: hidden; + } + } } \ No newline at end of file diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index 7325d7e..792ec97 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -6,6 +6,7 @@ import { createRenderEffect, createUniqueId, Index, + onMount, Show, untrack, useContext, @@ -15,9 +16,7 @@ import { createStore, unwrap } from "solid-js/store"; import "./StackedRouter.css"; import { animateSlideInFromRight, animateSlideOutToRight } from "./anim"; import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme"; -import { - makeEventListener, -} from "@solid-primitives/event-listener"; +import { makeEventListener } from "@solid-primitives/event-listener"; export type StackedRouterProps = Omit; @@ -25,6 +24,9 @@ export type StackFrame = { path: string; rootId: string; state: unknown; + + animateOpen?: (element: HTMLElement) => Animation; + animateClose?: (element: HTMLElement) => Animation; }; export type NewFrameOptions = (T extends undefined @@ -33,6 +35,8 @@ export type NewFrameOptions = (T extends undefined } : { state: T }) & { replace?: boolean; + animateOpen?: StackFrame["animateOpen"]; + animateClose?: StackFrame["animateClose"]; }; export type FramePusher = T[K] extends @@ -117,9 +121,11 @@ function animateClose(element: HTMLElement) { function animateOpen(element: HTMLElement) { if (window.innerWidth <= 560) { - animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION }); + return animateSlideInFromRight(element, { + easing: ANIM_CURVE_DECELERATION, + }); } else { - element.animate( + return element.animate( { opacity: [0.5, 1], }, @@ -128,6 +134,17 @@ function animateOpen(element: HTMLElement) { } } +function serializableStack(stack: readonly StackFrame[]) { + const frames = unwrap(stack); + return frames.map((fr) => { + return { + path: fr.path, + rootId: fr.rootId, + state: fr.state, + }; + }); +} + /** * The router that stacks the pages. */ @@ -140,13 +157,15 @@ const StackedRouter: Component = (oprops) => { path, state: opts?.state, rootId: createUniqueId(), + animateOpen: opts?.animateOpen, + animateClose: opts?.animateClose, }; mutStack(opts?.replace ? stack.length - 1 : stack.length, frame); if (opts?.replace) { - window.history.replaceState(unwrap(stack), "", path); + window.history.replaceState(serializableStack(stack), "", path); } else { - window.history.pushState(unwrap(stack), "", path); + window.history.pushState(serializableStack(stack), "", path); } return frame; }); @@ -165,10 +184,17 @@ const StackedRouter: Component = (oprops) => { } if (stack.length > 1) { const lastFrame = stack[stack.length - 1]; - const element = document.getElementById(lastFrame.rootId)!; + const element = document.getElementById( + lastFrame.rootId, + )! as HTMLDialogElement; + const createAnimation = lastFrame.animateClose ?? animateClose; requestAnimationFrame(() => { - const animation = animateClose(element); - animation.addEventListener("finish", () => onlyPopFrame(depth)); + element.classList.add("animating") + const animation = createAnimation(element); + animation.addEventListener("finish", () => { + element.classList.remove("animating") + onlyPopFrame(depth); + }); }); } else { onlyPopFrame(depth); @@ -195,10 +221,16 @@ const StackedRouter: Component = (oprops) => { }); const onBeforeDialogMount = (element: HTMLDialogElement) => { - createEffect(() => { + onMount(() => { + const lastFr = untrack(() => stack[stack.length - 1]); + const createAnimation = lastFr.animateOpen ?? animateOpen; requestAnimationFrame(() => { element.showModal(); - animateOpen(element); + element.classList.add("animating"); + const animation = createAnimation(element); + animation.addEventListener("finish", () => + element.classList.remove("animating"), + ); }); }); }; diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx index caaf4c3..38670e2 100644 --- a/src/timelines/TootList.tsx +++ b/src/timelines/TootList.tsx @@ -13,7 +13,6 @@ import { useDefaultSession } from "../masto/clients"; import { useHeroSource } from "../platform/anim"; import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; -import { useNavigate } from "@solidjs/router"; import RegularToot, { findElementActionable, findRootToot, @@ -21,6 +20,21 @@ import RegularToot, { import cardStyle from "../material/cards.module.css"; import type { ThreadNode } from "../masto/timelines"; import { useNavigator } from "../platform/StackedRouter"; +import { ANIM_CURVE_STD } from "../material/theme"; + +function durationOf(rect0: DOMRect, rect1: DOMRect) { + const distancelt = Math.sqrt( + Math.pow(Math.abs(rect0.top - rect1.top), 2) + + Math.pow(Math.abs(rect0.left - rect1.left), 2), + ); + const distancerb = Math.sqrt( + Math.pow(Math.abs(rect0.bottom - rect1.bottom), 2) + + Math.pow(Math.abs(rect0.right - rect1.right), 2), + ); + const distance = distancelt + distancerb; + const duration = distance / 1.6; + return duration; +} function positionTootInThread(index: number, threadLength: number) { if (index === 0) { @@ -41,7 +55,7 @@ const TootList: Component<{ const session = useDefaultSession(); const heroSrc = useHeroSource(); const [expandedThreadId, setExpandedThreadId] = createSignal(); - const {push} = useNavigator(); + const { push } = useNavigator(); const onBookmark = async (status: mastodon.v1.Status) => { const client = session()?.client; @@ -100,7 +114,7 @@ const TootList: Component<{ const openFullScreenToot = ( toot: mastodon.v1.Status, - srcElement?: HTMLElement, + srcElement: HTMLElement, reply?: boolean, ) => { const p = session()?.account; @@ -116,12 +130,55 @@ const TootList: Component<{ const acct = `${inf.username}@${p.site}`; setTootBottomSheetCache(acct, toot); + push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { - state: reply - ? { - tootReply: true, - } - : undefined, + animateOpen(element) { + const rect0 = srcElement.getBoundingClientRect(); // the start rect + const rect1 = element.getBoundingClientRect(); // the end rect + + const duration = durationOf(rect0, rect1); + + const keyframes = { + top: [`${rect0.top}px`, `${rect1.top}px`], + bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`], + left: [`${rect0.left}px`, `${rect1.left}px`], + right: [`${rect0.right}px`, `${rect1.right}px`], + height: [`${rect0.height}px`, `${rect1.height}px`], + margin: 0, + }; + + srcElement.style.visibility = "hidden"; + + const animation = element.animate(keyframes, { + duration, + easing: ANIM_CURVE_STD, + }); + return animation; + }, + + animateClose(element) { + const rect0 = element.getBoundingClientRect(); // the start rect + const rect1 = srcElement.getBoundingClientRect(); // the end rect + + const duration = durationOf(rect0, rect1); + + const keyframes = { + top: [`${rect0.top}px`, `${rect1.top}px`], + bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`], + left: [`${rect0.left}px`, `${rect1.left}px`], + right: [`${rect0.right}px`, `${rect1.right}px`], + height: [`${rect0.height}px`, `${rect1.height}px`], + margin: 0, + }; + + srcElement.style.visibility = ""; + + const animation = element.animate(keyframes, { + duration, + easing: ANIM_CURVE_STD, + }); + return animation; + }, }); };