FlowStates

Declarative animation system handling smooth, engine-driven transitions between public property values without manual tweening math.
Published: 7/10/2026

FlowStates provide a high-level declarative mechanism for managing smooth transitions between numeric or color properties in Aircada.

Instead of writing custom lerp, tween, or frame-tick algorithms, developers define target states, easing curves, and durations in a class configuration and let the underlying engine animate property values automatically.

API & Type Definitions#

Strict interfaces for defining states and easing types.
typescript
type EasingType = "linear" | "easeIn" | "easeOut" | "easeInOut" | "spring" | "easeOutBack" | string;

export interface FlowStateConfig<T> {
    duration?: number; // In seconds
    ease?: EasingType;
    properties: Partial<T>; // Must strictly map to @Property keys
    onEvent?: string | string[]; // Global events that trigger this state
}

export type FlowStateMap<T> = Record<string, FlowStateConfig<T>>;

Syntax: Mapping & Events#

FlowStates map local property targets to global event triggers, providing a bridge between the event bus and the visual state.

Property Mapping

The properties object inside a state configuration must contain keys that match your Operator's @Property fields. Values provided here become the animation targets.

Event Listening (onEvent)

By specifying an onEvent, you tell the engine to automatically trigger a transition when a specific signal is detected on the global message bus.

Event Emitting

While states define the visual goal, @Output properties are used to 'emit' signals when transitions occur, allowing the UI to react to state changes.

Code Implementation Template#

flawless structural template for implementing FlowStates in an Operator.
typescript
import { 
    Operator, FloatProperty, Input, Output, Context, States, 
    Setup, Dispose, AirEvent, FlowStateMap, AircadaContext 
} from "@aircada/spec";
import { Events, EventPayloads } from "./registry.generated"; 

@Operator({ type: "smart_door", name: "Smart Door" })
export class SmartDoorOperator {
    
    @Context()
    air!: AircadaContext;

    @FloatProperty()
    doorAngle: number = 0.0;

    @FloatProperty()
    doorOpacity: number = 1.0;

    // 1. DECLARE STATES: Use @States to define goal property values and easing
    @States()
    states = {
        closed: {
            duration: 0.5,
            ease: "easeOut",
            properties: { doorAngle: 0.0, doorOpacity: 1.0 },
            onEvent: [Events.RESET_CONFIG] 
        },
        open: {
            duration: 1.2,
            ease: "spring",
            properties: { doorAngle: 90.0, doorOpacity: 1.0 },
            onEvent: Events.OPEN_DOORS 
        }
    } satisfies FlowStateMap<SmartDoorOperator>;

    @Output({ name: "On Door Opened", emits: Events.DOOR_OPENED })
    onDoorOpened = new AirEvent<EventPayloads[typeof Events.DOOR_OPENED]>();

    private unsubDoorOpened?: () => void;

    @Setup()
    setup() {
        this.unsubDoorOpened = this.onDoorOpened.addListener(() => {
            console.log(`[Local Event Fired] Door reached open state.`);
        });
    }

    @Input({ name: "Force Open" })
    forceOpen() {
        // 2. TRIGGER TRANSITION: Use the flows.transitionTo helper to start the animation
        this.air.flows.transitionTo(this, "open"); 
        this.onDoorOpened.invoke();
    }

    @Dispose()
    dispose() {
        if (this.unsubDoorOpened) {
            this.unsubDoorOpened();
        }
    }
}