Files
company-celebration/packages/client-screen/src/pixi/BackgroundEffect.ts
empty 22141c9263 feat: festive Chinese red background and prize config unification
- Change all big screen backgrounds to pure Chinese red (#c41230)
- Remove vignette effect for consistent display on different screens
- Unify all prize data to match prizes.json configuration
- Add demo participants fallback when no data imported
- Fix particle visibility reset in startGalaxy()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 16:33:21 +08:00

268 lines
7.0 KiB
TypeScript

import { Application, Container, Graphics, Ticker } from 'pixi.js';
// Cinematic color palette
const COLORS = {
wineRed: 0xc41230, // Festive bright Chinese red
gold: 0xf0c239,
goldDark: 0xd4a84b,
goldLight: 0xffd700,
};
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
size: number;
alpha: number;
rotation: number;
rotationSpeed: number;
type: 'dust' | 'symbol' | 'streak';
life: number;
maxLife: number;
}
export class BackgroundEffect {
private app: Application;
private container: Container;
private particles: Particle[] = [];
private dustLayer: Graphics;
private symbolLayer: Graphics;
private streakLayer: Graphics;
private time = 0;
private windOffset = 0;
private isDestroyed = false;
// Configuration
private readonly DUST_COUNT = 120;
private readonly SYMBOL_COUNT = 25;
private readonly STREAK_INTERVAL = 3000; // ms between streaks
constructor() {
this.app = new Application();
this.container = new Container();
this.dustLayer = new Graphics();
this.symbolLayer = new Graphics();
this.streakLayer = new Graphics();
}
async init(canvas: HTMLCanvasElement): Promise<void> {
await this.app.init({
canvas,
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: COLORS.wineRed,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
// Setup layers (back to front)
this.container.addChild(this.dustLayer);
this.container.addChild(this.symbolLayer);
this.container.addChild(this.streakLayer);
this.app.stage.addChild(this.container);
// Draw vignette overlay
this.drawVignette();
// Initialize particles
this.initDustParticles();
this.initSymbolParticles();
// Start animation loop
this.app.ticker.add(this.update.bind(this));
// Spawn streaks periodically
this.spawnStreakLoop();
// Handle resize
window.addEventListener('resize', this.handleResize.bind(this));
}
private drawVignette(): void {
// Disabled vignette for pure solid color background
// Keep method for potential future use
}
private initDustParticles(): void {
const w = this.app.screen.width;
const h = this.app.screen.height;
for (let i = 0; i < this.DUST_COUNT; i++) {
this.particles.push({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.2,
size: Math.random() * 2 + 1,
alpha: Math.random() * 0.3 + 0.1,
rotation: 0,
rotationSpeed: 0,
type: 'dust',
life: 0,
maxLife: Infinity,
});
}
}
private initSymbolParticles(): void {
const w = this.app.screen.width;
const h = this.app.screen.height;
for (let i = 0; i < this.SYMBOL_COUNT; i++) {
this.particles.push({
x: Math.random() * w,
y: h + Math.random() * 100,
vx: (Math.random() - 0.5) * 0.5,
vy: -Math.random() * 0.8 - 0.3,
size: Math.random() * 8 + 4,
alpha: Math.random() * 0.4 + 0.2,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.02,
type: 'symbol',
life: 0,
maxLife: Infinity,
});
}
}
private spawnStreak(): void {
if (this.isDestroyed) return;
const w = this.app.screen.width;
const h = this.app.screen.height;
const startX = Math.random() * w * 0.3;
const startY = Math.random() * h * 0.3;
this.particles.push({
x: startX,
y: startY,
vx: Math.random() * 8 + 6,
vy: Math.random() * 4 + 3,
size: Math.random() * 3 + 2,
alpha: 0.9,
rotation: Math.atan2(4, 8),
rotationSpeed: 0,
type: 'streak',
life: 0,
maxLife: 60, // frames
});
}
private spawnStreakLoop(): void {
if (this.isDestroyed) return;
this.spawnStreak();
setTimeout(() => this.spawnStreakLoop(), this.STREAK_INTERVAL + Math.random() * 2000);
}
private update(ticker: Ticker): void {
if (this.isDestroyed) return;
this.time += ticker.deltaTime;
this.windOffset = Math.sin(this.time * 0.01) * 0.5;
// Clear graphics
this.dustLayer.clear();
this.symbolLayer.clear();
this.streakLayer.clear();
const w = this.app.screen.width;
const h = this.app.screen.height;
// Update and draw particles
this.particles = this.particles.filter((p) => {
// Update position with wind
p.x += p.vx + this.windOffset * (p.type === 'dust' ? 0.3 : 0.1);
p.y += p.vy;
p.rotation += p.rotationSpeed;
p.life++;
// Check bounds and lifecycle
if (p.type === 'streak') {
if (p.life >= p.maxLife || p.x > w || p.y > h) {
return false;
}
p.alpha = 0.9 * (1 - p.life / p.maxLife);
} else if (p.type === 'symbol') {
// Reset symbols that go off top
if (p.y < -20) {
p.y = h + 20;
p.x = Math.random() * w;
}
} else {
// Wrap dust particles
if (p.x < 0) p.x = w;
if (p.x > w) p.x = 0;
if (p.y < 0) p.y = h;
if (p.y > h) p.y = 0;
}
// Draw particle
this.drawParticle(p);
return true;
});
}
private drawParticle(p: Particle): void {
const layer = p.type === 'dust'
? this.dustLayer
: p.type === 'symbol'
? this.symbolLayer
: this.streakLayer;
if (p.type === 'dust') {
// Simple gold circle
layer.circle(p.x, p.y, p.size);
layer.fill({ color: COLORS.gold, alpha: p.alpha });
} else if (p.type === 'symbol') {
// Fortune symbols (circles and squares)
layer.setTransform(p.x, p.y, 1, 1, p.rotation);
if (Math.random() > 0.5) {
// Circle
layer.circle(0, 0, p.size);
} else {
// Square/diamond
layer.rect(-p.size / 2, -p.size / 2, p.size, p.size);
}
layer.fill({ color: COLORS.goldDark, alpha: p.alpha });
layer.setTransform(0, 0, 1, 1, 0);
} else if (p.type === 'streak') {
// Gold streak with trail
const trailLength = 80;
const dx = Math.cos(p.rotation);
const dy = Math.sin(p.rotation);
// Draw gradient trail
for (let i = 0; i < 10; i++) {
const ratio = i / 10;
const tx = p.x - dx * trailLength * ratio;
const ty = p.y - dy * trailLength * ratio;
const ta = p.alpha * (1 - ratio) * 0.5;
const ts = p.size * (1 - ratio * 0.5);
layer.circle(tx, ty, ts);
layer.fill({ color: COLORS.goldLight, alpha: ta });
}
// Draw head
layer.circle(p.x, p.y, p.size);
layer.fill({ color: COLORS.goldLight, alpha: p.alpha });
}
}
private handleResize(): void {
if (this.isDestroyed) return;
this.app.renderer.resize(window.innerWidth, window.innerHeight);
}
destroy(): void {
this.isDestroyed = true;
window.removeEventListener('resize', this.handleResize.bind(this));
this.app.destroy(true, { children: true });
}
}