217 lines
6.2 KiB
Vue
217 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, ref } from 'vue';
|
|
|
|
const heroCanvasRef = ref<HTMLCanvasElement | null>(null);
|
|
let particles: Particle[] = [];
|
|
const mouse = { x: -1000, y: -1000, radius: 120 };
|
|
let animationFrameIdHero: number;
|
|
|
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
};
|
|
|
|
class Particle {
|
|
baseX: number;
|
|
baseY: number;
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
isWhite: boolean;
|
|
blinkTimer: number;
|
|
opacity: number;
|
|
ease: number;
|
|
|
|
constructor(
|
|
x: number,
|
|
y: number,
|
|
private canvasWidth: number,
|
|
private canvasHeight: number,
|
|
) {
|
|
this.baseX = x;
|
|
this.baseY = y;
|
|
this.x = Math.random() * canvasWidth;
|
|
this.y = Math.random() * canvasHeight;
|
|
this.size = 1.6;
|
|
this.isWhite = false;
|
|
this.blinkTimer = 0;
|
|
this.opacity = 1;
|
|
this.ease = 0.06;
|
|
}
|
|
|
|
draw(ctx: CanvasRenderingContext2D) {
|
|
if (this.isWhite) {
|
|
this.opacity = Math.sin(this.blinkTimer) * 0.5 + 0.5;
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0.3, this.opacity)})`;
|
|
this.blinkTimer += 0.1;
|
|
if (this.blinkTimer > Math.PI) {
|
|
this.isWhite = false;
|
|
this.opacity = 1;
|
|
}
|
|
} else {
|
|
const ratio = this.x / this.canvasWidth;
|
|
const r = 168 - (168 - 59) * ratio;
|
|
const g = 85 + (130 - 85) * ratio;
|
|
ctx.fillStyle = `rgb(${r},${g},255)`;
|
|
}
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
update() {
|
|
let dx = mouse.x - this.x;
|
|
let dy = mouse.y - this.y;
|
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < mouse.radius) {
|
|
const force = (mouse.radius - distance) / mouse.radius;
|
|
this.x -= (dx / distance) * force * 7;
|
|
this.y -= (dy / distance) * force * 7;
|
|
} else {
|
|
this.x += (this.baseX - this.x) * this.ease;
|
|
this.y += (this.baseY - this.y) * this.ease;
|
|
}
|
|
}
|
|
}
|
|
|
|
function initHero() {
|
|
if (!heroCanvasRef.value) return;
|
|
const canvas = heroCanvasRef.value;
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
|
|
const tCanvas = document.createElement('canvas');
|
|
const tCtx = tCanvas.getContext('2d');
|
|
if (!tCtx) return;
|
|
|
|
tCanvas.width = canvas.width;
|
|
tCanvas.height = canvas.height;
|
|
|
|
// Wir verschieben den Text im Partikel-Canvas etwas nach unten,
|
|
// um Platz für das "Welcome" darüber zu schaffen
|
|
const fontSize = Math.min(canvas.width / 9, 90);
|
|
tCtx.fillStyle = 'white';
|
|
tCtx.font = `bold ${fontSize}px Arial`;
|
|
tCtx.textAlign = 'center';
|
|
tCtx.fillText(
|
|
'BRATANBONUS.NET',
|
|
tCanvas.width / 2,
|
|
tCanvas.height / 2 + 40,
|
|
);
|
|
|
|
const pixels = tCtx.getImageData(0, 0, tCanvas.width, tCanvas.height);
|
|
particles = [];
|
|
const step = 5;
|
|
for (let y = 0; y < pixels.height; y += step) {
|
|
for (let x = 0; x < pixels.width; x += step) {
|
|
if (pixels.data[y * 4 * pixels.width + x * 4 + 3] > 128) {
|
|
particles.push(new Particle(x, y, canvas.width, canvas.height));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function animateHero() {
|
|
if (!heroCanvasRef.value) return;
|
|
const ctx = heroCanvasRef.value.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.clearRect(0, 0, heroCanvasRef.value.width, heroCanvasRef.value.height);
|
|
if (Math.random() > 0.93 && particles.length > 0) {
|
|
const p = particles[Math.floor(Math.random() * particles.length)];
|
|
if (!p && !p.isWhite) {
|
|
p.isWhite = true;
|
|
p.blinkTimer = 0;
|
|
}
|
|
}
|
|
particles.forEach((p) => {
|
|
p.update();
|
|
p.draw(ctx);
|
|
});
|
|
animationFrameIdHero = requestAnimationFrame(animateHero);
|
|
}
|
|
|
|
const handleResize = () => {
|
|
initHero();
|
|
};
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('mousemove', handleGlobalMouseMove);
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
setTimeout(() => {
|
|
initHero();
|
|
animateHero();
|
|
}, 100);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('mousemove', handleGlobalMouseMove);
|
|
window.removeEventListener('resize', handleResize);
|
|
if (animationFrameIdHero) cancelAnimationFrame(animationFrameIdHero);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="relative flex h-screen flex-col items-center justify-center overflow-hidden bg-transparent"
|
|
>
|
|
<div
|
|
class="pointer-events-none absolute top-[32%] left-1/2 z-10 w-full -translate-x-1/2 text-center"
|
|
>
|
|
<h2
|
|
class="text-2xl font-thin tracking-[1em] uppercase opacity-30 select-none md:text-4xl"
|
|
>
|
|
Welcome
|
|
</h2>
|
|
</div>
|
|
|
|
<canvas
|
|
ref="heroCanvasRef"
|
|
class="pointer-events-none absolute inset-0 z-20 block h-screen w-full bg-transparent"
|
|
></canvas>
|
|
|
|
<a
|
|
href="#bonuses"
|
|
class="group absolute bottom-10 left-1/2 z-30 flex -translate-x-1/2 flex-col items-center gap-2 opacity-50 transition-all duration-300 hover:opacity-100"
|
|
>
|
|
<span
|
|
class="text-[10px] font-bold tracking-[0.3em] text-white/40 uppercase group-hover:text-white/80"
|
|
>Scroll</span
|
|
>
|
|
<div
|
|
class="flex h-10 w-6 justify-center rounded-full border-2 border-white/20 p-1"
|
|
>
|
|
<div
|
|
class="animate-scroll-dot h-2 w-1 rounded-full bg-purple-500"
|
|
></div>
|
|
</div>
|
|
<i
|
|
class="fas fa-chevron-down mt-1 animate-bounce text-sm text-white/30"
|
|
></i>
|
|
</a>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@keyframes scroll-dot {
|
|
0% {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translateY(15px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.animate-scroll-dot {
|
|
animation: scroll-dot 2s infinite;
|
|
}
|
|
|
|
/* Verhindert Text-Markierung während man mit der Maus über den Canvas fährt */
|
|
.select-none {
|
|
user-select: none;
|
|
}
|
|
</style>
|