Make sick emoji confetti a fullscreen downward spew with morph.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
🤢 now fills the viewport, morphs to 🤮 while particles spawn, hides the jumbo stand-in cleanly, and keeps green droplets out of the face morph.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { EmojiConfettiBurst } from './types';
|
||||
import { findJumboEmojiElement, getLocalBurstCanvasSize } from './findJumboMount';
|
||||
import { findJumboEmojiElement, getLocalBurstCanvasSize, measureJumboGlyph, setJumboEmojiHidden } from './findJumboMount';
|
||||
import {
|
||||
BurstParticle,
|
||||
drawBurstParticles,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
FireworkSim,
|
||||
getFireworkDpr,
|
||||
stepFireworkSim,
|
||||
syncFireworkHero,
|
||||
} from './fireworkParticleEngine';
|
||||
|
||||
const MAX_DPR = 2;
|
||||
@@ -81,36 +82,85 @@ function FireworkBurstCanvas({ burst, primaryEmoji, onComplete }: BurstCanvasPro
|
||||
};
|
||||
|
||||
const { width, height } = resize();
|
||||
const jumbo = findJumboEmojiElement(burst.targetEventId);
|
||||
const jumboRect = jumbo?.getBoundingClientRect();
|
||||
const profile = getEmojiBurstProfile(primaryEmoji);
|
||||
const glyph = measureJumboGlyph(burst.targetEventId);
|
||||
const pinToJumbo = Boolean(profile.morphTo && glyph);
|
||||
|
||||
const origin = {
|
||||
x: jumboRect ? jumboRect.left + jumboRect.width / 2 : burst.origin.x,
|
||||
y: jumboRect ? jumboRect.top + jumboRect.height / 2 : burst.origin.y,
|
||||
maskRadius: burst.origin.maskRadius ?? 36,
|
||||
x: glyph ? glyph.x : burst.origin.x,
|
||||
y: glyph ? glyph.y : burst.origin.y,
|
||||
maskRadius: glyph ? glyph.size * 0.5 : burst.origin.maskRadius ?? 36,
|
||||
};
|
||||
|
||||
// Clamp origin into the viewport so off-screen messages still blast on-screen.
|
||||
origin.x = Math.min(width - 24, Math.max(24, origin.x));
|
||||
origin.y = Math.min(height - 24, Math.max(24, origin.y));
|
||||
// Keep morph heroes glued to the message emoji; only clamp undirected fireworks.
|
||||
if (!pinToJumbo) {
|
||||
origin.x = Math.min(width - 24, Math.max(24, origin.x));
|
||||
origin.y = Math.min(height - 24, Math.max(24, origin.y));
|
||||
}
|
||||
|
||||
const profile = getEmojiBurstProfile(primaryEmoji);
|
||||
simRef.current = createFireworkSim(width, height, origin, primaryEmoji, profile);
|
||||
simRef.current = createFireworkSim(
|
||||
width,
|
||||
height,
|
||||
origin,
|
||||
primaryEmoji,
|
||||
profile,
|
||||
performance.now(),
|
||||
glyph?.size
|
||||
);
|
||||
|
||||
const revealJumbo = () => {
|
||||
setJumboEmojiHidden(burst.targetEventId, false);
|
||||
};
|
||||
|
||||
const syncHero = () => {
|
||||
const sim = simRef.current;
|
||||
if (!sim) return;
|
||||
if (!sim.hero) {
|
||||
revealJumbo();
|
||||
return;
|
||||
}
|
||||
const metrics = measureJumboGlyph(burst.targetEventId);
|
||||
syncFireworkHero(sim, metrics);
|
||||
// Re-query every frame so React remounts still stay hidden.
|
||||
setJumboEmojiHidden(burst.targetEventId, true);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
// Extra Things: match CSS size; sim keeps its launch-time dimensions.
|
||||
// Extra Things: match CSS size; sim keeps its launch-time world size.
|
||||
resize();
|
||||
syncHero();
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
window.addEventListener('scroll', syncHero, true);
|
||||
|
||||
const finish = () => {
|
||||
revealJumbo();
|
||||
onCompleteRef.current(burst.id);
|
||||
frameRef.current = null;
|
||||
lastFrameTimeRef.current = null;
|
||||
};
|
||||
|
||||
// Hide immediately before first paint of the stand-in.
|
||||
if (pinToJumbo) {
|
||||
setJumboEmojiHidden(burst.targetEventId, true);
|
||||
}
|
||||
|
||||
const tick = (now: number) => {
|
||||
const sim = simRef.current;
|
||||
if (!sim) return;
|
||||
|
||||
syncHero();
|
||||
|
||||
const last = lastFrameTimeRef.current ?? now;
|
||||
const dtSeconds = Math.min((now - last) / 1000, 0.05);
|
||||
lastFrameTimeRef.current = now;
|
||||
|
||||
const alive = stepFireworkSim(sim, now, dtSeconds);
|
||||
// Hero may have just been dismissed — reveal real jumbo immediately.
|
||||
if (!sim.hero) {
|
||||
revealJumbo();
|
||||
}
|
||||
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@@ -120,9 +170,7 @@ function FireworkBurstCanvas({ burst, primaryEmoji, onComplete }: BurstCanvasPro
|
||||
return;
|
||||
}
|
||||
|
||||
onCompleteRef.current(burst.id);
|
||||
frameRef.current = null;
|
||||
lastFrameTimeRef.current = null;
|
||||
finish();
|
||||
};
|
||||
|
||||
lastFrameTimeRef.current = performance.now();
|
||||
@@ -130,11 +178,15 @@ function FireworkBurstCanvas({ burst, primaryEmoji, onComplete }: BurstCanvasPro
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
window.removeEventListener('scroll', syncHero, true);
|
||||
revealJumbo();
|
||||
if (frameRef.current !== null) {
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
frameRef.current = null;
|
||||
}
|
||||
simRef.current = null;
|
||||
// Extra Things: allow Strict Mode remount to start a fresh burst.
|
||||
spawnedRef.current = false;
|
||||
};
|
||||
}, [burst.id, burst.origin.maskRadius, burst.origin.x, burst.origin.y, burst.targetEventId, primaryEmoji]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user