AI-News/frontend/app/components/home/RobotFaceParticles.vue
2025-12-04 10:04:21 +08:00

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>