Skip to content

Commit e135dfd

Browse files
committed
Merge branch 'wl-CG-0MM1OOEZH182AHBG-flip-card-animation' into main
2 parents c7e23d4 + 321e3c1 commit e135dfd

File tree

5 files changed

+495
-75
lines changed

5 files changed

+495
-75
lines changed

example-games/golf/scenes/GolfScene.ts

Lines changed: 26 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
dismissOverlay,
3030
createSceneTitle, createSceneMenuButton,
3131
PhaseManager,
32+
flipCard,
3233
} from '../../../src/ui';
3334
import type { HelpSection } from '../../../src/ui';
3435
import helpContent from '../help-content.json';
@@ -1024,28 +1025,15 @@ export class GolfScene extends CardGameScene {
10241025
sprite.setDepth(10);
10251026

10261027
// 1. Grid card: flip (reveal face) + translate to discard pile
1027-
// First half: scaleX → 0 while moving halfway to discard
1028-
this.tweens.add({
1029-
targets: sprite,
1030-
scaleX: 0,
1031-
x: (sprite.x + discardPos.x) / 2,
1032-
y: (sprite.y + discardPos.y) / 2,
1033-
duration: SWAP_ANIM_DURATION / 2,
1034-
ease: 'Power2',
1035-
onComplete: () => {
1036-
// Reveal the card's actual face at the midpoint of the flip
1037-
sprite.setTexture(getCardTexture(grid[idx]));
1038-
// Second half: scaleX → 1 while completing movement to discard
1039-
this.tweens.add({
1040-
targets: sprite,
1041-
scaleX: 1,
1042-
x: discardPos.x,
1043-
y: discardPos.y,
1044-
duration: SWAP_ANIM_DURATION / 2,
1045-
ease: 'Power2',
1046-
onComplete: checkDone,
1047-
});
1048-
},
1028+
flipCard({
1029+
scene: this,
1030+
target: sprite,
1031+
newTexture: getCardTexture(grid[idx]),
1032+
duration: SWAP_ANIM_DURATION,
1033+
easeClose: 'Power2',
1034+
destX: discardPos.x,
1035+
destY: discardPos.y,
1036+
onComplete: checkDone,
10491037
});
10501038

10511039
// 2. Drawn card: translate from display position to vacated grid slot
@@ -1076,21 +1064,13 @@ export class GolfScene extends CardGameScene {
10761064
this.hideDrawnCard();
10771065

10781066
// Phase 2: flip the grid card in place
1079-
this.tweens.add({
1080-
targets: sprite,
1081-
scaleX: 0,
1082-
duration: SWAP_ANIM_DURATION / 4,
1083-
ease: 'Power2',
1084-
onComplete: () => {
1085-
sprite.setTexture(getCardTexture(grid[idx]));
1086-
this.tweens.add({
1087-
targets: sprite,
1088-
scaleX: 1,
1089-
duration: SWAP_ANIM_DURATION / 4,
1090-
ease: 'Power2',
1091-
onComplete: onComplete, // Skip wrappedOnComplete; drawn card already hidden
1092-
});
1093-
},
1067+
flipCard({
1068+
scene: this,
1069+
target: sprite,
1070+
newTexture: getCardTexture(grid[idx]),
1071+
duration: SWAP_ANIM_DURATION / 2,
1072+
easeClose: 'Power2',
1073+
onComplete: onComplete, // Skip wrappedOnComplete; drawn card already hidden
10941074
});
10951075
};
10961076

@@ -1128,30 +1108,16 @@ export class GolfScene extends CardGameScene {
11281108
this.drawnCardSprite = this.add.image(startX, startY, 'card_back');
11291109
this.drawnCardSprite.setDepth(15);
11301110

1131-
// First half: move halfway + flip (scaleX → 0)
1132-
this.tweens.add({
1133-
targets: this.drawnCardSprite,
1134-
x: (startX + destX) / 2,
1135-
y: (startY + destY) / 2,
1136-
scaleX: 0,
1137-
duration: ANIM_DURATION / 2,
1138-
ease: 'Power2',
1111+
flipCard({
1112+
scene: this,
1113+
target: this.drawnCardSprite,
1114+
newTexture: faceTexture,
1115+
duration: ANIM_DURATION,
1116+
easeClose: 'Power2',
1117+
destX,
1118+
destY,
11391119
onComplete: () => {
1140-
if (!this.drawnCardSprite) return;
1141-
// Reveal face at midpoint
1142-
this.drawnCardSprite.setTexture(faceTexture);
1143-
// Second half: complete move + flip back (scaleX → 1)
1144-
this.tweens.add({
1145-
targets: this.drawnCardSprite,
1146-
x: destX,
1147-
y: destY,
1148-
scaleX: 1,
1149-
duration: ANIM_DURATION / 2,
1150-
ease: 'Power2',
1151-
onComplete: () => {
1152-
if (this.drawnCardSprite) this.drawnCardSprite.setDepth(0);
1153-
},
1154-
});
1120+
if (this.drawnCardSprite) this.drawnCardSprite.setDepth(0);
11551121
},
11561122
});
11571123
} else {

example-games/the-mind/scenes/TheMindScene.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
dismissOverlay,
4444
createSceneHeader,
4545
layoutCardPositions,
46+
flipCard,
4647
} from '../../../src/ui';
4748
import type { HelpSection } from '../../../src/ui';
4849
import helpContent from '../help-content.json';
@@ -960,9 +961,8 @@ export class TheMindScene extends CardGameScene {
960961
.setDepth(DEPTH_PLAYED_CARD);
961962

962963
const faceUpTex = getMindCardTexture({ value: cardValue, faceUp: true });
963-
const halfDuration = ANIM_DURATION / 2;
964964

965-
// Tween to pile position; flip to face-up at the midpoint
965+
// Tween to pile position (translation runs in parallel with flip)
966966
this.tweens.add({
967967
targets: tempSprite,
968968
x: PILE_X,
@@ -972,20 +972,15 @@ export class TheMindScene extends CardGameScene {
972972
});
973973

974974
// Midpoint flip: scale X to 0 then back to 1 with the face-up texture
975-
this.tweens.add({
976-
targets: tempSprite,
977-
scaleX: 0,
978-
duration: halfDuration,
979-
ease: 'Cubic.easeIn',
980-
onComplete: () => {
981-
tempSprite.setTexture(faceUpTex);
975+
flipCard({
976+
scene: this,
977+
target: tempSprite,
978+
newTexture: faceUpTex,
979+
duration: ANIM_DURATION,
980+
easeClose: 'Cubic.easeIn',
981+
easeOpen: 'Cubic.easeOut',
982+
onMidpoint: () => {
982983
tempSprite.setDisplaySize(CARD_W, CARD_H);
983-
this.tweens.add({
984-
targets: tempSprite,
985-
scaleX: 1,
986-
duration: halfDuration,
987-
ease: 'Cubic.easeOut',
988-
});
989984
},
990985
});
991986

src/ui/flipCard.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* flipCard – reusable two-phase card-flip animation helper.
3+
*
4+
* Performs the classic "scaleX → 0 → change texture → scaleX → 1" card flip.
5+
* Optionally translates the sprite to a destination during the flip.
6+
*
7+
* @module ui/flipCard
8+
*/
9+
10+
/** Options for the {@link flipCard} animation. */
11+
export interface FlipCardOptions {
12+
/** The Phaser scene that owns the tween timeline. */
13+
scene: Phaser.Scene;
14+
15+
/**
16+
* The sprite (or Image) to flip.
17+
* Must support `setTexture()` and be a valid Phaser tween target.
18+
*/
19+
target: Phaser.GameObjects.Image | Phaser.GameObjects.Sprite;
20+
21+
/** The texture key to apply at the midpoint of the flip. */
22+
newTexture: string;
23+
24+
/**
25+
* Total duration of the flip in milliseconds.
26+
* Each half (close + open) takes `duration / 2`.
27+
* @default 300
28+
*/
29+
duration?: number;
30+
31+
/**
32+
* Easing for the first half (scaleX → 0).
33+
* @default 'Power2'
34+
*/
35+
easeClose?: string;
36+
37+
/**
38+
* Easing for the second half (scaleX → 1).
39+
* Defaults to the value of `easeClose` for symmetric easing.
40+
*/
41+
easeOpen?: string;
42+
43+
/**
44+
* Optional destination to translate to during the flip.
45+
* When set, the sprite moves to `(destX, destY)` over both halves:
46+
* halfway during close, the rest during open.
47+
*/
48+
destX?: number;
49+
50+
/**
51+
* Optional destination Y. Required if `destX` is provided.
52+
*/
53+
destY?: number;
54+
55+
/**
56+
* Called at the midpoint of the flip, immediately after the new texture
57+
* is applied. Use this for side effects such as `setDisplaySize()`.
58+
*/
59+
onMidpoint?: () => void;
60+
61+
/**
62+
* Called after the full flip animation completes (end of the open phase).
63+
*/
64+
onComplete?: () => void;
65+
}
66+
67+
/**
68+
* Play a two-phase card-flip animation on a sprite.
69+
*
70+
* Phase 1 (close): scaleX tweens from current value to 0.
71+
* At the midpoint the texture is swapped and `onMidpoint` fires.
72+
* Phase 2 (open): scaleX tweens from 0 back to 1.
73+
*
74+
* If `destX`/`destY` are provided the sprite also translates — halfway
75+
* during phase 1 and the rest during phase 2 — so the flip and slide
76+
* are combined into a single smooth motion.
77+
*
78+
* @returns The phase-1 tween so the caller can cancel/chain if needed.
79+
*/
80+
export function flipCard(opts: FlipCardOptions): Phaser.Tweens.Tween {
81+
const {
82+
scene,
83+
target,
84+
newTexture,
85+
duration = 300,
86+
easeClose = 'Power2',
87+
easeOpen = easeClose,
88+
destX,
89+
destY,
90+
onMidpoint,
91+
onComplete,
92+
} = opts;
93+
94+
const half = duration / 2;
95+
96+
// Build the close-phase tween config
97+
const closeConfig: Phaser.Types.Tweens.TweenBuilderConfig = {
98+
targets: target,
99+
scaleX: 0,
100+
duration: half,
101+
ease: easeClose,
102+
onComplete: () => {
103+
target.setTexture(newTexture);
104+
onMidpoint?.();
105+
106+
// Build the open-phase tween config
107+
const openConfig: Phaser.Types.Tweens.TweenBuilderConfig = {
108+
targets: target,
109+
scaleX: 1,
110+
duration: half,
111+
ease: easeOpen,
112+
onComplete: onComplete ? () => onComplete() : undefined,
113+
};
114+
115+
// Add translation for the second half if destination was provided
116+
if (destX !== undefined && destY !== undefined) {
117+
openConfig.x = destX;
118+
openConfig.y = destY;
119+
}
120+
121+
scene.tweens.add(openConfig);
122+
},
123+
};
124+
125+
// Add translation for the first half (to the midpoint position)
126+
if (destX !== undefined && destY !== undefined) {
127+
const startX = (target as Phaser.GameObjects.Image).x;
128+
const startY = (target as Phaser.GameObjects.Image).y;
129+
closeConfig.x = (startX + destX) / 2;
130+
closeConfig.y = (startY + destY) / 2;
131+
}
132+
133+
return scene.tweens.add(closeConfig);
134+
}

src/ui/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export { CardGameScene } from './CardGameScene';
1313
export { PhaseManager } from './PhaseManager';
1414
export type { PhaseManagerConfig } from './PhaseManager';
1515

16+
// Card flip animation
17+
export { flipCard } from './flipCard';
18+
export type { FlipCardOptions } from './flipCard';
19+
1620
// Card layout helpers
1721
export { layoutCardPositions } from './layoutCardPositions';
1822
export type {

0 commit comments

Comments
 (0)