fix(game): center played cards — bake -50% into Framer transform (RTL)
Root cause: the trick cards used a Tailwind -translate-x-1/2 -translate-y-1/2 to center on the felt, but Framer Motion owns `transform` (from x/y/scale), so that centering class was clobbered. In RTL the auto-positioned card then anchored to the right edge and the whole trick cross drifted left of center. Fix: drop the size-0 anchor; position each card at left-1/2 top-1/2 and use Framer `transformTemplate` to prepend translate(-50%,-50%) before the animated translate(x,y) scale — so centering survives and the pile sits dead-center in both LTR and RTL. Burst particles re-centered too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -443,38 +443,43 @@ function TrickArea({
|
||||
const { front } = useCardSkins();
|
||||
return (
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute left-1/2 top-1/2 size-0">
|
||||
<AnimatePresence>
|
||||
{trick.map((pc) => {
|
||||
const off = { x: TRICK_OFFSET[pc.seat].x * scale, y: TRICK_OFFSET[pc.seat].y * scale };
|
||||
const enter = TRICK_ENTER[pc.seat];
|
||||
const isWinner = phase === "trick-complete" && winner === pc.seat;
|
||||
return (
|
||||
<motion.div
|
||||
key={pc.card.id}
|
||||
initial={{ x: enter.x, y: enter.y, opacity: 0, scale: 0.7 }}
|
||||
animate={{ x: off.x, y: off.y, opacity: 1, scale: isWinner ? 1.14 : 1 }}
|
||||
exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.25 } }}
|
||||
transition={{ type: "spring", stiffness: 260, damping: 26 }}
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
filter: isWinner
|
||||
? "drop-shadow(0 0 18px rgba(212,175,55,1)) drop-shadow(0 0 6px rgba(255,240,120,0.8))"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<PlayingCard card={pc.card} size={cardSize} front={front} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
{/* Burst particles when trick is won */}
|
||||
<AnimatePresence>
|
||||
{phase === "trick-complete" && winner != null && (
|
||||
<TrickBurst key={`burst-${winner}`} seat={winner} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{trick.map((pc) => {
|
||||
const off = { x: TRICK_OFFSET[pc.seat].x * scale, y: TRICK_OFFSET[pc.seat].y * scale };
|
||||
const enter = TRICK_ENTER[pc.seat];
|
||||
const isWinner = phase === "trick-complete" && winner === pc.seat;
|
||||
return (
|
||||
<motion.div
|
||||
key={pc.card.id}
|
||||
initial={{ x: enter.x, y: enter.y, opacity: 0, scale: 0.7 }}
|
||||
animate={{ x: off.x, y: off.y, opacity: 1, scale: isWinner ? 1.14 : 1 }}
|
||||
exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.25 } }}
|
||||
transition={{ type: "spring", stiffness: 260, damping: 26 }}
|
||||
className="absolute left-1/2 top-1/2"
|
||||
// Bake the -50% centering into Framer's transform — Framer owns the
|
||||
// `transform` (from x/y/scale), so a Tailwind -translate-x-1/2 class
|
||||
// gets clobbered (in RTL the card then anchors right → drifts left).
|
||||
transformTemplate={(t) =>
|
||||
`translate(-50%, -50%) translate(${t.x ?? "0px"}, ${t.y ?? "0px"}) scale(${t.scale ?? 1})`}
|
||||
style={{
|
||||
filter: isWinner
|
||||
? "drop-shadow(0 0 18px rgba(212,175,55,1)) drop-shadow(0 0 6px rgba(255,240,120,0.8))"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<PlayingCard card={pc.card} size={cardSize} front={front} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
{/* Burst particles when trick is won (centered on the felt) */}
|
||||
<AnimatePresence>
|
||||
{phase === "trick-complete" && winner != null && (
|
||||
<div key={`burst-${winner}`} className="absolute left-1/2 top-1/2 size-0">
|
||||
<TrickBurst seat={winner} />
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user