You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
7.6 KiB
304 lines
7.6 KiB
/**
|
|
* SEMA_MAP — typed perceptual map.
|
|
*
|
|
* For each canonical family declares a `base` set of real runtime-channel
|
|
* signatures (`sound`, `haptic`) plus the `activeChannels` list stating which
|
|
* channels participate. Visual concerns (motion, color, presence) live in
|
|
* Eidos recipes and react to `data-event-*` attrs projected by VisualChannel.
|
|
*
|
|
* Channel registry — `SemaChannelSignatures` — is OPEN via TypeScript
|
|
* declaration merging. Apps can extend it with their own channels (e.g.
|
|
* `a11y`, `voice`) and the override / cascade types automatically include
|
|
* the new entries. The canonical built-ins declared here are the non-visual
|
|
* runtime channels the framework ships with.
|
|
*
|
|
* Numeric deltas without an `op` are added to the base; strings / booleans /
|
|
* arrays replace the base; nested records merge recursively. Use `{ op:
|
|
* 'replace', value }` when a number must overwrite (e.g. hue), `{ op:
|
|
* 'multiply', factor }` for proportional scaling, or `{ op: 'add', value }`
|
|
* for explicit add semantics.
|
|
*/
|
|
|
|
import type { Intent } from '../intent';
|
|
import type { SemaFamily } from './types';
|
|
import type { SemaDurationSpec } from './durations';
|
|
import type {
|
|
DeltaValue,
|
|
SemaChannelId,
|
|
SemaChannelSignatures,
|
|
SemaSignatureOverride
|
|
} from './channels';
|
|
|
|
// ── Map shape ──────────────────────────────────────────────────────────────
|
|
|
|
export interface FamilyMapEntry {
|
|
/** Per-channel base signatures. Channels not present default to undefined. */
|
|
base: Partial<SemaChannelSignatures>;
|
|
/** Channels participating in the family's signal. */
|
|
activeChannels: readonly SemaChannelId[];
|
|
/**
|
|
* Perceptual hold — how long `data-event-*` tokens live in the DOM
|
|
* before the engine cleans them up. SEMANTIC timing (the duration
|
|
* of the perceived signal), independent of visual animation timing.
|
|
*
|
|
* Resolution chain (highest priority first):
|
|
* 1. `signal.hold` — explicit per-call override.
|
|
* 2. `family.hold` — this field.
|
|
* 3. VisualChannel default (`brief` = 240 ms).
|
|
*
|
|
* Phase 4 of the codex refactor (`refactorizacion_codex.md`) lifted
|
|
* hold out of visual motion so those concerns can move to Eidos without
|
|
* losing the semantic timing budget.
|
|
*/
|
|
hold?: SemaDurationSpec;
|
|
}
|
|
|
|
export interface IntentMapEntry {
|
|
deltas: Partial<Record<SemaChannelId, Record<string, DeltaValue>>>;
|
|
}
|
|
|
|
/**
|
|
* Per-component sema declaration — sibling to `Morfo`. Lives in
|
|
* `src/uix/sema/components/{name}.ts` alongside the component's morfo,
|
|
* soma and eidos counterparts.
|
|
*
|
|
* Naming: `dialogSema: Sema` matches the framework convention
|
|
* (`dialogMorfo: Morfo`).
|
|
*
|
|
* The engine flattens all `Sema` declarations into the cascade BEFORE
|
|
* app-level rules — so the framework ships sensible defaults and the
|
|
* app overrides them via `engineOpts.overrides.cascade`.
|
|
*
|
|
* `preloadSamples` are passed to `SoundChannel.preloadSamples()` at boot
|
|
* so the WAVs are decoded before the first emit.
|
|
*
|
|
* Forward reference: `SemaCascadeRule` is declared in `resolver.ts` to
|
|
* keep DOM-matching code colocated with the resolver. We use a structural
|
|
* type here to avoid a circular import.
|
|
*/
|
|
export interface Sema {
|
|
name: string;
|
|
cascade: readonly {
|
|
selector: string;
|
|
priority?: number;
|
|
channels?: readonly SemaChannelId[];
|
|
sound?: SemaSignatureOverride['sound'];
|
|
haptic?: SemaSignatureOverride['haptic'];
|
|
}[];
|
|
preloadSamples?: readonly string[];
|
|
}
|
|
|
|
export interface SemaMap {
|
|
readonly version: string;
|
|
readonly families: Record<SemaFamily, FamilyMapEntry>;
|
|
readonly intents: Record<Intent, IntentMapEntry>;
|
|
}
|
|
|
|
// ── The map ────────────────────────────────────────────────────────────────
|
|
|
|
export const SEMA_MAP: SemaMap = {
|
|
version: '0.8.0',
|
|
families: {
|
|
contact: {
|
|
base: {
|
|
sound: {
|
|
pitch: 800,
|
|
centroid: 2000,
|
|
roughness: 0.1,
|
|
attack: 4,
|
|
decay: 40,
|
|
duration: 60,
|
|
contour: 'flat',
|
|
gain: 0.25
|
|
},
|
|
haptic: {
|
|
kind: 'tick',
|
|
intensity: 0.3,
|
|
duration: 8,
|
|
delay: 0
|
|
}
|
|
},
|
|
activeChannels: ['sound', 'haptic'],
|
|
hold: 'glimpse'
|
|
},
|
|
commit: {
|
|
base: {
|
|
sound: {
|
|
pitch: 700,
|
|
centroid: 1800,
|
|
roughness: 0.1,
|
|
attack: 8,
|
|
decay: 120,
|
|
duration: 100,
|
|
contour: 'flat',
|
|
gain: 0.3
|
|
},
|
|
haptic: {
|
|
kind: 'tap',
|
|
intensity: 0.5,
|
|
duration: 20,
|
|
delay: 60
|
|
}
|
|
},
|
|
activeChannels: ['sound', 'haptic'],
|
|
hold: 'brief'
|
|
},
|
|
signal: {
|
|
base: {
|
|
sound: {
|
|
pitch: 900,
|
|
centroid: 2400,
|
|
roughness: 0.3,
|
|
attack: 3,
|
|
decay: 150,
|
|
duration: 180,
|
|
contour: 'arc',
|
|
gain: 0.4
|
|
},
|
|
haptic: {
|
|
kind: 'pulse',
|
|
intensity: 0.7,
|
|
duration: 40,
|
|
delay: 0
|
|
}
|
|
},
|
|
activeChannels: ['sound', 'haptic'],
|
|
hold: 'noticed'
|
|
},
|
|
emerge: {
|
|
base: {
|
|
sound: {
|
|
pitch: 600,
|
|
centroid: 1500,
|
|
roughness: 0.05,
|
|
attack: 12,
|
|
decay: 200,
|
|
duration: 150,
|
|
contour: 'ascending',
|
|
gain: 0.2
|
|
}
|
|
},
|
|
activeChannels: ['sound'],
|
|
hold: 'brief'
|
|
},
|
|
shift: {
|
|
// Shift = cambio de marco operativo (modal bloqueante, navegación,
|
|
// cambio de régimen). Se diferencia de emerge en que reorganiza
|
|
// los planos: backdrop pesado y shadow más profundo comunican
|
|
// "cruce de umbral", no solo "aparición". Sound es más sutil que
|
|
// emerge para no competir con el signal que el marco pueda
|
|
// contener (regla del libro: el marco no absorbe el mensaje).
|
|
base: {
|
|
sound: {
|
|
pitch: 500,
|
|
centroid: 1400,
|
|
roughness: 0.05,
|
|
attack: 14,
|
|
decay: 220,
|
|
duration: 160,
|
|
contour: 'ascending',
|
|
gain: 0.18
|
|
}
|
|
},
|
|
activeChannels: ['sound'],
|
|
hold: 'noticed'
|
|
},
|
|
handle: {
|
|
base: {
|
|
haptic: {
|
|
kind: 'tick',
|
|
intensity: 0.2,
|
|
duration: 6,
|
|
delay: 0
|
|
}
|
|
},
|
|
activeChannels: ['haptic'],
|
|
hold: 'brief'
|
|
},
|
|
sustain: {
|
|
base: {},
|
|
activeChannels: [],
|
|
hold: 'noticed'
|
|
},
|
|
// Delegate = reparto de iniciativa entre usuario y sistema (libro
|
|
// cap. 29). Estructural; no carga intent por sí mismo. Mantiene perfil
|
|
// bajo en sonido/háptica — la lectura se apoya en presencia + texto
|
|
// + sustain. Los canales fuertes aparecen cuando delegate se compone
|
|
// con signal (revisión, alerta) o commit (apply).
|
|
delegate: {
|
|
base: {},
|
|
activeChannels: [],
|
|
hold: 'noticed'
|
|
}
|
|
},
|
|
intents: {
|
|
threat: {
|
|
deltas: {
|
|
sound: {
|
|
pitch: -200,
|
|
roughness: 0.4,
|
|
contour: 'descending',
|
|
gain: 0.1
|
|
},
|
|
haptic: {
|
|
kind: 'error',
|
|
pattern: [40, 60, 40, 60, 40]
|
|
}
|
|
}
|
|
},
|
|
risk: {
|
|
deltas: {
|
|
sound: {
|
|
pitch: -100,
|
|
roughness: 0.2
|
|
},
|
|
haptic: {
|
|
kind: 'warning',
|
|
pattern: [30, 50, 30]
|
|
}
|
|
}
|
|
},
|
|
neutral: {
|
|
deltas: {}
|
|
},
|
|
affirm: {
|
|
deltas: {
|
|
sound: { pitch: 100 },
|
|
haptic: {
|
|
kind: 'success'
|
|
}
|
|
}
|
|
},
|
|
fulfill: {
|
|
deltas: {
|
|
sound: {
|
|
pitch: 300,
|
|
contour: 'ascending',
|
|
gain: 0.05
|
|
},
|
|
haptic: {
|
|
kind: 'success',
|
|
intensity: { op: 'add', value: 0.2 }
|
|
}
|
|
}
|
|
},
|
|
loss: {
|
|
// Loss = consecuencia consumada (negativo + baja activación,
|
|
// posterior). Se diferencia de threat (anterior, alta activación,
|
|
// convoca acción): loss registra, no convoca. Por eso desciende
|
|
// auditivamente y cae en haptic, sin arrastrar visual concerns.
|
|
deltas: {
|
|
sound: {
|
|
pitch: -150,
|
|
contour: 'descending'
|
|
},
|
|
haptic: {
|
|
kind: 'thud',
|
|
intensity: 0.8,
|
|
duration: 80
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|