forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user