@ -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 facet s 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.0 6em; padding : 0.15rem 0.45 rem ; border - radius : 4px ; }
.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 ; }
.track-tag[data-layer='eidos'] { background : color - mix ( in oklab , # 12 a594 22 % , transparent ); color : # 2 dd4bf ; }
.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 ; }