Logo aus dem Instagram-Profilbild als SVG, in "Über uns" eingebaut

- Profilbild mit potrace vektorisiert: src/assets/logo/emblem.svg (nur
  Emblem, currentColor) und public/logo-profilbild.svg (quadratisch,
  weiß auf dunklem Grund wie das Profilbild)
- "Sechs Profis. Ein Salon.": Foto der Logo-Wand durch das Logo ersetzt;
  die Linien zeichnen sich beim Scrollen, danach folgt ein Lichtreflex
  dem Mauszeiger (ohne Maus der Scroll-Position)
- Originalbild nach src/assets/logo verschoben, ungenutztes Foto und
  ImageReveal entfernt, README ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBHsCnWNXAB2Le3gAEsivF
This commit is contained in:
Claude committed 2026-09-29 14:45:27 +00:00
1 parent 4392285570
commit d4a0a3856e
10 files changed
+160 -45

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
import { useInView, useMotionValueEvent, useReducedMotion, useScroll } from 'motion/react'
import { useEffect, useId, useRef } from 'react'
import raw from '../assets/logo/emblem.svg?raw'
import { cn } from '../lib/cn'
// Das Logo (Instagram-Profilbild, vektorisiert) liegt als SVG-Datei vor und wird hier inline gezeichnet,
// damit die Linien animiert und vom Licht angestrahlt werden können.
const viewBox = raw.match(/viewBox="([^"]+)"/)![1]
const [, , VB_W, VB_H] = viewBox.split(' ').map(Number)
const paths = [...raw.matchAll(/<path d="([^"]+)"/g)].map((m) => m[1])
/**
* Logo-Emblem:
* - zeichnet sich beim ersten Erscheinen Linie für Linie, danach füllen sich die Linien
* - ein Lichtreflex wie auf Metall folgt dem Mauszeiger (ohne Maus: der Scroll-Position)
*/
export function Emblem({ className }: { className?: string }) {
const wrap = useRef<HTMLDivElement>(null)
const gradient = useRef<SVGRadialGradientElement>(null)
const id = `emblem-light-${useId().replace(/[^\w-]/g, '')}`
const reduce = useReducedMotion()
const drawn = useInView(wrap, { once: true, amount: 0.4 })
// Lichtposition (0–1 relativ zum Logo): Ziel und aktueller, weich nachgezogener Wert
const light = useRef({ x: 0.3, y: 0.25, tx: 0.3, ty: 0.25, hover: false, raf: 0 })
const render = () => {
const l = light.current
l.x += (l.tx - l.x) * 0.12
l.y += (l.ty - l.y) * 0.12
gradient.current?.setAttribute('cx', String(l.x * VB_W))
gradient.current?.setAttribute('cy', String(l.y * VB_H))
const settled = Math.abs(l.tx - l.x) < 0.001 && Math.abs(l.ty - l.y) < 0.001
l.raf = settled ? 0 : requestAnimationFrame(render)
}
const moveLight = (x: number, y: number) => {
const l = light.current
l.tx = x
l.ty = y
if (reduce) {
l.x = x
l.y = y
render()
} else if (!l.raf) l.raf = requestAnimationFrame(render)
}
const { scrollYProgress } = useScroll({ target: wrap, offset: ['start end', 'end start'] })
useMotionValueEvent(scrollYProgress, 'change', (p) => {
if (!light.current.hover) moveLight(0.1 + 0.8 * p, 0.15 + 0.5 * p)
})
useEffect(() => {
const l = light.current
return () => cancelAnimationFrame(l.raf)
}, [])
const onPointerMove = (e: React.PointerEvent) => {
const r = wrap.current!.getBoundingClientRect()
light.current.hover = true
moveLight((e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height)
}
const onPointerLeave = () => {
light.current.hover = false
const p = scrollYProgress.get()
moveLight(0.1 + 0.8 * p, 0.15 + 0.5 * p)
}
return (
<div
ref={wrap}
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
data-drawn={drawn || reduce ? '' : undefined}
className={cn('emblem', className)}
>
<svg viewBox={viewBox} role="img" aria-label="Logo von Amin B. Ahmadi" className="block h-auto w-full overflow-visible">
<defs>
<radialGradient ref={gradient} id={id} gradientUnits="userSpaceOnUse" cx={0.3 * VB_W} cy={0.25 * VB_H} r={VB_W * 0.55}>
<stop offset="0" stopColor="#ffffff" />
<stop offset="0.35" stopColor="#ece9e4" />
<stop offset="1" stopColor="#8d8a85" />
</radialGradient>
</defs>
{/* Konturen für die Zeichen-Animation */}
<g className="emblem-lines" aria-hidden="true">
{paths.map((d, i) => (
<path key={i} d={d} pathLength={1} style={{ '--i': i } as React.CSSProperties} />
))}
</g>
{/* Gefüllte Linien mit Lichtreflex */}
<g className="emblem-fill" fill={`url(#${id})`}>
{paths.map((d, i) => (
<path key={i} d={d} />
))}
</g>
</svg>
</div>
)
}
+7 -7
View File
@@ -1,15 +1,15 @@
import { image, salon } from '../data/salon'
import { FadeIn, ImageReveal, RevealLines } from './Reveal'
import { salon } from '../data/salon'
import { Emblem } from './Emblem'
import { FadeIn, RevealLines } from './Reveal'
export function Intro() {
return (
<section id="salon" className="px-4 py-24 md:px-8 md:py-36">
<div className="grid gap-10 md:grid-cols-12 md:gap-8">
<ImageReveal
src={image('photos/emblem')}
alt={`Das Logo von ${salon.name} aus Edelstahl an der Schieferwand im Salon`}
className="aspect-[4/5] md:col-span-4"
/>
{/* Logo wie auf Instagram: helle Linien auf dunklem Grund */}
<div className="grid aspect-square place-items-center bg-ink md:col-span-4">
<Emblem className="w-[84%]" />
</div>
<div className="flex flex-col justify-between gap-12 md:col-span-8">
<div className="mini flex justify-between">
-34
View File
@@ -1,6 +1,5 @@
import { motion, type Variants } from 'motion/react'
import type { ReactNode } from 'react'
import { cn } from '../lib/cn'
import { EASE_OUT_EXPO } from '../lib/motion'
const tags = { h1: motion.h1, h2: motion.h2, h3: motion.h3, p: motion.p, div: motion.div }
@@ -62,36 +61,3 @@ export function FadeIn({ children, className, delay = 0 }: { children: ReactNode
</motion.div>
)
}
/** Bild, das sich von unten aufdeckt und dabei leicht herauszoomt. */
export function ImageReveal({
src,
alt,
className,
imgClassName,
}: {
src: string
alt: string
className?: string
imgClassName?: string
}) {
const transition = { duration: 1.3, ease: EASE_OUT_EXPO }
// Beobachtet wird der ungeclippte Rahmen: ein komplett weggeclipptes Element gilt nie als sichtbar
return (
<motion.div className={className} initial="hidden" whileInView="show" viewport={{ once: true, amount: 0.25 }}>
<motion.div
className="size-full overflow-hidden"
variants={{ hidden: { clipPath: 'inset(100% 0% 0% 0%)' }, show: { clipPath: 'inset(0% 0% 0% 0%)', transition } }}
>
<motion.img
src={src}
alt={alt}
loading="lazy"
decoding="async"
className={cn('size-full object-cover', imgClassName)}
variants={{ hidden: { scale: 1.2 }, show: { scale: 1, transition: { ...transition, duration: 1.7 } } }}
/>
</motion.div>
</motion.div>
)
}