181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useRef } from 'react'
|
|
|
|
export function AsciiShaderBackground({ className = '' }: { className?: string }) {
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(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 getDynamicHue = (): number => {
|
|
try {
|
|
const hex = getComputedStyle(document.documentElement).getPropertyValue('--primary-custom').trim() || '#2563eb'
|
|
let c = hex.replace('#', '')
|
|
if (c.length === 3) c = c.split('').map(x => x + x).join('')
|
|
const r = parseInt(c.substring(0, 2), 16) / 255
|
|
const g = parseInt(c.substring(2, 4), 16) / 255
|
|
const b = parseInt(c.substring(4, 6), 16) / 255
|
|
const max = Math.max(r, g, b)
|
|
const min = Math.min(r, g, b)
|
|
let h = 0
|
|
if (max !== min) {
|
|
const d = max - min
|
|
switch (max) {
|
|
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
|
case g: h = (b - r) / d + 2; break
|
|
case b: h = (r - g) / d + 4; break
|
|
}
|
|
h /= 6
|
|
}
|
|
return Math.round(h * 360)
|
|
} catch (e) {
|
|
return 217
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
ctx.font = `${fontSize}px monospace`
|
|
ctx.textAlign = 'center'
|
|
ctx.textBaseline = 'middle'
|
|
|
|
const timeSec = time * 0.001
|
|
const activeHue = getDynamicHue()
|
|
|
|
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
|
|
|
|
const dx = x - mouse.x
|
|
const dy = y - mouse.y
|
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
const mouseInfluence = Math.exp(-dist / 250)
|
|
const mouseDistortion = (Math.atan2(dy, dx) + (dx + dy) * 0.003) * mouseInfluence * 3.0
|
|
|
|
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)
|
|
|
|
const normalizedFlow = (flowValue + 1) * 0.5
|
|
const charIndex = Math.floor(normalizedFlow * chars.length) % chars.length
|
|
const char = chars[charIndex]
|
|
|
|
if (char === ' ') continue
|
|
|
|
const blendFactor = Math.min(Math.max(mouseInfluence * 0.85 + normalizedFlow * 0.15, 0), 1)
|
|
const alpha = (0.35 + blendFactor * 0.6) * scrollFade
|
|
const lightness = 40 + Math.round(blendFactor * 50)
|
|
|
|
ctx.fillStyle = `hsl(${activeHue} 80% ${lightness}% / ${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 (
|
|
<div
|
|
className={`fixed inset-0 pointer-events-none z-0 overflow-hidden ${className}`}
|
|
style={{
|
|
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)',
|
|
WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)'
|
|
}}
|
|
>
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="absolute inset-0 w-full h-full block will-change-transform pointer-events-none"
|
|
/>
|
|
<div
|
|
className="absolute inset-0 w-full h-full pointer-events-none opacity-60"
|
|
style={{
|
|
backgroundSize: '40px 40px',
|
|
backgroundImage: `
|
|
linear-gradient(to right, var(--card-border-glow, rgba(37, 99, 235, 0.15)) 1px, transparent 1px),
|
|
linear-gradient(to bottom, var(--card-border-glow, rgba(37, 99, 235, 0.15)) 1px, transparent 1px)
|
|
`
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|