forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
<script setup lang="ts">
|
||||
import { hexToRgbTuple, shadeHex } from '@/lib/color'
|
||||
|
||||
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop,
|
||||
// Scroll-Grid + Hover-Fill/Trail auf 2D-Canvas
|
||||
const props = withDefaults(defineProps<{
|
||||
color?: string
|
||||
shape?: 'square' | 'hexagon' | 'circle' | 'triangle'
|
||||
}>(), {
|
||||
color: '#6366f1',
|
||||
shape: 'square',
|
||||
})
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const squareSize = 30
|
||||
const speed = 0.6
|
||||
const hoverTrailAmount = 10
|
||||
let borderColor = ''
|
||||
let hoverFill = ''
|
||||
let edgeRgba = ''
|
||||
const updateColors = () => {
|
||||
const [r, g, b] = hexToRgbTuple(props.color)
|
||||
borderColor = `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, 0.3)`
|
||||
hoverFill = props.color
|
||||
const edgeColor = hexToRgbTuple(shadeHex(props.color, -0.85))
|
||||
edgeRgba = `rgba(${Math.round(edgeColor[0] * 255)}, ${Math.round(edgeColor[1] * 255)}, ${Math.round(edgeColor[2] * 255)}, 0.9)`
|
||||
}
|
||||
updateColors()
|
||||
// Farbwechsel live übernehmen (Settings-Vorschau)
|
||||
watch(() => props.color, updateColors)
|
||||
|
||||
const isHex = props.shape === 'hexagon'
|
||||
const isTri = props.shape === 'triangle'
|
||||
const hexHoriz = squareSize * 1.5
|
||||
const hexVert = squareSize * Math.sqrt(3)
|
||||
|
||||
const offset = { x: 0, y: 0 }
|
||||
let hovered: { x: number, y: number } | null = null
|
||||
const trail: { x: number, y: number }[] = []
|
||||
const opacities = new Map<string, number>()
|
||||
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
}
|
||||
|
||||
const drawHex = (cx: number, cy: number, size: number) => {
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const angle = (Math.PI / 3) * i
|
||||
const vx = cx + size * Math.cos(angle)
|
||||
const vy = cy + size * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(vx, vy)
|
||||
else ctx.lineTo(vx, vy)
|
||||
}
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
const drawCircle = (cx: number, cy: number, size: number) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2)
|
||||
}
|
||||
|
||||
const drawTriangle = (cx: number, cy: number, size: number, flip: boolean) => {
|
||||
ctx.beginPath()
|
||||
if (flip) {
|
||||
ctx.moveTo(cx, cy + size / 2)
|
||||
ctx.lineTo(cx + size / 2, cy - size / 2)
|
||||
ctx.lineTo(cx - size / 2, cy - size / 2)
|
||||
}
|
||||
else {
|
||||
ctx.moveTo(cx, cy - size / 2)
|
||||
ctx.lineTo(cx + size / 2, cy + size / 2)
|
||||
ctx.lineTo(cx - size / 2, cy + size / 2)
|
||||
}
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
const drawGrid = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const cell = (key: string, draw: () => void) => {
|
||||
const alpha = opacities.get(key)
|
||||
ctx.strokeStyle = borderColor
|
||||
ctx.globalAlpha = 0.55
|
||||
if (alpha) {
|
||||
ctx.globalAlpha = alpha
|
||||
ctx.fillStyle = hoverFill
|
||||
draw()
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = 1
|
||||
draw()
|
||||
ctx.stroke()
|
||||
}
|
||||
else {
|
||||
draw()
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
if (isHex) {
|
||||
const colShift = Math.floor(offset.x / hexHoriz)
|
||||
const offsetX = ((offset.x % hexHoriz) + hexHoriz) % hexHoriz
|
||||
const offsetY = ((offset.y % hexVert) + hexVert) % hexVert
|
||||
const cols = Math.ceil(canvas.width / hexHoriz) + 3
|
||||
const rows = Math.ceil(canvas.height / hexVert) + 3
|
||||
for (let col = -2; col < cols; col++) {
|
||||
for (let row = -2; row < rows; row++) {
|
||||
const cx = col * hexHoriz + offsetX
|
||||
const cy = row * hexVert + ((col + colShift) % 2 !== 0 ? hexVert / 2 : 0) + offsetY
|
||||
cell(`${col},${row}`, () => drawHex(cx, cy, squareSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (isTri) {
|
||||
const halfW = squareSize / 2
|
||||
const colShift = Math.floor(offset.x / halfW)
|
||||
const rowShift = Math.floor(offset.y / squareSize)
|
||||
const offsetX = ((offset.x % halfW) + halfW) % halfW
|
||||
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
|
||||
const cols = Math.ceil(canvas.width / halfW) + 4
|
||||
const rows = Math.ceil(canvas.height / squareSize) + 4
|
||||
for (let col = -2; col < cols; col++) {
|
||||
for (let row = -2; row < rows; row++) {
|
||||
const cx = col * halfW + offsetX
|
||||
const cy = row * squareSize + squareSize / 2 + offsetY
|
||||
const flip = (((col + colShift + row + rowShift) % 2) + 2) % 2 !== 0
|
||||
cell(`${col},${row}`, () => drawTriangle(cx, cy, squareSize, flip))
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (props.shape === 'circle') {
|
||||
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
|
||||
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
|
||||
const cols = Math.ceil(canvas.width / squareSize) + 3
|
||||
const rows = Math.ceil(canvas.height / squareSize) + 3
|
||||
for (let col = -2; col < cols; col++) {
|
||||
for (let row = -2; row < rows; row++) {
|
||||
const cx = col * squareSize + squareSize / 2 + offsetX
|
||||
const cy = row * squareSize + squareSize / 2 + offsetY
|
||||
cell(`${col},${row}`, () => drawCircle(cx, cy, squareSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
|
||||
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
|
||||
const cols = Math.ceil(canvas.width / squareSize) + 3
|
||||
const rows = Math.ceil(canvas.height / squareSize) + 3
|
||||
for (let col = -2; col < cols; col++) {
|
||||
for (let row = -2; row < rows; row++) {
|
||||
const sx = col * squareSize + offsetX
|
||||
const sy = row * squareSize + offsetY
|
||||
cell(`${col},${row}`, () => {
|
||||
ctx.beginPath()
|
||||
ctx.rect(sx, sy, squareSize, squareSize)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dunkle Rand-Vignette, aus der Effektfarbe abgeleitet
|
||||
const gradient = ctx.createRadialGradient(
|
||||
canvas.width / 2, canvas.height / 2, 0,
|
||||
canvas.width / 2, canvas.height / 2,
|
||||
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2,
|
||||
)
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
|
||||
gradient.addColorStop(1, edgeRgba)
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
const updateCellOpacities = () => {
|
||||
const targets = new Map<string, number>()
|
||||
if (hovered) targets.set(`${hovered.x},${hovered.y}`, 1)
|
||||
for (let i = 0; i < trail.length; i++) {
|
||||
const t = trail[i]
|
||||
const key = `${t.x},${t.y}`
|
||||
if (!targets.has(key)) targets.set(key, (trail.length - i) / (trail.length + 1))
|
||||
}
|
||||
for (const [key] of targets) {
|
||||
if (!opacities.has(key)) opacities.set(key, 0)
|
||||
}
|
||||
for (const [key, opacity] of opacities) {
|
||||
const next = opacity + ((targets.get(key) || 0) - opacity) * 0.15
|
||||
if (next < 0.005) opacities.delete(key)
|
||||
else opacities.set(key, next)
|
||||
}
|
||||
}
|
||||
|
||||
let raf = 0
|
||||
let visible = true
|
||||
const updateAnimation = () => {
|
||||
if (!visible || document.hidden) {
|
||||
raf = 0
|
||||
return
|
||||
}
|
||||
raf = requestAnimationFrame(updateAnimation)
|
||||
|
||||
const wrapX = isHex ? hexHoriz * 2 : squareSize
|
||||
const wrapY = isHex ? hexVert : isTri ? squareSize * 2 : squareSize
|
||||
// diagonal
|
||||
offset.x = (offset.x - speed + wrapX) % wrapX
|
||||
offset.y = (offset.y - speed + wrapY) % wrapY
|
||||
|
||||
updateCellOpacities()
|
||||
drawGrid()
|
||||
}
|
||||
|
||||
const updateHovered = (col: number, row: number) => {
|
||||
if (!hovered || hovered.x !== col || hovered.y !== row) {
|
||||
if (hovered) {
|
||||
trail.unshift({ ...hovered })
|
||||
if (trail.length > hoverTrailAmount) trail.length = hoverTrailAmount
|
||||
}
|
||||
hovered = { x: col, y: row }
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (event.clientX < rect.left || event.clientX > rect.right
|
||||
|| event.clientY < rect.top || event.clientY > rect.bottom) {
|
||||
if (hovered) {
|
||||
trail.unshift({ ...hovered })
|
||||
if (trail.length > hoverTrailAmount) trail.length = hoverTrailAmount
|
||||
hovered = null
|
||||
}
|
||||
return
|
||||
}
|
||||
const mouseX = event.clientX - rect.left
|
||||
const mouseY = event.clientY - rect.top
|
||||
|
||||
if (isHex) {
|
||||
const colShift = Math.floor(offset.x / hexHoriz)
|
||||
const offsetX = ((offset.x % hexHoriz) + hexHoriz) % hexHoriz
|
||||
const offsetY = ((offset.y % hexVert) + hexVert) % hexVert
|
||||
const adjustedX = mouseX - offsetX
|
||||
const adjustedY = mouseY - offsetY
|
||||
const col = Math.round(adjustedX / hexHoriz)
|
||||
const rowOffset = (col + colShift) % 2 !== 0 ? hexVert / 2 : 0
|
||||
const row = Math.round((adjustedY - rowOffset) / hexVert)
|
||||
updateHovered(col, row)
|
||||
}
|
||||
else if (isTri) {
|
||||
const halfW = squareSize / 2
|
||||
const offsetX = ((offset.x % halfW) + halfW) % halfW
|
||||
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
|
||||
const col = Math.round((mouseX - offsetX) / halfW)
|
||||
const row = Math.floor((mouseY - offsetY) / squareSize)
|
||||
updateHovered(col, row)
|
||||
}
|
||||
else {
|
||||
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
|
||||
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
|
||||
const rounded = props.shape === 'circle' ? Math.round : Math.floor
|
||||
const col = rounded((mouseX - offsetX) / squareSize)
|
||||
const row = rounded((mouseY - offsetY) / squareSize)
|
||||
updateHovered(col, row)
|
||||
}
|
||||
}
|
||||
|
||||
const io = new IntersectionObserver(([entry]) => {
|
||||
visible = entry?.isIntersecting ?? true
|
||||
if (visible && !raf) raf = requestAnimationFrame(updateAnimation)
|
||||
})
|
||||
io.observe(canvas)
|
||||
|
||||
const ro = new ResizeObserver(resizeCanvas)
|
||||
ro.observe(canvas)
|
||||
|
||||
window.addEventListener('pointermove', onPointerMove, { passive: true })
|
||||
|
||||
resizeCanvas()
|
||||
raf = requestAnimationFrame(updateAnimation)
|
||||
|
||||
cleanup = () => {
|
||||
cancelAnimationFrame(raf)
|
||||
io.disconnect()
|
||||
ro.disconnect()
|
||||
window.removeEventListener('pointermove', onPointerMove)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.()
|
||||
cleanup = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="absolute inset-0 block size-full rounded-[inherit] border-none" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user