Skip to content

Animation

When the controller sends an item to a screen, two animations play in sequence:

  1. Controller OUT — the item flies off the controller toward the target screen’s direction.
  2. Screen IN — the item flies onto the target screen from the controller’s direction.

Both use the same direction vector, in opposite directions, giving the illusion of a single object travelling between them.

direction(settings, id) in config/settings.ts returns a 2D vector from the controller (always at (50, 50)) to a given screen’s position:

export function direction(settings: Settings, id: ScreenId) {
const pos = settings.screens[id]?.position
if (!pos) return { x: 0, y: 0 }
return { x: pos.left - 50, y: pos.top - 50 }
}

Range: roughly [-50, 50] on each axis.

gsap.fromTo(
flyingRef.current,
{ x: 0, y: 0, opacity: 1, scale: 1 },
{
x: dir.x * ANIM_SCALE,
y: dir.y * ANIM_SCALE,
opacity: 0,
scale: 0.4,
duration: 0.55,
ease: 'power2.in',
onComplete: () => {
channel.send({ type: 'show', target, itemId })
// ...
},
},
)

The broadcast fires on onComplete — the screen’s IN animation starts as soon as the controller’s OUT finishes, so the visual lands as a continuous motion.

gsap.fromTo(
blockRef.current,
{ x: -dir.x * ANIM_SCALE, y: -dir.y * ANIM_SCALE, opacity: 0, scale: 0.7 },
{ x: 0, y: 0, opacity: 1, scale: 1, duration: 0.7, ease: 'power3.out' },
)

The screen flips the sign on the direction vector — it comes in from where the controller is.

GSAP and @gsap/react are re-exported from @mars/motion so apps don’t depend on gsap directly:

import { gsap, useGSAP } from '@mars/motion'

useGSAP from @gsap/react wraps animations in a GSAP context that auto-reverts on cleanup — no manual gsap.killTweensOf calls.

  • ANIM_SCALE (currently 6) — converts [-50, 50] direction units into pixel distance. Tune for more or less dramatic travel.