mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
126 lines
5.2 KiB
TypeScript
126 lines
5.2 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
motion,
|
|
useAnimationFrame,
|
|
useInView,
|
|
useMotionValue,
|
|
useReducedMotionConfig,
|
|
useScroll,
|
|
useSpring,
|
|
useTransform,
|
|
useVelocity,
|
|
wrap,
|
|
type MotionValue,
|
|
} from "motion/react"
|
|
import { useRef } from "react"
|
|
import { cn } from "@/lib/utils"
|
|
import { Reveal, wordmarkSize } from "../shared/motion"
|
|
import type { LaufbandData, WortmaskeData } from "./definitions"
|
|
import type { BlockProps } from "./types"
|
|
import { container, Eyebrow } from "./ui"
|
|
|
|
// ── Text-Laufband ────────────────────────────────────────────────────────────
|
|
|
|
// Eine Zeile: läuft gleichmäßig, schnelles Scrollen beschleunigt sie (auch rückwärts) und neigt die Schrift
|
|
function VelocityRow({ text, speed, boost, skew, outline, className }: {
|
|
text: string
|
|
speed: number
|
|
boost: MotionValue<number>
|
|
skew: MotionValue<number>
|
|
outline?: boolean
|
|
className?: string
|
|
}) {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
const base = useMotionValue(0)
|
|
const direction = useRef(1)
|
|
const reduce = useReducedMotionConfig()
|
|
const visible = useInView(ref)
|
|
const x = useTransform(base, (value) => `${wrap(-50, 0, value)}%`)
|
|
|
|
useAnimationFrame((_, delta) => {
|
|
if (reduce || !visible) return
|
|
const factor = boost.get()
|
|
if (factor < 0) direction.current = -1
|
|
else if (factor > 0) direction.current = 1
|
|
let move = direction.current * speed * (delta / 1000)
|
|
move += direction.current * move * factor
|
|
base.set(base.get() + move)
|
|
})
|
|
|
|
return (
|
|
<div ref={ref} className={cn("overflow-hidden", className)}>
|
|
<motion.div
|
|
style={{ x, skewX: skew }}
|
|
className={cn(
|
|
"flex w-max font-t-display text-[clamp(3rem,9vw,8.5rem)] leading-[1.05] tracking-[-0.03em] whitespace-nowrap",
|
|
outline && "text-transparent [-webkit-text-stroke:1.5px_var(--t-fg)]"
|
|
)}
|
|
>
|
|
{[0, 1].map((copy) => (
|
|
<span key={copy} aria-hidden={copy === 1 || undefined} className="flex shrink-0 items-center">
|
|
{Array.from({ length: 4 }, (_, i) => (
|
|
<span key={i} className="flex items-center pr-[0.4em]">
|
|
{text}
|
|
<span aria-hidden className="ml-[0.4em] text-[0.5em] text-t-accent [-webkit-text-stroke:0]">✦</span>
|
|
</span>
|
|
))}
|
|
</span>
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Laufband({ data }: BlockProps<LaufbandData>) {
|
|
const { scrollY } = useScroll()
|
|
const velocity = useVelocity(scrollY)
|
|
const smooth = useSpring(velocity, { damping: 50, stiffness: 400 })
|
|
const boost = useTransform(smooth, [-2000, 0, 2000], [-5, 0, 5], { clamp: false })
|
|
const skew = useTransform(smooth, [-2500, 0, 2500], [7, 0, -7])
|
|
|
|
if (!data.text.trim()) return null
|
|
|
|
return (
|
|
<section className="border-y border-t-fg/10 py-8 md:py-12">
|
|
<VelocityRow text={data.text} speed={-2} boost={boost} skew={skew} />
|
|
{data.secondary.trim() && <VelocityRow text={data.secondary} speed={2} boost={boost} skew={skew} outline className="mt-1 md:mt-3" />}
|
|
</section>
|
|
)
|
|
}
|
|
|
|
// ── Bild im Schriftzug ───────────────────────────────────────────────────────
|
|
|
|
const IMAGE_SOURCE = /^(https?:\/\/|\/[^/])/
|
|
|
|
// Riesiges Wort, durch dessen Buchstaben ein Bild scheint; beim Scrollen wandert der Bildausschnitt mit
|
|
export function Wortmaske({ data, brand }: BlockProps<WortmaskeData>) {
|
|
const ref = useRef<HTMLElement>(null)
|
|
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
|
|
const position = useTransform(scrollYProgress, [0, 1], ["50% 0%", "50% 100%"])
|
|
const word = data.word.trim() || brand.name
|
|
const image = IMAGE_SOURCE.test(data.image.trim())
|
|
? `url("${data.image.trim().replace(/["\\\n]/g, "")}")`
|
|
: "linear-gradient(135deg, var(--t-accent), var(--t-fg))"
|
|
|
|
return (
|
|
<section ref={ref} className="overflow-hidden py-20 md:py-32">
|
|
<div className={container}>
|
|
<Eyebrow>{data.eyebrow}</Eyebrow>
|
|
</div>
|
|
<motion.h2
|
|
style={{ backgroundImage: image, backgroundPosition: position, fontSize: wordmarkSize(word, 30) }}
|
|
className="mt-6 bg-cover bg-clip-text text-center font-t-display leading-[0.86] font-bold tracking-[-0.05em] whitespace-nowrap text-transparent [-webkit-background-clip:text]"
|
|
>
|
|
{word}
|
|
</motion.h2>
|
|
{data.text.trim() && (
|
|
<div className={cn(container, "mt-10 grid md:mt-14 md:grid-cols-12")}>
|
|
<Reveal className="md:col-span-5 md:col-start-7">
|
|
<p className="text-lg leading-relaxed text-t-muted">{data.text}</p>
|
|
</Reveal>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|