CastStage
Several characters choreographed through a scripted scene.
Part of
@aora-bot/surfaces, not@aora-bot/react:pnpm add @aora-bot/surfaces @aora-bot/react, and import@aora-bot/surfaces/styles.cssafter the React stylesheet.
The script is data
A scene is a list of members and a list of steps. Each step says where everybody stands — x and y in pixels from the centre of the stage, an optional scale, and an optional state to adopt for that step — and how long it holds. A member left out of a step's positions is off stage for that step: it shrinks and fades rather than vanishing.
Because both halves are plain data, a host writes its own choreography and this component never learns what the scene is about. An onboarding flow can reorder its scenes without any of the movement logic knowing.
Springs without a frame loop
Moves are CSS transitions, but the easing is sampled from the engine's own spring into a linear() function. A real spring needs a frame loop, and a frame loop for four members that are merely gliding would compete with the animation each of them is already running for its face. Sampling the analytic solution gives the same curve — overshoot included — for the price of a string. spring tunes stiffness (omega) and damping (zeta); moveMs is the transition length.
onArrive(memberId, stepIndex) fires once per member per step when its transform has finished, so a host can trigger the next thing exactly when a character has landed rather than on a timer that has to guess.
useCastScene
The hook decides when, never where. It holds the current stepIndex, advances after each step's durationMs while playing, and stops at the last step unless loop is set. next, prev, play, pause and goTo are the transport; onStep reports each change. autoplay starts it running on mount.
CastMember and AvatarSlot
CastMember is one character in two nested boxes: the outer box owns the journey — position, scale and opacity under one transition — and the inner box owns the idle bob (bobPx, bobMs), so a member can drift on the spot while it is also gliding across the stage. Vary bobMs per member so the group does not breathe in unison. Members are StagedMarks, so the stage wants a MarkStageProvider above it.
AvatarSlot is the empty circle a character flies into at the end: a dashed outline that says "something belongs here" while the scene runs, and a cross-fade to children once arrived is set — two layers, so the box is never empty in between. It does not know where it is; position it with ordinary CSS, inside the stage or in a form beside it.
API
| Prop | Type | Default | Description |
|---|---|---|---|
members* | readonly CastMemberSpec[] | — | — |
steps* | readonly CastStep[] | — | — |
children | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | null | — | Scenery: a device frame, an `AvatarSlot`, a caption. Rendered over the stage. |
className | string | — | — |
face | "cute" | "eyes" | "line" | — | — |
heightPx | number | 320 | — |
lineWeight | number | — | — |
moveMs | number | 560 | How long a move takes. Also the transition duration. |
nose | "slant" | "angular" | "hook" | "long" | "curve" | "bow" | null | — | — |
onArrive | ((memberId: string, stepIndex: number) => void) | — | Called when a member finishes a move. Fires once per member per step. |
sizePx | number | 80 | Default member size, for members that do not set their own. |
spring | { omega: number; zeta: number; } | DEFAULT_SPRING | Spring the easing is sampled from: stiffness and damping. |
stepIndex | number | 0 | Which step is on screen. Drive it with `useCastScene`. |
style | CSSProperties | — | — |
widthPx | number | 320 | Stage box. Positions are measured from its centre, so both are needed. |
CastMember
| Prop | Type | Default | Description |
|---|---|---|---|
durationMs* | number | — | — |
easing* | string | — | Easing for the move, as a `linear()` function from the engine's spring. |
member* | CastMemberSpec | — | — |
position* | CastPosition | null | — | Where to stand. `null` takes the member off stage. |
className | string | — | — |
face | "cute" | "eyes" | "line" | — | — |
lineWeight | number | — | — |
nose | "slant" | "angular" | "hook" | "long" | "curve" | "bow" | null | — | — |
onArrive | ((id: string) => void) | — | Called when this member finishes moving. |
sizePx | number | — | — |
style | CSSProperties | — | — |
AvatarSlot
| Prop | Type | Default | Description |
|---|---|---|---|
arrived | boolean | false | The member has landed: the outline fades out and `children` fade in. |
children | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | null | — | What the slot holds once something has landed in it. |
className | string | — | — |
label | string | — | — |
sizePx | number | 64 | — |
style | CSSProperties | — | — |
Next
- Cast — the named characters
- Onboarding — a scripted first run