feat(demos): capstone — /temas/orquesta + CHANNELS_SYNTHESIS (one event, N channels)

The book thesis, playable: ONE press fires the firma across channels, with a mixer to
mute each and watch them layer. The 4 visible channels (color · motion · depth · shape)
compose from one event with REAL eidos tokens — verified: press -> primary->affirm-solid
+ scale(0.93) + shadow overlay->raised + --shape-smoothing 2->3.4. Sound + haptic via
Web Audio / Vibration (in-app the sema engine emits them from the same event). + the
two-moment model section.

CHANNELS_SYNTHESIS.md ties the five channel RFCs: two moments x N channels from one
event, the layer ownership (sema runs sound/haptic + projects data-event-*; eidos
materializes the visual channels), the cross-modal firma, the runtime-builder quintet.

Verified: check 0 errors. Browser: all four visible channels compose on press.

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

@ -0,0 +1,81 @@
# Los canales como un solo sistema — síntesis ("Diseñando lo que ocurre")
> **Capstone** de los RFCs por canal. La tesis del libro *Diseñando lo que ocurre*: una
> interacción **no es** color, ni movimiento, ni sonido — es **un solo suceso perceptual
> repartido en N canales**, bajo el modelo de **dos momentos**. Esto ata
> `COLOR_ENGINE_RFC` · `TYPOGRAPHY_ENGINE_RFC` · `DEPTH_ENGINE_RFC` · `SHAPE_ENGINE_RFC` ·
> `STRUCTURE_ENGINE_RFC`. Demo vivo: **`/temas/orquesta`** (el mezclador).
## 1. El modelo de dos momentos (transversal)
Todo canal —no solo motion— respeta el mismo eje doble:
| Momento | Atributo | Qué es | Quién |
|---|---|---|---|
| **estado** | `data-state` | lo que el elemento **es** (persistente): color de reposo, plano, forma, posición | soma / morfo |
| **evento** | `data-event-*` | lo que le **ocurre** (transitorio, durante el `hold`): el tinte que pulsa, la sombra que recede, la esquina que se afirma, el tick, la vibración | sema (proyecta) → eidos (materializa visual) |
El orden entre ambos lo fija `sequence` (`pre` / `coincident` / `post`). Eidos lee **los dos**:
`presets` sobre `data-state`, `signatures` sobre `data-event-*`.
## 2. Los 8 canales del libro → sistemas Eidos
| Canal (libro) | Sistema | RFC / motor |
|---|---|---|
| tiempo + movimiento | **motion** | `arts/motion` + `eidos/lib/motion` (firma + presets) |
| presencia + profundidad | **depth** | `DEPTH_ENGINE_RFC` |
| **forma** | **shape** | `SHAPE_ENGINE_RFC` |
| color | **color** | `COLOR_ENGINE_RFC` |
| sonido | **sema · sound** | `sema/chans/sound` |
| háptica | **sema · haptic** | `sema/chans/haptic` |
Más el **espacio** (`STRUCTURE_ENGINE_RFC`) — estructural, **solo-estado** (el escenario, no el
suceso): no tiene momento-evento, y eso es deliberado.
## 3. Quién posee qué (la narrativa de capas)
- **Sema** ejecuta **2 canales runtime** (`sound` + `haptic`) y **proyecta el meta-canal visual**
(estampa `data-event-*` durante el hold). El motor no conoce DOM/CSS.
- **Eidos** materializa los **canales visuales** (motion · depth · shape · color) reaccionando en
CSS a `data-event-*` + `data-state`. Es el único dueño visual.
(Narrativa canónica completa en `CLAUDE.md` → "Sema: open channel registry".)
## 4. La composición — un evento, N canales
La **firma** (`BUILTIN_SIGNATURES` + `BUILTIN_KEYFRAMES`) es **cross-modal**: un solo keyframe
lleva varias modalidades. Ejemplo real (`press-squeeze`, familia `contact`):
```
contact · press → scale 0.96 (motion)
+ box-shadow → plano (depth recede)
+ --shape-smoothing 2→3 (shape: la esquina se afirma)
+ tick (sema sound)
+ vibración (sema haptic)
```
Un `engine.emit(...)` estampa `data-event-*` (los canales visuales reaccionan) **y** dispara
sound + haptic — desde el **mismo** evento. No hay 6 sistemas coordinándose a mano; hay un
suceso que se expresa en 6 dimensiones.
## 5. Builders runtime — el quinteto (jaula abierta)
Cada eje retunable en runtime, mismo patrón (semilla → bloque gestionado), opt-in sobre la
escala authored:
`applyColorScheme` · `applyTypeScale` · `applyDepth` · `applyShape` · `applySpacing`
(Futuro: `applyTheme(seed)` que componga varios desde una semilla compartida.)
## 6. Posición frente a referentes
Ningún referente reúne los canales bajo **un modelo único**. Material tiene morph de 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
modelo de dos momentos, disparados desde un evento, con builders runtime y registro abierto.
Eso es el framework, no la suma de sus partes.
## 7. Demo
`/temas/orquesta` — el mezclador: pulsa un control y silencia cada canal para oír su parte. Los
4 visibles usan tokens reales; sound + haptic los aproxima el demo (en la app, motor sema).

@ -0,0 +1,281 @@
<script lang="ts">
/**
* Capstone — "Diseñando lo que ocurre" made literal: ONE interaction firing many channels at
* once, with a mixer to mute each and watch them layer.
*
* The 4 visible channels (color · motion · depth · shape) use REAL eidos tokens, composed
* explicitly per-channel so each is independently mutable. Sound + haptic are fired with the
* Web Audio / Vibration APIs directly — in-app the sema engine emits them from the SAME event
* (the canonical sound + haptic channels); here they're approximated so the mixer can mute them.
*
* 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.
*/
import '@/uix/eidos/index.css'
let theme = $state<'light' | 'dark'>('dark')
$effect(() => {
const el = document.documentElement
const prev = el.getAttribute('data-theme')
el.setAttribute('data-theme', theme)
return () => {
if (prev === null) el.removeAttribute('data-theme')
else el.setAttribute('data-theme', prev)
}
})
type ChannelId = 'color' | 'motion' | 'depth' | 'shape' | 'sound' | 'haptic';
const CHANNELS: { id: ChannelId; label: string; note: string; eidos: boolean }[] = [
{ id: 'color', label: 'color', note: 'tinte de intención (→ affirm)', eidos: true },
{ id: 'motion', label: 'movimiento', note: 'escala bajo el dedo', eidos: true },
{ id: 'depth', label: 'profundidad', note: 'la sombra recede', eidos: true },
{ id: 'shape', label: 'forma', note: 'la esquina se afirma', eidos: true },
{ id: 'sound', label: 'sonido', note: 'un tick (motor sema)', eidos: false },
{ id: 'haptic', label: 'háptica', note: 'una vibración (motor sema)', eidos: false }
];
// Visible channels on by default; sound + haptic OFF (audible/tactile side effects — sema canon).
let channels = $state<Record<ChannelId, boolean>>({
color: true,
motion: true,
depth: true,
shape: true,
sound: false,
haptic: false
});
let pressed = $state(false);
let pressCount = $state(0);
let lastFired = $state<string[]>([]);
let timer: ReturnType<typeof setTimeout> | undefined;
let actx: AudioContext | 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 {
pressCount += 1;
lastFired = CHANNELS.filter((c) => channels[c.id]).map((c) => c.label);
if (channels.sound) tick();
if (channels.haptic) navigator.vibrate?.(18);
pressed = true;
clearTimeout(timer);
timer = setTimeout(() => (pressed = false), 300);
}
const activeCount = $derived(Object.values(channels).filter(Boolean).length);
</script>
<svelte:head><title>La orquesta · Eidos</title></svelte:head>
<div class="root" data-theme={theme}>
<header class="masthead">
<div class="mast-row">
<p class="kicker">Eidos · diseñando lo que ocurre</p>
<button class="theme-toggle" onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '◐ claro' : '◑ oscuro'}
</button>
</div>
<h1 class="mast-title">Un evento, <em>seis canales</em></h1>
<p class="mast-lede">
Una interacción no es color, ni movimiento, ni sonido — es <strong>un solo suceso
perceptual</strong> repartido en varios canales, a la vez. Hemos construido los seis. Aquí
tocan juntos: pulsa, y <strong>silencia</strong> cada uno para oír su parte.
</p>
</header>
<!-- ── La orquesta ──────────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">El mezclador</h2>
<p class="sec-note">
Pulsa el botón: los canales activos disparan desde <em>un</em> evento. Apaga la forma y la
esquina deja de afirmarse; apaga la profundidad y la sombra no recede. Un instrumento
menos, el mismo gesto.
</p>
</div>
<div class="stage">
<div class="stage-target">
<button
class="target"
class:ch-color={channels.color}
class:ch-motion={channels.motion}
class:ch-depth={channels.depth}
class:ch-shape={channels.shape}
data-shape={channels.shape ? 'continuous' : undefined}
data-pressed={pressed}
onpointerdown={fire}
>Confirmar</button>
<span class="stage-hint">pulsa →</span>
</div>
<div class="mixer">
{#each CHANNELS 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>
<span class="track-tag" data-layer={c.eidos ? 'eidos' : 'sema'}>{c.eidos ? 'eidos' : 'sema'}</span>
</button>
{/each}
</div>
</div>
<div class="trace">
<span class="trace-k">último evento</span>
<span>1 press → {lastFired.length} canal(es){lastFired.length ? `: ${lastFired.join(' · ')}` : ' — pulsa'}</span>
<span class="trace-sep">·</span>
<span class="trace-k">activos</span><span>{activeCount}/6</span>
<span class="trace-sep">·</span>
<span class="trace-k">presses</span><span>{pressCount}</span>
</div>
</section>
<!-- ── Los dos momentos ─────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">Dos momentos, un solo modelo</h2>
<p class="sec-note">
Todo canal respeta el mismo rigor: lo que el elemento <em>es</em> (estado, persistente) vs
lo que le <em>ocurre</em> (evento, durante el hold). El mezclador de arriba dispara el
momento-<em>evento</em>; el reposo es el momento-<em>estado</em>.
</p>
</div>
<div class="moments">
<div class="moment">
<span class="moment-h">estado · <code>data-state</code></span>
<p class="moment-p">color de reposo · plano de profundidad · forma en reposo · posición. Persistente — soma/morfo.</p>
</div>
<div class="moment moment-event">
<span class="moment-h">evento · <code>data-event-*</code></span>
<p class="moment-p">el tinte que pulsa · la sombra que recede · la esquina que se afirma · el tick · la vibración. Transitorio — sema, durante el hold.</p>
</div>
</div>
</section>
<footer class="footnote">
<p>
<strong>Esto es el framework, no seis sistemas sueltos.</strong> Cada canal tiene su motor y
su demo (<code>/temas/{'{'}color · tipografia · profundidad · forma · estructura{'}'}</code>),
pero la tesis del libro es que <em>componen</em>: un evento sema reparte la firma por
motion + depth + shape + color (visual) y sound + haptic (sema), bajo el modelo de dos
momentos. Aquí los cuatro visibles usan tokens reales de eidos; sonido y háptica los
aproxima el demo (en la app los emite el motor sema desde el mismo evento). Ningún referente
reúne los seis canales sobre un modelo único.
</p>
</footer>
</div>
<style>
.root {
min-block-size: 100vh;
background:
radial-gradient(130% 90% at 50% -15%, var(--color-surface-raised), transparent 55%),
var(--color-surface-default);
color: var(--color-content-primary);
font-family: var(--font-family-primary);
padding: clamp(1.5rem, 5vw, 5rem) clamp(1.25rem, 5vw, 6rem) 6rem;
--rule: var(--color-border-default);
}
.masthead { max-inline-size: 56rem; margin-block-end: clamp(3rem, 8vw, 6rem); }
.mast-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-block-end: 2.5rem; }
.kicker { margin: 0; font-family: var(--font-family-mono); font-size: var(--font-size-xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-content-muted); }
.theme-toggle { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); background: var(--color-surface-raised); border: 1px solid var(--rule); border-radius: 999px; padding: 0.5rem 1rem; cursor: pointer; }
.theme-toggle:hover { color: var(--color-content-primary); }
.mast-title { font-family: var(--font-family-display); font-size: var(--font-size-xxxl); font-weight: 600; line-height: 1; letter-spacing: -0.03em; margin: 0 0 1.25rem; text-wrap: balance; }
.mast-title em { font-style: italic; color: var(--color-primary-text, var(--color-content-primary)); }
.mast-lede { max-inline-size: 52ch; font-size: var(--font-size-lg); line-height: 1.5; color: var(--color-content-secondary); margin: 0; text-wrap: pretty; }
.mast-lede strong { color: var(--color-content-primary); font-weight: 600; }
.section { margin-block-end: clamp(3.5rem, 9vw, 7rem); }
.sec-head { margin-block-end: 2.5rem; }
.sec-title { font-family: var(--font-family-display); font-size: var(--font-size-xl); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 0.5rem; }
.sec-note { margin: 0; max-inline-size: 64ch; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-muted); }
.sec-note em { color: var(--color-content-primary); font-style: normal; font-weight: 600; }
code { font-family: var(--font-family-mono); font-size: 0.9em; color: var(--color-content-secondary); background: var(--color-surface-muted); padding: 0.1em 0.4em; border-radius: 4px; }
/* The orchestra */
.stage { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
@media (min-width: 50rem) { .stage { grid-template-columns: 1fr 22rem; } }
.stage-target { display: flex; flex-direction: column; align-items: center; gap: 1.25rem; min-block-size: 16rem; justify-content: center; border-radius: var(--radius-lg); border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); background: var(--color-surface-default); padding: clamp(2rem, 5vw, 4rem); }
.stage-hint { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-muted); }
/* the target — each channel's effect gated by its class + [data-pressed], all transitioning
together from one event. --shape-smoothing transitions because it's a registered @property. */
.target {
font-family: var(--font-family-primary);
font-size: var(--font-size-lg);
font-weight: 600;
padding: 1.1rem 2.5rem;
border: 0;
border-radius: 22px;
background: var(--color-primary-solid);
color: var(--color-primary-on-solid, #fff);
cursor: pointer;
transition:
transform 150ms var(--ease-out, cubic-bezier(0.2, 0, 0, 1)),
box-shadow 150ms var(--ease-out, ease),
background-color 150ms ease,
--shape-smoothing 150ms ease;
will-change: transform, box-shadow;
}
.target.ch-depth { box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); }
.target.ch-motion[data-pressed='true'] { transform: scale(0.93); }
.target.ch-depth[data-pressed='true'] { box-shadow: var(--depth-raised-shadow); }
.target.ch-shape[data-pressed='true'] { --shape-smoothing: 3.4; }
.target.ch-color[data-pressed='true'] { background: var(--color-affirm-solid, var(--color-primary-solid)); }
@media (prefers-reduced-motion: reduce) {
.target { transition: background-color 150ms ease; }
.target[data-pressed='true'] { transform: none; }
}
.mixer { display: flex; flex-direction: column; gap: 0.5rem; }
.track { display: flex; align-items: center; gap: 0.85rem; padding: 0.7rem 0.9rem; border-radius: var(--radius-md); border: 1px solid var(--rule); background: var(--color-surface-raised); cursor: pointer; text-align: start; transition: border-color 160ms, background 160ms; }
.track[data-on='false'] { opacity: 0.5; }
.track[data-on='true']:hover { border-color: color-mix(in srgb, var(--color-primary-solid) 40%, var(--rule)); }
.track-led { inline-size: 0.7rem; block-size: 0.7rem; border-radius: 50%; flex: none; background: var(--color-surface-muted); border: 1px solid var(--rule); transition: background 160ms, box-shadow 160ms; }
.track-led[data-on='true'] { background: var(--color-primary-solid); border-color: var(--color-primary-solid); box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary-solid) 60%, transparent); }
.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-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; }
.track-tag[data-layer='eidos'] { background: color-mix(in oklab, #12a594 22%, transparent); color: #2dd4bf; }
.track-tag[data-layer='sema'] { background: color-mix(in oklab, var(--color-primary-solid) 24%, transparent); color: var(--color-primary-text, #c4b5fd); }
.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-sep { color: var(--color-content-faint, var(--color-content-muted)); }
/* Two moments */
.moments { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 44rem) { .moments { grid-template-columns: 1fr 1fr; } }
.moment { padding: 1.5rem; border-radius: var(--radius-lg); border: 1px solid var(--rule); background: var(--color-surface-raised); display: flex; flex-direction: column; gap: 0.6rem; }
.moment-event { background: var(--color-surface-overlay); box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); }
.moment-h { font-family: var(--font-family-mono); font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-content-secondary); }
.moment-p { margin: 0; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-muted); }
.footnote { max-inline-size: 60rem; padding-block-start: 2rem; border-block-start: 1px solid var(--rule); }
.footnote p { margin: 0; font-size: var(--font-size-sm); line-height: 1.6; color: var(--color-content-muted); text-wrap: pretty; }
.footnote strong { color: var(--color-content-primary); font-weight: 600; }
</style>
Loading…
Cancel
Save

Powered by TurnKey Linux.