mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
Add scroll hint
This commit is contained in:
parent
f1339901e6
commit
d6245c7c7e
3 changed files with 122 additions and 0 deletions
|
|
@ -97,4 +97,7 @@ export type UserEvent =
|
|||
| {
|
||||
type: "menu_click";
|
||||
menuItem: string;
|
||||
}
|
||||
| {
|
||||
type: "scroll_hint_shown";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState, useLayoutEffect, useEffect } from "react";
|
||||
import SkinPage from "./SkinPage";
|
||||
import { logUserEvent } from "./Events";
|
||||
import { useScrollHint } from "./useScrollHint";
|
||||
|
||||
export type ClientSkin = {
|
||||
screenshotUrl: string;
|
||||
|
|
@ -30,6 +31,25 @@ export default function SkinScroller({
|
|||
const [visibleSkinIndex, setVisibleSkinIndex] = useState(0);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
||||
const [hasEverScrolled, setHasEverScrolled] = useState(false);
|
||||
|
||||
// Track if user has ever scrolled to another skin
|
||||
useEffect(() => {
|
||||
if (visibleSkinIndex > 0) {
|
||||
setHasEverScrolled(true);
|
||||
}
|
||||
}, [visibleSkinIndex]);
|
||||
|
||||
// Show scroll hint only if user has never scrolled to another skin
|
||||
useScrollHint({
|
||||
containerRef,
|
||||
enabled: visibleSkinIndex === 0 && !hasEverScrolled,
|
||||
onHintShown: () => {
|
||||
logUserEvent(sessionId, {
|
||||
type: "scroll_hint_shown",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (containerRef == null) {
|
||||
|
|
|
|||
99
packages/skin-database/app/(modern)/scroll/useScrollHint.ts
Normal file
99
packages/skin-database/app/(modern)/scroll/useScrollHint.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
type UseScrollHintOptions = {
|
||||
containerRef: HTMLDivElement | null;
|
||||
enabled: boolean;
|
||||
delayMs?: number;
|
||||
scrollAmount?: number;
|
||||
animationDuration?: number;
|
||||
onHintShown?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that provides a gentle scroll hint animation to encourage user interaction.
|
||||
* After a delay, if the user hasn't scrolled, it will scroll down slightly and bounce back.
|
||||
*/
|
||||
export function useScrollHint({
|
||||
containerRef,
|
||||
enabled,
|
||||
delayMs = 5000,
|
||||
scrollAmount = 80,
|
||||
animationDuration = 1000,
|
||||
onHintShown,
|
||||
}: UseScrollHintOptions) {
|
||||
useEffect(() => {
|
||||
if (containerRef == null || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hintTimer = setTimeout(() => {
|
||||
if (!enabled || containerRef.scrollTop !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startScrollTop = containerRef.scrollTop;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Temporarily disable scroll snap for smooth animation
|
||||
const originalScrollSnapType = containerRef.style.scrollSnapType;
|
||||
containerRef.style.scrollSnapType = "none";
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
// Bouncy easing function - overshoots and bounces back
|
||||
const easeOutBounce = (t: number) => {
|
||||
const n1 = 7.5625;
|
||||
const d1 = 2.75;
|
||||
if (t < 1 / d1) {
|
||||
return n1 * t * t;
|
||||
} else if (t < 2 / d1) {
|
||||
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
||||
} else if (t < 2.5 / d1) {
|
||||
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
||||
} else {
|
||||
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a bounce effect: scroll down quickly, then bounce back
|
||||
let offset;
|
||||
if (progress < 0.4) {
|
||||
// First 40%: scroll down quickly
|
||||
const t = progress / 0.4;
|
||||
offset = scrollAmount * t * t; // Quadratic ease-in
|
||||
} else {
|
||||
// Last 60%: bounce back with overshoot
|
||||
const t = (progress - 0.4) / 0.6;
|
||||
offset = scrollAmount * (1 - easeOutBounce(t));
|
||||
}
|
||||
|
||||
containerRef.scrollTop = startScrollTop + offset;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
// Ensure we end exactly where we started
|
||||
containerRef.scrollTop = startScrollTop;
|
||||
// Re-enable scroll snap
|
||||
containerRef.style.scrollSnapType = originalScrollSnapType;
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
onHintShown?.();
|
||||
}, delayMs);
|
||||
|
||||
return () => {
|
||||
clearTimeout(hintTimer);
|
||||
};
|
||||
}, [
|
||||
containerRef,
|
||||
enabled,
|
||||
delayMs,
|
||||
scrollAmount,
|
||||
animationDuration,
|
||||
onHintShown,
|
||||
]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue