diff --git a/web/src/components/CardView.tsx b/web/src/components/CardView.tsx
index 710deda..168090f 100644
--- a/web/src/components/CardView.tsx
+++ b/web/src/components/CardView.tsx
@@ -67,6 +67,7 @@ export function CardView({
return (
setHover({ x: e.clientX, y: e.clientY })}
diff --git a/web/src/components/ShopPhase.tsx b/web/src/components/ShopPhase.tsx
index 16916fc..00ab4ab 100644
--- a/web/src/components/ShopPhase.tsx
+++ b/web/src/components/ShopPhase.tsx
@@ -1,7 +1,15 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
+import { createPortal } from 'react-dom'
import type { Card, ClientMessage, GameView, PlayerView } from '../types'
import { CardView } from './CardView'
+interface Flyer {
+ key: string
+ emoji: string
+ x: number
+ y: number
+}
+
interface Props {
view: GameView
you: PlayerView
@@ -23,6 +31,44 @@ export function ShopPhase({ view, you, send }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [you.deck])
+ // --- spawn animation: apples/bees fly out from the creature that made them.
+ // The log tells us which card (source) spawned what; we fly an emoji from
+ // that card's on-screen position. For sells the source is already gone, so
+ // we grab its rect at click time (see act()).
+ const seenSeqRef = useRef(null)
+ const capturedRef = useRef
)}
+
+ {flyers.length > 0 &&
+ createPortal(
+ <>
+ {flyers.map((fl) => (
+
+ setFlyers((f) => f.filter((x) => x.key !== fl.key))
+ }
+ >
+ {fl.emoji}
+
+ ))}
+ >,
+ document.body,
+ )}
)
}
diff --git a/web/src/styles.css b/web/src/styles.css
index be105be..5c2f66b 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -1048,6 +1048,31 @@ h3 {
animation-name: summon-from-left;
}
+/* Apple/bee spawned in the shop, flying out from the creature that made it. */
+@keyframes spawn-fly {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.4) rotate(-10deg);
+ }
+ 20% {
+ opacity: 1;
+ transform: translate(-50%, -85%) scale(1.35) rotate(4deg);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(-50%, -165%) scale(1) rotate(0);
+ }
+}
+
+.spawn-flyer {
+ position: fixed;
+ z-index: 60;
+ font-size: 2rem;
+ pointer-events: none;
+ animation: spawn-fly 1000ms ease-out forwards;
+ filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.5));
+}
+
/* --- foods waiting for a pet --- */
.pending-foods {