Commit c125345a authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: emotes fly from sender's position to center of board

Emote animation redesigned:
- showReceived() now accepts 'fromElement' parameter
- Emote starts at sender's panel position (scaled small)
- Pops up to full size at origin (0.2s)
- Flies to center of game board (0.5s)
- Grows large and fades out at center (1.8s total)
- Uses Web Animations API for smooth 60fps path

Chess:
- YOUR emotes start from your player bar (bottom)
- Opponent emotes start from their bar (top)

Ludo:
- YOUR emotes start from your panel (pp-{myPlayerIndex})
- Opponent emotes start from their panel (pp-{senderIdx})

Both games now have directional emote animation — you can SEE
who sent what because it originates from their side of the screen.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent f0ab8811
......@@ -81,12 +81,43 @@ function showSentFeedback(btn) {
setTimeout(() => btn.classList.remove('cooldown'), COOLDOWN);
}
export function showReceived(container, emote) {
export function showReceived(container, emote, fromElement) {
const emojiText = emote.emoji || emote;
const el = document.createElement('div');
el.className = 'emote-received';
el.textContent = emote.emoji || emote;
container.appendChild(el);
setTimeout(() => el.remove(), 2000);
el.textContent = emojiText;
el.style.cssText = 'position:fixed;font-size:40px;z-index:999;pointer-events:none;';
document.body.appendChild(el);
// Calculate start position (from sender's panel or default top)
const containerRect = container.getBoundingClientRect();
const centerX = containerRect.left + containerRect.width / 2;
const centerY = containerRect.top + containerRect.height / 2;
let startX = centerX;
let startY = containerRect.top + 20;
if (fromElement) {
const fromRect = fromElement.getBoundingClientRect();
startX = fromRect.left + fromRect.width / 2;
startY = fromRect.top + fromRect.height / 2;
}
// Animate from sender position → center of board with scale + fade
el.animate([
{ left: startX + 'px', top: startY + 'px', transform: 'translate(-50%,-50%) scale(0.3)', opacity: 0 },
{ left: startX + 'px', top: startY + 'px', transform: 'translate(-50%,-50%) scale(1.3)', opacity: 1, offset: 0.2 },
{ left: centerX + 'px', top: centerY + 'px', transform: 'translate(-50%,-50%) scale(1.5)', opacity: 1, offset: 0.5 },
{ left: centerX + 'px', top: centerY + 'px', transform: 'translate(-50%,-50%) scale(2)', opacity: 0 }
], {
duration: 1800,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
fill: 'forwards'
}).onfinish = () => el.remove();
}
// Legacy function for backward compat
export function showReceivedAt(container, emote) {
showReceived(container, emote, null);
}
export function destroy() {
......
......@@ -186,10 +186,11 @@ export function mountGame(el, params) {
}
mp.startDisconnectWatch(matchId, 'chess', 60000);
// Synced emotes — send to opponent via server
// Synced emotes — animate from OPPONENT bar to center
mp.onEmoteReceived((emote) => {
const emoteContainer = el.querySelector('#board-container');
emoteSystem.showReceived(emoteContainer, emote.key === 'gg' ? '🤝' : emote.key === 'good_move' ? '👏' : '😮');
const boardContainer = el.querySelector('#board-container');
const oppBar = el.querySelector('.chess-bar'); // opponent bar is first chess-bar
emoteSystem.showReceived(boardContainer, emote.key === 'gg' ? '🤝' : emote.key === 'good_move' ? '👏' : '😮', oppBar);
audio.play('notification');
});
}
......@@ -198,7 +199,9 @@ export function mountGame(el, params) {
const emoteContainer = el.querySelector('#board-container');
emoteSystem.create(emoteContainer, (emote) => {
audio.play('notification');
emoteSystem.showReceived(emoteContainer, emote.emoji);
// Animate from MY player bar to center
const myBar = el.querySelectorAll('.chess-bar')[1]; // player bar is second
emoteSystem.showReceived(emoteContainer, emote.emoji, myBar);
// Sync to opponent in live mode
if (gameState.mode === 'live' && matchId) {
mp.sendEmote(matchId, 'chess', emote.key);
......@@ -490,8 +493,9 @@ function startLivePolling(el) {
const myId = store.get('auth.userId');
const emoteData = mp.checkForEmote(data.game_state, myId);
if (emoteData) {
const emoteContainer = el.querySelector('#board-container');
emoteSystem.showReceived(emoteContainer, emoteData.key === 'gg' ? '🤝' : '👏');
const boardContainer = el.querySelector('#board-container');
const oppBar = el.querySelector('.chess-bar');
emoteSystem.showReceived(boardContainer, emoteData.key === 'gg' ? '🤝' : '👏', oppBar);
audio.play('notification');
}
......
......@@ -90,7 +90,9 @@ export function mountGame(el, params) {
const emoteWrap = el.querySelector('#ludo-wrap');
emoteSystem.create(emoteWrap, (emote) => {
audio.play('notification');
emoteSystem.showReceived(emoteWrap, emote.emoji);
// Animate from MY panel position to center
const myPanel = el.querySelector(`#pp-${myPlayerIndex}`);
emoteSystem.showReceived(emoteWrap, emote.emoji, myPanel);
if (game.mode === 'live' && matchId) {
mp.sendEmote(matchId, 'ludo', emote.key);
}
......@@ -100,7 +102,11 @@ export function mountGame(el, params) {
if (mode === 'live' && matchId) {
mp.startDisconnectWatch(matchId, 'ludo', 60000);
mp.onEmoteReceived((emote) => {
emoteSystem.showReceived(emoteWrap, emote.key === 'gg' ? '🤝' : '👏');
// Animate from OPPONENT's panel position to center
// Find which player sent it (not me)
const senderIdx = emote.from === store.get('auth.userId') ? myPlayerIndex : (myPlayerIndex === 0 ? 1 : 0);
const oppPanel = el.querySelector(`#pp-${senderIdx}`);
emoteSystem.showReceived(emoteWrap, emote.key === 'gg' ? '🤝' : emote.key === 'good_move' ? '👏' : '😮', oppPanel);
audio.play('notification');
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment