You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/temas/orquesta/+page.svelte

300 lines
16 KiB

<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, 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>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(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); }
.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 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>

Powered by TurnKey Linux.