From d4afacb1d2048fb78221072a6230066dd468d44a Mon Sep 17 00:00:00 2001 From: Vishva Date: Sat, 26 Sep 2026 18:11:51 +0530 Subject: [PATCH] Add glowing golden ambient particle system to home page --- css/styles.css | 10 ++ index.html | 2 + js/game.js | 4 + js/particles.js | 319 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+) diff --git a/css/styles.css b/css/styles.css index f859f4e..c75711c 100644 --- a/css/styles.css +++ b/css/styles.css @@ -432,6 +432,16 @@ html, body { 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 { position: absolute; top: 78.5%; diff --git a/index.html b/index.html index 23c4c90..eb3ab9c 100644 --- a/index.html +++ b/index.html @@ -81,6 +81,8 @@
PPE Safety Challenge + + diff --git a/js/game.js b/js/game.js index dc6fa3b..8a31835 100644 --- a/js/game.js +++ b/js/game.js @@ -100,6 +100,7 @@ class SafetyGame { init() { this.avatar = new WorkerAvatar('avatar-container'); this.particles = new ParticleEngine('stage-fx-canvas'); + this.homeParticles = new GoldenParticleEngine('home-golden-particles'); this.bindEvents(); this.updateHUD(); @@ -376,6 +377,9 @@ class SafetyGame { if (screenName === 'start') { this.stopTimer(); 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 === 'summary') { diff --git a/js/particles.js b/js/particles.js index ca8f125..3cee031 100644 --- a/js/particles.js +++ b/js/particles.js @@ -220,3 +220,322 @@ class ParticleEngine { 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; +