Desktop
A real library actor end to end, with signals for state, derived chains for presentation, slots for hosted apps and generator commands for motion.
Desktop animates an operating system desktop: a dock, windows that open out of their dock
icon, and a cursor. It ships in the component library and is large
enough to show how the patterns hold up in a real actor. The code below is trimmed from its source.
The helpers that compute dock metrics and window transforms come from an import, and it keeps
one or two of each kind of command.
import {
function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived,
const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing,
function group<T extends Record<string, unknown>>(value: T): T & SignalGroupDefinitiongroup,
class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor,
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal,
function slot<const TAccepts extends SlotAccepts>(options: {
accepts: TAccepts;
multiple?: false;
}): OnePlaceField<TAccepts> (+1 overload)
Declare a place: a named position this actor's children are hosted in.
Written as a field; the field reads as a handle once the actor spawns.
```ts
screen = slot({ accepts: LayoutActor }); // this.screen.current
letters = slot({ accepts: Letter, multiple: true }); // this.letters.all()
```slot,
type interface ActorClass<TInstance extends object = object>ActorClass,
type type Duration = numberDuration,
} from "@motionactor/core";
import {
const animateWindowField: (window: DesktopWindowRecord, progress: number, updates: Partial<Record<WindowField, number>>) => DesktopWindowRecordanimateWindowField,
const buildWindowTransform: (window: DesktopWindowRecord, dockCenter: {
x: number;
y: number;
} | null) => string
buildWindowTransform,
const clamp01: (value: number) => numberclamp01,
const mapWindowById: (windows: DesktopWindowRecord[], windowId: string, update: (window: DesktopWindowRecord) => DesktopWindowRecord) => DesktopWindowRecord[]mapWindowById,
const moveWindowToFront: (windows: DesktopWindowRecord[], windowId: string) => DesktopWindowRecord[]moveWindowToFront,
const resolveDockMetrics: (skin: DesktopSkin, itemCount: number, width: number, height: number) => DesktopDockMetricsresolveDockMetrics,
type type DesktopActorWindowOptions = Pick<DesktopWindowRecord, "x" | "y" | "width" | "height" | "appId" | "title"> & {
windowId: string;
}
DesktopActorWindowOptions,
type type DesktopDockItem = {
id: string;
label: string;
icon?: string;
}
DesktopDockItem,
type type DesktopDockMetrics = {
iconSize: number;
gap: number;
paddingX: number;
dockWidth: number;
left: number;
centerY: number;
}
DesktopDockMetrics,
type type DesktopSkin = "aqua" | "retro"DesktopSkin,
type type DesktopWindowRecord = {
id: string;
appId: string;
title: string;
x: number;
y: number;
width: number;
height: number;
isOpen: boolean;
isMinimized: boolean;
openProgress: number;
minimizeProgress: number;
content: {
type: "hosted";
} | {
type: "blank";
};
}
DesktopWindowRecord,
type type DesktopWindowVisual = {
id: string;
transform: string;
opacity: number;
zIndex: number;
isFocused: boolean;
visible: boolean;
}
DesktopWindowVisual,
} from "./desktop-helpers";
export class class DesktopWindowHostDesktopWindowHost extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
DesktopWindowHost.content: OnePlaceField<typeof LayoutActor>content = slot<typeof LayoutActor>(options: {
accepts: typeof LayoutActor;
multiple?: false;
}): OnePlaceField<typeof LayoutActor> (+1 overload)
Declare a place: a named position this actor's children are hosted in.
Written as a field; the field reads as a handle once the actor spawns.
```ts
screen = slot({ accepts: LayoutActor }); // this.screen.current
letters = slot({ accepts: Letter, multiple: true }); // this.letters.all()
```slot({ accepts: typeof LayoutActoraccepts: class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor });
DesktopWindowHost.window: {
windowId: ActorFieldSignal<string>;
} & SignalGroupDefinition
window = group<{
windowId: ActorFieldSignal<string>;
}>(value: {
windowId: ActorFieldSignal<string>;
}): {
windowId: ActorFieldSignal<string>;
} & SignalGroupDefinition
group({ windowId: ActorFieldSignal<string>windowId: signal<string>(initial: string): ActorFieldSignal<string> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal("") });
}
export class class DesktopDesktop extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Desktop.windowHosts: ManyPlaceField<typeof DesktopWindowHost>windowHosts = slot<typeof DesktopWindowHost>(options: {
accepts: typeof DesktopWindowHost;
multiple: true;
}): ManyPlaceField<typeof DesktopWindowHost> (+1 overload)
Declare a place: a named position this actor's children are hosted in.
Written as a field; the field reads as a handle once the actor spawns.
```ts
screen = slot({ accepts: LayoutActor }); // this.screen.current
letters = slot({ accepts: Letter, multiple: true }); // this.letters.all()
```slot({ accepts: typeof DesktopWindowHostaccepts: class DesktopWindowHostDesktopWindowHost, multiple: truemultiple: true });
Desktop.skin: ActorFieldSignal<DesktopSkin>skin = signal<DesktopSkin>(initial: DesktopSkin): ActorFieldSignal<DesktopSkin> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal<type DesktopSkin = "aqua" | "retro"DesktopSkin>("aqua");
Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows = signal<DesktopWindowRecord[]>(initial: DesktopWindowRecord[]): ActorFieldSignal<DesktopWindowRecord[]> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal<type DesktopWindowRecord = {
id: string;
appId: string;
title: string;
x: number;
y: number;
width: number;
height: number;
isOpen: boolean;
isMinimized: boolean;
openProgress: number;
minimizeProgress: number;
content: {
type: "hosted";
} | {
type: "blank";
};
}
DesktopWindowRecord[]>([]);
Desktop.focusedWindowId: ActorFieldSignal<string>focusedWindowId = signal<string>(initial: string): ActorFieldSignal<string> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal("");
Desktop.dock: {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
} & SignalGroupDefinition
dock = group<{
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
}>(value: {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
}): {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
} & SignalGroupDefinition
group({
items: ActorFieldSignal<DesktopDockItem[]>items: signal<DesktopDockItem[]>(initial: DesktopDockItem[]): ActorFieldSignal<DesktopDockItem[]> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal<type DesktopDockItem = {
id: string;
label: string;
icon?: string;
}
DesktopDockItem[]>([]),
highlights: ActorFieldSignal<Record<string, number>>highlights: signal<Record<string, number>>(initial: Record<string, number>): ActorFieldSignal<Record<string, number>> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal<type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type TRecord<string, number>>({}),
});
Desktop.mouse: {
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
} & SignalGroupDefinition
mouse = group<{
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
}>(value: {
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
}): {
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
} & SignalGroupDefinition
group({
x: ActorFieldSignal<number>x: signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal(0),
y: ActorFieldSignal<number>y: signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal(0),
clickProgress: ActorFieldSignal<number>clickProgress: signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal(0),
});
Desktop.dockMetrics: DerivedDefinition<DesktopDockMetrics, unknown>dockMetrics = derived<unknown, DesktopDockMetrics>(fn: (this: unknown) => DesktopDockMetrics): DerivedDefinition<DesktopDockMetrics, unknown>derived((): type DesktopDockMetrics = {
iconSize: number;
gap: number;
paddingX: number;
dockWidth: number;
left: number;
centerY: number;
}
DesktopDockMetrics => {
const { const width: numberwidth, const height: numberheight } = this.LayoutActor<{}>.localLayout(): ResolvedLayoutBoxlocalLayout();
return function resolveDockMetrics(skin: DesktopSkin, itemCount: number, width: number, height: number): DesktopDockMetricsresolveDockMetrics(this.Desktop.skin: () => DesktopSkinskin(), this.Desktop.dock: {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
} & SignalGroupDefinition
dock.items: () => DesktopDockItem[]items().Array<T>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.length, const width: numberwidth, const height: numberheight);
});
/** appId to the screen point a window opens from and minimizes into. */
Desktop.dockIconCenters: DerivedDefinition<Record<string, {
x: number;
y: number;
}>, unknown>
appId to the screen point a window opens from and minimizes into.dockIconCenters = derived<unknown, Record<string, {
x: number;
y: number;
}>>(fn: (this: unknown) => Record<string, {
x: number;
y: number;
}>): DerivedDefinition<Record<string, {
x: number;
y: number;
}>, unknown>
derived(() => {
const { const iconSize: numbericonSize, const gap: numbergap, const paddingX: numberpaddingX, const left: numberleft, const centerY: numbercenterY } = this.Desktop.dockMetrics: DerivedDefinition
() => DesktopDockMetrics
dockMetrics();
const const centers: Record<string, {
x: number;
y: number;
}>
centers: type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type TRecord<string, { x: numberx: number; y: numbery: number }> = {};
this.Desktop.dock: {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
} & SignalGroupDefinition
dock.items: () => DesktopDockItem[]items().Array<DesktopDockItem>.forEach(callbackfn: (value: DesktopDockItem, index: number, array: DesktopDockItem[]) => void, thisArg?: any): voidPerforms the specified action for each element in an array.forEach((item: DesktopDockItemitem, index: numberindex) => {
const centers: Record<string, {
x: number;
y: number;
}>
centers[item: DesktopDockItemitem.id: stringid] = {
x: numberx: const left: numberleft + const paddingX: numberpaddingX + index: numberindex * (const iconSize: numbericonSize + const gap: numbergap) + const iconSize: numbericonSize / 2,
y: numbery: const centerY: numbercenterY,
};
});
return const centers: Record<string, {
x: number;
y: number;
}>
centers;
});
Desktop.windowVisuals: DerivedDefinition<DesktopWindowVisual[], unknown>windowVisuals = derived<unknown, DesktopWindowVisual[]>(fn: (this: unknown) => DesktopWindowVisual[]): DerivedDefinition<DesktopWindowVisual[], unknown>derived((): type DesktopWindowVisual = {
id: string;
transform: string;
opacity: number;
zIndex: number;
isFocused: boolean;
visible: boolean;
}
DesktopWindowVisual[] => {
const const centers: Record<string, {
x: number;
y: number;
}>
centers = this.Desktop.dockIconCenters: DerivedDefinition
() => Record<string, {
x: number;
y: number;
}>
appId to the screen point a window opens from and minimizes into.dockIconCenters();
const const focused: stringfocused = this.Desktop.focusedWindowId: () => stringfocusedWindowId();
return this.Desktop.windows: () => DesktopWindowRecord[]windows().Array<DesktopWindowRecord>.map<{
id: string;
transform: string;
opacity: number;
zIndex: number;
isFocused: boolean;
visible: boolean;
}>(callbackfn: (value: DesktopWindowRecord, index: number, array: DesktopWindowRecord[]) => {
id: string;
transform: string;
opacity: number;
zIndex: number;
isFocused: boolean;
visible: boolean;
}, thisArg?: any): {
id: string;
transform: string;
opacity: number;
zIndex: number;
isFocused: boolean;
visible: boolean;
}[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((window: DesktopWindowRecordwindow, index: numberindex) => {
const const opacity: numberopacity =
function clamp01(value: number): numberclamp01(window: DesktopWindowRecordwindow.openProgress: numberopenProgress) * (1 - function clamp01(value: number): numberclamp01(window: DesktopWindowRecordwindow.minimizeProgress: numberminimizeProgress));
return {
id: stringid: window: DesktopWindowRecordwindow.id: stringid,
transform: stringtransform: function buildWindowTransform(window: DesktopWindowRecord, dockCenter: {
x: number;
y: number;
} | null): string
buildWindowTransform(window: DesktopWindowRecordwindow, const centers: Record<string, {
x: number;
y: number;
}>
centers[window: DesktopWindowRecordwindow.appId: stringappId] ?? null),
opacity: numberopacity,
zIndex: numberzIndex: 100 + index: numberindex,
isFocused: booleanisFocused: const focused: stringfocused === window: DesktopWindowRecordwindow.id: stringid,
visible: booleanvisible: (window: DesktopWindowRecordwindow.isOpen: booleanisOpen || window: DesktopWindowRecordwindow.openProgress: numberopenProgress > 0) && const opacity: numberopacity > 0.001,
};
});
});
Desktop.setDockItems(items: DesktopDockItem[]): voidsetDockItems(items: DesktopDockItem[]items: type DesktopDockItem = {
id: string;
label: string;
icon?: string;
}
DesktopDockItem[]) {
this.Desktop.dock: {
items: ActorFieldSignal<DesktopDockItem[]>;
highlights: ActorFieldSignal<Record<string, number>>;
} & SignalGroupDefinition
dock.items: ActorFieldSignal<DesktopDockItem[]>items.LiveSignal<DesktopDockItem[]>.set(value: DesktopDockItem[]): voidSet value instantly at current cursor (no yield needed)set(items: DesktopDockItem[]items);
}
Desktop.spawnActorWindow<T extends LayoutActor>(definition: ActorClass<T>, window: DesktopActorWindowOptions): TspawnActorWindow<function (type parameter) T in Desktop.spawnActorWindow<T extends LayoutActor>(definition: ActorClass<T>, window: DesktopActorWindowOptions): TT extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor>(
definition: ActorClass<T>definition: interface ActorClass<TInstance extends object = object>ActorClass<function (type parameter) T in Desktop.spawnActorWindow<T extends LayoutActor>(definition: ActorClass<T>, window: DesktopActorWindowOptions): TT>,
window: DesktopActorWindowOptionswindow: type DesktopActorWindowOptions = Pick<DesktopWindowRecord, "x" | "y" | "width" | "height" | "appId" | "title"> & {
windowId: string;
}
DesktopActorWindowOptions,
): function (type parameter) T in Desktop.spawnActorWindow<T extends LayoutActor>(definition: ActorClass<T>, window: DesktopActorWindowOptions): TT;
Desktop.spawnActorWindow<T extends LayoutActor>(definition: ActorClass<T>, window: DesktopActorWindowOptions): TspawnActorWindow(
definition: ActorClass<LayoutActor<{}>>definition: interface ActorClass<TInstance extends object = object>ActorClass<class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor>,
window: DesktopActorWindowOptionswindow: type DesktopActorWindowOptions = Pick<DesktopWindowRecord, "x" | "y" | "width" | "height" | "appId" | "title"> & {
windowId: string;
}
DesktopActorWindowOptions,
): unknown {
const const host: DesktopWindowHosthost = this.Desktop.windowHosts: ManyPlaceField<typeof DesktopWindowHost>windowHosts.function spawn(overrides?: {
measuredBox?: undefined;
transform?: {
scale?: SignalInputValue<number> | undefined;
rotation?: SignalInputValue<number> | undefined;
} | undefined;
id?: undefined;
window?: {
windowId?: SignalInputValue<string> | undefined;
} | undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<...>> | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 18 more ...;
parent?: undefined;
} | undefined): DesktopWindowHost (+1 overload)
Host a child of the one declared class.spawn({
window?: {
windowId?: SignalInputValue<string> | undefined;
} | undefined
window: { windowId?: SignalInputValue<string> | undefinedwindowId: window: DesktopActorWindowOptionswindow.windowId: stringwindowId },
});
const const actor: LayoutActor<{}>actor = const host: DesktopWindowHosthost.DesktopWindowHost.content: OnePlaceField<typeof LayoutActor>content.ClassSpawn<typeof LayoutActor>.spawn<ActorClass<LayoutActor<{}>>>(blueprint: ActorClass<LayoutActor<{}>>, overrides?: {
measuredBox?: undefined;
transform?: {
scale?: SignalInputValue<number> | undefined;
rotation?: SignalInputValue<number> | undefined;
} | undefined;
id?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<...> | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 18 more ...;
parent?: undefined;
} | undefined): LayoutActor<...>
Host a child of `blueprint`, which must be an accepted class.spawn(definition: ActorClass<LayoutActor<{}>>definition);
const { const windowId: stringwindowId, ...const placement: {
x: number;
y: number;
width: number;
height: number;
appId: string;
title: string;
}
placement } = window: DesktopActorWindowOptionswindow;
this.Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows.LiveSignal<DesktopWindowRecord[]>.set(value: DesktopWindowRecord[]): voidSet value instantly at current cursor (no yield needed)set([
...this.Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows.ReadSignal<DesktopWindowRecord[]>.peek(): DesktopWindowRecord[]Read current value at compile cursor or current resolve framepeek(),
{
...const placement: {
x: number;
y: number;
width: number;
height: number;
appId: string;
title: string;
}
placement,
id: stringid: const windowId: stringwindowId,
isOpen: booleanisOpen: false,
isMinimized: booleanisMinimized: false,
openProgress: numberopenProgress: 0,
minimizeProgress: numberminimizeProgress: 0,
content: {
type: "hosted";
} | {
type: "blank";
}
content: { type: "hosted"type: "hosted" },
},
]);
return const actor: LayoutActor<{}>actor;
}
*Desktop.moveCursor({ x, y }: {
x: number;
y: number;
}, transition?: Duration): Generator<number, void, unknown>
moveCursor({ x: numberx, y: numbery }: { x: numberx: number; y: numbery: number }, transition: numbertransition: type Duration = numberDuration = 0.6667) {
yield* this.Actor<{}>.runtime: ActorRuntimeOrchestration<this>runtime.ActorRuntimeOrchestration<this>.all(...generators: Generator<Frame, unknown, unknown>[]): Generator<Frame, void, unknown>all(
this.Desktop.mouse: {
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
} & SignalGroupDefinition
mouse.x: ActorFieldSignal<number>x.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(x: numberx, { SignalTweenOptions<number>.duration: numberduration: transition: numbertransition, SignalTweenOptions<number>.easing?: EasingFunction | undefinedeasing: const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing.inOutCubic: EasingFunctioninOutCubic }),
this.Desktop.mouse: {
x: ActorFieldSignal<number>;
y: ActorFieldSignal<number>;
clickProgress: ActorFieldSignal<number>;
} & SignalGroupDefinition
mouse.y: ActorFieldSignal<number>y.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(y: numbery, { SignalTweenOptions<number>.duration: numberduration: transition: numbertransition, SignalTweenOptions<number>.easing?: EasingFunction | undefinedeasing: const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing.inOutCubic: EasingFunctioninOutCubic }),
);
}
*Desktop.openWindow(windowId: string, duration?: Duration): Generator<number, void, unknown>openWindow(windowId: stringwindowId: string, duration: numberduration: type Duration = numberDuration = 0.6667) {
const const baseline: DesktopWindowRecord[]baseline = function moveWindowToFront(windows: DesktopWindowRecord[], windowId: string): DesktopWindowRecord[]moveWindowToFront(
function mapWindowById(windows: DesktopWindowRecord[], windowId: string, update: (window: DesktopWindowRecord) => DesktopWindowRecord): DesktopWindowRecord[]mapWindowById(this.Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows.ReadSignal<DesktopWindowRecord[]>.peek(): DesktopWindowRecord[]Read current value at compile cursor or current resolve framepeek(), windowId: stringwindowId, (window: DesktopWindowRecordwindow) => ({
...window: DesktopWindowRecordwindow,
isOpen: booleanisOpen: true,
isMinimized: booleanisMinimized: false,
minimizeProgress: numberminimizeProgress: 0,
})),
windowId: stringwindowId,
);
this.Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows.LiveSignal<DesktopWindowRecord[]>.set(value: DesktopWindowRecord[]): voidSet value instantly at current cursor (no yield needed)set(const baseline: DesktopWindowRecord[]baseline);
this.Desktop.focusedWindowId: ActorFieldSignal<string>focusedWindowId.LiveSignal<string>.set(value: string): voidSet value instantly at current cursor (no yield needed)set(windowId: stringwindowId);
yield* this.Desktop.windows: ActorFieldSignal<DesktopWindowRecord[]>windows.LiveSignal<DesktopWindowRecord[]>.transition(fn: (progress: number, from: DesktopWindowRecord[]) => DesktopWindowRecord[], options: number | SignalTransitionOptions<DesktopWindowRecord[]>): Generator<Frame, void, unknown>Custom transition — must yield*transition(
(progress: numberprogress) =>
const baseline: DesktopWindowRecord[]baseline.Array<DesktopWindowRecord>.map<DesktopWindowRecord>(callbackfn: (value: DesktopWindowRecord, index: number, array: DesktopWindowRecord[]) => DesktopWindowRecord, thisArg?: any): DesktopWindowRecord[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((window: DesktopWindowRecordwindow) =>
window: DesktopWindowRecordwindow.id: stringid === windowId: stringwindowId
? function animateWindowField(window: DesktopWindowRecord, progress: number, updates: Partial<Record<WindowField, number>>): DesktopWindowRecordanimateWindowField(window: DesktopWindowRecordwindow, progress: numberprogress, { openProgress?: number | undefinedopenProgress: 1 })
: window: DesktopWindowRecordwindow,
),
{ SignalTransitionOptions<DesktopWindowRecord[]>.duration: numberduration, SignalTransitionOptions<DesktopWindowRecord[]>.easing?: EasingFunction | undefinedeasing: const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing.out: (easing: EasingFunction) => EasingFunctionout(const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing.inCubic: EasingFunctioninCubic) },
);
}
}
State
Everything the scene writes is a signal, with related fields
namespaced by group. The open windows are one signal holding an
array. Opening a window changes several of its fields as one step, and the window list
changes as a whole, so one value with one writer is simpler than a nested
reactiveSignal.
Presentation
The renderers draw finished numbers. windowVisuals reads dockIconCenters, which reads
dockMetrics, which reads the resolved box and the dock’s signals. Each is a
derived, recomputed for the frame being resolved. The Aqua and
retro skins read the same windowVisuals, and a test can check where a window minimizes to
without drawing it.
Hosting
Window chrome belongs to the desktop. What a window shows is any other actor. Each window
is a DesktopWindowHost in the desktop’s windowHosts slot, and the app
goes into that host’s content slot. accepts: LayoutActor lets any actor that can be laid
out be a window’s content, so the desktop hosts apps it does not import.
spawnActorWindow does all three steps: spawn a host, spawn the app into it and add the
window’s record to windows. The record starts closed, and openWindow animates it.
Commands
moveCursor moves both axes together with this.runtime.all. openWindow writes the
structural facts first: the window is open, frontmost and focused from the first frame of
its animation. Then a transition moves openProgress
inside the window list, so the list changes as one value each frame.
A scene
import { function createScene<TSceneExports, const I extends SceneInputs = Readonly<Record<never, SceneInput>>, const L extends SceneLayers = Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<I, L, TSceneExports>, options?: SceneDefinitionOptions<I, L>): SceneDefinition<I, L, TSceneExports>Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor, function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal } from "@motionactor/core";
import { class DesktopDesktop } from "./desktop";
class class TerminalTerminal extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Terminal.output: ActorFieldSignal<string>output = signal<string>(initial: string): ActorFieldSignal<string> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal("");
}
export const const compiled: CompiledScene<{
desktop: Desktop;
terminal: Terminal;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
desktop: Desktop;
terminal: Terminal;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
desktop: Desktop;
terminal: Terminal;
}>, options?: SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, Readonly<...>> | undefined): SceneDefinition<...>
Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene(function* (ctx: SceneContextctx) {
const const desktop: Desktopdesktop = ctx: SceneContextctx.CompileContext.spawn<Desktop>(definition: ActorClass<Desktop>, overrides?: {
measuredBox?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<...> | undefined;
} | undefined;
... 32 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Desktop (+1 overload)
spawn(class DesktopDesktop, {
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
} | undefined
layout: { width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
width: 1920, height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
height: 1080 },
});
const desktop: Desktopdesktop.Desktop.setDockItems(items: DesktopDockItem[]): voidsetDockItems([
{ id: stringid: "browser", label: stringlabel: "Browser" },
{ id: stringid: "terminal", label: stringlabel: "Terminal" },
]);
const const terminal: Terminalterminal = const desktop: Desktopdesktop.Desktop.spawnActorWindow<Terminal>(definition: ActorClass<Terminal>, window: DesktopActorWindowOptions): TerminalspawnActorWindow(class TerminalTerminal, {
windowId: stringwindowId: "terminal-window",
appId: stringappId: "terminal",
title: stringtitle: "Terminal",
x: numberx: 460,
y: numbery: 210,
width: numberwidth: 760,
height: numberheight: 420,
});
yield* const desktop: Desktopdesktop.Desktop.moveCursor({ x, y }: {
x: number;
y: number;
}, transition?: Duration): Generator<number, void, unknown>
moveCursor({ x: numberx: 960, y: numbery: 1030 }, 0.5);
yield* const desktop: Desktopdesktop.Desktop.openWindow(windowId: string, duration?: Duration): Generator<number, void, unknown>openWindow("terminal-window", 0.5);
const terminal: Terminalterminal.Terminal.output: ActorFieldSignal<string>output.LiveSignal<string>.set(value: string): voidSet value instantly at current cursor (no yield needed)set("$ pnpm test");
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { desktop: Desktopdesktop, terminal: Terminalterminal };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
desktop: Desktop;
terminal: Terminal;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
The terminal is an ordinary actor with its own state, written on the same timeline as the desktop’s commands. Nothing coordinates them beyond the scene’s single cursor.