The pack
cardMotion
function cardMotion(): Pack<{ cardMotion: CardMotionApi }>
The card-motion pack: a card's moves as pure pose maths for any presenter. Needs no backend.
Helpers
DEAL_STYLES
const DEAL_STYLES: Record<DealStyle, DealStyleDef>
The built-in deal styles, by name.
dealDuration
function dealDuration(slots: readonly DealSlot[]): number
When the last card lands (0 for an empty deal).
dealOrder
function dealOrder(counts: readonly number[], mode: 'rows' | 'columns' = 'rows'): Array<{ col: number; row: number }>
The order cards leave the stock, given how many go in each column.
'rows' is how a dealer actually deals Klondike — one card to every column
that still needs one, left to right, then the next row. 'columns' fills
each column before starting the next.
const order = dealOrder([1, 2, 3, 4, 5, 6, 7]); // Klondike tableau
const slots = dealSchedule(order.length, 'arc');
// order[i]! is the {col, row} the i-th dealt card belongs to
dealPose
function dealPose( from: CardPoint, to: CardPoint, t: number, style: DealStyle | DealStyleDef = 'arc', index = 0, via?: CardPoint | null, ): DealPose
One card's full pose at progress t under a named style — travel, arc,
sideways bow, spin, flex, the departure hold and the lane offset in a
single call.
index is the card's place in the launch order (its position in the array
dealSchedule returned). It drives lane and jitter, so PASS IT: cards
in the air together share one corridor and fly through each other without it.
via is the rally point for a style with gather > 0 (pyramid,
explode) — build it from formationSlot. Styles that fly straight to the
seat ignore it.
const p = dealProgress(slots[i]!, now);
const pose = dealPose(STOCK, seat[i], p, 'flick', i);
card.x = pose.x; card.z = pose.z; card.y = pose.y + pose.lift;
card.yaw = seatYaw + pose.yaw;
card.deform.amount = pose.flex;
dealProgress
function dealProgress(slot: DealSlot, time: number): number
A card's 0..1 through its own flight: 0 before it leaves, 1 once landed.
dealSchedule
function dealSchedule(count: number, opts: DealOptions | DealStyle | DealStyleDef = {}): DealSlot[]
Stagger count cards out of the stock. Returns each card's window; feed
dealProgress the clock and you get its 0..1 for glide().
const slots = dealSchedule(28, { stagger: 0.06 });
const total = dealDuration(slots); // when the whole deal is done
for (let i = 0; i < slots.length; i++) {
const p = dealProgress(slots[i]!, now);
if (p <= 0) continue; // hasn't left the stock yet
const pose = glide(STOCK, target[i], p, { arc: 1.2 });
}
dealStyle
function dealStyle(style: DealStyle | DealStyleDef = 'arc'): DealStyleDef
Resolve a style name (or a literal def) to its knobs.
dropShadow
function dropShadow(height: number, opts: ShadowOptions = {}): ShadowPose
Drive a drop shadow from a card's height above the table. Works for the 2D layer (draw the shadow sprite first, offset and scaled) and for a 3D game that would rather place its own quad than take the engine's automatic blob.
easeCard
function easeCard(t: number, ease: CardEase = 'inOut'): number
Apply one of the four easing curves to a 0..1 progress.
flex
function flex(t: number, amount = 0.25): number
The in-flight FLEX amount for a 3D card: bowed at the apex, flat at both
ends. Feed it straight to the engine's vertex bend —
world.plane({ deform: { kind: 'curl', amount: 0 } }), then
bend.amount = flex(p, 0.3) each frame. Real paper is stiffest at rest and
bows through the throw, which is the whole reason a dealt card reads as card
rather than tile.
flip
function flip(t: number, opts: FlipOptions = {}): FlipPose
A card turning over. The art swaps at the halfway pinch, where scaleX
passes through 0 and the card is edge-on — do it any earlier and the player
sees the new face through the old one.
flipAfter
function flipAfter(slot: DealSlot, opts: { delay?: number; duration?: number } = {}): DealSlot
The window for a card's turn-over AFTER it lands — a Klondike deal turns
the last card of each column face-up as it settles. Feed the result to
dealProgress like any other slot, then to flip.
const turn = flipAfter(slots[i]!, { delay: 0.1 });
const f = flip(dealProgress(turn, now), { lift: cardWidth / 2 });
card.roll = f.angle; // 3D: turning about the long axis IS the flip
formationSlot
function formationSlot(kind: DealFormation, index: number, count: number): { x: number; y: number; z: number }
Where card index of count sits in a rally formation, NORMALISED: x and
z in [-1, 1], y in [0, 1]. Scale it into your own layout and pass the
result to dealPose as via.
const s = formationSlot('pyramid', i, n);
const via = { x: table.x + s.x * 7, y: 5 + s.y * 3, z: table.z + s.z * 4 };
const pose = dealPose(from, seat, p, 'pyramid', i, via);
glide
function glide(from: CardPoint, to: CardPoint, t: number, opts: GlideOptions = {}): GlidePose
Eased travel from one point to another with a mid-flight arc — the single move every card game is built out of (deal, auto-move, return-to-pile).
const pose = glide(fromSlot, toSlot, elapsed / 0.35, { arc: 1.2 });
card.x = pose.x; card.z = pose.z;
card.y = pose.y + pose.lift; // 3D: up is +y
Types
CardEase
type CardEase = 'linear' | 'in' | 'out' | 'inOut'
The easing curve names easeCard accepts.
CardMotionApi
type CardMotionApi = typeof kit
game.cardMotion — glides, flips, flexes, shadows and deal styles, as one namespace.
CardPoint
interface CardPoint
A point in the game's own units. z is optional — 2D games ignore it.
x: number— The x position, in your units.y: number— The y position, in your units.z?: number— The z position, in your units. Optional — 2D games ignore it.
DealFormation
type DealFormation = 'pyramid' | 'ring' | 'block'
Shape the cards rally into before they drop into their seats.
DealOptions
interface DealOptions
Timing for dealSchedule.
duration?: number— Seconds each card spends in flight (default 0.32).stagger?: number— Seconds between successive cards leaving the stock (default 0.07). This is the knob that makes a deal read as dealt rather than appearing.delay?: number— Seconds before the first card leaves (default 0).
DealPose
interface DealPose extends GlidePose
A card mid-deal: the glide pose plus the style's spin and flex.
yaw: number— Yaw in radians at this instant (0 at both ends of the flight).flex: number— Vertex-bend amount at this instant (0 at both ends).
DealSlot
interface DealSlot
One card's slot in a staggered deal.
start: number— The second the card leaves the stock.end: number— The second the card lands.
DealStyle
type DealStyle = | 'arc' | 'slide' | 'flick' | 'cascade' | 'swirl' | 'snap' | 'pyramid' | 'scatter' | 'explode'
Named deal styles — pass to dealSchedule and dealPose.
DealStyleDef
interface DealStyleDef
Every knob a deal style sets. Spread one and override to make your own.
duration: number— Seconds one card spends in flight.stagger: number— Seconds between successive cards leaving the stock (0 = all at once).arc: number— Peak height above the straight line, in your units.ease: CardEase— Easing on the travel.spin: number— Radians of yaw the card turns through in flight. The card LANDS at this yaw, so use a multiple of π: a playing card is 180°-symmetric, anything else lands crooked.sway: number— Peak sideways bow, as a fraction of the travel distance. The SIGN picks the side: positive bows left of the travel direction, negative right. Pick the one that curves over the table rather than off it — a stock on the left wants a negative sway, or the short near-vertical throws swing out past the edge.flex: number— Peak vertex-bend amount for a 3D card (feed todeform).flexFrom: number— Where in the flight the bend STARTS — the card stays flat until here, then bows and flattens again by the landing. Keep it at or past the apex (0.5): cards are at their most crowded coming off the deck, and a card that is already curling there digs into its neighbours. By the apex they have fanned out to their own columns and have room to bend.hold: number— Fraction of the flight the card keeps its STARTING height before it settles to the seat. Without this a card leaving the top of the deck starts descending immediately and cuts down through the pile it just came off — which reads as dealing off the BOTTOM. 0.2–0.35 clears a deck.lane: number— Altitude step between consecutive cards in the launch order. Cards in the air together are onlystagger × speedapart along a shared corridor, far less than a card is wide, so without this they fly THROUGH each other. Keep it aboveflex, or a card's bend pokes through the one above.lanes: number— How many altitudes the lane cycles through before repeating. Cardslanesapart in the order are a full flight-window apart, so they are never airborne together and can share a height — which is what keeps the fan bounded instead of climbing forever across a 52-card deal. 0 = never repeat (forstagger: 0, where every card IS concurrent).gather: number— RALLY: the fraction of the flight spent reaching theviapoint you pass todealPose, before the card drops to its seat. 0 = fly straight there, andviais ignored.dwell: number— Fraction of the flight spent WAITING at the rally point.shake: number— How far the card trembles while it waits at the rally, in your units.jitter: number— Per-card scatter, 0..1: wonky arc height, sway, an extra half-turn and an uneven departure, all from a hash of the card's index — deterministic, so the same deal looks the same twice.landEase: CardEase— Easing for the rally → seat leg.'in'gives a slam,'out'a settle.formation: DealFormation | null— Which shapeformationSlotshould build for this style's rally, if any.
FlipOptions
interface FlipOptions
Configures flip.
lift?: number— Peak rise at the midpoint of the turn (default 0).from?: boolean— The face showing BEFORE the flip (default false — face-down, turning up).
FlipPose
interface FlipPose
Where a flipping card is in its half-turn.
angle: number— Rotation about the card's long axis, 0..π (a half turn).lift: number— Height above the resting plane — a card rises as it turns.faceUp: boolean— Which face should be showing NOW (swap the texture when this changes).scaleX: number— |cos(angle)| — the horizontal squash a 2D card flip needs (no 3D required: draw the cardscaleXnarrower and swap the art at the pinch).
GlideOptions
interface GlideOptions
Configures glide.
arc?: number— Peak arc height at mid-flight, in your units (default 0 — a flat slide).ease?: CardEase— Easing on the travel. Default 'inOut' — a dealt card accelerates away and settles, which is what gives a deal weight instead of reading as a cursor jump.
GlidePose
interface GlidePose
Where a gliding card is right now. lift is kept SEPARATE from the
interpolated point because up is +y in 3D and −y in 2D: apply it in your
own convention (mesh.y = pose.y + pose.lift, sprite.y = pose.y - pose.lift).
x: number— The x position at this instant, in your units.y: number— The y position at this instant, in your units.z: number— The z position at this instant, in your units (0 for 2D).lift: number— Height above the straight line between the endpoints, at this instant.eased: number— The EASED progress (the rawtafter the easing curve) — feed it toflex()/dropShadow()so every part of the motion agrees.
ShadowOptions
interface ShadowOptions
Configures dropShadow.
range?: number— Height at which the shadow has fully spread out (default 2).alpha?: number— Opacity when the card is resting on the table (default 0.35).slantX?: number— How far the shadow slides per unit of height, as a fraction of the card's width — the light's lean. Default 0.25 right, 0.25 down.slantY?: number— The downward slide per unit of height, as a fraction of the card's width. Default 0.25.spread?: number— Extra size at full height, as a fraction (default 0.35).
ShadowPose
interface ShadowPose
The contact shadow under a card that is off the table.
alpha: number— Fade with height — a shadow far from its caster is faint.scale: number— Grow with height — and softer, if your art supports it.offsetX: number— Sideways slide of the shadow, from the light's lean.offsetY: number— Downward slide of the shadow, from the light's lean.


