Files
BRETT/app/components/effects/EffectLetterGlitch.vue
T
2026-10-03 20:07:49 +02:00

149 lines
4.2 KiB
Vue

<script setup lang="ts">
import { hexToRgbTuple, mixHex, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#61dca3',
})
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const fontSize = 14
const charWidth = 9
const charHeight = 18
const lettersAndSymbols = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>?0123456789'
const glitchColors = computed(() => [
shadeHex(props.color, -0.5),
props.color,
shadeHex(props.color, 0.35),
])
interface GlitchLetter {
char: string
color: string
targetColor: string
colorProgress: number
}
let letters: GlitchLetter[] = []
let grid = { columns: 0, rows: 0 }
let ctx: CanvasRenderingContext2D | null = null
let animationId = 0
let lastGlitchTime = 0
let ro: ResizeObserver | null = null
let cornerRadius = 0
const getRandomChar = () => lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]!
const getRandomColor = () => glitchColors.value[Math.floor(Math.random() * glitchColors.value.length)]!
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, () => {
for (const l of letters) {
l.color = getRandomColor()
l.targetColor = getRandomColor()
l.colorProgress = 1
}
draw()
})
function resizeCanvas() {
const canvas = canvasRef.value
const container = containerRef.value
if (!canvas || !container) return
const dpr = window.devicePixelRatio || 1
const width = container.offsetWidth
const height = container.offsetHeight
canvas.width = width * dpr
canvas.height = height * dpr
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0)
cornerRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
grid = {
columns: Math.ceil(width / charWidth),
rows: Math.ceil(height / charHeight),
}
letters = Array.from({ length: grid.columns * grid.rows }, () => ({
char: getRandomChar(),
color: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1,
}))
draw()
}
function draw() {
const canvas = canvasRef.value
if (!ctx || !canvas || !letters.length) return
const { width, height } = canvas.getBoundingClientRect()
ctx.clearRect(0, 0, width, height)
// Rounded-Rect-Clip direkt im Canvas — zuverlässig auch wenn der
// Compositor den Layer nicht an CSS-Clips bindet (Waterfox)
ctx.save()
ctx.beginPath()
ctx.roundRect(0, 0, width, height, cornerRadius)
ctx.clip()
ctx.font = `${fontSize}px monospace`
ctx.textBaseline = 'top'
letters.forEach((letter, index) => {
const x = (index % grid.columns) * charWidth
const y = Math.floor(index / grid.columns) * charHeight
ctx!.fillStyle = letter.color
ctx!.fillText(letter.char, x, y)
})
ctx.restore()
}
function animate() {
const now = Date.now()
if (now - lastGlitchTime >= 60) {
const updateCount = Math.max(1, Math.floor(letters.length * 0.05))
for (let i = 0; i < updateCount; i++) {
const index = Math.floor(Math.random() * letters.length)
const letter = letters[index]
if (!letter) continue
letter.char = getRandomChar()
letter.targetColor = getRandomColor()
letter.colorProgress = 0
}
lastGlitchTime = now
}
let needsRedraw = false
for (const letter of letters) {
if (letter.colorProgress < 1) {
letter.colorProgress = Math.min(1, letter.colorProgress + 0.05)
letter.color = mixHex(letter.color, letter.targetColor, letter.colorProgress)
needsRedraw = true
}
}
if (needsRedraw || now - lastGlitchTime < 70) draw()
animationId = requestAnimationFrame(animate)
}
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
ctx = canvas.getContext('2d')
resizeCanvas()
animate()
ro = new ResizeObserver(resizeCanvas)
if (containerRef.value) ro.observe(containerRef.value)
})
onUnmounted(() => {
cancelAnimationFrame(animationId)
ro?.disconnect()
})
</script>
<template>
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit] opacity-40">
<canvas ref="canvasRef" class="absolute inset-0 size-full rounded-[inherit]" />
</div>
</template>