Your own UI on core
If your product has its own way of moving, keep core and leave ui out. The rules stay the same (the queue, the presses, when a card counts as opened) and the picture is whatever you draw. This guide builds a working reveal from a div and CSS transitions.
phase: idle
- Unopened
- Unopened
- Unopened
- Unopened
- Unopened
- Unopened
No ui package and no GSAP in this one. Press the first button, then the card.
What a renderer owes the rules
- Say where the card is. Call
opening.enter(phase)as the reveal moves on:hum,turn,hold,land. - Say when it is face up. Call
opening.reveal(). That is whenonOpenfires. It is safe to call twice. - Say when it is home. Call
opening.landed(). The next card comes up, or the run ends. - Be ready to jump. Hand
opening.attach()four functions: turn now, finish now, cancel, close.
The effect
import { holdSeconds, showFor, tierOf, DEFAULT_RARITIES, useCardOpening, useOpeningPhase } from "@react-cards-opening/core";
function Reveal({ cards, opened, onOpen }) {
const hunt = useCardOpening({ cards, opened, onOpen });
const { opening, run, step, current } = hunt;
const phase = useOpeningPhase(opening);
useEffect(() => {
if (!run || !current) return;
const plan = showFor(tierOf(DEFAULT_RARITIES, current.rarity).effect);
let timers: number[] = [];
const after = (ms: number, act: () => void) => timers.push(window.setTimeout(act, ms));
const clear = () => timers.splice(0).forEach(clearTimeout);
const land = () => {
clear();
opening.reveal();
opening.enter("land");
after(320, opening.landed);
};
const turn = () => {
clear();
opening.enter("turn");
after(420, () => {
opening.reveal();
opening.enter("hold");
});
after(420 + holdSeconds([current.name, current.line], plan) * 1000, land);
};
opening.enter("hum");
after(900, turn);
const detach = opening.attach({ turn, finish: land, cancel: clear, close: () => {} });
return () => {
clear();
detach();
};
}, [run?.id, step]);
return (
<button className="card" data-phase={phase} onClick={hunt.pressCard} disabled={!run}>
{current?.name}
</button>
);
}The look
.card {
transition: transform 0.42s cubic-bezier(0.34, 1.4, 0.64, 1), opacity 0.3s;
transform: rotateY(180deg);
}
.card[data-phase="hum"] { animation: shake 0.3s 2; }
.card[data-phase="turn"],
.card[data-phase="hold"] { transform: rotateY(0); }
.card[data-phase="land"] { transform: scale(0.6); opacity: 0; }What you get for free
hunt.pressCardturns a face-down card and sends a face-up one home. You did not write that branch.hunt.pressBackdropstops the run the right way: a turned card still lands, a face-down one goes back unopened.hunt.openAllopens everything and calls yourcancel.- A replay never calls
onOpen. - The show plan tells you how loud to be for the tier, and
holdSecondshow long to wait.
With an animation library
The same contract works with any clock. With a timeline, turn is a seek to a label and finish is a jump to the end. That is exactly what CardOpening does with GSAP, and reading its source is the fastest way to a renderer of your own.