Animation
When the controller sends an item to a screen, two animations play in sequence:
- Controller OUT — the item flies off the controller toward the target screen’s direction.
- 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 vector
Section titled “Direction vector”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.
Controller OUT
Section titled “Controller OUT”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.
Screen IN
Section titled “Screen IN”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.
@mars/motion
Section titled “@mars/motion”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.
Constants
Section titled “Constants”ANIM_SCALE(currently6) — converts[-50, 50]direction units into pixel distance. Tune for more or less dramatic travel.