275 lines
6.8 KiB
Vue
275 lines
6.8 KiB
Vue
<template>
|
|
<div class="robot-particle-wrapper">
|
|
<div class="robot-particle-scene">
|
|
<!-- Static Contour Particles -->
|
|
<div
|
|
v-for="(p, i) in particles"
|
|
:key="`static-${i}`"
|
|
:style="p.style"
|
|
class="robot-particle"
|
|
></div>
|
|
|
|
<!-- Evaporating Particles -->
|
|
<div
|
|
v-for="p in driftParticles"
|
|
:key="p.id"
|
|
:style="p.style"
|
|
class="robot-drift-particle"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
interface ParticleStyle {
|
|
left: string
|
|
top: string
|
|
width: string
|
|
height: string
|
|
backgroundColor: string
|
|
boxShadow: string
|
|
opacity: number
|
|
animation: string
|
|
}
|
|
|
|
interface DriftParticle {
|
|
id: number
|
|
style: any
|
|
}
|
|
|
|
const particles = ref<{ style: ParticleStyle }[]>([])
|
|
const driftParticles = ref<DriftParticle[]>([])
|
|
let driftIdCounter = 0
|
|
let animationFrameId: number
|
|
|
|
// Helper to add line of particles
|
|
const addLine = (x1: number, y1: number, x2: number, y2: number, count: number, sizeBase: number = 2, color: string = '#00f3ff') => {
|
|
for (let i = 0; i <= count; i++) {
|
|
const t = i / count
|
|
const x = x1 + (x2 - x1) * t
|
|
const y = y1 + (y2 - y1) * t
|
|
|
|
// Randomize slightly
|
|
const ox = (Math.random() - 0.5) * 1
|
|
const oy = (Math.random() - 0.5) * 1
|
|
|
|
const size = Math.random() * sizeBase + 1
|
|
const opacity = Math.random() * 0.5 + 0.5
|
|
|
|
// Animation delay
|
|
const delay = Math.random() * 2
|
|
const duration = Math.random() * 2 + 2
|
|
|
|
particles.value.push({
|
|
style: {
|
|
left: `${x + ox}%`,
|
|
top: `${y + oy}%`,
|
|
width: `${size}px`,
|
|
height: `${size}px`,
|
|
backgroundColor: color,
|
|
boxShadow: `0 0 ${size * 2}px ${color}`,
|
|
opacity: opacity,
|
|
animation: `robot-float ${duration}s ease-in-out ${delay}s infinite alternate`
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper for curves (Quadratic Bezier)
|
|
const addCurve = (x1: number, y1: number, cx: number, cy: number, x2: number, y2: number, count: number, sizeBase: number = 2, color: string = '#00f3ff') => {
|
|
for (let i = 0; i <= count; i++) {
|
|
const t = i / count
|
|
const invT = 1 - t
|
|
const x = invT * invT * x1 + 2 * invT * t * cx + t * t * x2
|
|
const y = invT * invT * y1 + 2 * invT * t * cy + t * t * y2
|
|
|
|
const size = Math.random() * sizeBase + 1
|
|
const opacity = Math.random() * 0.5 + 0.5
|
|
const delay = Math.random() * 2
|
|
const duration = Math.random() * 2 + 2
|
|
|
|
particles.value.push({
|
|
style: {
|
|
left: `${x}%`,
|
|
top: `${y}%`,
|
|
width: `${size}px`,
|
|
height: `${size}px`,
|
|
backgroundColor: color,
|
|
boxShadow: `0 0 ${size * 2}px ${color}`,
|
|
opacity: opacity,
|
|
animation: `robot-float ${duration}s ease-in-out ${delay}s infinite alternate`
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const initFace = () => {
|
|
// Coordinates are roughly 0-100% within the container
|
|
// Facing LEFT (so nose is on left side of the component)
|
|
|
|
const C_SKIN = '#00f3ff' // Cyan
|
|
const C_DEEP = '#bc13fe' // Purple/Deep Blue
|
|
|
|
// 1. Forehead
|
|
addCurve(40, 15, 35, 25, 35, 35, 15, 2, C_SKIN)
|
|
|
|
// 2. Nose
|
|
addLine(35, 35, 30, 45, 8, 2, C_SKIN) // Bridge
|
|
addLine(30, 45, 32, 48, 4, 2, C_SKIN) // Tip return
|
|
|
|
// 3. Lips
|
|
addLine(34, 50, 38, 52, 5, 1.5, C_SKIN) // Upper
|
|
addLine(35, 53, 38, 55, 5, 1.5, C_SKIN) // Lower
|
|
|
|
// 4. Chin & Jaw
|
|
addCurve(38, 55, 38, 60, 45, 65, 10, 2, C_SKIN) // Chin
|
|
addLine(45, 65, 65, 55, 15, 2, C_SKIN) // Jawline
|
|
|
|
// 5. Eye (Oval cluster)
|
|
const eyeCx = 45
|
|
const eyeCy = 38
|
|
for(let i=0; i<12; i++) {
|
|
const angle = (i / 12) * Math.PI * 2
|
|
const r = 3
|
|
const x = eyeCx + Math.cos(angle) * r * 0.8 // Slightly wider
|
|
const y = eyeCy + Math.sin(angle) * r
|
|
particles.value.push({
|
|
style: {
|
|
left: `${x}%`,
|
|
top: `${y}%`,
|
|
width: `2px`,
|
|
height: `2px`,
|
|
backgroundColor: C_DEEP,
|
|
boxShadow: `0 0 5px ${C_DEEP}`,
|
|
opacity: 0.8,
|
|
animation: `robot-flicker 3s infinite ${Math.random()}s`
|
|
}
|
|
})
|
|
}
|
|
// Pupil
|
|
particles.value.push({
|
|
style: {
|
|
left: `${eyeCx}%`,
|
|
top: `${eyeCy}%`,
|
|
width: `3px`,
|
|
height: `3px`,
|
|
backgroundColor: '#fff',
|
|
boxShadow: `0 0 8px #fff`,
|
|
opacity: 1,
|
|
animation: `robot-flicker 0.2s infinite`
|
|
}
|
|
})
|
|
|
|
// 6. Ear / Tech area
|
|
const earX = 65
|
|
const earY = 45
|
|
addCurve(earX, earY - 5, earX - 5, earY, earX, earY + 8, 10, 2, C_DEEP)
|
|
addLine(earX, earY - 5, earX + 5, earY, 5, 2, C_DEEP)
|
|
|
|
// 7. Cranium / Back of head
|
|
addCurve(40, 15, 60, 10, 75, 25, 15, 2, C_SKIN)
|
|
addCurve(75, 25, 80, 40, 65, 55, 15, 2, C_SKIN)
|
|
|
|
// 8. Neck
|
|
addLine(50, 65, 50, 85, 10, 2, C_DEEP) // Front neck
|
|
addLine(65, 55, 70, 80, 10, 2, C_DEEP) // Back neck
|
|
|
|
// Internal details (Brain/Cheek lines)
|
|
addLine(45, 45, 60, 50, 8, 1, C_DEEP)
|
|
addLine(50, 25, 65, 35, 8, 1, C_DEEP)
|
|
}
|
|
|
|
const spawnDriftParticle = () => {
|
|
// Spawn from back of head area
|
|
const startX = 60 + Math.random() * 20
|
|
const startY = 20 + Math.random() * 40
|
|
|
|
const id = driftIdCounter++
|
|
const size = Math.random() * 2 + 1
|
|
const duration = Math.random() * 2 + 2
|
|
|
|
driftParticles.value.push({
|
|
id,
|
|
style: {
|
|
left: `${startX}%`,
|
|
top: `${startY}%`,
|
|
width: `${size}px`,
|
|
height: `${size}px`,
|
|
backgroundColor: '#bc13fe',
|
|
boxShadow: '0 0 4px #bc13fe',
|
|
opacity: 1,
|
|
animation: `robot-drift ${duration}s linear forwards`
|
|
}
|
|
})
|
|
|
|
// Cleanup
|
|
setTimeout(() => {
|
|
driftParticles.value = driftParticles.value.filter(p => p.id !== id)
|
|
}, duration * 1000)
|
|
}
|
|
|
|
onMounted(() => {
|
|
initFace()
|
|
|
|
// Drift loop
|
|
const loop = () => {
|
|
if (Math.random() < 0.1) { // Chance to spawn per frame
|
|
spawnDriftParticle()
|
|
}
|
|
animationFrameId = requestAnimationFrame(loop)
|
|
}
|
|
loop()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
cancelAnimationFrame(animationFrameId)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.robot-particle-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: visible;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.robot-particle-scene {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.robot-particle, .robot-drift-particle {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
@keyframes robot-float {
|
|
0% { transform: translate(-50%, -50%) translateY(0px); }
|
|
100% { transform: translate(-50%, -50%) translateY(-5px); }
|
|
}
|
|
|
|
@keyframes robot-flicker {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
52% { opacity: 0.8; }
|
|
54% { opacity: 0.3; }
|
|
}
|
|
|
|
@keyframes robot-drift {
|
|
0% {
|
|
transform: translate(-50%, -50%) scale(1);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: translate(-50%, -50%) translate(30px, -30px) scale(0);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
</style>
|