'use client' import React, { useEffect, useRef } from 'react' interface OKLCHColor { l: number c: number h: number } // Monochromes Farbkonzept im OKLCH-Raum const BASE_BG = { l: 0.145, c: 0.01, h: 148 } const BASE_DIM = { l: 0.50, c: 0.01, h: 148 } const HIGHLIGHT_BRIGHT = { l: 0.95, c: 0.01, h: 148 } function lerp(a: number, b: number, t: number): number { return a + (b - a) * t } function lerpOKLCH(c1: OKLCHColor, c2: OKLCHColor, t: number): OKLCHColor { return { l: lerp(c1.l, c2.l, t), c: lerp(c1.c, c2.c, t), h: lerp(c1.h, c2.h, t) } } export function AsciiShaderBackground({ className = '' }: { className?: string }) { const canvasRef = useRef(null) useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext('2d') if (!ctx) return let animationFrameId: number // Minimalistisches, ruhiges ASCII-Array const chars = ['•', '∘', '+', '▪', '▫', ' '] const fontSize = 18 let cols = 0 let rows = 0 const mouse = { x: 0, y: 0, targetX: 0, targetY: 0 } let scrollProgress = 0 let targetScrollProgress = 0 const resize = () => { const width = window.innerWidth const height = window.innerHeight const dpr = window.devicePixelRatio || 1 canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr) cols = Math.ceil(width / fontSize) rows = Math.ceil(height / fontSize) if (mouse.x === 0 && mouse.y === 0) { mouse.x = width / 2 mouse.y = height / 2 mouse.targetX = width / 2 mouse.targetY = height / 2 } } const onPointerMove = (e: PointerEvent) => { mouse.targetX = e.clientX mouse.targetY = e.clientY } const onScroll = () => { const maxScroll = Math.max( document.body.scrollHeight - window.innerHeight, 1 ) targetScrollProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1) } resize() window.addEventListener('resize', resize) window.addEventListener('pointermove', onPointerMove) window.addEventListener('scroll', onScroll, { passive: true }) const render = (time: number) => { mouse.x += (mouse.targetX - mouse.x) * 0.05 mouse.y += (mouse.targetY - mouse.y) * 0.05 scrollProgress += (targetScrollProgress - scrollProgress) * 0.05 ctx.clearRect(0, 0, canvas.width, canvas.height) // Beim Scrollen sofort ausblenden (vollständig weg ab 150px Scroll-Tiefe) const scrollFade = Math.max(1 - (window.scrollY / 150), 0) if (scrollFade <= 0) { animationFrameId = requestAnimationFrame(render) return } // Scroll-gesteuerter Dimmer const scrollDimFactor = (1 - scrollProgress * 0.4) * scrollFade const currentDimColor = { ...BASE_DIM, l: BASE_DIM.l * scrollDimFactor } ctx.font = `${fontSize}px monospace` ctx.textAlign = 'center' ctx.textBaseline = 'middle' const timeSec = time * 0.001 for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const x = c * fontSize + fontSize / 2 const y = r * fontSize + fontSize / 2 // Abstandsvektor zur gedämpften Mausposition const dx = x - mouse.x const dy = y - mouse.y const dist = Math.sqrt(dx * dx + dy * dy) // Maus-Störung (Vektorfeld-Ablenkung): Phase & Amplitude lokal beeinflussen const mouseInfluence = Math.exp(-dist / 250) const mouseDistortion = (Math.atan2(dy, dx) + (dx + dy) * 0.003) * mouseInfluence * 3.0 // Diagonale 2D-Strömung / Wellen-Synthese aus Sinus & Kosinus const waveX = Math.sin(c * 0.08 + timeSec * 0.8 + mouseDistortion) const waveY = Math.cos(r * 0.08 + timeSec * 0.6 + mouseDistortion) const flowValue = Math.sin(waveX + waveY + (c + r) * 0.04 + timeSec * 0.4) // Zeichenauswahl basierend auf Strömungsfeld const normalizedFlow = (flowValue + 1) * 0.5 // Range 0..1 const charIndex = Math.floor(normalizedFlow * chars.length) % chars.length const char = chars[charIndex] // Leerzeichen für ruhiges Raster überspringen if (char === ' ') continue // Helligkeits-Highlighting durch Maus-Störung und Wellenkamm const blendFactor = Math.min(Math.max(mouseInfluence * 0.85 + normalizedFlow * 0.15, 0), 1) const currentColor = lerpOKLCH(currentDimColor, HIGHLIGHT_BRIGHT, blendFactor) // Stärkerer Kontrast: min 0.35, max 0.95 Deckkraft const alpha = (0.35 + blendFactor * 0.6) * scrollDimFactor ctx.fillStyle = `oklch(${currentColor.l.toFixed(3)} ${currentColor.c.toFixed(3)} ${currentColor.h.toFixed(1)} / ${alpha.toFixed(2)})` ctx.fillText(char, x, y) } } animationFrameId = requestAnimationFrame(render) } animationFrameId = requestAnimationFrame(render) return () => { window.removeEventListener('resize', resize) window.removeEventListener('pointermove', onPointerMove) window.removeEventListener('scroll', onScroll) cancelAnimationFrame(animationFrameId) } }, []) return (
) }