mirror of
https://github.com/Since-3/since3-website.git
synced 2026-10-12 19:51:16 +00:00
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
"use client"
|
|
|
|
import { useRef } from "react"
|
|
import {
|
|
motion,
|
|
useAnimationFrame,
|
|
useMotionValue,
|
|
useReducedMotion,
|
|
useScroll,
|
|
useSpring,
|
|
useTransform,
|
|
useVelocity,
|
|
} from "motion/react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function wrap(min: number, max: number, value: number) {
|
|
const range = max - min
|
|
return ((((value - min) % range) + range) % range) + min
|
|
}
|
|
|
|
/**
|
|
* Endless text band. Scrolling speeds it up, scrolling up reverses it.
|
|
* `baseVelocity` is in percent of one copy per second.
|
|
*/
|
|
function VelocityRow({
|
|
items,
|
|
baseVelocity,
|
|
className,
|
|
}: {
|
|
items: string[]
|
|
baseVelocity: number
|
|
className?: string
|
|
}) {
|
|
const reduced = useReducedMotion()
|
|
const baseX = useMotionValue(0)
|
|
const { scrollY } = useScroll()
|
|
const velocity = useVelocity(scrollY)
|
|
const smoothVelocity = useSpring(velocity, { damping: 50, stiffness: 400 })
|
|
const factor = useTransform(smoothVelocity, [0, 1000], [0, 4], { clamp: false })
|
|
const x = useTransform(baseX, (value) => `${wrap(-50, 0, value)}%`)
|
|
const direction = useRef(1)
|
|
|
|
useAnimationFrame((_, delta) => {
|
|
if (reduced) return
|
|
const f = factor.get()
|
|
if (f < 0) direction.current = -1
|
|
else if (f > 0) direction.current = 1
|
|
let move = direction.current * baseVelocity * (delta / 1000)
|
|
move += move * Math.abs(f)
|
|
baseX.set(baseX.get() + move)
|
|
})
|
|
|
|
const content = (
|
|
<span className="flex shrink-0 items-center">
|
|
{items.map((item, index) => (
|
|
<span key={index} className="flex items-center">
|
|
<span className="px-[0.35em]">{item}</span>
|
|
<span className="text-[0.5em]">✦</span>
|
|
</span>
|
|
))}
|
|
</span>
|
|
)
|
|
|
|
return (
|
|
<div className={cn("flex overflow-clip whitespace-nowrap", className)}>
|
|
<motion.div style={{ x }} className="flex w-max">
|
|
{content}
|
|
{content}
|
|
</motion.div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Two crossing bands between hero and statement. */
|
|
export function MarqueeBand({ items }: { items: string[] }) {
|
|
return (
|
|
<div aria-hidden="true" className="relative h-[clamp(9rem,20vw,19rem)] overflow-x-clip bg-black">
|
|
<div className="absolute inset-x-[-5%] top-1/2 -translate-y-1/2 rotate-3">
|
|
<VelocityRow
|
|
items={items}
|
|
baseVelocity={-2}
|
|
className="py-2 text-display type-poster text-outline [--stroke-color:var(--color-ink-600)]"
|
|
/>
|
|
</div>
|
|
<div
|
|
data-theme="light"
|
|
className="absolute inset-x-[-5%] top-1/2 -translate-y-1/2 -rotate-2 shadow-[0_30px_80px_-20px_rgb(0_0_0/0.8)]"
|
|
>
|
|
<VelocityRow items={items} baseVelocity={3} className="py-3 text-display type-poster" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|