Signal engine for living interfaces

Latido turns signals into perceptual interfaces.

Connect changing data to behavior so users feel what a system is doing before they read a number.

source signal interpretation behavior target

Not a renderer.

Not a framework.

Not only audio.

A signal engine for living interfaces.

Quick Start

One source, one signal, one binding.

Use @latido/web for common browser projects. It reexports core plus DOM, audio, targets, events, network and WAAPI plugins.

source
where values enter Latido
signal
a named stream such as audio.energy
transform
normalization, smoothing, thresholds or custom mapping
binding
how a value changes a target
target
DOM, Canvas, PixiJS, Three.js, WAAPI or your object

Signal names come from sources. The audio plugin registers names like audio.energy. Custom sources use the name you pass to latido.source(name, reader).

import { createLatido, dom, audio, signals } from "@latido/web"

const latido = createLatido()
  .use(dom())
  .use(audio({ element: audioEl }))

latido.signal(signals.audio.energy)
  .bindCSSVar(document.body, "--energy")

latido.start()

Concepts

Raw data is not enough.

Latido helps you turn values into behavior, and behavior into perception.

API / audio / events / sensors
        ↓
source → signal → transform → interpretation → binding → target

Source

A source reads a value on every Latido tick and owns its signal name. Plugins register source names for you.

Signal

A signal subscribes to a source by name. If no source is registered with that name, the signal reads as 0.

Transform

Transforms shape raw values into usable ranges: normalize, clamp, smooth, decay, threshold, pulse or map.

Binding

A binding applies the transformed value to a target. DOM bindings can write CSS variables, styles, classes and attributes.

Target

A target is what moves, breathes or reacts: DOM, Web Animations, PixiJS, Canvas, Three.js or plain objects.

Interpretation Layer

Interpretation combines several signals into a state such as calm, stressed, thriving or recovering.

How do I know the signal name?

Each plugin exports its own catalog: @latido/audio/signals, @latido/events/signals. @latido/web aggregates them as signals.audio.energy. It gives editors something to autocomplete. Use strings directly for custom sources and network names.

audio.energyregistered by audio()
audio.beatregistered by audio()
event.click.pulseregistered by events()
event.pointer.progressXregistered by events()
feed.energyregistered from a network source config
system.healthScoreregistered by your own latido.source()

Simple signal pipes

Most mappings do not need custom pipeline code.

signalPipe helpers are normal functions that apply common transform chains. Use them in binding lists, adapters or small integration layers.

signalPipe.normalized(min, max, smooth)
normalize input range, clamp to 0..1, then smooth
signalPipe.clamped(smooth)
clamp to 0..1, then smooth
signalPipe.smooth(amount)
smooth only
signalPipe.zero()
always emit 0
signalPipe.constant(value)
always emit a fixed value
signalPipe.beat(threshold, pulse, decay)
threshold, pulse, then decay
signalPipe.raw()
pass the signal through unchanged
import { createLatido, signalPipe } from "@latido/core"

bindSystemSignals(latido, view, [
  {
    system: "weather",
    source: "weather.temperature",
    target: "tone",
    pipe: signalPipe.normalized(-4, 36, 0.08)
  },
  {
    system: "weather",
    source: "weather.visualBeat",
    target: "beat",
    pipe: signalPipe.beat(0.2, 280, 0.1)
  }
])

Custom pipes

Helper pipes are just functions. Advanced users can still write their own pipeline when they need nonlinear mapping, custom compression or domain-specific behavior.

pipe: source => source
  .normalize(0, 100)
  .map(value => value * value)
  .smooth(0.1)

Rule utilities

Small building blocks for custom interpretation layers.

@latido/core includes pure helpers for low-level rule and scoring systems. They do not define market, weather or health behavior; your application owns those rules.

clamp(value, min, max)
restrict a number to a range
read(values, name)
read a finite number or return 0
readOr(values, name, fallback)
read a finite number with fallback
previousValues(history)
read the latest value snapshot from history
scoreFromFactors(initial, factors, context)
combine named scoring factors
collectRisks(groups, context)
collect labels from matching grouped rules
createBaseHealth(system, score, risks, metrics)
create a normalized scored-state descriptor
import {
  clamp,
  collectRisks,
  createBaseHealth,
  read,
  scoreFromFactors
} from "@latido/core"

const factors = [
  [
    "windPenalty",
    ({ wind }) => -clamp((wind - 20) / 35) * 0.26
  ],
  [
    "rainPenalty",
    ({ rain }) => -clamp(rain / 8) * 0.2
  ]
]

const risks = [
  {
    name: "weather",
    rules: [
      ["high wind", ({ wind }) => wind > 30]
    ]
  }
]

const context = {
  wind: read(values, "weather.wind"),
  rain: read(values, "weather.rain")
}
const score = scoreFromFactors(0.98, factors, context)

return createBaseHealth(
  "weather",
  score,
  collectRisks(risks, context),
  context
)

Health interpreter

Temporal state interpretation without domain rules.

createHealthInterpreter handles history, trend detection, hysteresis, recovering and unstable transitions. Your app still owns the base score, risks and reason text.

Use it when a latest value is not enough and you need a narrative state over time: stable, stressed, sick, recovering, healthy or custom names.

deriveBase(system, values, history)
returns score, base state, risks and metrics
reasonFor(state, base, temporal)
keeps domain-specific explanations in your app
scoreThresholds
configures candidate states from 0..1 scores
minStateDuration
prevents small fluctuations from changing state too quickly
import { createHealthInterpreter } from "@latido/core"

const interpretHealth = createHealthInterpreter({
  deriveBase(system, values, history) {
    return deriveWeatherBase(values, history)
  },
  reasonFor(state, base, temporal) {
    if (state === "recovering") {
      return "Recovering after sustained weather stress"
    }

    if (base.domainRisks.includes("high wind")) {
      return "High wind and precipitation"
    }

    return "Stable conditions"
  }
})

const health = interpretHealth("weather", values, history)

Packages

Use the web bundle first. Split packages when you need control.

Examples

Product-style demos, not test pages.

Each example focuses on behavior first. Numbers are useful, but perception is the interface.

Recipes

Copy, paste, adapt.

API Reference

The small surface area.

Roadmap

Adoption first.

0.5.x

Documentation polish, more recipes, stronger examples, clearer onboarding and stable package guidance.

0.6.x

Production examples for network-driven signals and more real-world interpretation layers.

Later

More adapters, package-level guides, advanced renderer examples and higher-level perceptual components.