fix(game): center played cards — bake -50% into Framer transform (RTL)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 51s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 56s

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:
soroush.asadi
2026-06-15 20:34:43 +03:30
parent 7c6c9fcd90
commit 99b9ee5c91
+10 -5
View File
@@ -443,7 +443,6 @@ function TrickArea({
const { front } = useCardSkins(); const { front } = useCardSkins();
return ( return (
<div className="absolute inset-0"> <div className="absolute inset-0">
<div className="absolute left-1/2 top-1/2 size-0">
<AnimatePresence> <AnimatePresence>
{trick.map((pc) => { {trick.map((pc) => {
const off = { x: TRICK_OFFSET[pc.seat].x * scale, y: TRICK_OFFSET[pc.seat].y * scale }; const off = { x: TRICK_OFFSET[pc.seat].x * scale, y: TRICK_OFFSET[pc.seat].y * scale };
@@ -456,7 +455,12 @@ function TrickArea({
animate={{ x: off.x, y: off.y, opacity: 1, scale: isWinner ? 1.14 : 1 }} animate={{ x: off.x, y: off.y, opacity: 1, scale: isWinner ? 1.14 : 1 }}
exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.25 } }} exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.25 } }}
transition={{ type: "spring", stiffness: 260, damping: 26 }} transition={{ type: "spring", stiffness: 260, damping: 26 }}
className="absolute -translate-x-1/2 -translate-y-1/2" 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={{ style={{
filter: isWinner filter: isWinner
? "drop-shadow(0 0 18px rgba(212,175,55,1)) drop-shadow(0 0 6px rgba(255,240,120,0.8))" ? "drop-shadow(0 0 18px rgba(212,175,55,1)) drop-shadow(0 0 6px rgba(255,240,120,0.8))"
@@ -468,14 +472,15 @@ function TrickArea({
); );
})} })}
</AnimatePresence> </AnimatePresence>
{/* Burst particles when trick is won */} {/* Burst particles when trick is won (centered on the felt) */}
<AnimatePresence> <AnimatePresence>
{phase === "trick-complete" && winner != null && ( {phase === "trick-complete" && winner != null && (
<TrickBurst key={`burst-${winner}`} seat={winner} /> <div key={`burst-${winner}`} className="absolute left-1/2 top-1/2 size-0">
<TrickBurst seat={winner} />
</div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</div>
); );
} }