diff --git a/bun.lockb b/bun.lockb index be11679..85207b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html index 2712433..b97ae88 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ Tutu + diff --git a/package.json b/package.json index 54e32ca..e0b0a54 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "nanostores": "^0.9.5", "solid-js": "^1.8.18", "solid-styled": "^0.11.1", - "stacktrace-js": "^2.0.2" + "stacktrace-js": "^2.0.2", + "web-animations-js": "^2.3.2" }, "packageManager": "bun@1.1.21" } diff --git a/src/App.tsx b/src/App.tsx index 1522795..c3573a5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ const AccountMastodonOAuth2Callback = lazy( ); const TimelineHome = lazy(() => import("./timelines/Home.js")); const Settings = lazy(() => import("./settings/Settings.js")); +const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js")); const Routing: Component = () => { return ( @@ -29,6 +30,7 @@ const Routing: Component = () => { + @@ -53,7 +55,7 @@ const App: Component = () => { ); }); -const UnexpectedError = lazy(() => import("./UnexpectedError.js")) + const UnexpectedError = lazy(() => import("./UnexpectedError.js")); return ( ) { + return { + top: `${top}px`, + left: `${left}px`, + height: `${height}px`, + width: `${width}px`, + }; +} + +const MOVE_SPEED = 1400; // 1400px/s, bottom sheet is big and a bit heavier than small papers + const BottomSheet: ParentComponent = (props) => { let element: HTMLDialogElement; + let animation: Animation | undefined; + const hero = useHeroSignal(HERO); createEffect(() => { if (props.open) { if (!element.open) { element.showModal(); + animateOpen(); } } else { if (element.open) { + if (animation) { + animation.cancel(); + } element.close(); } } }); + const animateOpen = () => { + // Do hero animation + const startRect = hero(); + console.debug("capture hero source", startRect); + if (!startRect) return; + const endRect = element.getBoundingClientRect(); + const easing = "ease-in-out"; + console.debug("easing", easing); + element.classList.add(styles.animated); + const distance = Math.sqrt( + Math.pow(Math.abs(startRect.top - endRect.top), 2) + + Math.pow(Math.abs(startRect.left - startRect.top), 2), + ); + const duration = (distance / MOVE_SPEED) * 1000; + animation = element.animate( + [composeAnimationFrame(startRect), composeAnimationFrame(endRect)], + { easing, duration }, + ); + const onAnimationEnd = () => { + element.classList.remove(styles.animated); + animation = undefined; + }; + animation.addEventListener("finish", onAnimationEnd); + animation.addEventListener("cancel", onAnimationEnd); + }; + + onCleanup(() => { + if (animation) { + animation.cancel(); + } + }); + return ( {props.children} diff --git a/src/platform/anim.ts b/src/platform/anim.ts index 75446ef..8fbcf29 100644 --- a/src/platform/anim.ts +++ b/src/platform/anim.ts @@ -1,13 +1,47 @@ -import { createContext, useContext, type Accessor } from "solid-js"; +import { + createContext, + createRenderEffect, + createSignal, + untrack, + useContext, + type Accessor, + type Signal, +} from "solid-js"; export type HeroSource = { - [key: string | symbol | number]: HTMLElement | undefined; + [key: string | symbol | number]: DOMRect | undefined; }; -const HeroSourceContext = createContext>(() => ({})); +const HeroSourceContext = createContext>(undefined); export const HeroSourceProvider = HeroSourceContext.Provider; -export function useHeroSource() { +function useHeroSource() { return useContext(HeroSourceContext); } + +export function useHeroSignal( + key: string | symbol | number, +): Accessor { + const source = useHeroSource(); + if (source) { + const [get, set] = createSignal(); + + createRenderEffect(() => { + const value = source[0](); + console.debug("value", value); + if (value[key]) { + set(value[key]); + source[1]((x) => { + const cpy = Object.assign({}, x); + delete cpy[key]; + return cpy; + }); + } + }); + + return get; + } else { + return () => undefined; + } +} diff --git a/src/platform/host.ts b/src/platform/host.ts new file mode 100644 index 0000000..f1a2e27 --- /dev/null +++ b/src/platform/host.ts @@ -0,0 +1,12 @@ +export function isiOS() { + return [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod' + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes("Mac") && "ontouchend" in document) +} \ No newline at end of file diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts new file mode 100644 index 0000000..6897af7 --- /dev/null +++ b/src/platform/polyfills.ts @@ -0,0 +1,8 @@ +//! This module has side effect. +//! It recommended to include the module by