Add glowing golden ambient particle system to home page
This commit is contained in:
@@ -432,6 +432,16 @@ html, body {
|
|||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-particles-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-start-hero-overlay {
|
.btn-start-hero-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 78.5%;
|
top: 78.5%;
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
<section id="screen-start" class="screen-view active">
|
<section id="screen-start" class="screen-view active">
|
||||||
<div class="home-screen-backdrop">
|
<div class="home-screen-backdrop">
|
||||||
<img src="images/home_page_bg.png" alt="PPE Safety Challenge" class="home-bg-image" />
|
<img src="images/home_page_bg.png" alt="PPE Safety Challenge" class="home-bg-image" />
|
||||||
|
<!-- High-fidelity Golden Ambient Particle Canvas -->
|
||||||
|
<canvas id="home-golden-particles" class="home-particles-canvas"></canvas>
|
||||||
<button id="btn-start-game" class="btn-start-hero-overlay" aria-label="Start Challenge" title="Click or Press Space to Start Challenge">
|
<button id="btn-start-game" class="btn-start-hero-overlay" aria-label="Start Challenge" title="Click or Press Space to Start Challenge">
|
||||||
<span class="btn-hero-hover-text">▶ START CHALLENGE</span>
|
<span class="btn-hero-hover-text">▶ START CHALLENGE</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class SafetyGame {
|
|||||||
init() {
|
init() {
|
||||||
this.avatar = new WorkerAvatar('avatar-container');
|
this.avatar = new WorkerAvatar('avatar-container');
|
||||||
this.particles = new ParticleEngine('stage-fx-canvas');
|
this.particles = new ParticleEngine('stage-fx-canvas');
|
||||||
|
this.homeParticles = new GoldenParticleEngine('home-golden-particles');
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.updateHUD();
|
this.updateHUD();
|
||||||
@@ -376,6 +377,9 @@ class SafetyGame {
|
|||||||
if (screenName === 'start') {
|
if (screenName === 'start') {
|
||||||
this.stopTimer();
|
this.stopTimer();
|
||||||
this.dom.screenStart.classList.add('active');
|
this.dom.screenStart.classList.add('active');
|
||||||
|
if (this.homeParticles) this.homeParticles.start();
|
||||||
|
} else {
|
||||||
|
if (this.homeParticles) this.homeParticles.stop();
|
||||||
}
|
}
|
||||||
if (screenName === 'game') this.dom.screenGame.classList.add('active');
|
if (screenName === 'game') this.dom.screenGame.classList.add('active');
|
||||||
if (screenName === 'summary') {
|
if (screenName === 'summary') {
|
||||||
|
|||||||
+319
@@ -220,3 +220,322 @@ class ParticleEngine {
|
|||||||
requestAnimationFrame(() => this.animate());
|
requestAnimationFrame(() => this.animate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GOLDEN PARTICLE ENGINE FOR HOME SCREEN
|
||||||
|
* Generates floating golden ambient dust, sparkling stars, glowing bokeh orbs, and embers.
|
||||||
|
*/
|
||||||
|
class GoldenParticleEngine {
|
||||||
|
constructor(canvasId) {
|
||||||
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
this.ctx = this.canvas ? this.canvas.getContext('2d') : null;
|
||||||
|
this.particles = [];
|
||||||
|
this.mouse = { x: -1000, y: -1000, isHovering: false };
|
||||||
|
this.isRunning = false;
|
||||||
|
this.maxParticles = 75;
|
||||||
|
this.animId = null;
|
||||||
|
|
||||||
|
if (this.canvas) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.resize();
|
||||||
|
window.addEventListener('resize', () => this.resize());
|
||||||
|
|
||||||
|
// Interactive mouse glow and cursor sparkle trail
|
||||||
|
const backdrop = this.canvas.parentElement;
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.addEventListener('mousemove', (e) => {
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
this.mouse.x = e.clientX - rect.left;
|
||||||
|
this.mouse.y = e.clientY - rect.top;
|
||||||
|
this.mouse.isHovering = true;
|
||||||
|
|
||||||
|
if (Math.random() < 0.4 && this.isRunning) {
|
||||||
|
this.spawnCursorSparkle(this.mouse.x, this.mouse.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
backdrop.addEventListener('mouseleave', () => {
|
||||||
|
this.mouse.isHovering = false;
|
||||||
|
this.mouse.x = -1000;
|
||||||
|
this.mouse.y = -1000;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createInitialParticles();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
if (!this.canvas) return;
|
||||||
|
const rect = this.canvas.parentElement ? this.canvas.parentElement.getBoundingClientRect() : this.canvas.getBoundingClientRect();
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
this.canvas.width = (rect.width || 1024) * dpr;
|
||||||
|
this.canvas.height = (rect.height || 458) * dpr;
|
||||||
|
this.dpr = dpr;
|
||||||
|
this.logicalWidth = rect.width || 1024;
|
||||||
|
this.logicalHeight = rect.height || 458;
|
||||||
|
}
|
||||||
|
|
||||||
|
createInitialParticles() {
|
||||||
|
this.particles = [];
|
||||||
|
for (let i = 0; i < this.maxParticles; i++) {
|
||||||
|
this.particles.push(this.createParticle(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticle(randomY = false) {
|
||||||
|
const types = ['dust', 'dust', 'sparkle', 'orb', 'ember'];
|
||||||
|
const type = types[Math.floor(Math.random() * types.length)];
|
||||||
|
const w = this.logicalWidth || 1024;
|
||||||
|
const h = this.logicalHeight || 458;
|
||||||
|
|
||||||
|
const palettes = [
|
||||||
|
{ fill: '#ffd700', glow: '#f59e0b' }, // 24k Gold
|
||||||
|
{ fill: '#fef08a', glow: '#fbbf24' }, // Light Champagne Sparkle
|
||||||
|
{ fill: '#f59e0b', glow: '#d97706' }, // Deep Warm Amber Gold
|
||||||
|
{ fill: '#fffbeb', glow: '#facc15' }, // Star Core Glow
|
||||||
|
{ fill: '#eab308', glow: '#ca8a04' } // Lustrous Yellow Gold
|
||||||
|
];
|
||||||
|
const palette = palettes[Math.floor(Math.random() * palettes.length)];
|
||||||
|
|
||||||
|
let size, baseAlpha, speedY, speedX, wobbleSpeed;
|
||||||
|
if (type === 'orb') {
|
||||||
|
size = Math.random() * 4.5 + 2.5;
|
||||||
|
baseAlpha = Math.random() * 0.22 + 0.08;
|
||||||
|
speedY = -(Math.random() * 0.35 + 0.12);
|
||||||
|
speedX = (Math.random() - 0.5) * 0.22;
|
||||||
|
wobbleSpeed = Math.random() * 0.015 + 0.005;
|
||||||
|
} else if (type === 'sparkle') {
|
||||||
|
size = Math.random() * 3.5 + 2.2;
|
||||||
|
baseAlpha = Math.random() * 0.55 + 0.35;
|
||||||
|
speedY = -(Math.random() * 0.6 + 0.25);
|
||||||
|
speedX = (Math.random() - 0.5) * 0.35;
|
||||||
|
wobbleSpeed = Math.random() * 0.03 + 0.01;
|
||||||
|
} else if (type === 'ember') {
|
||||||
|
size = Math.random() * 2.2 + 1.2;
|
||||||
|
baseAlpha = Math.random() * 0.75 + 0.25;
|
||||||
|
speedY = -(Math.random() * 0.85 + 0.35);
|
||||||
|
speedX = (Math.random() - 0.5) * 0.4;
|
||||||
|
wobbleSpeed = Math.random() * 0.04 + 0.015;
|
||||||
|
} else { // dust
|
||||||
|
size = Math.random() * 1.8 + 0.8;
|
||||||
|
baseAlpha = Math.random() * 0.45 + 0.2;
|
||||||
|
speedY = -(Math.random() * 0.4 + 0.15);
|
||||||
|
speedX = (Math.random() - 0.5) * 0.2;
|
||||||
|
wobbleSpeed = Math.random() * 0.02 + 0.008;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
x: Math.random() * w,
|
||||||
|
y: randomY ? Math.random() * h : h + Math.random() * 15,
|
||||||
|
size,
|
||||||
|
palette,
|
||||||
|
baseAlpha,
|
||||||
|
currentAlpha: baseAlpha,
|
||||||
|
speedY,
|
||||||
|
speedX,
|
||||||
|
wobbleAngle: Math.random() * Math.PI * 2,
|
||||||
|
wobbleSpeed,
|
||||||
|
wobbleRadius: Math.random() * 20 + 8,
|
||||||
|
twinkleAngle: Math.random() * Math.PI * 2,
|
||||||
|
twinkleSpeed: Math.random() * 0.05 + 0.02,
|
||||||
|
rot: Math.random() * Math.PI * 2,
|
||||||
|
rotSpeed: (Math.random() - 0.5) * 0.025,
|
||||||
|
life: 1.0,
|
||||||
|
isCursorParticle: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnCursorSparkle(x, y) {
|
||||||
|
if (this.particles.length > this.maxParticles + 25) return;
|
||||||
|
|
||||||
|
this.particles.push({
|
||||||
|
type: 'sparkle',
|
||||||
|
x: x + (Math.random() - 0.5) * 14,
|
||||||
|
y: y + (Math.random() - 0.5) * 14,
|
||||||
|
size: Math.random() * 3 + 2,
|
||||||
|
palette: { fill: '#ffffff', glow: '#ffd700' },
|
||||||
|
baseAlpha: 0.95,
|
||||||
|
currentAlpha: 0.95,
|
||||||
|
speedY: (Math.random() - 0.7) * 1.4,
|
||||||
|
speedX: (Math.random() - 0.5) * 1.4,
|
||||||
|
wobbleAngle: Math.random() * Math.PI * 2,
|
||||||
|
wobbleSpeed: 0.05,
|
||||||
|
wobbleRadius: 6,
|
||||||
|
twinkleAngle: 0,
|
||||||
|
twinkleSpeed: 0.12,
|
||||||
|
rot: Math.random() * Math.PI,
|
||||||
|
rotSpeed: 0.06,
|
||||||
|
life: 1.0,
|
||||||
|
decay: 0.022,
|
||||||
|
isCursorParticle: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
this.animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
if (this.animId) {
|
||||||
|
cancelAnimationFrame(this.animId);
|
||||||
|
this.animId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawStarSparkle(ctx, x, y, size, rot, alpha, palette) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(x, y);
|
||||||
|
ctx.rotate(rot);
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillStyle = palette.fill;
|
||||||
|
ctx.shadowColor = palette.glow;
|
||||||
|
ctx.shadowBlur = 10;
|
||||||
|
|
||||||
|
const spikes = 4;
|
||||||
|
const outerRadius = size * 1.9;
|
||||||
|
const innerRadius = size * 0.35;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < spikes * 2; i++) {
|
||||||
|
const r = (i % 2 === 0) ? outerRadius : innerRadius;
|
||||||
|
const angle = (i * Math.PI) / spikes;
|
||||||
|
const sx = Math.cos(angle) * r;
|
||||||
|
const sy = Math.sin(angle) * r;
|
||||||
|
if (i === 0) ctx.moveTo(sx, sy);
|
||||||
|
else ctx.lineTo(sx, sy);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Inner bright core
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0, 0, innerRadius * 0.75, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
const w = this.logicalWidth || 1024;
|
||||||
|
const h = this.logicalHeight || 458;
|
||||||
|
const dpr = this.dpr || 1;
|
||||||
|
|
||||||
|
if (this.ctx && this.canvas) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
this.ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||||
|
const p = this.particles[i];
|
||||||
|
|
||||||
|
// Motion & Oscillation
|
||||||
|
p.wobbleAngle += p.wobbleSpeed;
|
||||||
|
p.twinkleAngle += p.twinkleSpeed;
|
||||||
|
p.rot += p.rotSpeed;
|
||||||
|
|
||||||
|
p.y += p.speedY;
|
||||||
|
p.x += p.speedX + Math.sin(p.wobbleAngle) * 0.4;
|
||||||
|
|
||||||
|
// Twinkle factor
|
||||||
|
const twinkleFactor = 0.65 + 0.35 * Math.sin(p.twinkleAngle);
|
||||||
|
p.currentAlpha = p.baseAlpha * twinkleFactor;
|
||||||
|
|
||||||
|
// Cursor gentle magnetic attraction / deflection
|
||||||
|
if (this.mouse.isHovering) {
|
||||||
|
const dx = p.x - this.mouse.x;
|
||||||
|
const dy = p.y - this.mouse.y;
|
||||||
|
const dist = Math.hypot(dx, dy);
|
||||||
|
if (dist < 65 && dist > 1) {
|
||||||
|
const force = (65 - dist) / 65 * 0.65;
|
||||||
|
p.x += (dx / dist) * force;
|
||||||
|
p.y += (dy / dist) * force;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor-spawned decay
|
||||||
|
if (p.isCursorParticle) {
|
||||||
|
p.life -= p.decay || 0.02;
|
||||||
|
p.currentAlpha *= Math.max(0, p.life);
|
||||||
|
if (p.life <= 0) {
|
||||||
|
this.particles.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recycle particles drifting off screen
|
||||||
|
if (p.y < -25 || p.x < -30 || p.x > w + 30) {
|
||||||
|
if (!p.isCursorParticle) {
|
||||||
|
this.particles[i] = this.createParticle(false);
|
||||||
|
} else {
|
||||||
|
this.particles.splice(i, 1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render particle
|
||||||
|
if (p.type === 'sparkle') {
|
||||||
|
this.drawStarSparkle(this.ctx, p.x, p.y, p.size, p.rot, Math.min(1, p.currentAlpha), p.palette);
|
||||||
|
} else if (p.type === 'orb') {
|
||||||
|
// Soft golden bokeh orb
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.globalAlpha = Math.min(1, p.currentAlpha);
|
||||||
|
this.ctx.fillStyle = p.palette.fill;
|
||||||
|
this.ctx.shadowColor = p.palette.glow;
|
||||||
|
this.ctx.shadowBlur = 10;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// Outer translucent halo
|
||||||
|
this.ctx.fillStyle = p.palette.glow;
|
||||||
|
this.ctx.globalAlpha = p.currentAlpha * 0.35;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(p.x, p.y, p.size * 1.7, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
this.ctx.restore();
|
||||||
|
} else {
|
||||||
|
// Ambient dust or ember
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.globalAlpha = Math.min(1, p.currentAlpha);
|
||||||
|
this.ctx.fillStyle = p.palette.fill;
|
||||||
|
this.ctx.shadowColor = p.palette.glow;
|
||||||
|
this.ctx.shadowBlur = p.type === 'ember' ? 8 : 4;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
if (p.type === 'ember') {
|
||||||
|
this.ctx.fillStyle = '#ffffff';
|
||||||
|
this.ctx.globalAlpha = p.currentAlpha * 0.85;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(p.x, p.y, p.size * 0.4, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animId = requestAnimationFrame(() => this.animate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ParticleEngine = ParticleEngine;
|
||||||
|
window.GoldenParticleEngine = GoldenParticleEngine;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user