All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
Sparkle 🎆/🎇 from the jumbo emoji with a lightweight particle fountain that piles on the floor. Prefer text/plain over sticky clipboard images on Linux so address-bar URLs do not also attach a leftover bitmap.
438 lines
8.8 KiB
TypeScript
438 lines
8.8 KiB
TypeScript
export type BurstMotionStyle =
|
|
| 'explode'
|
|
| 'bounce'
|
|
| 'rise'
|
|
| 'sparkle'
|
|
| 'splash'
|
|
| 'spiral'
|
|
| 'punch'
|
|
| 'scatter'
|
|
| 'shower'
|
|
| 'firework';
|
|
|
|
export type EmojiBurstProfile = {
|
|
style: BurstMotionStyle;
|
|
particleCount: number;
|
|
gravity: number;
|
|
drag: number;
|
|
speedMin: number;
|
|
speedMax: number;
|
|
launchUpMin: number;
|
|
launchUpMax: number;
|
|
spinMin: number;
|
|
spinMax: number;
|
|
fontSizeMin: number;
|
|
fontSizeMax: number;
|
|
heroFontSizeMin: number;
|
|
heroFontSizeMax: number;
|
|
companions?: string[];
|
|
companionChance?: number;
|
|
/** Bias burst angle in radians. 0 = right, -π/2 = up. */
|
|
angleBias?: number;
|
|
/** Limit spawn to a cone (radians). Omit for full 360°. */
|
|
angleSpread?: number;
|
|
twinkle?: boolean;
|
|
wobble?: boolean;
|
|
/** Full-viewport overlay (Box2D pile for fireworks). */
|
|
fullscreen?: boolean;
|
|
};
|
|
|
|
const DEFAULT_PROFILE: EmojiBurstProfile = {
|
|
style: 'explode',
|
|
particleCount: 36,
|
|
gravity: 400,
|
|
drag: 0.935,
|
|
speedMin: 240,
|
|
speedMax: 560,
|
|
launchUpMin: 40,
|
|
launchUpMax: 130,
|
|
spinMin: -180,
|
|
spinMax: 180,
|
|
fontSizeMin: 14,
|
|
fontSizeMax: 26,
|
|
heroFontSizeMin: 26,
|
|
heroFontSizeMax: 34,
|
|
};
|
|
|
|
const PROFILE_OVERRIDES: Record<string, Partial<EmojiBurstProfile>> = {
|
|
'😆': {
|
|
style: 'bounce',
|
|
particleCount: 28,
|
|
gravity: 520,
|
|
speedMin: 180,
|
|
speedMax: 360,
|
|
launchUpMin: 120,
|
|
launchUpMax: 220,
|
|
spinMin: -240,
|
|
spinMax: 240,
|
|
wobble: true,
|
|
},
|
|
'😂': {
|
|
style: 'bounce',
|
|
particleCount: 30,
|
|
gravity: 500,
|
|
speedMin: 160,
|
|
speedMax: 340,
|
|
launchUpMin: 110,
|
|
launchUpMax: 210,
|
|
spinMin: -220,
|
|
spinMax: 220,
|
|
wobble: true,
|
|
},
|
|
'🤣': {
|
|
style: 'bounce',
|
|
particleCount: 32,
|
|
gravity: 480,
|
|
speedMin: 200,
|
|
speedMax: 380,
|
|
launchUpMin: 130,
|
|
launchUpMax: 240,
|
|
wobble: true,
|
|
},
|
|
'🔥': {
|
|
style: 'rise',
|
|
particleCount: 24,
|
|
gravity: -120,
|
|
drag: 0.96,
|
|
speedMin: 80,
|
|
speedMax: 200,
|
|
launchUpMin: 60,
|
|
launchUpMax: 160,
|
|
spinMin: -90,
|
|
spinMax: 90,
|
|
fontSizeMin: 16,
|
|
fontSizeMax: 28,
|
|
twinkle: true,
|
|
wobble: true,
|
|
companions: ['✨'],
|
|
companionChance: 0.35,
|
|
},
|
|
'❤️': {
|
|
style: 'rise',
|
|
particleCount: 22,
|
|
gravity: -80,
|
|
drag: 0.97,
|
|
speedMin: 60,
|
|
speedMax: 150,
|
|
launchUpMin: 40,
|
|
launchUpMax: 110,
|
|
spinMin: -40,
|
|
spinMax: 40,
|
|
fontSizeMin: 14,
|
|
fontSizeMax: 24,
|
|
companions: ['💕', '💖'],
|
|
companionChance: 0.4,
|
|
},
|
|
'💕': {
|
|
style: 'rise',
|
|
particleCount: 20,
|
|
gravity: -70,
|
|
drag: 0.97,
|
|
speedMin: 50,
|
|
speedMax: 140,
|
|
launchUpMin: 35,
|
|
launchUpMax: 100,
|
|
spinMin: -35,
|
|
spinMax: 35,
|
|
},
|
|
'💖': {
|
|
style: 'rise',
|
|
particleCount: 20,
|
|
gravity: -90,
|
|
drag: 0.968,
|
|
speedMin: 55,
|
|
speedMax: 150,
|
|
launchUpMin: 45,
|
|
launchUpMax: 115,
|
|
twinkle: true,
|
|
},
|
|
'⭐': {
|
|
style: 'sparkle',
|
|
particleCount: 26,
|
|
gravity: 40,
|
|
drag: 0.94,
|
|
speedMin: 100,
|
|
speedMax: 260,
|
|
launchUpMin: 20,
|
|
launchUpMax: 80,
|
|
spinMin: -120,
|
|
spinMax: 120,
|
|
twinkle: true,
|
|
companions: ['✨'],
|
|
companionChance: 0.5,
|
|
},
|
|
'✨': {
|
|
style: 'sparkle',
|
|
particleCount: 30,
|
|
gravity: 30,
|
|
drag: 0.945,
|
|
speedMin: 90,
|
|
speedMax: 240,
|
|
launchUpMin: 15,
|
|
launchUpMax: 70,
|
|
spinMin: -200,
|
|
spinMax: 200,
|
|
twinkle: true,
|
|
},
|
|
'💀': {
|
|
style: 'scatter',
|
|
particleCount: 20,
|
|
gravity: 620,
|
|
drag: 0.92,
|
|
speedMin: 200,
|
|
speedMax: 420,
|
|
launchUpMin: 20,
|
|
launchUpMax: 90,
|
|
spinMin: -360,
|
|
spinMax: 360,
|
|
fontSizeMin: 16,
|
|
fontSizeMax: 28,
|
|
},
|
|
'🎉': {
|
|
style: 'shower',
|
|
particleCount: 40,
|
|
gravity: 280,
|
|
drag: 0.93,
|
|
speedMin: 200,
|
|
speedMax: 480,
|
|
launchUpMin: 80,
|
|
launchUpMax: 200,
|
|
companions: ['🎊', '✨', '🎈'],
|
|
companionChance: 0.45,
|
|
},
|
|
'🎊': {
|
|
style: 'shower',
|
|
particleCount: 38,
|
|
gravity: 260,
|
|
drag: 0.932,
|
|
speedMin: 190,
|
|
speedMax: 460,
|
|
launchUpMin: 70,
|
|
launchUpMax: 190,
|
|
companions: ['🎉', '✨'],
|
|
companionChance: 0.4,
|
|
},
|
|
'👏': {
|
|
style: 'punch',
|
|
particleCount: 18,
|
|
gravity: 320,
|
|
speedMin: 220,
|
|
speedMax: 400,
|
|
launchUpMin: 30,
|
|
launchUpMax: 100,
|
|
angleBias: -Math.PI / 2,
|
|
angleSpread: Math.PI * 0.85,
|
|
spinMin: -100,
|
|
spinMax: 100,
|
|
},
|
|
'👍': {
|
|
style: 'punch',
|
|
particleCount: 14,
|
|
gravity: 380,
|
|
speedMin: 260,
|
|
speedMax: 440,
|
|
launchUpMin: 100,
|
|
launchUpMax: 200,
|
|
angleBias: -Math.PI / 2,
|
|
angleSpread: Math.PI * 0.55,
|
|
spinMin: -60,
|
|
spinMax: 60,
|
|
},
|
|
'💯': {
|
|
style: 'punch',
|
|
particleCount: 16,
|
|
gravity: 300,
|
|
speedMin: 200,
|
|
speedMax: 380,
|
|
launchUpMin: 140,
|
|
launchUpMax: 240,
|
|
angleBias: -Math.PI / 2,
|
|
angleSpread: Math.PI * 0.45,
|
|
fontSizeMin: 16,
|
|
fontSizeMax: 30,
|
|
wobble: true,
|
|
},
|
|
'💦': {
|
|
style: 'splash',
|
|
particleCount: 32,
|
|
gravity: 540,
|
|
drag: 0.925,
|
|
speedMin: 280,
|
|
speedMax: 520,
|
|
launchUpMin: 160,
|
|
launchUpMax: 300,
|
|
spinMin: -140,
|
|
spinMax: 140,
|
|
angleBias: -Math.PI / 2,
|
|
angleSpread: Math.PI * 1.1,
|
|
},
|
|
'🌊': {
|
|
style: 'splash',
|
|
particleCount: 28,
|
|
gravity: 420,
|
|
drag: 0.93,
|
|
speedMin: 220,
|
|
speedMax: 460,
|
|
launchUpMin: 100,
|
|
launchUpMax: 220,
|
|
angleBias: -Math.PI / 2,
|
|
angleSpread: Math.PI,
|
|
companions: ['💧'],
|
|
companionChance: 0.3,
|
|
},
|
|
'⚡': {
|
|
style: 'scatter',
|
|
particleCount: 14,
|
|
gravity: 180,
|
|
drag: 0.88,
|
|
speedMin: 380,
|
|
speedMax: 680,
|
|
launchUpMin: 10,
|
|
launchUpMax: 60,
|
|
spinMin: -30,
|
|
spinMax: 30,
|
|
fontSizeMin: 18,
|
|
fontSizeMax: 32,
|
|
angleSpread: Math.PI * 1.2,
|
|
},
|
|
'🌀': {
|
|
style: 'spiral',
|
|
particleCount: 24,
|
|
gravity: 60,
|
|
drag: 0.96,
|
|
speedMin: 120,
|
|
speedMax: 260,
|
|
launchUpMin: 0,
|
|
launchUpMax: 40,
|
|
spinMin: -420,
|
|
spinMax: 420,
|
|
},
|
|
'🤯': {
|
|
style: 'explode',
|
|
particleCount: 34,
|
|
gravity: 360,
|
|
speedMin: 280,
|
|
speedMax: 580,
|
|
launchUpMin: 60,
|
|
launchUpMax: 160,
|
|
companions: ['💥', '✨', '⭐'],
|
|
companionChance: 0.5,
|
|
},
|
|
'😭': {
|
|
style: 'splash',
|
|
particleCount: 26,
|
|
gravity: 480,
|
|
drag: 0.94,
|
|
speedMin: 140,
|
|
speedMax: 300,
|
|
launchUpMin: 40,
|
|
launchUpMax: 120,
|
|
angleBias: Math.PI / 2,
|
|
angleSpread: Math.PI * 0.7,
|
|
companions: ['💧'],
|
|
companionChance: 0.55,
|
|
},
|
|
'🥳': {
|
|
style: 'shower',
|
|
particleCount: 36,
|
|
gravity: 250,
|
|
speedMin: 180,
|
|
speedMax: 440,
|
|
launchUpMin: 90,
|
|
launchUpMax: 210,
|
|
companions: ['🎉', '🎊', '🎈'],
|
|
companionChance: 0.4,
|
|
},
|
|
'🎆': {
|
|
style: 'firework',
|
|
fullscreen: true,
|
|
particleCount: 320,
|
|
gravity: 980,
|
|
drag: 1,
|
|
speedMin: 420,
|
|
speedMax: 980,
|
|
launchUpMin: 180,
|
|
launchUpMax: 420,
|
|
spinMin: -520,
|
|
spinMax: 520,
|
|
fontSizeMin: 16,
|
|
fontSizeMax: 30,
|
|
heroFontSizeMin: 40,
|
|
heroFontSizeMax: 52,
|
|
companions: ['🎇', '✨', '💥', '⭐', '🎉'],
|
|
companionChance: 0.55,
|
|
},
|
|
'🎇': {
|
|
style: 'firework',
|
|
fullscreen: true,
|
|
particleCount: 280,
|
|
gravity: 960,
|
|
drag: 1,
|
|
speedMin: 380,
|
|
speedMax: 920,
|
|
launchUpMin: 160,
|
|
launchUpMax: 400,
|
|
spinMin: -480,
|
|
spinMax: 480,
|
|
fontSizeMin: 14,
|
|
fontSizeMax: 28,
|
|
heroFontSizeMin: 36,
|
|
heroFontSizeMax: 48,
|
|
companions: ['🎆', '✨', '⭐', '💥'],
|
|
companionChance: 0.5,
|
|
twinkle: true,
|
|
},
|
|
'🐱': {
|
|
style: 'bounce',
|
|
particleCount: 20,
|
|
gravity: 440,
|
|
speedMin: 150,
|
|
speedMax: 320,
|
|
launchUpMin: 90,
|
|
launchUpMax: 180,
|
|
spinMin: -160,
|
|
spinMax: 160,
|
|
},
|
|
'🐶': {
|
|
style: 'bounce',
|
|
particleCount: 20,
|
|
gravity: 460,
|
|
speedMin: 160,
|
|
speedMax: 330,
|
|
launchUpMin: 85,
|
|
launchUpMax: 175,
|
|
wobble: true,
|
|
},
|
|
};
|
|
|
|
export function getEmojiBurstProfile(emoji: string): EmojiBurstProfile {
|
|
const override = PROFILE_OVERRIDES[emoji];
|
|
if (!override) return DEFAULT_PROFILE;
|
|
return { ...DEFAULT_PROFILE, ...override };
|
|
}
|
|
|
|
export function isFullscreenBurstEmoji(emoji: string): boolean {
|
|
return getEmojiBurstProfile(emoji).fullscreen === true;
|
|
}
|
|
|
|
export function pickParticleEmoji(profile: EmojiBurstProfile, primaryEmoji: string): string {
|
|
if (!profile.companions?.length || !profile.companionChance) {
|
|
return primaryEmoji;
|
|
}
|
|
if (Math.random() >= profile.companionChance) {
|
|
return primaryEmoji;
|
|
}
|
|
const companion = profile.companions[Math.floor(Math.random() * profile.companions.length)];
|
|
return companion ?? primaryEmoji;
|
|
}
|
|
|
|
export function sampleBurstAngle(profile: EmojiBurstProfile): number {
|
|
if (profile.angleSpread === undefined) {
|
|
return Math.random() * Math.PI * 2;
|
|
}
|
|
|
|
const bias = profile.angleBias ?? -Math.PI / 2;
|
|
const halfSpread = profile.angleSpread / 2;
|
|
return bias + (Math.random() - 0.5) * profile.angleSpread * (0.6 + Math.random() * 0.4);
|
|
}
|