mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-10 10:48:45 +00:00
142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
import { type JSX, onCleanup, splitProps } from "solid-js"
|
|
import { ScrollFade, type ScrollFadeProps } from "./scroll-fade"
|
|
|
|
const SCROLL_SPEED = 60
|
|
const PAUSE_DURATION = 800
|
|
|
|
type ScrollAnimationState = {
|
|
rafId: number | null
|
|
startTime: number
|
|
running: boolean
|
|
}
|
|
|
|
const startScrollAnimation = (containerEl: HTMLElement): ScrollAnimationState | null => {
|
|
containerEl.offsetHeight
|
|
|
|
const extraWidth = containerEl.scrollWidth - containerEl.clientWidth
|
|
|
|
if (extraWidth <= 0) {
|
|
return null
|
|
}
|
|
|
|
const scrollDuration = (extraWidth / SCROLL_SPEED) * 1000
|
|
const totalDuration = PAUSE_DURATION + scrollDuration + PAUSE_DURATION + scrollDuration + PAUSE_DURATION
|
|
|
|
const state: ScrollAnimationState = {
|
|
rafId: null,
|
|
startTime: performance.now(),
|
|
running: true,
|
|
}
|
|
|
|
const animate = (currentTime: number) => {
|
|
if (!state.running) return
|
|
|
|
const elapsed = currentTime - state.startTime
|
|
const progress = (elapsed % totalDuration) / totalDuration
|
|
|
|
const pausePercent = PAUSE_DURATION / totalDuration
|
|
const scrollPercent = scrollDuration / totalDuration
|
|
|
|
const pauseEnd1 = pausePercent
|
|
const scrollEnd1 = pauseEnd1 + scrollPercent
|
|
const pauseEnd2 = scrollEnd1 + pausePercent
|
|
const scrollEnd2 = pauseEnd2 + scrollPercent
|
|
|
|
let scrollPos = 0
|
|
|
|
if (progress < pauseEnd1) {
|
|
scrollPos = 0
|
|
} else if (progress < scrollEnd1) {
|
|
const scrollProgress = (progress - pauseEnd1) / scrollPercent
|
|
scrollPos = scrollProgress * extraWidth
|
|
} else if (progress < pauseEnd2) {
|
|
scrollPos = extraWidth
|
|
} else if (progress < scrollEnd2) {
|
|
const scrollProgress = (progress - pauseEnd2) / scrollPercent
|
|
scrollPos = extraWidth * (1 - scrollProgress)
|
|
} else {
|
|
scrollPos = 0
|
|
}
|
|
|
|
containerEl.scrollLeft = scrollPos
|
|
state.rafId = requestAnimationFrame(animate)
|
|
}
|
|
|
|
state.rafId = requestAnimationFrame(animate)
|
|
return state
|
|
}
|
|
|
|
const stopScrollAnimation = (state: ScrollAnimationState | null, containerEl?: HTMLElement) => {
|
|
if (state) {
|
|
state.running = false
|
|
if (state.rafId !== null) {
|
|
cancelAnimationFrame(state.rafId)
|
|
}
|
|
}
|
|
if (containerEl) {
|
|
containerEl.scrollLeft = 0
|
|
}
|
|
}
|
|
|
|
export interface ScrollRevealProps extends Omit<ScrollFadeProps, "direction"> {
|
|
hoverDelay?: number
|
|
}
|
|
|
|
export function ScrollReveal(props: ScrollRevealProps) {
|
|
const [local, others] = splitProps(props, ["children", "hoverDelay", "ref"])
|
|
|
|
const hoverDelay = () => local.hoverDelay ?? 300
|
|
|
|
let containerRef: HTMLDivElement | undefined
|
|
let hoverTimeout: ReturnType<typeof setTimeout> | undefined
|
|
let scrollAnimationState: ScrollAnimationState | null = null
|
|
|
|
const handleMouseEnter: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
|
hoverTimeout = setTimeout(() => {
|
|
if (!containerRef) return
|
|
|
|
containerRef.offsetHeight
|
|
|
|
const isScrollable = containerRef.scrollWidth > containerRef.clientWidth + 1
|
|
|
|
if (isScrollable) {
|
|
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
scrollAnimationState = startScrollAnimation(containerRef)
|
|
}
|
|
}, hoverDelay())
|
|
}
|
|
|
|
const handleMouseLeave: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
|
if (hoverTimeout) {
|
|
clearTimeout(hoverTimeout)
|
|
hoverTimeout = undefined
|
|
}
|
|
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
scrollAnimationState = null
|
|
}
|
|
|
|
onCleanup(() => {
|
|
if (hoverTimeout) {
|
|
clearTimeout(hoverTimeout)
|
|
}
|
|
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
})
|
|
|
|
return (
|
|
<ScrollFade
|
|
ref={(el) => {
|
|
containerRef = el
|
|
local.ref?.(el)
|
|
}}
|
|
fadeStartSize={8}
|
|
fadeEndSize={8}
|
|
direction="horizontal"
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
{...others}
|
|
>
|
|
{local.children}
|
|
</ScrollFade>
|
|
)
|
|
}
|