inial impl
This commit is contained in:
@@ -0,0 +1,642 @@
|
||||
// Faraón Game FX Engine: Procedural Audio, Particles, Screen Shake & Animations
|
||||
|
||||
window.FaraonFx = (function () {
|
||||
let audioCtx = null;
|
||||
let isMuted = false;
|
||||
let particleCanvas = null;
|
||||
let particleCtx = null;
|
||||
let particles = [];
|
||||
let animationFrameId = null;
|
||||
|
||||
function getAudioContext() {
|
||||
if (!audioCtx) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContext) {
|
||||
audioCtx = new AudioContext();
|
||||
}
|
||||
}
|
||||
if (audioCtx && audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
return audioCtx;
|
||||
}
|
||||
|
||||
// --- PROCEDURAL SOUND GENERATORS ---
|
||||
function playCardSnap() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const bufferSize = ctx.sampleRate * 0.05;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.01));
|
||||
}
|
||||
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1400 + Math.random() * 400, now);
|
||||
filter.Q.setValueAtTime(3, now);
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const oscGain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(160, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(40, now + 0.08);
|
||||
|
||||
oscGain.gain.setValueAtTime(0.3, now);
|
||||
oscGain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.setValueAtTime(0.5, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(noiseGain);
|
||||
noiseGain.connect(ctx.destination);
|
||||
|
||||
osc.connect(oscGain);
|
||||
oscGain.connect(ctx.destination);
|
||||
|
||||
noise.start(now);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.09);
|
||||
}
|
||||
|
||||
function playCardDraw() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const bufferSize = ctx.sampleRate * 0.08;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize);
|
||||
}
|
||||
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(2200, now);
|
||||
filter.frequency.exponentialRampToValueAtTime(800, now + 0.08);
|
||||
filter.Q.setValueAtTime(2, now);
|
||||
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(0.25, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
noise.start(now);
|
||||
}
|
||||
|
||||
function playSevenStack(level) {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const rootFreq = 180 + Math.min(level, 4) * 80;
|
||||
|
||||
const osc1 = ctx.createOscillator();
|
||||
const osc2 = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc1.type = 'sawtooth';
|
||||
osc1.frequency.setValueAtTime(rootFreq, now);
|
||||
osc1.frequency.exponentialRampToValueAtTime(rootFreq * 0.5, now + 0.4);
|
||||
|
||||
osc2.type = 'triangle';
|
||||
osc2.frequency.setValueAtTime(rootFreq * 1.5, now);
|
||||
osc2.frequency.exponentialRampToValueAtTime(rootFreq * 0.75, now + 0.4);
|
||||
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(2000, now);
|
||||
filter.frequency.exponentialRampToValueAtTime(200, now + 0.4);
|
||||
|
||||
gain.gain.setValueAtTime(0.4 + Math.min(level * 0.1, 0.4), now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
|
||||
|
||||
osc1.connect(filter);
|
||||
osc2.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc1.start(now);
|
||||
osc2.start(now);
|
||||
osc1.stop(now + 0.45);
|
||||
osc2.stop(now + 0.45);
|
||||
|
||||
triggerScreenShake(level * 3 + 2);
|
||||
}
|
||||
|
||||
function playAceFreeze() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const freqs = [880, 1174, 1567, 2093];
|
||||
freqs.forEach((freq, idx) => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now + idx * 0.04);
|
||||
|
||||
gain.gain.setValueAtTime(0.15, now + idx * 0.04);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.04 + 0.35);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start(now + idx * 0.04);
|
||||
osc.stop(now + idx * 0.04 + 0.36);
|
||||
});
|
||||
|
||||
triggerScreenShake(3);
|
||||
}
|
||||
|
||||
function playHornikChime() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const notes = [523.25, 659.25, 783.99, 1046.50];
|
||||
notes.forEach((freq, i) => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now + i * 0.05);
|
||||
|
||||
gain.gain.setValueAtTime(0.2, now + i * 0.05);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.05 + 0.6);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start(now + i * 0.05);
|
||||
osc.stop(now + i * 0.05 + 0.61);
|
||||
});
|
||||
}
|
||||
|
||||
function playBurnExplosion() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const osc = ctx.createOscillator();
|
||||
const oscGain = ctx.createGain();
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(140, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.7);
|
||||
|
||||
oscGain.gain.setValueAtTime(0.6, now);
|
||||
oscGain.gain.exponentialRampToValueAtTime(0.001, now + 0.7);
|
||||
|
||||
const bufferSize = ctx.sampleRate * 0.6;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.15));
|
||||
}
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(800, now);
|
||||
filter.frequency.exponentialRampToValueAtTime(100, now + 0.6);
|
||||
|
||||
const noiseGain = ctx.createGain();
|
||||
noiseGain.gain.setValueAtTime(0.6, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(noiseGain);
|
||||
noiseGain.connect(ctx.destination);
|
||||
|
||||
osc.connect(oscGain);
|
||||
oscGain.connect(ctx.destination);
|
||||
|
||||
noise.start(now);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.71);
|
||||
|
||||
triggerScreenShake(12);
|
||||
}
|
||||
|
||||
function playVictory() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const melody = [
|
||||
{ f: 261.63, t: 0.00, d: 0.15 },
|
||||
{ f: 392.00, t: 0.15, d: 0.15 },
|
||||
{ f: 523.25, t: 0.30, d: 0.20 },
|
||||
{ f: 659.25, t: 0.50, d: 0.20 },
|
||||
{ f: 783.99, t: 0.70, d: 0.60 }
|
||||
];
|
||||
|
||||
melody.forEach(note => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(note.f, now + note.t);
|
||||
|
||||
gain.gain.setValueAtTime(0.3, now + note.t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + note.t + note.d);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start(now + note.t);
|
||||
osc.stop(now + note.t + note.d + 0.05);
|
||||
});
|
||||
|
||||
triggerConfetti();
|
||||
}
|
||||
|
||||
function playHeartbeat() {
|
||||
if (isMuted) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
[0, 0.18].forEach(offset => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(80, now + offset);
|
||||
osc.frequency.exponentialRampToValueAtTime(45, now + offset + 0.1);
|
||||
|
||||
gain.gain.setValueAtTime(0.4, now + offset);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.12);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start(now + offset);
|
||||
osc.stop(now + offset + 0.13);
|
||||
});
|
||||
}
|
||||
|
||||
// --- SCREEN SHAKE ---
|
||||
function triggerScreenShake(intensity = 5) {
|
||||
const board = document.getElementById('game-table-container');
|
||||
if (!board) return;
|
||||
|
||||
board.classList.remove('shake-fx');
|
||||
void board.offsetWidth;
|
||||
board.style.setProperty('--shake-intensity', `${intensity}px`);
|
||||
board.classList.add('shake-fx');
|
||||
|
||||
setTimeout(() => {
|
||||
board.classList.remove('shake-fx');
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// --- 3D CARD PLAY & DRAW ANIMATIONS ---
|
||||
function getElementCenter(elem) {
|
||||
if (!elem) return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
const rect = elem.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
}
|
||||
|
||||
function getPositionCoords(position) {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
switch (position) {
|
||||
case 'left':
|
||||
case 'slot-left':
|
||||
return { x: w * 0.12, y: h * 0.45 };
|
||||
case 'top':
|
||||
case 'slot-top':
|
||||
return { x: w * 0.50, y: h * 0.14 };
|
||||
case 'right':
|
||||
case 'slot-right':
|
||||
return { x: w * 0.88, y: h * 0.45 };
|
||||
case 'bottom':
|
||||
case 'slot-bottom':
|
||||
default:
|
||||
return { x: w * 0.50, y: h * 0.82 };
|
||||
}
|
||||
}
|
||||
|
||||
function animateCardPlay(sourcePosition, cardInfo) {
|
||||
return new Promise((resolve) => {
|
||||
const container = document.getElementById('game-table-container') || document.body;
|
||||
const discardTargetElem = document.querySelector('.discard-pile-container');
|
||||
const targetPos = discardTargetElem ? getElementCenter(discardTargetElem) : { x: window.innerWidth / 2 + 50, y: window.innerHeight / 2 };
|
||||
const startPos = getPositionCoords(sourcePosition);
|
||||
|
||||
// Vytvoríme lietajúcu kartu
|
||||
const flyingCard = document.createElement('div');
|
||||
flyingCard.className = 'game-card marias-card flying-card-anim';
|
||||
flyingCard.style.position = 'fixed';
|
||||
flyingCard.style.left = `${startPos.x - 55}px`;
|
||||
flyingCard.style.top = `${startPos.y - 80}px`;
|
||||
flyingCard.style.zIndex = '999';
|
||||
flyingCard.style.pointerEvents = 'none';
|
||||
flyingCard.style.boxShadow = '0 20px 40px rgba(0,0,0,0.8), 0 0 30px rgba(245, 176, 65, 0.9)';
|
||||
|
||||
const deck = cardInfo.deck || 'original';
|
||||
const imgPath = (cardInfo.suit && cardInfo.rank)
|
||||
? `assets/cards/${deck}/${cardInfo.suit}_${cardInfo.rank}.webp`
|
||||
: `assets/cards/${deck}/Card_Back.webp`;
|
||||
|
||||
flyingCard.innerHTML = `<img src="${imgPath}" class="marias-card-img" />`;
|
||||
|
||||
container.appendChild(flyingCard);
|
||||
|
||||
const dx = targetPos.x - startPos.x;
|
||||
const dy = targetPos.y - startPos.y;
|
||||
const randomRotation = (Math.random() - 0.5) * 16; // -8 to +8 deg landing tilt
|
||||
|
||||
const animation = flyingCard.animate([
|
||||
{
|
||||
transform: 'translate3d(0, 0, 0) scale(0.85) rotate(0deg)',
|
||||
opacity: 0.9
|
||||
},
|
||||
{
|
||||
transform: `translate3d(${dx * 0.45}px, ${dy * 0.45 - 60}px, 0) scale(1.35) rotate(${randomRotation * 0.5}deg)`,
|
||||
opacity: 1,
|
||||
offset: 0.5
|
||||
},
|
||||
{
|
||||
transform: `translate3d(${dx}px, ${dy}px, 0) scale(1) rotate(${randomRotation}deg)`,
|
||||
opacity: 1
|
||||
}
|
||||
], {
|
||||
duration: 420,
|
||||
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.2)'
|
||||
});
|
||||
|
||||
animation.onfinish = () => {
|
||||
playCardSnap();
|
||||
triggerImpactRipple(targetPos.x, targetPos.y);
|
||||
flyingCard.remove();
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function animateCardDraw(targetPosition, deck = 'original') {
|
||||
return new Promise((resolve) => {
|
||||
const container = document.getElementById('game-table-container') || document.body;
|
||||
const deckElem = document.querySelector('.deck-card-container');
|
||||
const startPos = deckElem ? getElementCenter(deckElem) : { x: window.innerWidth / 2 - 50, y: window.innerHeight / 2 };
|
||||
const targetPos = getPositionCoords(targetPosition);
|
||||
|
||||
const flyingBack = document.createElement('div');
|
||||
flyingBack.className = 'game-card marias-card flying-card-anim';
|
||||
flyingBack.style.position = 'fixed';
|
||||
flyingBack.style.left = `${startPos.x - 55}px`;
|
||||
flyingBack.style.top = `${startPos.y - 80}px`;
|
||||
flyingBack.style.zIndex = '999';
|
||||
flyingBack.style.pointerEvents = 'none';
|
||||
flyingBack.style.boxShadow = '0 10px 25px rgba(0,0,0,0.7)';
|
||||
flyingBack.innerHTML = `<img src="assets/cards/${deck || 'original'}/Card_Back.webp" class="marias-card-img" />`;
|
||||
|
||||
container.appendChild(flyingBack);
|
||||
playCardDraw();
|
||||
|
||||
|
||||
const dx = targetPos.x - startPos.x;
|
||||
const dy = targetPos.y - startPos.y;
|
||||
|
||||
const animation = flyingBack.animate([
|
||||
{ transform: 'translate3d(0, 0, 0) scale(1) rotate(0deg)', opacity: 1 },
|
||||
{ transform: `translate3d(${dx * 0.5}px, ${dy * 0.5 - 30}px, 0) scale(1.15) rotate(10deg)`, opacity: 1, offset: 0.5 },
|
||||
{ transform: `translate3d(${dx}px, ${dy}px, 0) scale(0.6) rotate(0deg)`, opacity: 0.2 }
|
||||
], {
|
||||
duration: 380,
|
||||
easing: 'cubic-bezier(0.25, 1, 0.5, 1)'
|
||||
});
|
||||
|
||||
animation.onfinish = () => {
|
||||
flyingBack.remove();
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function triggerImpactRipple(x, y) {
|
||||
if (!particleCanvas) return;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const angle = (i / 12) * Math.PI * 2;
|
||||
const speed = 2 + Math.random() * 3;
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
gravity: 0,
|
||||
size: 3 + Math.random() * 3,
|
||||
color: '#ffd700',
|
||||
shape: 'circle',
|
||||
life: 0.6,
|
||||
decay: 0.04,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- PARTICLE ENGINE ---
|
||||
function initParticles(canvasId) {
|
||||
particleCanvas = document.getElementById(canvasId);
|
||||
if (!particleCanvas) return;
|
||||
particleCtx = particleCanvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
if (particleCanvas) {
|
||||
particleCanvas.width = window.innerWidth;
|
||||
particleCanvas.height = window.innerHeight;
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
if (!animationFrameId) {
|
||||
renderParticles();
|
||||
}
|
||||
}
|
||||
|
||||
function renderParticles() {
|
||||
if (particleCtx && particleCanvas) {
|
||||
particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height);
|
||||
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy += p.gravity || 0;
|
||||
p.rotation += p.vRot || 0;
|
||||
p.life -= p.decay || 0.02;
|
||||
|
||||
if (p.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
particleCtx.save();
|
||||
particleCtx.translate(p.x, p.y);
|
||||
particleCtx.rotate(p.rotation);
|
||||
particleCtx.globalAlpha = Math.max(0, p.life);
|
||||
|
||||
if (p.shape === 'circle') {
|
||||
particleCtx.fillStyle = p.color;
|
||||
particleCtx.beginPath();
|
||||
particleCtx.arc(0, 0, p.size, 0, Math.PI * 2);
|
||||
particleCtx.fill();
|
||||
} else if (p.shape === 'rect') {
|
||||
particleCtx.fillStyle = p.color;
|
||||
particleCtx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * (p.aspect || 1));
|
||||
} else if (p.shape === 'spark') {
|
||||
particleCtx.strokeStyle = p.color;
|
||||
particleCtx.lineWidth = p.size;
|
||||
particleCtx.beginPath();
|
||||
particleCtx.moveTo(-p.vx * 3, -p.vy * 3);
|
||||
particleCtx.lineTo(0, 0);
|
||||
particleCtx.stroke();
|
||||
}
|
||||
particleCtx.restore();
|
||||
}
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(renderParticles);
|
||||
}
|
||||
|
||||
function triggerFireParticles(x, y, count = 25) {
|
||||
if (!particleCanvas) return;
|
||||
const colors = ['#ff4500', '#ff8c00', '#ffd700', '#ffffff'];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.5;
|
||||
const speed = 3 + Math.random() * 6;
|
||||
particles.push({
|
||||
x: x || window.innerWidth / 2,
|
||||
y: y || window.innerHeight / 2,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
gravity: -0.05,
|
||||
size: 4 + Math.random() * 6,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
shape: 'circle',
|
||||
life: 1.0,
|
||||
decay: 0.03 + Math.random() * 0.03,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function triggerIceParticles(x, y, count = 30) {
|
||||
if (!particleCanvas) return;
|
||||
const colors = ['#00ffff', '#e0ffff', '#87cefa', '#ffffff'];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 2 + Math.random() * 7;
|
||||
particles.push({
|
||||
x: x || window.innerWidth / 2,
|
||||
y: y || window.innerHeight / 2,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
gravity: 0.1,
|
||||
size: 3 + Math.random() * 5,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
shape: 'spark',
|
||||
life: 1.0,
|
||||
decay: 0.02 + Math.random() * 0.02,
|
||||
rotation: Math.random() * Math.PI
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function triggerBurnExplosion(x, y) {
|
||||
if (!particleCanvas) return;
|
||||
const colors = ['#ff0000', '#ff4500', '#ffaa00', '#333333', '#ffffff'];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 4 + Math.random() * 10;
|
||||
particles.push({
|
||||
x: x || window.innerWidth / 2,
|
||||
y: y || window.innerHeight / 2,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
gravity: 0.15,
|
||||
size: 5 + Math.random() * 8,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
shape: Math.random() > 0.5 ? 'rect' : 'circle',
|
||||
aspect: 1 + Math.random() * 2,
|
||||
life: 1.0,
|
||||
decay: 0.02 + Math.random() * 0.02,
|
||||
rotation: Math.random() * Math.PI,
|
||||
vRot: (Math.random() - 0.5) * 0.2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function triggerConfetti() {
|
||||
if (!particleCanvas) return;
|
||||
const colors = ['#ffd700', '#ff4500', '#00ffcc', '#ff007f', '#ffffff', '#7b2cbf'];
|
||||
for (let i = 0; i < 150; i++) {
|
||||
const x = Math.random() * window.innerWidth;
|
||||
const y = -20 - Math.random() * 50;
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: 3 + Math.random() * 5,
|
||||
gravity: 0.08,
|
||||
size: 8 + Math.random() * 6,
|
||||
aspect: 0.6 + Math.random() * 0.8,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
shape: 'rect',
|
||||
life: 1.0,
|
||||
decay: 0.005 + Math.random() * 0.005,
|
||||
rotation: Math.random() * Math.PI * 2,
|
||||
vRot: (Math.random() - 0.5) * 0.15
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
return isMuted;
|
||||
}
|
||||
|
||||
return {
|
||||
playCardSnap,
|
||||
playCardDraw,
|
||||
playSevenStack,
|
||||
playAceFreeze,
|
||||
playHornikChime,
|
||||
playBurnExplosion,
|
||||
playVictory,
|
||||
playHeartbeat,
|
||||
triggerScreenShake,
|
||||
initParticles,
|
||||
triggerFireParticles,
|
||||
triggerIceParticles,
|
||||
triggerBurnExplosion,
|
||||
triggerConfetti,
|
||||
animateCardPlay,
|
||||
animateCardDraw,
|
||||
toggleMute
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user