833 lines
26 KiB
HTML
833 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PRAGATIVEER · Streak</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--badge: 240px;
|
|
--grow: 1.18;
|
|
--halo-w: 170%;
|
|
--font: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: transparent !important;
|
|
font-family: var(--font);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
-webkit-font-smoothing: antialiased;
|
|
user-select: none;
|
|
}
|
|
|
|
/* Canvases fill entire view and pass clicks through */
|
|
#fw, #ec {
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
#fw { z-index: 5; }
|
|
#ec { z-index: 7; }
|
|
|
|
/* Euphoric flash overlay on big burst */
|
|
.flash {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 6;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
background: radial-gradient(circle at 50% 46%, rgba(255, 240, 190, 0.6), rgba(212, 175, 55, 0.15) 45%, transparent 70%);
|
|
}
|
|
body.light-theme .flash {
|
|
background: radial-gradient(circle at 50% 46%, rgba(12, 96, 96, 0.22), rgba(12, 96, 96, 0.08) 45%, transparent 70%);
|
|
}
|
|
.stage.play.tier3 .flash { animation: flash 760ms ease-out 2600ms; }
|
|
|
|
.stage {
|
|
position: relative;
|
|
z-index: 10;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.scene {
|
|
position: relative;
|
|
width: var(--badge);
|
|
height: var(--badge);
|
|
perspective: 1300px;
|
|
perspective-origin: 50% 46%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.stage.play.tier3 .scene { animation: shake 520ms ease-out 2620ms; }
|
|
|
|
/* Halo glow behind spinning badge */
|
|
.halo {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: var(--halo-w);
|
|
aspect-ratio: 1;
|
|
transform: translate(-50%, -50%) scale(0.3);
|
|
opacity: 0;
|
|
border-radius: 50%;
|
|
filter: blur(6px);
|
|
z-index: 0;
|
|
background: radial-gradient(circle,
|
|
rgba(246, 221, 147, 0.55) 0%,
|
|
rgba(212, 175, 55, 0.35) 34%,
|
|
rgba(212, 175, 55, 0.08) 58%,
|
|
transparent 72%);
|
|
}
|
|
body.light-theme .halo {
|
|
background: radial-gradient(circle,
|
|
rgba(12, 96, 96, 0.35) 0%,
|
|
rgba(12, 96, 96, 0.18) 34%,
|
|
rgba(12, 96, 96, 0.05) 58%,
|
|
transparent 72%);
|
|
}
|
|
.stage.play.happy .halo {
|
|
animation: bloom 850ms cubic-bezier(0.2, 0.8, 0.3, 1) 1850ms forwards, pulse 2.8s ease-in-out 2700ms infinite;
|
|
}
|
|
.stage.sad .halo {
|
|
background: radial-gradient(circle,
|
|
rgba(59, 130, 246, 0.25) 0%,
|
|
rgba(100, 116, 139, 0.20) 36%,
|
|
transparent 70%);
|
|
}
|
|
body.light-theme .stage.sad .halo {
|
|
background: radial-gradient(circle,
|
|
rgba(37, 99, 235, 0.22) 0%,
|
|
rgba(148, 163, 184, 0.16) 36%,
|
|
transparent 70%);
|
|
}
|
|
.stage.play.sad .halo {
|
|
animation: bloomcold 1000ms ease-out 1900ms forwards;
|
|
}
|
|
|
|
.scaler {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
transform: scale(1);
|
|
}
|
|
.stage.play.happy .scaler {
|
|
animation: grow 680ms cubic-bezier(0.34, 1.56, 0.5, 1) 2050ms forwards, litglow 700ms ease-out 1900ms forwards;
|
|
}
|
|
body.light-theme .stage.play.happy .scaler {
|
|
animation: grow 680ms cubic-bezier(0.34, 1.56, 0.5, 1) 2050ms forwards, litglow-teal 700ms ease-out 1900ms forwards;
|
|
}
|
|
.stage.play.sad .scaler {
|
|
animation: droop 950ms cubic-bezier(0.3, 0.7, 0.4, 1) 2050ms forwards, dimglow 800ms ease-out 1900ms forwards;
|
|
}
|
|
|
|
.spinner {
|
|
position: absolute;
|
|
inset: 0;
|
|
will-change: transform;
|
|
transform: rotateY(0deg) scale(0);
|
|
}
|
|
.stage.play .spinner {
|
|
animation: spin 1800ms cubic-bezier(0.14, 0.62, 0.2, 1) 150ms forwards;
|
|
}
|
|
|
|
/* Badge styling */
|
|
.badge {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
background: url('/PragatiVeer_Logo/SVG_Logos/Square_dark_background.svg') center/cover no-repeat !important;
|
|
transition: background 0.3s ease;
|
|
}
|
|
body.light-theme .badge {
|
|
background: url('/PragatiVeer_Logo/SVG_Logos/Square_light_background.svg') center/cover no-repeat !important;
|
|
}
|
|
.stage.sad .badge {
|
|
filter: saturate(0.8) brightness(0.9);
|
|
}
|
|
|
|
/* Replay tooltip / test hint */
|
|
.replay-hint {
|
|
position: absolute;
|
|
bottom: 8px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
color: rgba(148, 163, 184, 0.8);
|
|
pointer-events: none;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
body.light-theme .replay-hint { color: rgba(100, 116, 139, 0.85); }
|
|
.stage:hover .replay-hint { opacity: 1; }
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotateY(0deg) scale(0); }
|
|
100% { transform: rotateY(1440deg) scale(1); }
|
|
}
|
|
@keyframes bloom {
|
|
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
|
|
60% { opacity: 1; transform: translate(-50%, -50%) scale(1.06); }
|
|
100% { opacity: 0.9; transform: translate(-50%, -50%) scale(1); }
|
|
}
|
|
@keyframes bloomcold {
|
|
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
|
|
100% { opacity: 0.6; transform: translate(-50%, -50%) scale(0.85); }
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.75; transform: translate(-50%, -50%) scale(1); }
|
|
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }
|
|
}
|
|
@keyframes grow {
|
|
0% { transform: scale(1); }
|
|
70% { transform: scale(calc(var(--grow) + 0.06)); }
|
|
100% { transform: scale(var(--grow)); }
|
|
}
|
|
@keyframes droop {
|
|
0% { transform: scale(1) translateY(0); }
|
|
100% { transform: scale(0.93) translateY(8px); }
|
|
}
|
|
@keyframes litglow {
|
|
0% { filter: drop-shadow(0 0 0 rgba(212, 175, 55, 0)); }
|
|
100% { filter: drop-shadow(0 0 28px rgba(233, 196, 99, 0.65)); }
|
|
}
|
|
@keyframes litglow-teal {
|
|
0% { filter: drop-shadow(0 0 0 rgba(12, 96, 96, 0)); }
|
|
100% { filter: drop-shadow(0 0 26px rgba(12, 96, 96, 0.55)); }
|
|
}
|
|
@keyframes dimglow {
|
|
0% { filter: drop-shadow(0 0 0 rgba(100, 116, 139, 0)); }
|
|
100% { filter: drop-shadow(0 6px 18px rgba(30, 41, 59, 0.4)); }
|
|
}
|
|
@keyframes flash {
|
|
0% { opacity: 0; }
|
|
18% { opacity: 0.85; }
|
|
100% { opacity: 0; }
|
|
}
|
|
@keyframes shake {
|
|
0%, 100% { transform: translate(0, 0); }
|
|
20% { transform: translate(-4px, 2px); }
|
|
40% { transform: translate(4px, -2px); }
|
|
60% { transform: translate(-3px, -1px); }
|
|
80% { transform: translate(3px, 1px); }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.stage.play .spinner { animation: none; transform: rotateY(0) scale(1); }
|
|
.stage.play.happy .scaler { animation: none; transform: scale(var(--grow)); }
|
|
.stage.play.sad .scaler { animation: none; transform: scale(0.93); }
|
|
.stage.play.tier3 .scene, .stage.play.tier3 .flash { animation: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<canvas id="fw"></canvas>
|
|
<canvas id="ec"></canvas>
|
|
<div class="flash"></div>
|
|
|
|
<div class="stage" id="stage" title="Click to replay animation">
|
|
<div class="scene">
|
|
<div class="halo"></div>
|
|
<div class="scaler">
|
|
<div class="spinner">
|
|
<div class="badge"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="replay-hint">✨ Click badge to replay</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
// ── 1. Theme Detection & Synchronization ─────────────────────────────────
|
|
function detectLight() {
|
|
const p = new URLSearchParams(window.location.search);
|
|
const param = p.get('theme');
|
|
if (param) return param === 'light';
|
|
try {
|
|
if (window.parent && window.parent.document && window.parent.document.documentElement) {
|
|
return !window.parent.document.documentElement.classList.contains('dark');
|
|
}
|
|
} catch (e) {}
|
|
return false;
|
|
}
|
|
|
|
let isLight = detectLight();
|
|
window.isLightTheme = isLight;
|
|
|
|
function applyThemeClass(light) {
|
|
if (light) {
|
|
document.body.classList.add('light-theme');
|
|
} else {
|
|
document.body.classList.remove('light-theme');
|
|
}
|
|
}
|
|
applyThemeClass(isLight);
|
|
|
|
// Poll for parent theme switch (e.g. user toggles theme on dashboard)
|
|
setInterval(() => {
|
|
const nowLight = detectLight();
|
|
if (nowLight !== isLight) {
|
|
isLight = nowLight;
|
|
window.isLightTheme = isLight;
|
|
applyThemeClass(isLight);
|
|
rebuildColors();
|
|
play();
|
|
}
|
|
}, 200);
|
|
|
|
// ── 2. Canvases & Sizing ────────────────────────────────────────────────
|
|
const cv = document.getElementById('fw');
|
|
const ctx = cv.getContext('2d');
|
|
const ec = document.getElementById('ec');
|
|
const ectx = ec.getContext('2d');
|
|
|
|
let W, H, DPR;
|
|
function resize() {
|
|
DPR = Math.min(2, window.devicePixelRatio || 1);
|
|
W = cv.width = ec.width = Math.floor(innerWidth * DPR);
|
|
H = cv.height = ec.height = Math.floor(innerHeight * DPR);
|
|
cv.style.width = ec.style.width = innerWidth + 'px';
|
|
cv.style.height = ec.style.height = innerHeight + 'px';
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
// ── 3. High-Contrast Palettes ───────────────────────────────────────────
|
|
let SPARK_COLORS, CONFETTI_COLORS, FLASH_COLORS;
|
|
function rebuildColors() {
|
|
if (isLight) {
|
|
// Saturated jewel tones that POP vividly against white background
|
|
SPARK_COLORS = [
|
|
'#0c6060', // Cyan AI / Deep Teal
|
|
'#e11d48', // Vivid Crimson
|
|
'#2563eb', // Royal Blue
|
|
'#d97706', // Amber Gold
|
|
'#7c3aed', // Purple
|
|
'#059669', // Emerald
|
|
'#dc2626', // Bright Red
|
|
'#ea580c', // Tangerine
|
|
];
|
|
CONFETTI_COLORS = [
|
|
'#0c6060', '#e11d48', '#2563eb', '#d97706',
|
|
'#7c3aed', '#059669', '#ea580c', '#0284c7'
|
|
];
|
|
FLASH_COLORS = ['#ffffff', '#fde047', '#38bdf8'];
|
|
} else {
|
|
// Luminous neon & gold that glow against dark background
|
|
SPARK_COLORS = [
|
|
'#fde047', // Brilliant Gold
|
|
'#f59e0b', // Warm Gold
|
|
'#00F0FF', // Cyan AI Neon
|
|
'#38bdf8', // Sky Blue
|
|
'#f43f5e', // Neon Rose
|
|
'#a855f7', // Purple Neon
|
|
'#34d399', // Mint
|
|
'#ffffff', // Pure White Flash
|
|
];
|
|
CONFETTI_COLORS = [
|
|
'#fde047', '#00F0FF', '#f43f5e', '#a855f7',
|
|
'#34d399', '#ffffff', '#fbbf24'
|
|
];
|
|
FLASH_COLORS = ['#ffffff', '#fef08a', '#7dd3fc'];
|
|
}
|
|
}
|
|
rebuildColors();
|
|
|
|
// ── 4. Particle Systems ─────────────────────────────────────────────────
|
|
let particles = [];
|
|
let emojiParticles = [];
|
|
let shockwaves = [];
|
|
let running = false;
|
|
let emojiRunning = false;
|
|
let rafFW = null;
|
|
let rafEmoji = null;
|
|
let fwTimers = [];
|
|
let mode = 'fireworks'; // 'fireworks' | 'faces'
|
|
|
|
// Firecracker Shell Burst
|
|
function firecrackerBurst(xr, yr, count) {
|
|
const x = xr * W;
|
|
const y = yr * H;
|
|
const n = count | 0;
|
|
|
|
// Center shockwave
|
|
shockwaves.push({
|
|
x, y,
|
|
r: 6 * DPR,
|
|
maxR: (35 + Math.random() * 25) * DPR,
|
|
vr: 2.5 * DPR,
|
|
life: 1,
|
|
col: isLight ? 'rgba(12, 96, 96, 0.45)' : 'rgba(253, 224, 71, 0.6)'
|
|
});
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const a = (Math.PI * 2 * i) / n + (Math.random() - 0.5) * 0.35;
|
|
const sp = (2.2 + Math.random() * 4.8) * DPR;
|
|
const color = SPARK_COLORS[(Math.random() * SPARK_COLORS.length) | 0];
|
|
|
|
particles.push({
|
|
t: 'spark',
|
|
x, y,
|
|
vx: Math.cos(a) * sp,
|
|
vy: Math.sin(a) * sp - 1.2 * DPR,
|
|
life: 1,
|
|
decay: 0.012 + Math.random() * 0.014,
|
|
col: color,
|
|
size: (1.8 + Math.random() * 2.2) * DPR,
|
|
isCrackle: Math.random() < 0.25 // occasional crackle spark
|
|
});
|
|
}
|
|
}
|
|
|
|
// Spinning Confetti Strips
|
|
function spawnConfetti(count) {
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
t: 'conf',
|
|
x: (0.2 + Math.random() * 0.6) * W,
|
|
y: -25 * DPR,
|
|
vx: (-1.4 + Math.random() * 2.8) * DPR,
|
|
vy: (1.6 + Math.random() * 2.6) * DPR,
|
|
rot: Math.random() * Math.PI * 2,
|
|
vr: (-0.15 + Math.random() * 0.3),
|
|
life: 1,
|
|
w: (4.5 + Math.random() * 4) * DPR,
|
|
h: (8 + Math.random() * 7) * DPR,
|
|
col: CONFETTI_COLORS[(Math.random() * CONFETTI_COLORS.length) | 0]
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── 5. Emojis Raining System (Celebration OR Crying) ─────────────────────
|
|
const CELEBRATE_EMOJIS = ['✨', '🎉', '🎊', '🔥', '⚡', '🌟', '💥', '🚀', '🏆', '💫'];
|
|
const CRYING_EMOJIS = ['😭', '😢', '💧', '🥺', '💔', '🌧️', '😿', '⚡'];
|
|
|
|
function spawnEmojiRain(count, isSad) {
|
|
const list = isSad ? CRYING_EMOJIS : CELEBRATE_EMOJIS;
|
|
for (let i = 0; i < count; i++) {
|
|
emojiParticles.push({
|
|
x: (0.08 + Math.random() * 0.84) * W,
|
|
y: -35 * DPR,
|
|
vx: (-0.8 + Math.random() * 1.6) * DPR,
|
|
vy: (1.5 + Math.random() * 2.6) * DPR,
|
|
rot: (-25 + Math.random() * 50) * Math.PI / 180,
|
|
vr: (-0.02 + Math.random() * 0.04),
|
|
life: 1,
|
|
decay: 0.0035 + Math.random() * 0.004,
|
|
size: (18 + Math.random() * 14) * DPR,
|
|
emoji: list[(Math.random() * list.length) | 0],
|
|
swayPhase: Math.random() * Math.PI * 2,
|
|
swaySpeed: 0.03 + Math.random() * 0.03
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── 6. Sad Cartoon Faces with Animated Tears ────────────────────────────
|
|
function drawSadFace(p) {
|
|
const r = p.r;
|
|
const s = p.s;
|
|
const alpha = Math.min(1, p.fi);
|
|
const yb = Math.sin(p.phase) * 3 * DPR + s * 3 * DPR;
|
|
|
|
ctx.save();
|
|
ctx.translate(p.x, p.y + yb);
|
|
ctx.globalAlpha = alpha;
|
|
|
|
const line = Math.max(2, r * 0.08);
|
|
// Light mode: deep slate navy; Dark mode: glowing soft blue-white
|
|
const stroke = isLight ? '#0f172a' : 'rgba(186, 210, 245, 0.95)';
|
|
const fill = isLight ? '#f1f5f9' : 'rgba(148, 163, 184, 0.18)';
|
|
|
|
// Face Circle
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, r, 0, Math.PI * 2);
|
|
ctx.fillStyle = fill;
|
|
ctx.fill();
|
|
ctx.lineWidth = line;
|
|
ctx.strokeStyle = stroke;
|
|
ctx.stroke();
|
|
|
|
// Eyebrows (drooping down sad angle)
|
|
const ex = r * 0.42, ey = -r * 0.12, er = r * 0.11;
|
|
const bl = r * 0.32, tilt = (0.2 + s * 0.5);
|
|
|
|
ctx.lineWidth = Math.max(2, r * 0.085);
|
|
ctx.strokeStyle = stroke;
|
|
ctx.lineCap = 'round';
|
|
|
|
// Left brow
|
|
ctx.beginPath();
|
|
ctx.moveTo(-ex - bl * 0.5, ey - r * 0.32 + bl * tilt);
|
|
ctx.lineTo(-ex + bl * 0.5, ey - r * 0.32 - bl * tilt * 0.6);
|
|
ctx.stroke();
|
|
|
|
// Right brow
|
|
ctx.beginPath();
|
|
ctx.moveTo(ex - bl * 0.5, ey - r * 0.32 - bl * tilt * 0.6);
|
|
ctx.lineTo(ex + bl * 0.5, ey - r * 0.32 + bl * tilt);
|
|
ctx.stroke();
|
|
|
|
// Eyes
|
|
ctx.fillStyle = stroke;
|
|
ctx.beginPath();
|
|
ctx.arc(-ex, ey, er, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(ex, ey, er, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Downward curved trembling sad mouth
|
|
const mw = r * 0.44;
|
|
const my = r * 0.52;
|
|
const dip = r * (0.12 + s * 0.55);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-mw, my);
|
|
ctx.quadraticCurveTo(0, my - dip, mw, my);
|
|
ctx.lineWidth = line;
|
|
ctx.strokeStyle = stroke;
|
|
ctx.stroke();
|
|
|
|
// Animated Tear Drops Streaming Down
|
|
const drop1 = (p.tp % 1);
|
|
const drop2 = ((p.tp + 0.5) % 1);
|
|
const tearColor = isLight ? '#0284c7' : '#38bdf8';
|
|
|
|
const cheeks = [-ex, ex];
|
|
for (const cx of cheeks) {
|
|
[drop1, drop2].forEach((drop) => {
|
|
const ty = ey + er + drop * r * 1.05;
|
|
const tr = r * 0.09 * (1 - drop * 0.3);
|
|
ctx.save();
|
|
ctx.globalAlpha = alpha * (1 - drop * 0.5);
|
|
ctx.fillStyle = tearColor;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, ty, tr, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
});
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// ── 7. Animation Loop (Fireworks Canvas) ─────────────────────────────────
|
|
function tick() {
|
|
// ALWAYS clearRect so modal background remains 100% crystal clean!
|
|
ctx.clearRect(0, 0, W, H);
|
|
|
|
// Shockwaves
|
|
for (let i = shockwaves.length - 1; i >= 0; i--) {
|
|
const sw = shockwaves[i];
|
|
sw.r += sw.vr;
|
|
sw.life -= 0.035;
|
|
if (sw.life <= 0) {
|
|
shockwaves.splice(i, 1);
|
|
continue;
|
|
}
|
|
ctx.save();
|
|
ctx.globalAlpha = Math.max(0, sw.life * 0.6);
|
|
ctx.lineWidth = 2 * DPR;
|
|
ctx.strokeStyle = sw.col;
|
|
ctx.beginPath();
|
|
ctx.arc(sw.x, sw.y, sw.r, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|
|
// Particles (sparks, confetti, sad faces)
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const p = particles[i];
|
|
|
|
if (p.t === 'spark') {
|
|
p.vx *= 0.982;
|
|
p.vy = p.vy * 0.982 + 0.06 * DPR; // gravity
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
p.life -= p.decay;
|
|
|
|
if (p.life <= 0) {
|
|
particles.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = Math.max(0, p.life);
|
|
|
|
// Photorealistic Spark Trail (drawn directly with math — no canvas haze!)
|
|
ctx.strokeStyle = p.col;
|
|
ctx.lineWidth = Math.max(1, p.size * p.life * 0.75);
|
|
ctx.lineCap = 'round';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x, p.y);
|
|
ctx.lineTo(p.x - p.vx * 2.8, p.y - p.vy * 2.8);
|
|
ctx.stroke();
|
|
|
|
// Glowing Core / Head of spark
|
|
ctx.fillStyle = p.col;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size * p.life * 0.9, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Crackle mini twinkle for select sparks
|
|
if (p.isCrackle && Math.random() < 0.3) {
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size * 1.4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
} else if (p.t === 'sadface') {
|
|
p.fi += 0.035;
|
|
p.phase += 0.035;
|
|
p.tp += 0.015;
|
|
drawSadFace(p);
|
|
|
|
} else if (p.t === 'conf') {
|
|
p.vy += 0.045 * DPR;
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
p.rot += p.vr;
|
|
p.life -= 0.0055;
|
|
|
|
if (p.y > H + 25 || p.life <= 0) {
|
|
particles.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = Math.max(0, Math.min(1, p.life));
|
|
ctx.translate(p.x, p.y);
|
|
ctx.rotate(p.rot);
|
|
ctx.fillStyle = p.col;
|
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
if (running || particles.length || shockwaves.length) {
|
|
rafFW = requestAnimationFrame(tick);
|
|
} else {
|
|
ctx.clearRect(0, 0, W, H);
|
|
rafFW = null;
|
|
}
|
|
}
|
|
|
|
// ── 8. Animation Loop (Emojis Canvas) ───────────────────────────────────
|
|
function emojiTick() {
|
|
ectx.clearRect(0, 0, W, H);
|
|
|
|
for (let i = emojiParticles.length - 1; i >= 0; i--) {
|
|
const p = emojiParticles[i];
|
|
p.swayPhase += p.swaySpeed;
|
|
p.x += p.vx + Math.sin(p.swayPhase) * 0.8 * DPR;
|
|
p.y += p.vy;
|
|
p.rot += p.vr;
|
|
p.life -= p.decay;
|
|
|
|
if (p.y > H + 40 || p.life <= 0) {
|
|
emojiParticles.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
ectx.save();
|
|
ectx.globalAlpha = Math.min(1, Math.max(0, p.life));
|
|
ectx.translate(p.x, p.y);
|
|
ectx.rotate(p.rot);
|
|
ectx.font = `${Math.round(p.size)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
|
ectx.textAlign = 'center';
|
|
ectx.textBaseline = 'middle';
|
|
ectx.fillText(p.emoji, 0, 0);
|
|
ectx.restore();
|
|
}
|
|
|
|
if (emojiRunning || emojiParticles.length) {
|
|
rafEmoji = requestAnimationFrame(emojiTick);
|
|
} else {
|
|
ectx.clearRect(0, 0, W, H);
|
|
rafEmoji = null;
|
|
}
|
|
}
|
|
|
|
// ── 9. Stop & Cleanup ───────────────────────────────────────────────────
|
|
function stopAll() {
|
|
fwTimers.forEach(clearTimeout);
|
|
fwTimers = [];
|
|
running = false;
|
|
emojiRunning = false;
|
|
particles = [];
|
|
emojiParticles = [];
|
|
shockwaves = [];
|
|
if (W && H) {
|
|
ctx.clearRect(0, 0, W, H);
|
|
ectx.clearRect(0, 0, W, H);
|
|
}
|
|
}
|
|
|
|
// ── 10. Celebrate (Firecrackers + Sparkles + Emojis) ────────────────────
|
|
function celebrate(t) {
|
|
stopAll();
|
|
mode = 'fireworks';
|
|
running = true;
|
|
if (!rafFW) rafFW = requestAnimationFrame(tick);
|
|
|
|
// Emoji celebration rain
|
|
emojiRunning = true;
|
|
if (!rafEmoji) rafEmoji = requestAnimationFrame(emojiTick);
|
|
|
|
const rounds = Math.round(5 + t * 9);
|
|
const gap = Math.round(330 - t * 140);
|
|
const base = Math.round(32 + t * 40);
|
|
|
|
for (let k = 0; k < rounds; k++) {
|
|
const d = k * gap;
|
|
// Alternating left & right burst
|
|
fwTimers.push(setTimeout(() => firecrackerBurst(0.12 + Math.random() * 0.08, 0.28 + Math.random() * 0.22, base), d));
|
|
fwTimers.push(setTimeout(() => firecrackerBurst(0.80 + Math.random() * 0.08, 0.28 + Math.random() * 0.22, base), d + 60));
|
|
|
|
// Center burst on higher streak intensity
|
|
if (t > 0.35) {
|
|
fwTimers.push(setTimeout(() => firecrackerBurst(0.44 + Math.random() * 0.12, 0.18 + Math.random() * 0.14, base + 8), d + 120));
|
|
}
|
|
|
|
// Confetti & celebratory emoji waves
|
|
fwTimers.push(setTimeout(() => spawnConfetti(4 + Math.round(t * 5)), d + 40));
|
|
fwTimers.push(setTimeout(() => spawnEmojiRain(2 + Math.round(t * 3), false), d + 100));
|
|
}
|
|
|
|
fwTimers.push(setTimeout(() => {
|
|
running = false;
|
|
emojiRunning = false;
|
|
}, rounds * gap + 1200));
|
|
}
|
|
|
|
// ── 11. Crying Sad Mood (Broken Streak / Streak = 0) ────────────────────
|
|
function sadFaces(s) {
|
|
stopAll();
|
|
mode = 'faces';
|
|
running = true;
|
|
if (!rafFW) rafFW = requestAnimationFrame(tick);
|
|
|
|
// Sad crying emojis rain
|
|
emojiRunning = true;
|
|
if (!rafEmoji) rafEmoji = requestAnimationFrame(emojiTick);
|
|
|
|
const r = Math.min(W, H) * 0.095 + s * Math.min(W, H) * 0.02;
|
|
const y = H * 0.42;
|
|
|
|
// Two persistent weeping cartoon faces
|
|
particles.push({ t: 'sadface', x: W * 0.16, y, r, s, phase: 0, tp: 0.1, fi: 0, life: 1 });
|
|
particles.push({ t: 'sadface', x: W * 0.84, y, r, s, phase: 1.6, tp: 0.6, fi: 0, life: 1 });
|
|
|
|
// Ongoing waves of crying emojis and raindrops
|
|
for (let k = 0; k < 8; k++) {
|
|
fwTimers.push(setTimeout(() => spawnEmojiRain(3 + Math.round(s * 3), true), k * 650 + 400));
|
|
}
|
|
}
|
|
|
|
// ── 12. Orchestrate Play ────────────────────────────────────────────────
|
|
const stage = document.getElementById('stage');
|
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
let moodTimer = null;
|
|
|
|
function parseStreak() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const s = params.get('streak');
|
|
const v = parseFloat(s);
|
|
return isFinite(v) ? v : 0;
|
|
}
|
|
|
|
function parseIsSad(streakVal) {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('mood') === 'sad') return true;
|
|
if (params.get('mood') === 'happy') return false;
|
|
// Streak 0 is Day 1 awaits / broken streak — triggers crying mood!
|
|
return streakVal <= 0;
|
|
}
|
|
|
|
function play() {
|
|
clearTimeout(moodTimer);
|
|
stopAll();
|
|
|
|
const streak = parseStreak();
|
|
const isSad = parseIsSad(streak);
|
|
const intensity = isSad ? 0 : Math.min(1, Math.log10(Math.max(streak, 1) + 1) / 2.5);
|
|
|
|
// Mood class updates
|
|
stage.classList.remove('play', 'happy', 'sad', 'tier1', 'tier2', 'tier3');
|
|
document.body.classList.remove('sad');
|
|
|
|
if (isSad) {
|
|
stage.classList.add('sad');
|
|
document.body.classList.add('sad');
|
|
} else {
|
|
stage.classList.add('happy');
|
|
stage.classList.add(intensity < 0.34 ? 'tier1' : intensity < 0.7 ? 'tier2' : 'tier3');
|
|
stage.style.setProperty('--grow', (1.12 + intensity * 0.16).toFixed(3));
|
|
stage.style.setProperty('--halo-w', (160 + intensity * 70).toFixed(0) + '%');
|
|
}
|
|
|
|
// Force CSS reflow & trigger coin flip animation
|
|
void stage.offsetWidth;
|
|
stage.classList.add('play');
|
|
|
|
const sadness = isSad ? 1 : 0;
|
|
if (reduce.matches) {
|
|
if (isSad) setTimeout(() => sadFaces(sadness), 0);
|
|
return;
|
|
}
|
|
|
|
// Trigger after coin spin settles (~2400ms)
|
|
moodTimer = setTimeout(() => {
|
|
if (isSad) {
|
|
sadFaces(sadness);
|
|
} else {
|
|
celebrate(intensity);
|
|
}
|
|
}, 2450);
|
|
}
|
|
|
|
// Click badge/stage to replay animation anytime
|
|
stage.addEventListener('click', () => {
|
|
play();
|
|
});
|
|
|
|
// Keyboard shortcut: Space / R to replay, S to toggle sad/happy for testing
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === ' ' || e.key === 'r' || e.key === 'R') {
|
|
play();
|
|
} else if (e.key === 's' || e.key === 'S') {
|
|
const p = new URLSearchParams(window.location.search);
|
|
const currentMood = p.get('mood');
|
|
p.set('mood', currentMood === 'sad' ? 'happy' : 'sad');
|
|
window.history.replaceState(null, '', '?' + p.toString());
|
|
play();
|
|
}
|
|
});
|
|
|
|
// Auto-play on load
|
|
window.addEventListener('load', () => setTimeout(play, 150));
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |