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.
svelte-kit-vice/src/uix/sema/sema-map.ts

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
}
}
}
}
};

Powered by TurnKey Linux.