mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 00:19:31 -06:00
Landing Page
This commit is contained in:
315
src/content/Backgrounds/DotGrid/DotGrid.vue
Normal file
315
src/content/Backgrounds/DotGrid/DotGrid.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<section :class="`flex items-center justify-center h-full w-full relative ${className}`" :style="style">
|
||||
<div ref="wrapperRef" class="w-full h-full relative">
|
||||
<canvas ref="canvasRef" class="absolute inset-0 w-full h-full pointer-events-none" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { InertiaPlugin } from 'gsap/InertiaPlugin'
|
||||
|
||||
gsap.registerPlugin(InertiaPlugin)
|
||||
|
||||
const throttle = <T extends unknown[]>(func: (...args: T) => void, limit: number) => {
|
||||
let lastCall = 0
|
||||
return function (this: unknown, ...args: T) {
|
||||
const now = performance.now()
|
||||
if (now - lastCall >= limit) {
|
||||
lastCall = now
|
||||
func.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Dot {
|
||||
cx: number
|
||||
cy: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
_inertiaApplied: boolean
|
||||
}
|
||||
|
||||
export interface DotGridProps {
|
||||
dotSize?: number
|
||||
gap?: number
|
||||
baseColor?: string
|
||||
activeColor?: string
|
||||
proximity?: number
|
||||
speedTrigger?: number
|
||||
shockRadius?: number
|
||||
shockStrength?: number
|
||||
maxSpeed?: number
|
||||
resistance?: number
|
||||
returnDuration?: number
|
||||
className?: string
|
||||
style?: Record<string, string | number>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DotGridProps>(), {
|
||||
dotSize: 16,
|
||||
gap: 32,
|
||||
baseColor: '#5227FF',
|
||||
activeColor: '#5227FF',
|
||||
proximity: 150,
|
||||
speedTrigger: 100,
|
||||
shockRadius: 250,
|
||||
shockStrength: 5,
|
||||
maxSpeed: 5000,
|
||||
resistance: 750,
|
||||
returnDuration: 1.5,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
|
||||
const wrapperRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const dots = ref<Dot[]>([])
|
||||
const pointer = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
speed: 0,
|
||||
lastTime: 0,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
})
|
||||
|
||||
function hexToRgb(hex: string) {
|
||||
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
|
||||
if (!m) return { r: 0, g: 0, b: 0 }
|
||||
return {
|
||||
r: parseInt(m[1], 16),
|
||||
g: parseInt(m[2], 16),
|
||||
b: parseInt(m[3], 16),
|
||||
}
|
||||
}
|
||||
|
||||
const baseRgb = computed(() => hexToRgb(props.baseColor))
|
||||
const activeRgb = computed(() => hexToRgb(props.activeColor))
|
||||
|
||||
const circlePath = computed(() => {
|
||||
if (typeof window === 'undefined' || !window.Path2D) return null
|
||||
|
||||
const p = new Path2D()
|
||||
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2)
|
||||
return p
|
||||
})
|
||||
|
||||
const buildGrid = () => {
|
||||
const wrap = wrapperRef.value
|
||||
const canvas = canvasRef.value
|
||||
if (!wrap || !canvas) return
|
||||
|
||||
const { width, height } = wrap.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) ctx.scale(dpr, dpr)
|
||||
|
||||
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap))
|
||||
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap))
|
||||
const cell = props.dotSize + props.gap
|
||||
|
||||
const gridW = cell * cols - props.gap
|
||||
const gridH = cell * rows - props.gap
|
||||
|
||||
const extraX = width - gridW
|
||||
const extraY = height - gridH
|
||||
|
||||
const startX = extraX / 2 + props.dotSize / 2
|
||||
const startY = extraY / 2 + props.dotSize / 2
|
||||
|
||||
const newDots: Dot[] = []
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const cx = startX + x * cell
|
||||
const cy = startY + y * cell
|
||||
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false })
|
||||
}
|
||||
}
|
||||
dots.value = newDots
|
||||
}
|
||||
|
||||
let rafId: number
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
const draw = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const { x: px, y: py } = pointer.value
|
||||
const proxSq = props.proximity * props.proximity
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const ox = dot.cx + dot.xOffset
|
||||
const oy = dot.cy + dot.yOffset
|
||||
const dx = dot.cx - px
|
||||
const dy = dot.cy - py
|
||||
const dsq = dx * dx + dy * dy
|
||||
|
||||
let style = props.baseColor
|
||||
if (dsq <= proxSq) {
|
||||
const dist = Math.sqrt(dsq)
|
||||
const t = 1 - dist / props.proximity
|
||||
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t)
|
||||
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t)
|
||||
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t)
|
||||
style = `rgb(${r},${g},${b})`
|
||||
}
|
||||
|
||||
if (circlePath.value) {
|
||||
ctx.save()
|
||||
ctx.translate(ox, oy)
|
||||
ctx.fillStyle = style
|
||||
ctx.fill(circlePath.value)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(draw)
|
||||
}
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const now = performance.now()
|
||||
const pr = pointer.value
|
||||
const dt = pr.lastTime ? now - pr.lastTime : 16
|
||||
const dx = e.clientX - pr.lastX
|
||||
const dy = e.clientY - pr.lastY
|
||||
let vx = (dx / dt) * 1000
|
||||
let vy = (dy / dt) * 1000
|
||||
let speed = Math.hypot(vx, vy)
|
||||
if (speed > props.maxSpeed) {
|
||||
const scale = props.maxSpeed / speed
|
||||
vx *= scale
|
||||
vy *= scale
|
||||
speed = props.maxSpeed
|
||||
}
|
||||
pr.lastTime = now
|
||||
pr.lastX = e.clientX
|
||||
pr.lastY = e.clientY
|
||||
pr.vx = vx
|
||||
pr.vy = vy
|
||||
pr.speed = speed
|
||||
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
pr.x = e.clientX - rect.left
|
||||
pr.y = e.clientY - rect.top
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y)
|
||||
if (speed > props.speedTrigger && dist < props.proximity && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const pushX = dot.cx - pr.x + vx * 0.005
|
||||
const pushY = dot.cy - pr.y + vy * 0.005
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
gsap.to(dot, {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const cx = e.clientX - rect.left
|
||||
const cy = e.clientY - rect.top
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - cx, dot.cy - cy)
|
||||
if (dist < props.shockRadius && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const falloff = Math.max(0, 1 - dist / props.shockRadius)
|
||||
const pushX = (dot.cx - cx) * props.shockStrength * falloff
|
||||
const pushY = (dot.cy - cy) * props.shockStrength * falloff
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
gsap.to(dot, {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const throttledMove = throttle(onMove, 50)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
buildGrid()
|
||||
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
}
|
||||
|
||||
if ('ResizeObserver' in window) {
|
||||
resizeObserver = new ResizeObserver(buildGrid)
|
||||
if (wrapperRef.value) {
|
||||
resizeObserver.observe(wrapperRef.value)
|
||||
}
|
||||
} else {
|
||||
(window as Window).addEventListener('resize', buildGrid)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', throttledMove, { passive: true })
|
||||
window.addEventListener('click', onClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
} else {
|
||||
window.removeEventListener('resize', buildGrid)
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', throttledMove)
|
||||
window.removeEventListener('click', onClick)
|
||||
})
|
||||
|
||||
watch([() => props.dotSize, () => props.gap], () => {
|
||||
buildGrid()
|
||||
})
|
||||
|
||||
watch([() => props.proximity, () => props.baseColor, activeRgb, baseRgb, circlePath], () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
246
src/content/Backgrounds/LetterGlitch/LetterGlitch.vue
Normal file
246
src/content/Backgrounds/LetterGlitch/LetterGlitch.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<canvas ref="canvasRef" class="block w-full h-full" />
|
||||
<div v-if="outerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]" />
|
||||
<div v-if="centerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
glitchColors?: string[]
|
||||
glitchSpeed?: number
|
||||
centerVignette?: boolean
|
||||
outerVignette?: boolean
|
||||
smooth?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
glitchColors: () => ["#2b4539", "#61dca3", "#61b3dc"],
|
||||
glitchSpeed: 50,
|
||||
centerVignette: false,
|
||||
outerVignette: false,
|
||||
smooth: true
|
||||
})
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const animationRef = ref<number | null>(null)
|
||||
const letters = ref<{
|
||||
char: string
|
||||
color: string
|
||||
targetColor: string
|
||||
colorProgress: number
|
||||
}[]>([])
|
||||
const grid = ref({ columns: 0, rows: 0 })
|
||||
const context = ref<CanvasRenderingContext2D | null>(null)
|
||||
const lastGlitchTime = ref(Date.now())
|
||||
|
||||
const fontSize = 16
|
||||
const charWidth = 10
|
||||
const charHeight = 20
|
||||
|
||||
const lettersAndSymbols = [
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
"!", "@", "#", "$", "&", "*", "(", ")", "-", "_", "+", "=", "/",
|
||||
"[", "]", "{", "}", ";", ":", "<", ">", ",", "0", "1", "2", "3",
|
||||
"4", "5", "6", "7", "8", "9"
|
||||
]
|
||||
|
||||
const getRandomChar = () => {
|
||||
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]
|
||||
}
|
||||
|
||||
const getRandomColor = () => {
|
||||
return props.glitchColors[Math.floor(Math.random() * props.glitchColors.length)]
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
|
||||
return r + r + g + g + b + b
|
||||
})
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
const interpolateColor = (
|
||||
start: { r: number; g: number; b: number },
|
||||
end: { r: number; g: number; b: number },
|
||||
factor: number
|
||||
) => {
|
||||
const result = {
|
||||
r: Math.round(start.r + (end.r - start.r) * factor),
|
||||
g: Math.round(start.g + (end.g - start.g) * factor),
|
||||
b: Math.round(start.b + (end.b - start.b) * factor),
|
||||
}
|
||||
return `rgb(${result.r}, ${result.g}, ${result.b})`
|
||||
}
|
||||
|
||||
const calculateGrid = (width: number, height: number) => {
|
||||
const columns = Math.ceil(width / charWidth)
|
||||
const rows = Math.ceil(height / charHeight)
|
||||
return { columns, rows }
|
||||
}
|
||||
|
||||
const initializeLetters = (columns: number, rows: number) => {
|
||||
grid.value = { columns, rows }
|
||||
const totalLetters = columns * rows
|
||||
letters.value = Array.from({ length: totalLetters }, () => ({
|
||||
char: getRandomChar(),
|
||||
color: getRandomColor(),
|
||||
targetColor: getRandomColor(),
|
||||
colorProgress: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const parent = canvas.parentElement
|
||||
if (!parent) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const rect = parent.getBoundingClientRect()
|
||||
|
||||
canvas.width = rect.width * dpr
|
||||
canvas.height = rect.height * dpr
|
||||
|
||||
canvas.style.width = `${rect.width}px`
|
||||
canvas.style.height = `${rect.height}px`
|
||||
|
||||
if (context.value) {
|
||||
context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
}
|
||||
|
||||
const { columns, rows } = calculateGrid(rect.width, rect.height)
|
||||
initializeLetters(columns, rows)
|
||||
drawLetters()
|
||||
}
|
||||
|
||||
const drawLetters = () => {
|
||||
if (!context.value || letters.value.length === 0) return
|
||||
const ctx = context.value
|
||||
const { width, height } = canvasRef.value!.getBoundingClientRect()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.font = `${fontSize}px monospace`
|
||||
ctx.textBaseline = "top"
|
||||
|
||||
letters.value.forEach((letter, index) => {
|
||||
const x = (index % grid.value.columns) * charWidth
|
||||
const y = Math.floor(index / grid.value.columns) * charHeight
|
||||
ctx.fillStyle = letter.color
|
||||
ctx.fillText(letter.char, x, y)
|
||||
})
|
||||
}
|
||||
|
||||
const updateLetters = () => {
|
||||
if (!letters.value || letters.value.length === 0) return
|
||||
|
||||
const updateCount = Math.max(1, Math.floor(letters.value.length * 0.05))
|
||||
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
const index = Math.floor(Math.random() * letters.value.length)
|
||||
if (!letters.value[index]) continue
|
||||
|
||||
letters.value[index].char = getRandomChar()
|
||||
letters.value[index].targetColor = getRandomColor()
|
||||
|
||||
if (!props.smooth) {
|
||||
letters.value[index].color = letters.value[index].targetColor
|
||||
letters.value[index].colorProgress = 1
|
||||
} else {
|
||||
letters.value[index].colorProgress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSmoothTransitions = () => {
|
||||
let needsRedraw = false
|
||||
letters.value.forEach((letter) => {
|
||||
if (letter.colorProgress < 1) {
|
||||
letter.colorProgress += 0.05
|
||||
if (letter.colorProgress > 1) letter.colorProgress = 1
|
||||
|
||||
const startRgb = hexToRgb(letter.color)
|
||||
const endRgb = hexToRgb(letter.targetColor)
|
||||
if (startRgb && endRgb) {
|
||||
letter.color = interpolateColor(
|
||||
startRgb,
|
||||
endRgb,
|
||||
letter.colorProgress
|
||||
)
|
||||
needsRedraw = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (needsRedraw) {
|
||||
drawLetters()
|
||||
}
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
const now = Date.now()
|
||||
if (now - lastGlitchTime.value >= props.glitchSpeed) {
|
||||
updateLetters()
|
||||
drawLetters()
|
||||
lastGlitchTime.value = now
|
||||
}
|
||||
|
||||
if (props.smooth) {
|
||||
handleSmoothTransitions()
|
||||
}
|
||||
|
||||
animationRef.value = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
let resizeTimeout: number
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimeout)
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
}
|
||||
resizeCanvas()
|
||||
animate()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
context.value = canvas.getContext("2d")
|
||||
resizeCanvas()
|
||||
animate()
|
||||
|
||||
window.addEventListener("resize", handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
}
|
||||
window.removeEventListener("resize", handleResize)
|
||||
})
|
||||
|
||||
watch([() => props.glitchSpeed, () => props.smooth], () => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
}
|
||||
animate()
|
||||
})
|
||||
</script>
|
||||
197
src/content/Backgrounds/Squares/Squares.vue
Normal file
197
src/content/Backgrounds/Squares/Squares.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="w-full h-full border-none block" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern
|
||||
|
||||
interface GridOffset {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
direction?: "diagonal" | "up" | "right" | "down" | "left"
|
||||
speed?: number
|
||||
borderColor?: CanvasStrokeStyle
|
||||
squareSize?: number
|
||||
hoverFillColor?: CanvasStrokeStyle
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
direction: "right",
|
||||
speed: 1,
|
||||
borderColor: "#999",
|
||||
squareSize: 40,
|
||||
hoverFillColor: "#222"
|
||||
})
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const requestRef = ref<number | null>(null)
|
||||
const numSquaresX = ref<number>(0)
|
||||
const numSquaresY = ref<number>(0)
|
||||
const gridOffset = ref<GridOffset>({ x: 0, y: 0 })
|
||||
const hoveredSquareRef = ref<GridOffset | null>(null)
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
numSquaresX.value = Math.ceil(canvas.width / props.squareSize) + 1
|
||||
numSquaresY.value = Math.ceil(canvas.height / props.squareSize) + 1
|
||||
}
|
||||
|
||||
const drawGrid = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!ctx || !canvas) return
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||
|
||||
for (let x = startX; x < canvas.width + props.squareSize; x += props.squareSize) {
|
||||
for (let y = startY; y < canvas.height + props.squareSize; y += props.squareSize) {
|
||||
const squareX = x - (gridOffset.value.x % props.squareSize)
|
||||
const squareY = y - (gridOffset.value.y % props.squareSize)
|
||||
|
||||
if (
|
||||
hoveredSquareRef.value &&
|
||||
Math.floor((x - startX) / props.squareSize) === hoveredSquareRef.value.x &&
|
||||
Math.floor((y - startY) / props.squareSize) === hoveredSquareRef.value.y
|
||||
) {
|
||||
ctx.fillStyle = props.hoverFillColor
|
||||
ctx.fillRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||
}
|
||||
|
||||
ctx.strokeStyle = props.borderColor
|
||||
ctx.strokeRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||
}
|
||||
}
|
||||
|
||||
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, "#0e0e0e")
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
const updateAnimation = () => {
|
||||
const effectiveSpeed = Math.max(props.speed, 0.1)
|
||||
|
||||
switch (props.direction) {
|
||||
case "right":
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "left":
|
||||
gridOffset.value.x = (gridOffset.value.x + effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "up":
|
||||
gridOffset.value.y = (gridOffset.value.y + effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "down":
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "diagonal":
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
drawGrid()
|
||||
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const mouseX = event.clientX - rect.left
|
||||
const mouseY = event.clientY - rect.top
|
||||
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||
|
||||
const hoveredSquareX = Math.floor(
|
||||
(mouseX + gridOffset.value.x - startX) / props.squareSize
|
||||
)
|
||||
const hoveredSquareY = Math.floor(
|
||||
(mouseY + gridOffset.value.y - startY) / props.squareSize
|
||||
)
|
||||
|
||||
if (
|
||||
!hoveredSquareRef.value ||
|
||||
hoveredSquareRef.value.x !== hoveredSquareX ||
|
||||
hoveredSquareRef.value.y !== hoveredSquareY
|
||||
) {
|
||||
hoveredSquareRef.value = { x: hoveredSquareX, y: hoveredSquareY }
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoveredSquareRef.value = null
|
||||
}
|
||||
|
||||
const initializeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
ctx = canvas.getContext("2d")
|
||||
resizeCanvas()
|
||||
|
||||
canvas.addEventListener("mousemove", handleMouseMove)
|
||||
canvas.addEventListener("mouseleave", handleMouseLeave)
|
||||
window.addEventListener("resize", resizeCanvas)
|
||||
|
||||
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
const canvas = canvasRef.value
|
||||
|
||||
if (requestRef.value) {
|
||||
cancelAnimationFrame(requestRef.value)
|
||||
requestRef.value = null
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
canvas.removeEventListener("mousemove", handleMouseMove)
|
||||
canvas.removeEventListener("mouseleave", handleMouseLeave)
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", resizeCanvas)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeCanvas()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
watch(
|
||||
[() => props.direction, () => props.speed, () => props.borderColor, () => props.hoverFillColor, () => props.squareSize],
|
||||
() => {
|
||||
cleanup()
|
||||
initializeCanvas()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
Reference in New Issue
Block a user