diff --git a/shop/components/AsciiShaderBackground.tsx b/shop/components/AsciiShaderBackground.tsx new file mode 100644 index 0000000..bcf9ad9 --- /dev/null +++ b/shop/components/AsciiShaderBackground.tsx @@ -0,0 +1,191 @@ +'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 ( +
+ +
+
+ ) +}