forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
<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>
|
||||
@property --beam-angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
.border-beam {
|
||||
border: 2px solid transparent;
|
||||
background: conic-gradient(from var(--beam-angle), transparent 60%, var(--be-color) 80%, transparent 100%) border-box;
|
||||
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
animation: be-rotate 3s linear infinite;
|
||||
}
|
||||
|
||||
.border-pulse {
|
||||
border: 2px solid var(--be-color);
|
||||
box-shadow: 0 0 12px 1px var(--be-color);
|
||||
animation: be-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.border-rainbow {
|
||||
border: 2px solid transparent;
|
||||
background: conic-gradient(from var(--beam-angle), var(--be-color), #ffffff, var(--be-color)) border-box;
|
||||
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
animation: be-rotate 4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes be-rotate {
|
||||
to { --beam-angle: 360deg; }
|
||||
}
|
||||
|
||||
@keyframes be-pulse {
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user