Compare commits
6 commits
10b517bceb
...
8a435be4c8
Author | SHA1 | Date | |
---|---|---|---|
|
8a435be4c8 | ||
|
06e988e0e5 | ||
|
c69d54e171 | ||
|
012b078cee | ||
|
20a5e565b1 | ||
|
b61012f12b |
5 changed files with 216 additions and 78 deletions
|
@ -11,6 +11,15 @@ 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,
|
||||||
|
ANIM_CURVE_STD,
|
||||||
|
} from "./theme";
|
||||||
|
import {
|
||||||
|
animateSlideInFromRight,
|
||||||
|
animateSlideOutToRight,
|
||||||
|
} from "../platform/anim";
|
||||||
|
|
||||||
export type BottomSheetProps = {
|
export type BottomSheetProps = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
|
@ -39,7 +48,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 +96,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,37 +123,18 @@ 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 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 = (
|
const animateSlideInFromBottom = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
reserve?: boolean,
|
reserve?: boolean,
|
||||||
|
@ -176,13 +171,19 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
reserve?: boolean,
|
reserve?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const easing = "cubic-bezier(0.4, 0, 0.2, 1)";
|
const easing = ANIM_CURVE_STD;
|
||||||
element.classList.add("animated");
|
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.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(
|
animation = element.animate(
|
||||||
[
|
[
|
||||||
composeAnimationFrame(startRect, { transform: "none" }),
|
composeAnimationFrame(startRect, { transform: "none" }),
|
||||||
|
|
|
@ -13,10 +13,12 @@ import {
|
||||||
animateShrinkToTopRight,
|
animateShrinkToTopRight,
|
||||||
} from "../platform/anim";
|
} from "../platform/anim";
|
||||||
|
|
||||||
|
type Anchor = Pick<DOMRect, "top" | "left" | "right">
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>;
|
onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>;
|
||||||
anchor: () => DOMRect;
|
anchor: () => Anchor;
|
||||||
};
|
};
|
||||||
|
|
||||||
function px(n?: number) {
|
function px(n?: number) {
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function useHeroSignal(
|
||||||
|
|
||||||
return [get, set];
|
return [get, set];
|
||||||
} else {
|
} else {
|
||||||
console.debug("no hero source")
|
console.debug("no hero source");
|
||||||
return [() => undefined, () => undefined];
|
return [() => undefined, () => undefined];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,6 @@ export function animateRollOutFromTop(
|
||||||
return animation;
|
return animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function animateRollInFromBottom(
|
export function animateRollInFromBottom(
|
||||||
root: HTMLElement,
|
root: HTMLElement,
|
||||||
options?: Omit<KeyframeAnimationOptions, "duration">,
|
options?: Omit<KeyframeAnimationOptions, "duration">,
|
||||||
|
@ -126,8 +125,10 @@ export function animateGrowFromTopRight(
|
||||||
|
|
||||||
const { width, height } = root.getBoundingClientRect();
|
const { width, height } = root.getBoundingClientRect();
|
||||||
|
|
||||||
const durationX = Math.floor((height / 1600) * 1000);
|
const speed = transitionSpeedForEnter(window.innerHeight);
|
||||||
const durationY = Math.floor((width / 1600) * 1000);
|
|
||||||
|
const durationX = Math.floor(height / speed);
|
||||||
|
const durationY = Math.floor(width / speed);
|
||||||
|
|
||||||
// finds the offset for the center frame,
|
// finds the offset for the center frame,
|
||||||
// it will stops at the (minDuration / maxDuration)%
|
// it will stops at the (minDuration / maxDuration)%
|
||||||
|
@ -137,20 +138,19 @@ export function animateGrowFromTopRight(
|
||||||
const centerOffset = minDuration / maxDuration;
|
const centerOffset = minDuration / maxDuration;
|
||||||
|
|
||||||
const keyframes = [
|
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`,
|
height: `${(minDuration === durationY ? 1 : centerOffset) * height}px`,
|
||||||
offset: centerOffset,
|
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(
|
const animation = root.animate(keyframes, {
|
||||||
keyframes,
|
...options,
|
||||||
{ ...options, duration: maxDuration },
|
duration: maxDuration,
|
||||||
);
|
});
|
||||||
|
|
||||||
const restore = () => {
|
const restore = () => {
|
||||||
root.style.transformOrigin = transformOrigin;
|
root.style.transformOrigin = transformOrigin;
|
||||||
|
@ -173,9 +173,9 @@ export function animateShrinkToTopRight(
|
||||||
|
|
||||||
const { width, height } = root.getBoundingClientRect();
|
const { width, height } = root.getBoundingClientRect();
|
||||||
|
|
||||||
const duration = Math.floor(
|
const speed = transitionSpeedForLeave(window.innerWidth);
|
||||||
Math.max((width / 1600) * 1000, (height / 1600) * 1000),
|
|
||||||
);
|
const duration = Math.floor(Math.max(width / speed, height / speed));
|
||||||
|
|
||||||
const animation = root.animate(
|
const animation = root.animate(
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -26,10 +26,14 @@ import {
|
||||||
Close,
|
Close,
|
||||||
Edit,
|
Edit,
|
||||||
ExpandMore,
|
ExpandMore,
|
||||||
|
Group,
|
||||||
MoreVert,
|
MoreVert,
|
||||||
OpenInBrowser,
|
OpenInBrowser,
|
||||||
|
PersonOff,
|
||||||
|
PlaylistAdd,
|
||||||
Send,
|
Send,
|
||||||
Share,
|
Share,
|
||||||
|
Translate,
|
||||||
Verified,
|
Verified,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
import { Title } from "../material/typography";
|
import { Title } from "../material/typography";
|
||||||
|
@ -90,6 +94,10 @@ const Profile: Component = () => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isCurrentSessionProfile = () => {
|
||||||
|
return session().account?.inf?.url === profile()?.url;
|
||||||
|
};
|
||||||
|
|
||||||
const [recentTootFilter, setRecentTootFilter] = createSignal({
|
const [recentTootFilter, setRecentTootFilter] = createSignal({
|
||||||
pinned: true,
|
pinned: true,
|
||||||
boost: false,
|
boost: false,
|
||||||
|
@ -261,18 +269,48 @@ const Profile: Component = () => {
|
||||||
document.getElementById(menuButId)!.getBoundingClientRect()
|
document.getElementById(menuButId)!.getBoundingClientRect()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Show
|
||||||
|
when={isCurrentSessionProfile()}
|
||||||
|
fallback={
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PlaylistAdd />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Subscribe...</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Edit />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Edit...</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
</Show>
|
||||||
|
<Divider />
|
||||||
<MenuItem disabled>
|
<MenuItem disabled>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Edit />
|
<Group />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Edit...</ListItemText>
|
<ListItemText>Subscribers</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PersonOff />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Blocklist</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Translate />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>Translate Name and Bio...</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
|
||||||
<MenuItem disabled>
|
<MenuItem disabled>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Send />
|
<Send />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Mention {profile()?.displayName || ""}...</ListItemText>
|
<ListItemText>Mention in...</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -399,7 +437,7 @@ const Profile: Component = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TimeSourceProvider value={time}>
|
<TimeSourceProvider value={time}>
|
||||||
<Show when={recentTootFilter().pinned}>
|
<Show when={recentTootFilter().pinned && pinnedToots.list.length > 0}>
|
||||||
<TootList
|
<TootList
|
||||||
threads={pinnedToots.list}
|
threads={pinnedToots.list}
|
||||||
onUnknownThread={pinnedToots.getPath}
|
onUnknownThread={pinnedToots.getPath}
|
||||||
|
|
|
@ -1,15 +1,5 @@
|
||||||
import {
|
import { Button, MenuItem, Checkbox, ListItemText } from "@suid/material";
|
||||||
Button,
|
import { createMemo, createSignal, createUniqueId, For } from "solid-js";
|
||||||
MenuItem,
|
|
||||||
Checkbox,
|
|
||||||
ListItemText,
|
|
||||||
} from "@suid/material";
|
|
||||||
import {
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
createUniqueId,
|
|
||||||
For,
|
|
||||||
} from "solid-js";
|
|
||||||
import Menu from "../material/Menu";
|
import Menu from "../material/Menu";
|
||||||
import { FilterList, FilterListOff } from "@suid/icons-material";
|
import { FilterList, FilterListOff } from "@suid/icons-material";
|
||||||
|
|
||||||
|
@ -68,9 +58,20 @@ function TootFilterButton<F extends Record<string, string>>(props: Props<F>) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button size="large" onClick={[setOpen, true]} id={buttonId}>
|
<Button size="large" onClick={onClick} id={buttonId}>
|
||||||
{appliedKeys().length === optionKeys().length ? (
|
{appliedKeys().length === optionKeys().length ? (
|
||||||
<FilterListOff />
|
<FilterListOff />
|
||||||
) : (
|
) : (
|
||||||
|
@ -79,13 +80,7 @@ function TootFilterButton<F extends Record<string, string>>(props: Props<F>) {
|
||||||
|
|
||||||
<span style={{ "margin-left": "0.5em" }}>{text()}</span>
|
<span style={{ "margin-left": "0.5em" }}>{text()}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu open={open()} onClose={[setOpen, false]} anchor={() => anchor}>
|
||||||
open={open()}
|
|
||||||
onClose={[setOpen, false]}
|
|
||||||
anchor={() =>
|
|
||||||
document.getElementById(buttonId)!.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={Object.keys(props.options)}>
|
<For each={Object.keys(props.options)}>
|
||||||
{(item, idx) => (
|
{(item, idx) => (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in a new issue