@ -1,17 +1,20 @@
< 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.
*
* 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 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 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 { EngineSemantic , type SemaChannelId } from '$uix/sema'
let theme = $state< 'light' | 'dark'>('dark')
$effect(() => {
@ -25,16 +28,23 @@
})
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 }
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];
// Visible channels on by default; sound + haptic OFF (audible/tactile side effects — sema canon).
// Visual facet s on by default; sound + haptic OFF (audible/tactile side effects — sema canon).
let channels = $state< Record < ChannelId , boolean > >({
color: true,
motion: true,
@ -48,33 +58,28 @@
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 */
}
}
let targetEl = $state< HTMLElement | null > (null);
let engine: EngineSemantic | undefined;
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);
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);
@ -106,15 +111,17 @@
< 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.
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 }
@ -128,14 +135,24 @@
< / div >
< 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 ])} >
< 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 >
@ -175,13 +192,14 @@
< 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.
< 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 >
@ -259,9 +277,9 @@
.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.0 6em; padding : 0.15rem 0.45 rem ; border - radius : 4px ; }
.track-tag[data-layer='eidos'] { background : color - mix ( in oklab , # 12 a594 22 % , transparent ); color : # 2 dd4bf ; }
.track-tag[data-layer='sema'] { background : color - mix ( in oklab , var ( -- color - primary - solid ) 24 % , transparent ); color : var ( -- color - primary - text , # c4b5fd ) ; }
.mixer-group { font - family : var ( -- font - family - mono ); font - size : 0.62rem ; text - transform : uppercase ; letter - spacing : 0.0 8em; 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 ; }