Creating actors
Where an actor belongs, how to lay out its state, what its render path must be, and the rules that keep it reusable.
1. Choose the owner
Three repositories, and the choice is not negotiable by convenience:
Keep the actor, view, renderer and feature exports colocated. Do not create a family folder in the runtime repo to work around a missing library abstraction — that is the abstraction asking to be written.
2. Implement the actor
Use the primitives intentionally. The Reactivity section has the full decision table; the short version:
| For | Use |
|---|---|
| Flat state | signal |
| Nested state bags | reactiveSignal |
| Grouped state | group / layoutGroup |
| Computed reads | derived |
| Host-published values | channel |
| Coordination | events and subscribe |
| Local reactive response | watch |
| Owned semantic children | child |
import { LayoutActor, group, layoutGroup, signal } from "@motionactor/core";
export class Image extends LayoutActor {
layout = layoutGroup({
width: 320,
height: 180,
});
content = group({
src: signal(""),
objectFit: signal<"cover" | "contain">("cover"),
});
}Rules
- Authored durations are seconds, never frames.
- Keep actor state semantic and renderer-agnostic.
isHighlighted, notborderColor. - Prefer object parameters for commands with more than one scalar argument.
- Prefer semantic commands over direct signal tweening as the main authoring surface —
card.reveal({ duration })reads better than three tweens at every call site, and it keeps the motion in one place when it changes. LayoutActoralready declareslayout,transform,appearance,flexItemandgridItem. Overridelayout = layoutGroup({...})to change defaults rather than redeclaring the group.
Layout authoring
layoutis authored input;localLayoutandworldLayoutare resolved output.layout.x,layout.y,layout.widthandlayout.heightaccept numbers,pct(n), or percentage strings like"50%".- Root actors resolve percentages against the scene viewport. Nested positioned actors resolve against their containing block — usually the nearest positioned ancestor's local box.
- Resolved boxes are always numeric, even when the authored value was a percentage.
- Renderers size their element, SVG, canvas or backend surface from resolved
localLayout.
3. Give it a render path
Every visual actor needs one:
- an actor
render(...)method — preferred for React-first visual actors, or - a
defineActorRenderer(...)renderer — valid for migration, specialization and renderer overrides, or - both, when a renderer overrides a default.
When both exist they share one render-time model: actor is resolved frame state, slots
is rendered child placement, targets is the render-facing target projection.
Nonvisual actors need behavioral examples instead of a visual demo.
View rules
- Keep
classNamestatic. - Compute dynamic visual values into local variables and pass them through
style. - Do not use generic signal-reader helpers or string-key probing when the actor type is known.
- Do the geometry in
derivedon the actor, not in the view. The view should read finished numbers — see the Desktop example.
4. Wire the exports
- Feature barrel:
<family>/src/<feature>/index.ts - Family package root:
<family>/src/index.ts - Primitive package root:
packages/primitives/src/index.ts
5. Demonstrate it
Visual actors get a showcase composition in the owning library, built with
defineSceneComposition(...). Nonvisual actors get a behavioral test instead —
see Testing.
Traits were removed
Shared state uses group / layoutGroup; reusable motion is expressed through actor
commands and the existing signal and effect primitives. If you find yourself wanting a
mixin, you probably want a child actor or a shared command helper.