forked from BRETT/BRETT
204 lines
6.2 KiB
Vue
204 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop,
|
|
// ohne Cursor-Interaktion (Hover-Card ist zu klein dafür)
|
|
const props = withDefaults(defineProps<{ color?: string }>(), {
|
|
color: '#6366f1',
|
|
})
|
|
|
|
class Grad {
|
|
constructor(
|
|
public x: number,
|
|
public y: number,
|
|
public z: number,
|
|
) {}
|
|
|
|
dot2(x: number, y: number): number {
|
|
return this.x * x + this.y * y
|
|
}
|
|
}
|
|
|
|
class Noise {
|
|
grad3 = [
|
|
new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0),
|
|
new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1),
|
|
new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1),
|
|
]
|
|
|
|
p = [
|
|
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240,
|
|
21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88,
|
|
237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83,
|
|
111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216,
|
|
80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186,
|
|
3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58,
|
|
17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
|
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193,
|
|
238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
|
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128,
|
|
195, 78, 66, 215, 61, 156, 180,
|
|
]
|
|
|
|
perm = new Array(512)
|
|
gradP = new Array(512)
|
|
|
|
constructor(seed = 0) {
|
|
this.seed(seed)
|
|
}
|
|
|
|
seed(seed: number) {
|
|
if (seed > 0 && seed < 1) seed *= 65536
|
|
seed = Math.floor(seed)
|
|
if (seed < 256) seed |= seed << 8
|
|
|
|
for (let i = 0; i < 256; i++) {
|
|
const v = i & 1 ? this.p[i]! ^ (seed & 255) : this.p[i]! ^ ((seed >> 8) & 255)
|
|
this.perm[i] = this.perm[i + 256] = v
|
|
this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]
|
|
}
|
|
}
|
|
|
|
fade(t: number): number {
|
|
return t * t * t * (t * (t * 6 - 15) + 10)
|
|
}
|
|
|
|
lerp(a: number, b: number, t: number): number {
|
|
return (1 - t) * a + t * b
|
|
}
|
|
|
|
perlin2(x: number, y: number): number {
|
|
let X = Math.floor(x)
|
|
let Y = Math.floor(y)
|
|
x -= X
|
|
y -= Y
|
|
X &= 255
|
|
Y &= 255
|
|
|
|
const n00 = this.gradP[X + this.perm[Y]!]!.dot2(x, y)
|
|
const n01 = this.gradP[X + this.perm[Y + 1]!]!.dot2(x, y - 1)
|
|
const n10 = this.gradP[X + 1 + this.perm[Y]!]!.dot2(x - 1, y)
|
|
const n11 = this.gradP[X + 1 + this.perm[Y + 1]!]!.dot2(x - 1, y - 1)
|
|
const u = this.fade(x)
|
|
|
|
return this.lerp(this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y))
|
|
}
|
|
}
|
|
|
|
interface Point {
|
|
x: number
|
|
y: number
|
|
wave: { x: number, y: number }
|
|
}
|
|
|
|
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
|
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
|
|
|
let ctx: CanvasRenderingContext2D | null = null
|
|
let bounding = { width: 0, height: 0 }
|
|
let cornerRadius = 0
|
|
let noise: Noise | null = null
|
|
let lines: Point[][] = []
|
|
let frameId: number | null = null
|
|
|
|
const xGap = 10
|
|
const yGap = 26
|
|
|
|
function setSize() {
|
|
const container = containerRef.value
|
|
const canvas = canvasRef.value
|
|
if (!container || !canvas) return
|
|
const rect = container.getBoundingClientRect()
|
|
bounding = { width: rect.width, height: rect.height }
|
|
const dpr = window.devicePixelRatio || 1
|
|
canvas.width = rect.width * dpr
|
|
canvas.height = rect.height * dpr
|
|
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
cornerRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
|
|
}
|
|
|
|
function setLines() {
|
|
const { width, height } = bounding
|
|
lines = []
|
|
const oWidth = width + 200
|
|
const oHeight = height + 30
|
|
const totalLines = Math.ceil(oWidth / xGap)
|
|
const totalPoints = Math.ceil(oHeight / yGap)
|
|
const xStart = (width - xGap * totalLines) / 2
|
|
const yStart = (height - yGap * totalPoints) / 2
|
|
|
|
for (let i = 0; i <= totalLines; i++) {
|
|
const pts: Point[] = []
|
|
for (let j = 0; j <= totalPoints; j++) {
|
|
pts.push({ x: xStart + xGap * i, y: yStart + yGap * j, wave: { x: 0, y: 0 } })
|
|
}
|
|
lines.push(pts)
|
|
}
|
|
}
|
|
|
|
function tick(t: number) {
|
|
if (!ctx || !noise) return
|
|
const { width, height } = bounding
|
|
|
|
for (const pts of lines) {
|
|
for (const p of pts) {
|
|
const move = noise.perlin2((p.x + t * 0.0125) * 0.002, (p.y + t * 0.005) * 0.0015) * 12
|
|
p.wave.x = Math.cos(move) * 32
|
|
p.wave.y = Math.sin(move) * 16
|
|
}
|
|
}
|
|
|
|
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.beginPath()
|
|
ctx.strokeStyle = props.color
|
|
for (const points of lines) {
|
|
let p1 = points[0]!
|
|
ctx.moveTo(p1.x + p1.wave.x, p1.y + p1.wave.y)
|
|
for (const p of points) {
|
|
ctx.lineTo(Math.round((p.x + p.wave.x) * 10) / 10, Math.round((p.y + p.wave.y) * 10) / 10)
|
|
}
|
|
p1 = points[points.length - 1]!
|
|
ctx.moveTo(p1.x + p1.wave.x, p1.y + p1.wave.y)
|
|
}
|
|
ctx.stroke()
|
|
ctx.restore()
|
|
|
|
frameId = requestAnimationFrame(tick)
|
|
}
|
|
|
|
let ro: ResizeObserver | null = null
|
|
|
|
onMounted(() => {
|
|
const canvas = canvasRef.value
|
|
const container = containerRef.value
|
|
if (!canvas || !container) return
|
|
|
|
ctx = canvas.getContext('2d')
|
|
noise = new Noise(Math.random())
|
|
setSize()
|
|
setLines()
|
|
frameId = requestAnimationFrame(tick)
|
|
|
|
ro = new ResizeObserver(() => {
|
|
setSize()
|
|
setLines()
|
|
})
|
|
ro.observe(container)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
ro?.disconnect()
|
|
if (frameId !== null) cancelAnimationFrame(frameId)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit] opacity-50">
|
|
<canvas ref="canvasRef" class="block size-full rounded-[inherit]" />
|
|
</div>
|
|
</template>
|