< script lang = "ts" >
/**
* 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.
*
* The channels are SEMANTIC (sema). Sema executes `sound` + `haptic` (runtime) and projects the
* `visual` channel (via `data-event-*`), which it DELEGATES to eidos to materialize — into the
* book's visual facets: color · motion · depth · forma. So those four are NOT "eidos channels":
* 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
* across sema's channels, under the two-moment model. This is the book's thesis, playable.
*/
import '@/uix/eidos/index.css'
import { EngineSemantic , type SemaChannelId } from '$uix/sema'
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';
type Facet = { id : ChannelId ; label : string ; note : string } ;
// sema's channels. `visual` is ONE channel that eidos materializes — shown here as its facets,
// each independently mutable. `sound` + `haptic` are sema's runtime channels.
const VISUAL_FACETS: Facet[] = [
{ id : 'color' , label : 'color' , note : 'tinte de intención (→ affirm)' } ,
{ id : 'motion' , label : 'movimiento' , note : 'escala bajo el dedo' } ,
{ 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];
// Visual facets 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 targetEl = $state< HTMLElement | null > (null);
let engine: EngineSemantic | undefined;
function fire(): void {
pressCount += 1;
lastFired = ALL_CHANNELS.filter((c) => channels[c.id]).map((c) => c.label);
// Real sema engine for sound + haptic — the canonical channels, from one emit. The 4 visible
// 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;
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: los canales de < em > sema< / em > disparan desde < em > un< / em > evento. Sema proyecta el
canal < em > visual< / em > —que eidos materializa en color · movimiento · profundidad · forma— y
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 >
< / div >
< div class = "stage" >
< div class = "stage-target" >
< button
bind:this={ targetEl }
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" >
< 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 ])} >
< 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 }
< / 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: sema emite, eidos lee< / h2 >
< p class = "sec-note" >
El suceso fluye por dos momentos unidos por un < em > token< / em > . Sema lo < em > emite< / em > ; eidos
lo < em > lee< / em > y lo materializa. Sema no conoce el DOM; eidos es el único dueño visual. El
token (< code > data-event-*< / code > ) es el contrato entre ambos.
< / p >
< / div >
< div class = "moments" >
< div class = "moment" >
< span class = "moment-h" > momento de < b > sema< / b > · emite< / span >
< p class = "moment-p" > Evalúa el suceso —familia · intención · fase— y lo estampa como tokens < code > data-event-*< / code > . Sonido + háptica los ejecuta aquí mismo. No toca el DOM/CSS.< / p >
< / div >
< div class = "moment moment-event" >
< span class = "moment-h" > momento de < b > eidos< / b > · lee< / span >
< p class = "moment-p" > Lee esos tokens (+ < code > data-state< / code > ) y los materializa en CSS — el canal visual: el tinte, la sombra que recede, la esquina que se afirma, la escala.< / p >
< / div >
< / div >
< / section >
< footer class = "footnote" >
< p >
< strong > Todo es sema; los canales son semánticos.< / strong > Un evento sema reparte la firma por
sus canales: el canal < strong > visual< / strong > —que sema proyecta vía < code > data-event-*< / code >
y < strong > delega en eidos< / strong > para materializarlo en color · movimiento · profundidad ·
forma— y los canales < strong > sound< / strong > + < strong > haptic< / strong > que sema ejecuta. No son
"canales de eidos": son el canal visual de sema, hecho visible por eidos. Aquí los disparamos
todos desde un < code > EngineSemantic.emit< / code > real (sonido + háptica) + los tokens de eidos
para el visual, bajo el modelo de dos momentos. Ningún referente reúne los canales sobre un
modelo semántico ú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 ( 3 rem , 8 vw , 6 rem ); }
.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 1 rem ; 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.03 em ; margin : 0 0 1.25 rem ; 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.5 rem , 9 vw , 7 rem ); }
.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.01 em ; margin : 0 0 0.5 rem ; }
.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.4 em ; border - radius : 4px ; }
/* The orchestra */
.stage { display : grid ; grid - template - columns : 1fr ; gap : clamp ( 1.5 rem , 4 vw , 3 rem ); align - items : center ; }
@media (min-width: 50rem) { . stage { grid - template - columns : 1fr 22 rem ; } }
.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 ( 2 rem , 5 vw , 4 rem ); }
.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 150 ms 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.9 rem ; border - radius : var ( -- radius - md ); border : 1px solid var ( -- rule ); background : var ( -- color - surface - raised ); cursor : pointer ; text - align : start ; transition : border - color 160 ms , background 160 ms ; }
.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 160 ms , box - shadow 160 ms ; }
.track-led[data-on='true'] { background : var ( -- color - primary - solid ); border - color : var ( -- color - primary - solid ); box - shadow : 0 0 8 px 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 ); }
.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 ; }
.mixer-group:first-child { margin - block - start : 0 ; }
.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-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 1 fr ; } }
.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 >