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 { 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 | undefined let scrollAnimationState: ScrollAnimationState | null = null const handleMouseEnter: JSX.EventHandler = () => { 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 = () => { if (hoverTimeout) { clearTimeout(hoverTimeout) hoverTimeout = undefined } stopScrollAnimation(scrollAnimationState, containerRef) scrollAnimationState = null } onCleanup(() => { if (hoverTimeout) { clearTimeout(hoverTimeout) } stopScrollAnimation(scrollAnimationState, containerRef) }) return ( { containerRef = el local.ref?.(el) }} fadeStartSize={8} fadeEndSize={8} direction="horizontal" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} {...others} > {local.children} ) }