fix(demos): orquesta — real sema engine for sound/haptic + correct channel framing

Two fixes:
- Sound + haptic now fire from the REAL EngineSemantic.emit (channels filtered by the
  mixer; `visual` excluded so no data-event-* conflict with the toggleable facets) —
  replacing the Web Audio / Vibration approximation. One real emit, the chosen channels.
- Correct the architecture framing (was conflating LAYER with CHANNEL): the channels are
  SEMANTIC (sema). Sema projects the `visual` channel (-> eidos materializes color ·
  motion · depth · forma) and executes sound + haptic. motion/depth/shape/color are NOT
  "eidos channels" — they are sema visual channel, made visible by eidos. Mixer now groups
  "canal visual · sema -> eidos" vs "canales runtime · sema"; dropped the misleading
  per-track eidos/sema tag. CHANNELS_SYNTHESIS reframed: 8 book expression channels -> 3
  sema runtime channels (visual / sound / haptic).

Verified: check 0 errors; browser — two mixer groups render, real engine.emit fires
without error, visual facets compose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 21057c398f
commit 898af6419a

@ -18,19 +18,26 @@ Todo canal —no solo motion— respeta el mismo eje doble:
El orden entre ambos lo fija `sequence` (`pre` / `coincident` / `post`). Eidos lee **los dos**: El orden entre ambos lo fija `sequence` (`pre` / `coincident` / `post`). Eidos lee **los dos**:
`presets` sobre `data-state`, `signatures` sobre `data-event-*`. `presets` sobre `data-state`, `signatures` sobre `data-event-*`.
## 2. Los 8 canales del libro → sistemas Eidos ## 2. Canales de expresión (libro) vs canales de sema (runtime)
| Canal (libro) | Sistema | RFC / motor | El libro tiene **8 canales de expresión** (dimensiones perceptuales). El framework los implementa
con los **canales de sema** (runtime), que son **3** — y aquí está la clave que evita la confusión
clásica:
| Canal de sema (runtime) | Cubre (expresión del libro) | Quién lo materializa |
|---|---|---| |---|---|---|
| tiempo + movimiento | **motion** | `arts/motion` + `eidos/lib/motion` (firma + presets) | | **visual** (sema proyecta `data-event-*`) | movimiento · presencia · profundidad · forma · color | **eidos** — CSS sobre `data-event-*` + `data-state` |
| presencia + profundidad | **depth** | `DEPTH_ENGINE_RFC` | | **sound** | sonido | sema (`chans/sound`) |
| **forma** | **shape** | `SHAPE_ENGINE_RFC` | | **haptic** | háptica | sema (`chans/haptic`) |
| color | **color** | `COLOR_ENGINE_RFC` |
| sonido | **sema · sound** | `sema/chans/sound` | > **No te confundas**: **motion · depth · shape · color NO son "canales de eidos"** — son el
| háptica | **sema · haptic** | `sema/chans/haptic` | > **canal `visual` de sema**, que sema proyecta y **eidos materializa**. Eidos es el
> *materializador* del canal visual, no el dueño de unos canales propios. Los RFCs de eidos
> (`COLOR/DEPTH/SHAPE_ENGINE`) describen *cómo* eidos materializa cada faceta visual — no canales
> independientes.
Más el **espacio** (`STRUCTURE_ENGINE_RFC`) — estructural, **solo-estado** (el escenario, no el Más el **espacio** (`STRUCTURE_ENGINE_RFC`) — **estructural, no de expresión**: no "ocurre", es el
suceso): no tiene momento-evento, y eso es deliberado. escenario (solo-estado).
## 3. Quién posee qué (la narrativa de capas) ## 3. Quién posee qué (la narrativa de capas)
@ -54,9 +61,9 @@ contact · press → scale 0.96 (motion)
+ vibración (sema haptic) + vibración (sema haptic)
``` ```
Un `engine.emit(...)` estampa `data-event-*` (los canales visuales reaccionan) **y** dispara Un `engine.emit(...)` estampa `data-event-*` (el canal **visual** reacciona — eidos lo materializa
sound + haptic — desde el **mismo** evento. No hay 6 sistemas coordinándose a mano; hay un en color/motion/depth/forma) **y** dispara sound + haptic — desde el **mismo** evento. No hay
suceso que se expresa en 6 dimensiones. sistemas coordinándose a mano; hay un suceso que se expresa por los canales de sema.
## 5. Builders runtime — el quinteto (jaula abierta) ## 5. Builders runtime — el quinteto (jaula abierta)
@ -69,13 +76,15 @@ escala authored:
## 6. Posición frente a referentes ## 6. Posición frente a referentes
Ningún referente reúne los canales bajo **un modelo único**. Material tiene morph de forma + Ningún referente reúne los canales bajo **un modelo semántico único**. Material tiene morph de
motion (ad-hoc, sin sonido/háptica integrados); Apple, continuidad + materiales (atado a forma + motion (ad-hoc, sin sonido/háptica integrados); Apple, continuidad + materiales (atado a
plataforma); todos, color. **Eidos los reúne** —los 6 perceptuales + el estructural— sobre el plataforma); todos, color. **La UIX los reúne** —el canal visual (sema → eidos) + sound + haptic
modelo de dos momentos, disparados desde un evento, con builders runtime y registro abierto. (sema)— sobre el modelo de dos momentos, disparados desde un evento, con builders runtime y
Eso es el framework, no la suma de sus partes. registro abierto. Eso es el framework, no la suma de sus partes.
## 7. Demo ## 7. Demo
`/temas/orquesta` — el mezclador: pulsa un control y silencia cada canal para oír su parte. Los `/temas/orquesta` — el mezclador: pulsa un control y silencia cada faceta para oír su parte. Las
4 visibles usan tokens reales; sound + haptic los aproxima el demo (en la app, motor sema). 4 facetas del **canal visual** se materializan con tokens de eidos (toggleables para poder
silenciarlas una a una); **sound + haptic** los dispara el motor sema real
(`EngineSemantic.emit`, canales filtrados). Un evento, los canales de sema.

@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
/** /**
* Capstone — "Diseñando lo que ocurre" made literal: ONE interaction firing many channels at * Capstone — "Diseñando lo que ocurre" made literal: ONE interaction firing sema's channels at
* once, with a mixer to mute each and watch them layer. * once, with a mixer to mute each and watch them layer.
* *
* The 4 visible channels (color · motion · depth · shape) use REAL eidos tokens, composed * The channels are SEMANTIC (sema). Sema executes `sound` + `haptic` (runtime) and projects the
* explicitly per-channel so each is independently mutable. Sound + haptic are fired with the * `visual` channel (via `data-event-*`), which it DELEGATES to eidos to materialize — into the
* Web Audio / Vibration APIs directly — in-app the sema engine emits them from the SAME event * book's visual facets: color · motion · depth · forma. So those four are NOT "eidos channels":
* (the canonical sound + haptic channels); here they're approximated so the mixer can mute them. * they are sema's visual channel, made visible by eidos. Here the visual facets are toggled as
* eidos token effects (so each facet is independently mutable); sound + haptic fire from the
* REAL `EngineSemantic.emit` (channels filtered by the mixer).
* *
* The point: an interaction is not color, nor motion, nor sound — it is one perceptual event * The point: an interaction is not color, nor motion, nor sound — it is one perceptual event
* across N channels, under the two-moment model. This is the book's thesis, playable. * across sema's channels, under the two-moment model. This is the book's thesis, playable.
*/ */
import '@/uix/eidos/index.css' import '@/uix/eidos/index.css'
import { EngineSemantic, type SemaChannelId } from '$uix/sema'
let theme = $state<'light' | 'dark'>('dark') let theme = $state<'light' | 'dark'>('dark')
$effect(() => { $effect(() => {
@ -25,16 +28,23 @@
}) })
type ChannelId = 'color' | 'motion' | 'depth' | 'shape' | 'sound' | 'haptic'; type ChannelId = 'color' | 'motion' | 'depth' | 'shape' | 'sound' | 'haptic';
const CHANNELS: { id: ChannelId; label: string; note: string; eidos: boolean }[] = [ type Facet = { id: ChannelId; label: string; note: string };
{ id: 'color', label: 'color', note: 'tinte de intención (→ affirm)', eidos: true }, // sema's channels. `visual` is ONE channel that eidos materializes — shown here as its facets,
{ id: 'motion', label: 'movimiento', note: 'escala bajo el dedo', eidos: true }, // each independently mutable. `sound` + `haptic` are sema's runtime channels.
{ id: 'depth', label: 'profundidad', note: 'la sombra recede', eidos: true }, const VISUAL_FACETS: Facet[] = [
{ id: 'shape', label: 'forma', note: 'la esquina se afirma', eidos: true }, { id: 'color', label: 'color', note: 'tinte de intención (→ affirm)' },
{ id: 'sound', label: 'sonido', note: 'un tick (motor sema)', eidos: false }, { id: 'motion', label: 'movimiento', note: 'escala bajo el dedo' },
{ id: 'haptic', label: 'háptica', note: 'una vibración (motor sema)', eidos: false } { id: 'depth', label: 'profundidad', note: 'la sombra recede' },
{ id: 'shape', label: 'forma', note: 'la esquina se afirma' }
]; ];
const RUNTIME_CHANNELS: Facet[] = [
{ id: 'sound', label: 'sonido', note: 'síntesis por intención' },
{ id: 'haptic', label: 'háptica', note: 'Vibration API' }
];
const ALL_CHANNELS = [...VISUAL_FACETS, ...RUNTIME_CHANNELS];
// Visible channels on by default; sound + haptic OFF (audible/tactile side effects — sema canon). // Visual facets on by default; sound + haptic OFF (audible/tactile side effects — sema canon).
let channels = $state<Record<ChannelId, boolean>>({ let channels = $state<Record<ChannelId, boolean>>({
color: true, color: true,
motion: true, motion: true,
@ -48,33 +58,28 @@
let pressCount = $state(0); let pressCount = $state(0);
let lastFired = $state<string[]>([]); let lastFired = $state<string[]>([]);
let timer: ReturnType<typeof setTimeout> | undefined; let timer: ReturnType<typeof setTimeout> | undefined;
let actx: AudioContext | undefined; let targetEl = $state<HTMLElement | null>(null);
let engine: EngineSemantic | undefined;
function tick(): void {
try {
actx ??= new (window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
const o = actx.createOscillator();
const g = actx.createGain();
o.type = 'triangle';
o.frequency.value = 660;
o.connect(g);
g.connect(actx.destination);
const t = actx.currentTime;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.16, t + 0.005);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.13);
o.start(t);
o.stop(t + 0.14);
} catch {
/* audio unavailable — no-op */
}
}
function fire(): void { function fire(): void {
pressCount += 1; pressCount += 1;
lastFired = CHANNELS.filter((c) => channels[c.id]).map((c) => c.label); lastFired = ALL_CHANNELS.filter((c) => channels[c.id]).map((c) => c.label);
if (channels.sound) tick(); // Real sema engine for sound + haptic — the canonical channels, from one emit. The 4 visible
if (channels.haptic) navigator.vibrate?.(18); // channels stay toggleable eidos classes (independently mutable), so we exclude 'visual' from
// the emit (no data-event-* stamp → no conflict). The mixer filters which channels fire.
const ch: SemaChannelId[] = [];
if (channels.sound) ch.push('sound');
if (channels.haptic) ch.push('haptic');
if (ch.length && targetEl) {
engine ??= new EngineSemantic({ visual: false, sound: true, haptic: true });
void engine.emit({
target: targetEl,
name: 'press',
family: 'contact',
intent: 'affirm',
channels: ch
});
}
pressed = true; pressed = true;
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(() => (pressed = false), 300); timer = setTimeout(() => (pressed = false), 300);
@ -106,15 +111,17 @@
<div class="sec-head"> <div class="sec-head">
<h2 class="sec-title">El mezclador</h2> <h2 class="sec-title">El mezclador</h2>
<p class="sec-note"> <p class="sec-note">
Pulsa el botón: los canales activos disparan desde <em>un</em> evento. Apaga la forma y la Pulsa: los canales de <em>sema</em> disparan desde <em>un</em> evento. Sema proyecta el
esquina deja de afirmarse; apaga la profundidad y la sombra no recede. Un instrumento canal <em>visual</em> —que eidos materializa en color · movimiento · profundidad · forma— y
menos, el mismo gesto. ejecuta <em>sonido</em> + <em>háptica</em>. Apaga la forma y la esquina deja de afirmarse;
apaga la profundidad y la sombra no recede. Un instrumento menos, el mismo gesto.
</p> </p>
</div> </div>
<div class="stage"> <div class="stage">
<div class="stage-target"> <div class="stage-target">
<button <button
bind:this={targetEl}
class="target" class="target"
class:ch-color={channels.color} class:ch-color={channels.color}
class:ch-motion={channels.motion} class:ch-motion={channels.motion}
@ -128,14 +135,24 @@
</div> </div>
<div class="mixer"> <div class="mixer">
{#each CHANNELS as c} <span class="mixer-group">canal <b>visual</b> · sema proyecta → eidos materializa</span>
{#each VISUAL_FACETS as c}
<button class="track" data-on={channels[c.id]} onclick={() => (channels[c.id] = !channels[c.id])}>
<span class="track-led" data-on={channels[c.id]}></span>
<span class="track-meta">
<span class="track-label">{c.label}</span>
<span class="track-note">{c.note}</span>
</span>
</button>
{/each}
<span class="mixer-group">canales <b>runtime</b> · sema ejecuta</span>
{#each RUNTIME_CHANNELS as c}
<button class="track" data-on={channels[c.id]} onclick={() => (channels[c.id] = !channels[c.id])}> <button class="track" data-on={channels[c.id]} onclick={() => (channels[c.id] = !channels[c.id])}>
<span class="track-led" data-on={channels[c.id]}></span> <span class="track-led" data-on={channels[c.id]}></span>
<span class="track-meta"> <span class="track-meta">
<span class="track-label">{c.label}</span> <span class="track-label">{c.label}</span>
<span class="track-note">{c.note}</span> <span class="track-note">{c.note}</span>
</span> </span>
<span class="track-tag" data-layer={c.eidos ? 'eidos' : 'sema'}>{c.eidos ? 'eidos' : 'sema'}</span>
</button> </button>
{/each} {/each}
</div> </div>
@ -175,13 +192,14 @@
<footer class="footnote"> <footer class="footnote">
<p> <p>
<strong>Esto es el framework, no seis sistemas sueltos.</strong> Cada canal tiene su motor y <strong>Todo es sema; los canales son semánticos.</strong> Un evento sema reparte la firma por
su demo (<code>/temas/{'{'}color · tipografia · profundidad · forma · estructura{'}'}</code>), sus canales: el canal <strong>visual</strong> —que sema proyecta vía <code>data-event-*</code>
pero la tesis del libro es que <em>componen</em>: un evento sema reparte la firma por y <strong>delega en eidos</strong> para materializarlo en color · movimiento · profundidad ·
motion + depth + shape + color (visual) y sound + haptic (sema), bajo el modelo de dos forma— y los canales <strong>sound</strong> + <strong>haptic</strong> que sema ejecuta. No son
momentos. Aquí los cuatro visibles usan tokens reales de eidos; sonido y háptica los "canales de eidos": son el canal visual de sema, hecho visible por eidos. Aquí los disparamos
aproxima el demo (en la app los emite el motor sema desde el mismo evento). Ningún referente todos desde un <code>EngineSemantic.emit</code> real (sonido + háptica) + los tokens de eidos
reúne los seis canales sobre un modelo único. para el visual, bajo el modelo de dos momentos. Ningún referente reúne los canales sobre un
modelo semántico único.
</p> </p>
</footer> </footer>
</div> </div>
@ -259,9 +277,9 @@
.track-meta { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; min-inline-size: 0; } .track-meta { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; min-inline-size: 0; }
.track-label { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-content-primary); } .track-label { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-content-primary); }
.track-note { font-size: var(--font-size-xs); color: var(--color-content-muted); } .track-note { font-size: var(--font-size-xs); color: var(--color-content-muted); }
.track-tag { font-family: var(--font-family-mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.15rem 0.45rem; border-radius: 4px; } .mixer-group { font-family: var(--font-family-mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-content-muted); margin-block-start: 0.5rem; padding-inline: 0.2rem; }
.track-tag[data-layer='eidos'] { background: color-mix(in oklab, #12a594 22%, transparent); color: #2dd4bf; } .mixer-group:first-child { margin-block-start: 0; }
.track-tag[data-layer='sema'] { background: color-mix(in oklab, var(--color-primary-solid) 24%, transparent); color: var(--color-primary-text, #c4b5fd); } .mixer-group b { color: var(--color-content-secondary); font-weight: 600; }
.trace { margin-block-start: 2rem; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); padding-block-start: 1.25rem; border-block-start: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); } .trace { margin-block-start: 2rem; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); padding-block-start: 1.25rem; border-block-start: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); }
.trace-k { color: var(--color-content-muted); text-transform: uppercase; letter-spacing: 0.06em; } .trace-k { color: var(--color-content-muted); text-transform: uppercase; letter-spacing: 0.06em; }

Loading…
Cancel
Save

Powered by TurnKey Linux.