@ferret/motion
the motion language: S-expressions and JSON, waits that suspend across steps, par/race/states, budget, seeded random and snapshots.
Read the guide: how to use it →
Classes
Module class
class Modulereadonly globals: Env | |
readonly main: Form[] | top-level forms that are not definitions: the programs of the file |
constructor(readonly forms: Form[]) |
packages/motion/src/runtime.ts:72
MotionBridge class
class MotionBridgemodule: Module | the module that load builds; handles started from earlier versions keep theirs |
constructor(readonly rt: MotionRuntime, private opts: BridgeOptions = {}) | |
define(name: string, sig: Signature, fn: (ctx: Ctx, ...args: any[]) => unknown): this | Instant: returns a value now. Arguments typed function arrive as plain JS functions. |
lasting(name: string, sig: Signature, fn: LastingFn): this | Lasting: returns a waitable ({ done(), result }) or a promise; the program sleeps on it as on wait. A snapshot taken meanwhile resumes after it. |
action(name: string, sig: Signature, spec: ActionSpec): this | Action: start returns { step, stop }, run every fixed step while the form is active. |
canCall(name: string): boolean | may a program call this command? Editing, configuring and loading never; the rest by the allow list. |
load(text: string, file?: string): Module | Read definitions into the bridge's module (earlier ones stay). |
tag(strings: TemplateStringsArray, ...values: unknown[]): MotionSource | The tagged template: ` motion(par (stroke left) (stroke right)) `. Numbers, strings, symbols, lists and booleans are printed in. |
run(program: string | MotionSource | Form | unknown, params: Record<string, unknown> = {}, opts: { owner?: boolean } = {}): Program | Start a program from text, a tree, a template result or JSON; params are the program's parameters. |
get(name: string): (...args: unknown[]) => Program | A defmotion as a TypeScript function: get('stroke')(sym('left'), { power: 700 }) starts it and returns the handle. |
parse(text: string, file?: string): Form[] | |
print(f: Form): string | |
toJson(f: Form): unknown | |
fromJson(j: unknown): Form | |
js(v: Value): unknown | A language value as plain JS: symbols are their names, a lambda is a function that runs to the end (it must not wait). |
packages/motion/src/bridge.ts:42
MotionError class
class MotionError extends ErrorAn error with the place in the source: parse errors name the file, line and column; run-time errors name the form.
constructor(message: string, readonly pos?: Pos, readonly form?: string) |
packages/motion/src/reader.ts:26
MotionRuntime class
class MotionRuntimetime: number | |
readonly seed: number | |
readonly budget: number | |
readonly fns: Map<string, InstantFn> | |
readonly actions: Map<string, ActionSpec> | |
readonly macros: Map<string, (form: Form[]) => Form> | forms a package rewrites into core forms when a file is loaded (over-angle becomes a loop of wait-until and while) |
readonly programs: Map<number, Program> | |
readonly sigs: Map<string, Signature> | signatures of functions defined from code (MotionBridge): arity and literal types are checked at load |
constructor(opts: MotionOptions = {}) | |
define(name: string, fn: InstantFn): this | a function that returns a value now: a sensor, a calculation; usable in conditions and arguments |
macro(name: string, fn: (form: Form[]) => Form): this | a form rewritten into core forms at load: the rewrite sees the unexpanded arguments, and its result is expanded again |
expand(f: Form): Form | |
action(name: string, spec: ActionSpec): this | an action that lasts while its form is active (a force, a motor) |
on(event: string, fn: (payload: any) => void): () => void | events of the runtime for a host: motion:error, motion:done, motion:stop, and whatever programs emit |
emitHost(event: string, payload: unknown) | |
load(source: string | Form[] | unknown, file?: string): Module | Read a file of definitions (defmotion, defn) and programs; every call and its arguments are checked here, not in the middle of a game. |
validate(mod: Module, program: string | Form | unknown, params: string[] = []): void | check a program against a module without starting it (names, arity, keywords, states): throws with file:line:col |
run(mod: Module, program: string | Form | unknown, params: Record<string, Value> = {}, opts: { owner?: boolean } = {}): Program | Start a program: a form of the module (or text, or JSON) with parameters; it runs from the next tick. |
spawn(p: Program, parent: Thread | null, ctl: Ctl): Thread | |
kill(p: Program, th: Thread): void | |
tick(dt: number): void | One fixed step for every program. |
deliverTo(p: Program, name: string, payload: Value): void | |
deliver(name: string, payload: Value = null, only?: Program): void | An event from the outside (a game, the physics): the threads that wait for it see it on the next step. |
evalInstant(form: Form, env: Env, ctx: Ctx): Value | |
snapshot(p: Program): unknown | |
restore(snap: any): Program | |
internals() | |
nextProgramId(): number | |
setNextProgramId(n: number) | |
makeProgram(mod: Module, root: Form, params: Record<string, Value>, id: number, seed: number): Program | |
statesSpecOf(f: Form[]) | |
callClosure(c: Closure, args: Value[]): Value | Call a lambda or a defn from code, to the end (it must not wait); the arguments are plain values. |
get isClosure() | |
get isMotionVal() |
packages/motion/src/runtime.ts:164
Program class
class ProgramA running program: the handle a host or a game holds.
status: ProgramStatus | |
error?: MotionError | |
time: number | |
params: Record<string, Value> | |
bb: Record<string, Value> | |
triggers: Set<string> | |
rng: number | |
threads: Thread[] | |
nextThread: number | |
left: number | |
readonly done: Promise<Value> | resolves with the program's value when it ends, or rejects with its error |
result: Value | |
constructor(readonly rt: MotionRuntime, readonly id: number, readonly module: Module, readonly root: Form, params: Record<string, Value>, seed: number) | |
stop(): void | |
pause(): void | |
resume(): void | |
set(param: string, value: Value): void | |
trigger(name: string): void | |
on(event: string, fn: (payload: Value) => void): () => void | |
state() | what each thread waits for, and the params, for the inspector and motion.state |
snapshot() | |
emitLocal(name: string, payload: Value) | |
killAll() | |
fail(e: unknown) | |
end(v: Value) |
packages/motion/src/runtime.ts:88
Sym class
class SymThe reader of the motion language: S-expressions in text, and the same tree as a JSON array. No Babylon, no physics.
A form is a number, true / false, null (nil), a string, a symbol (Sym), a list (a JS array) or a condition object (the animation graph's { param, op, value } vocabulary, only in JSON or written inline as {...}). In JSON a symbol or keyword is a plain string ("oarlock-left", ":max-torque"), a string literal is { "s": "text" }, so (force oarlock-left forward 600) is ["force", "oarlock-left", "forward", 600] and (emit "stroke" side) is ["emit", {"s": "stroke"}, "side"].
static of(name: string): Sym | |
get isKeyword(): boolean | |
toString() |
packages/motion/src/reader.ts:9
Thread class
class Threadstack: Frame[] | |
ctl: Ctl | |
wait: Wait | null | |
status: Status | |
value: Value | |
kids: Thread[] | |
daemons: Array<{ thread: Thread; depth: number }> | |
stateT0: number | |
constructor(readonly id: number, readonly parent: Thread | null) |
packages/motion/src/runtime.ts:58
Functions
brief function
function brief(f: unknown, max = 80): stringA short text of a form for error messages.
packages/motion/src/reader.ts:151
fromJson function
function fromJson(j: unknown): FormBack from JSON; file names the source for errors (a JSON program has no lines).
packages/motion/src/reader.ts:137
netPrograms function
function netPrograms(rt: MotionRuntime, net: MotionNetLike, onAdopt: (name: string, program: Program, from: string) => void): MotionHandoverpackages/motion/src/net.ts:18
parse function
function parse(src: string, file?: string): Form[]Read the text of a program: a list of top-level forms.
packages/motion/src/reader.ts:52
parseOne function
function parseOne(src: string, file?: string): FormRead exactly one form.
packages/motion/src/reader.ts:111
posOf function
function posOf: (f: unknown) => Pos | undefinedpackages/motion/src/reader.ts:23
print function
function print(f: Form): stringpackages/motion/src/reader.ts:117
toJson function
function toJson(f: Form): unknownThe JSON array form of a tree (one to one with the text).
packages/motion/src/reader.ts:129
Interfaces
ActionSpec interface
interface ActionSpecstart(ctx: Ctx, args: Value[]): HoldHandle | void | called once when an action form starts; the handle's step runs every step with the arguments evaluated again |
packages/motion/src/runtime.ts:31
BridgeOptions interface
interface BridgeOptionscommands?: CommandRunner | |
allow?: string[] | exact names or prefixes ending in "." that programs may call through (cmd ...) (default DEFAULT_ALLOW) |
packages/motion/src/bridge.ts:20
Closure interface
interface Closurepackages/motion/src/runtime.ts:16
CommandRunner interface
interface CommandRunnerexecute(name: string, args?: unknown): Promise<unknown> | unknown |
packages/motion/src/bridge.ts:13
Ctx interface
interface Ctxrt: MotionRuntime | |
program: Program | |
thread: Thread | |
env: Env | |
age: number | seconds since the action that is being read started (the clock of half-sine and the like); 0 outside an action |
scale: number | 0..1 while a ramp is rising, else 1 |
dt: number |
packages/motion/src/runtime.ts:21
Env interface
interface Envvars: Map<string, Value> | |
parent: Env | null |
packages/motion/src/runtime.ts:15
HoldHandle interface
interface HoldHandlepackages/motion/src/runtime.ts:30
MotionHandover interface
interface MotionHandoverhandOver(name: string, program: Program, peer: string): void | stop program here and send its snapshot to peer, who runs it on from the same point |
dispose(): void |
packages/motion/src/net.ts:13
MotionNetLike interface
interface MotionNetLikePrograms over the net. A program that acts on a body runs only on the machine that owns the body. When the ownership moves (a player takes the wheel, the owner leaves), the old owner hands the program over as a snapshot and the new owner continues it exactly where it was, as long as the same actions are defined there. Duck-typed on NetService.
readonly id: string | |
send(name: string, data: unknown): void | |
on(name: string, fn: (data: any, from: string) => void): () => void |
packages/motion/src/net.ts:8
MotionOptions interface
interface MotionOptionsseed?: number | the world seed: with the program id it seeds random |
budget?: number | reductions per thread and step before a loop that never waits is stopped (default 10000) |
packages/motion/src/runtime.ts:157
MotionSource interface
interface MotionSourceA program written in code: the text and the tree.
readonly text: string | |
readonly form: Form |
packages/motion/src/bridge.ts:27
MotionVal interface
interface MotionValpackages/motion/src/runtime.ts:17
Pos interface
interface Posline: number | |
col: number | |
file?: string |
packages/motion/src/reader.ts:21
Signature interface
interface Signatureargs: SigType[] | argument types; the first min are required (default all) |
min?: number | |
rest?: SigType | every argument after args has this type |
returns?: SigType | |
where?: 'everywhere' | 'owner' | owner: it reads something local (input, the camera, a UI); a program that is not owner-run may not use it |
packages/motion/src/runtime.ts:147
Waitable interface
interface WaitableCode and the language, both ways. Functions from code come in three kinds: instant (a value now, usable in conditions), lasting (a waitable the program sleeps on, like wait) and action (step every fixed step while its form is active, stop when it ends). Every function declares a signature, checked when the program loads and when it is called, and where it may run (everywhere or owner). Programs come back the other way as handles and as TypeScript functions, a tagged template writes them inline, and a lambda handed to a function arrives as a plain JS function.
done(): boolean | |
result?: unknown |
packages/motion/src/bridge.ts:11
Types
CondObj type
type CondObj = { [k: string]: unknown }packages/motion/src/reader.ts:18
Form type
type Form = number | boolean | null | string | Sym | Form[] | CondObjpackages/motion/src/reader.ts:19
InstantFn type
type InstantFn = (ctx: Ctx, ...args: Value[]) => Valuepackages/motion/src/runtime.ts:29
LastingFn type
type LastingFn = (ctx: Ctx, ...args: any[]) => Waitable | Promise<unknown>packages/motion/src/bridge.ts:12
ProgramStatus type
type ProgramStatus = 'running' | 'paused' | 'done' | 'error' | 'stopped'packages/motion/src/runtime.ts:70
SigType type
type SigType = 'number' | 'boolean' | 'string' | 'symbol' | 'vec3' | 'list' | 'function' | 'any'Types a function signature can name (checked when the program loads for literals, and when the function is called for values).
packages/motion/src/runtime.ts:146
Value type
type Value = number | boolean | null | string | Sym | Value[] | Closure | MotionValThe evaluator of the motion language. A program runs for many fixed steps, so it is a stack machine of plain data, not a recursive interpreter:
- a *thread* has a stack of frames (`seq`, `repeat`, `loop`) and at most one wait (time, a condition, an event, a hold, its children);
- `par`, `race`, `while`, `guard`, `for`, `on` and `states` start child threads, so killing a form kills what it started;
- an action (`force` ...) is a *hold*: the thread waits on it for ever, its arguments are read again every step, and when something kills
the thread (`race`, `for`, `while`, a guard) the action stops;
- conditions, arguments, `defn` and lambdas are evaluated *instantly* by a recursive evaluator: they cannot wait;
- one budget of reductions per thread and step stops a loop that never waits; the clock is the runtime's own (`tick(dt)`), the random
numbers are seeded: the same program and the same inputs give the same run, and a snapshot continues it exactly.packages/motion/src/runtime.ts:14
Constants
DEFAULT_ALLOW const
const DEFAULT_ALLOW: string[]Commands a level program may call: run-time commands of the physics packages; the game can replace this list.
packages/motion/src/bridge.ts:16
sym const
const sym: (name: string) => Sympackages/motion/src/reader.ts:16