diff --git a/src/game/Game.ts b/src/game/Game.ts index df043f2..32b8bee 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -73,6 +73,9 @@ export class Game { // Successful match - draw path first this.renderer.drawPath(result.path!); + // Start match animation (concurrent with path display) + this.renderer.animateMatch([tile1, tile2]); + // Emit tilesMatched event this.events.emit('tilesMatched', { tile1, diff --git a/src/rendering/Renderer.ts b/src/rendering/Renderer.ts index b9cb35b..9de725b 100644 --- a/src/rendering/Renderer.ts +++ b/src/rendering/Renderer.ts @@ -197,9 +197,11 @@ export class Renderer { private fadeAnimationStartTimes: Map = new Map(); private readonly FADE_DURATION = 100; // ms per CONTEXT.md private shakeAnimations: Map = new Map(); + private matchAnimations: Map = new Map(); private rippleAnimations: RippleAnimation[] = []; private pathAnimation: { path: TilePosition[], startTime: number } | null = null; private readonly PATH_DISPLAY_DURATION = 300; // ms per CONTEXT.md + private readonly MATCH_ANIMATION_DURATION = CONFIG.animation.matchDuration; constructor(ctx: CanvasRenderingContext2D, gridManager: GridManager) { this.ctx = ctx; @@ -284,12 +286,33 @@ export class Renderer { const x = offsetX + tile.position.col * (CONFIG.tile.size + CONFIG.tile.gap) + CONFIG.tile.gap; const y = offsetY + tile.position.row * (CONFIG.tile.size + CONFIG.tile.gap) + CONFIG.tile.gap; - // Save context before applying shake + // Calculate tile center for match animation scaling + const centerX = x + CONFIG.tile.size / 2; + const centerY = y + CONFIG.tile.size / 2; + + // Check for match animation + const matchAnimation = this.matchAnimations.get(tile.id); + + // Save context before applying transforms ctx.save(); // Apply shake offset ctx.translate(shakeOffset.x, shakeOffset.y); + // Apply match animation transforms if active + if (matchAnimation) { + const { scale, alpha } = matchAnimation.getScaleAndAlpha(); + ctx.globalAlpha = alpha; + ctx.translate(centerX, centerY); + ctx.scale(scale, scale); + ctx.translate(-centerX, -centerY); + + // Clean up completed animations + if (matchAnimation.isComplete()) { + this.matchAnimations.delete(tile.id); + } + } + // Draw rounded rectangle background this.drawRoundedRect(ctx, x, y, CONFIG.tile.size, CONFIG.tile.size, CONFIG.tile.cornerRadius); ctx.fillStyle = CONFIG.colors.tile; @@ -302,7 +325,7 @@ export class Renderer { ctx.textBaseline = 'middle'; ctx.fillText(tile.emoji, x + CONFIG.tile.size / 2, y + CONFIG.tile.size / 2); - // Restore context after shake + // Restore context ctx.restore(); } @@ -421,6 +444,18 @@ export class Renderer { } } + /** + * Start match animation for specified tiles + * @param tiles - Tiles to animate with scale+fade effect + */ + animateMatch(tiles: Tile[]): void { + for (const tile of tiles) { + const animation = new MatchAnimation(this.MATCH_ANIMATION_DURATION); + animation.start(); + this.matchAnimations.set(tile.id, animation); + } + } + /** * Add ripple effect at touch/click coordinates * @param x - Canvas X coordinate