Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
213 lines
6.0 KiB
Vue
213 lines
6.0 KiB
Vue
<script setup lang="ts">
|
|
import type { CSSProperties } from 'vue'
|
|
import type { CardBorderEffectKey } from '~~/shared/utils/cardBorderEffects'
|
|
import { hexToRgbTuple } from '@/lib/color'
|
|
|
|
// Electric-Variante portiert aus Vue Bits (DavidHDev/vue-bits, ElectricBorder)
|
|
const props = withDefaults(defineProps<{
|
|
effect: CardBorderEffectKey
|
|
color?: string
|
|
}>(), {
|
|
color: '#6366f1',
|
|
})
|
|
|
|
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
|
|
|
function random(x: number): number {
|
|
return (Math.sin(x * 12.9898) * 43758.5453) % 1
|
|
}
|
|
|
|
function noise2D(x: number, y: number): number {
|
|
const i = Math.floor(x)
|
|
const j = Math.floor(y)
|
|
const fx = x - i
|
|
const fy = y - j
|
|
const a = random(i + j * 57)
|
|
const b = random(i + 1 + j * 57)
|
|
const c = random(i + (j + 1) * 57)
|
|
const d = random(i + 1 + (j + 1) * 57)
|
|
const ux = fx * fx * (3 - 2 * fx)
|
|
const uy = fy * fy * (3 - 2 * fy)
|
|
return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy
|
|
}
|
|
|
|
function octavedNoise(x: number, time: number, seed: number): number {
|
|
let y = 0
|
|
let amplitude = 0.12
|
|
let frequency = 10
|
|
for (let i = 0; i < 10; i++) {
|
|
y += amplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3)
|
|
frequency *= 1.6
|
|
amplitude *= 0.7
|
|
}
|
|
return y
|
|
}
|
|
|
|
function roundedRectPoint(t: number, w: number, h: number, r: number): { x: number, y: number } {
|
|
const sw = w - 2 * r
|
|
const sh = h - 2 * r
|
|
const arc = (Math.PI * r) / 2
|
|
const total = 2 * sw + 2 * sh + 4 * arc
|
|
let d = t * total
|
|
|
|
if (d <= sw) return { x: r + d, y: 0 }
|
|
d -= sw
|
|
if (d <= arc) {
|
|
const a = -Math.PI / 2 + (d / arc) * (Math.PI / 2)
|
|
return { x: w - r + r * Math.cos(a), y: r + r * Math.sin(a) }
|
|
}
|
|
d -= arc
|
|
if (d <= sh) return { x: w, y: r + d }
|
|
d -= sh
|
|
if (d <= arc) {
|
|
const a = (d / arc) * (Math.PI / 2)
|
|
return { x: w - r + r * Math.cos(a), y: h - r + r * Math.sin(a) }
|
|
}
|
|
d -= arc
|
|
if (d <= sw) return { x: w - r - d, y: h }
|
|
d -= sw
|
|
if (d <= arc) {
|
|
const a = Math.PI / 2 + (d / arc) * (Math.PI / 2)
|
|
return { x: r + r * Math.cos(a), y: h - r + r * Math.sin(a) }
|
|
}
|
|
d -= arc
|
|
if (d <= sh) return { x: 0, y: h - r - d }
|
|
d -= sh
|
|
const a = Math.PI + (d / arc) * (Math.PI / 2)
|
|
return { x: r + r * Math.cos(a), y: r + r * Math.sin(a) }
|
|
}
|
|
|
|
let cleanup: (() => void) | null = null
|
|
|
|
onMounted(() => {
|
|
if (props.effect !== 'electric') return
|
|
const canvas = canvasRef.value
|
|
const container = containerRef.value
|
|
if (!canvas || !container) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
let width = 0
|
|
let height = 0
|
|
let radius = 0
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
const updateSize = () => {
|
|
const rect = container.getBoundingClientRect()
|
|
width = rect.width
|
|
height = rect.height
|
|
canvas.width = width * dpr
|
|
canvas.height = height * dpr
|
|
canvas.style.width = `${width}px`
|
|
canvas.style.height = `${height}px`
|
|
radius = parseFloat(getComputedStyle(container).borderRadius) || 0
|
|
}
|
|
updateSize()
|
|
const ro = new ResizeObserver(updateSize)
|
|
ro.observe(container)
|
|
|
|
let raf = 0
|
|
let time = 0
|
|
let last = 0
|
|
|
|
const draw = (now: number) => {
|
|
time += ((now - last) / 1000) * 1.2
|
|
last = now
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
ctx.clearRect(0, 0, width, height)
|
|
ctx.strokeStyle = props.color
|
|
ctx.lineWidth = 1.5
|
|
ctx.lineCap = 'round'
|
|
ctx.lineJoin = 'round'
|
|
|
|
const inset = 1
|
|
const w = width - inset * 2
|
|
const h = height - inset * 2
|
|
const r = Math.max(0, radius - inset)
|
|
const perimeter = 2 * (w + h) + 2 * Math.PI * r
|
|
const samples = Math.floor(perimeter / 2)
|
|
|
|
ctx.beginPath()
|
|
for (let i = 0; i <= samples; i++) {
|
|
const p = i / samples
|
|
const pt = roundedRectPoint(p, w, h, r)
|
|
const nx = octavedNoise(p * 8, time, 0) * 14
|
|
const ny = octavedNoise(p * 8, time, 1) * 14
|
|
if (i === 0) ctx.moveTo(inset + pt.x + nx, inset + pt.y + ny)
|
|
else ctx.lineTo(inset + pt.x + nx, inset + pt.y + ny)
|
|
}
|
|
ctx.closePath()
|
|
ctx.stroke()
|
|
raf = requestAnimationFrame(draw)
|
|
}
|
|
raf = requestAnimationFrame(draw)
|
|
|
|
cleanup = () => {
|
|
cancelAnimationFrame(raf)
|
|
ro.disconnect()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
cleanup?.()
|
|
cleanup = null
|
|
})
|
|
|
|
const rgb = computed(() => hexToRgbTuple(props.color).map(v => Math.round(v * 255)).join(' '))
|
|
const cssVars = computed<CSSProperties>(() => ({ '--be-color': `rgb(${rgb.value})` }) as CSSProperties)
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="containerRef" class="pointer-events-none absolute inset-0 rounded-[inherit]" :style="cssVars">
|
|
<canvas v-if="effect === 'electric'" ref="canvasRef" class="absolute inset-0" />
|
|
<div v-else-if="effect === 'beam'" class="border-beam absolute inset-0 rounded-[inherit]" />
|
|
<div v-else-if="effect === 'pulse'" class="border-pulse absolute inset-0 rounded-[inherit]" />
|
|
<div v-else-if="effect === 'rainbow'" class="border-rainbow absolute inset-0 rounded-[inherit]" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.border-beam {
|
|
border: 2px solid transparent;
|
|
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
|
mask-composite: exclude;
|
|
}
|
|
|
|
.border-beam::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -100%;
|
|
background: conic-gradient(transparent 60%, var(--be-color) 80%, transparent 100%);
|
|
animation: be-rotate 3s linear infinite;
|
|
}
|
|
|
|
.border-pulse {
|
|
border: 2px solid var(--be-color);
|
|
box-shadow: 0 0 16px 2px var(--be-color);
|
|
animation: be-pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
.border-rainbow {
|
|
border: 2px solid transparent;
|
|
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
|
mask-composite: exclude;
|
|
}
|
|
|
|
.border-rainbow::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -100%;
|
|
background: conic-gradient(var(--be-color), #ffffff, var(--be-color));
|
|
animation: be-rotate 4s linear infinite;
|
|
}
|
|
|
|
@keyframes be-rotate {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@keyframes be-pulse {
|
|
0%, 100% { opacity: 0.35; }
|
|
50% { opacity: 1; }
|
|
}
|
|
</style>
|