forked from BRETT/BRETT
149 lines
4.2 KiB
Vue
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>
|