@ -1,11 +1,11 @@
< script lang = "ts" >
/**
* Ambient pack — catalog. One route for the whole pack (packs live outside
* the component matrix and its 9-tab template): pick an effect, see it run
* with THEME-token colors (P-4) — switch the shell's light/dark mode and
* the token-driven effects re-tint live .
* Ambient pack — interactive catalog. Pick an effect, tune EVERY param
* live (controls auto-generated from the effect's `defaults`), pick the
* stage background, copy the usage snippet. Colors default to theme tokens
* (P-4) so a light/dark switch re-tints the token-driven effects.
*/
import { Ambient , type AmbientEffectName } from '$packs/ambient';
import { Ambient , resolveAmbientEffect , type AmbientEffectName } from '$packs/ambient';
import '$packs/ambient/effects/mesh';
import '$packs/ambient/effects/aurora';
import '$packs/ambient/effects/dot-grid';
@ -39,145 +39,128 @@
import '$packs/ambient/effects/pixel-blast';
import '$packs/ambient/effects/hyperspeed';
// Agentic set first (aurora/silk = thinking · orb = listening · lumen =
// acting · radar = searching · threads = streaming), then Tier A
// decoratives, then Tier B.
const effects: readonly AmbientEffectName[] = [
'aurora',
'silk',
'orb',
'lumen',
'radar',
'threads',
'waves',
'mesh',
'dot-grid',
'fog',
'grainient',
'veil',
'dots',
'beam',
'ray',
'snow-pixels',
'galaxy',
'eter',
'hyperspeed',
'particles',
'pixel-blast',
'grid',
'prismatic',
'pillar',
'dither',
'blinds',
'balastro',
'iridiscence',
'liquid-metal',
'line-waves',
'floating-lines',
'letters'
];
let active = $state< AmbientEffectName > ('aurora');
// Token-first params: concrete theming, not hardcoded palettes (P-4).
const paramsByEffect: Record< AmbientEffectName , Record < string , unknown > > = {
aurora: {
colorStops: ['--color-primary-solid', '--color-tertiary-solid', '--color-affirm-solid'],
amplitude: 1,
blend: 0.5
},
silk: {
color: '--color-neutral-solid'
},
orb: {
backgroundColor: '--color-neutral-bg2'
},
lumen: {
bgColor: '--color-neutral-bg2',
trailColorA: '--color-primary-solid',
trailColorB: '--color-tertiary-solid'
},
radar: {
color: '--color-affirm-solid'
// Effect groups (for the selector) — agentic first, then Tier A, then Tier B.
const GROUPS: { heading : string ; names : AmbientEffectName [] } [] = [
{ heading : 'Agentic' , names : [ 'aurora' , 'silk' , 'orb' , 'lumen' , 'radar' , 'threads' ] } ,
{
heading: 'Tier A',
names: ['waves', 'mesh', 'dot-grid', 'fog', 'grainient', 'veil', 'dots', 'beam', 'ray', 'snow-pixels']
},
threads: {
color: '--color-neutral-solid'
},
waves: {
lineColor: '--color-neutral-solid'
},
mesh: {
colors: [
'--color-primary-element',
'--color-secondary-element',
'--color-tertiary-element',
'--color-neutral-element'
{
heading: 'Tier B',
names: [
'galaxy', 'eter', 'hyperspeed', 'particles', 'pixel-blast', 'grid', 'prismatic', 'pillar',
'dither', 'blinds', 'balastro', 'iridiscence', 'liquid-metal', 'line-waves', 'floating-lines', 'letters'
]
},
'dot-grid': {
baseColor: '--color-neutral-border',
activeColor: '--color-primary-solid'
},
fog: {
bgColor: '--color-neutral-bg2',
fogColor: '--color-neutral-element'
},
grainient: {
color1: '--color-primary-solid',
color2: '--color-tertiary-solid',
color3: '--color-secondary-element'
},
veil: {} ,
dots: {
gradientFrom: '--color-primary-solid',
gradientTo: '--color-tertiary-solid',
glowColor: '--color-neutral-bg2'
},
beam: {} ,
ray: {} ,
'snow-pixels': {
color: '--color-neutral-text'
},
galaxy: {} ,
eter: {
colors: ['--color-primary-solid', '--color-secondary-solid', '--color-tertiary-solid']
},
hyperspeed: {} ,
particles: {
particleColors: ['--color-primary-solid', '--color-tertiary-solid', '--color-neutral-text']
},
'pixel-blast': {
color: '--color-primary-solid'
},
grid: {
linesColor: '--color-neutral-border',
scanColor: '--color-primary-solid'
},
prismatic: {} ,
pillar: {
topColor: '--color-primary-solid',
bottomColor: '--color-tertiary-solid'
},
dither: {
waveColor: '--color-neutral-solid'
},
blinds: {
colors: ['--color-primary-solid', '--color-tertiary-solid']
},
balastro: {} ,
iridiscence: {} ,
'liquid-metal': {} ,
'line-waves': {
color1: '--color-primary-solid',
color2: '--color-secondary-solid',
color3: '--color-tertiary-solid'
},
'floating-lines': {
linesGradient: ['--color-primary-solid', '--color-tertiary-solid']
},
letters: {
glitchColors: ['--color-neutral-element', '--color-primary-solid', '--color-tertiary-solid']
}
];
// Curated VISIBLE token colors per effect (the base theme is dark, so these
// separate from the stage; the auto-controls let you change them). Only the
// color params are seeded here; every other param comes from the effect's
// own `defaults`.
const COLOR_SEED: Partial< Record < AmbientEffectName , Record < string , unknown > >> = {
aurora: { colorStops : [ '--color-primary-solid' , '--color-tertiary-solid' , '--color-affirm-solid' ] } ,
silk: { color : '--color-primary-solid' } ,
orb: { backgroundColor : '--color-surface-default' } ,
lumen: { bgColor : '--color-surface-default' , trailColorA : '--color-primary-solid' , trailColorB : '--color-tertiary-solid' } ,
radar: { color : '--color-affirm-solid' } ,
threads: { color : '--color-neutral-text' } ,
waves: { lineColor : '--color-neutral-text' } ,
mesh: { colors : [ '--color-primary-solid' , '--color-secondary-solid' , '--color-tertiary-solid' , '--color-affirm-solid' ] } ,
'dot-grid': { baseColor : '--color-neutral-text' , activeColor : '--color-primary-solid' } ,
fog: { bgColor : '--color-surface-default' , fogColor : '--color-neutral-text' } ,
grainient: { color1 : '--color-primary-solid' , color2 : '--color-tertiary-solid' , color3 : '--color-affirm-solid' } ,
dots: { gradientFrom : '--color-primary-solid' , gradientTo : '--color-tertiary-solid' , glowColor : '--color-primary-solid' } ,
'snow-pixels': { color : '--color-neutral-text' } ,
eter: { colors : [ '--color-primary-solid' , '--color-secondary-solid' , '--color-tertiary-solid' ] } ,
particles: { particleColors : [ '--color-primary-solid' , '--color-tertiary-solid' , '--color-neutral-text' ] } ,
'pixel-blast': { color : '--color-primary-solid' } ,
grid: { linesColor : '--color-neutral-text' , scanColor : '--color-primary-solid' } ,
pillar: { topColor : '--color-primary-solid' , bottomColor : '--color-tertiary-solid' } ,
dither: { waveColor : '--color-primary-solid' } ,
blinds: { colors : [ '--color-primary-solid' , '--color-tertiary-solid' ] } ,
'line-waves': { color1 : '--color-primary-solid' , color2 : '--color-secondary-solid' , color3 : '--color-tertiary-solid' } ,
'floating-lines': { linesGradient : [ '--color-primary-solid' , '--color-tertiary-solid' ] } ,
letters: { glitchColors : [ '--color-primary-solid' , '--color-affirm-solid' , '--color-tertiary-solid' ] }
};
let active = $state< AmbientEffectName > ('aurora');
let params = $state< Record < string , unknown > >({} );
let stageBg = $state< 'surface' | 'black' | 'white' | 'transparent'>('surface');
const STAGE_BG: Record< typeof stageBg , string > = {
surface: 'var(--color-surface-default)',
black: '#000',
white: '#fff',
transparent: 'transparent'
};
// The selected effect's authoring defaults (typed `never` at the registry
// boundary; the runtime object is the real defaults).
const defaults = $derived(
(resolveAmbientEffect(active)?.defaults ?? {} ) as Record< string , unknown >
);
const driver = $derived(resolveAmbientEffect(active)?.driver ?? '—');
// Reseed params whenever the effect changes: effect defaults + curated colors.
function seed(name: AmbientEffectName) {
const base = { ...(( resolveAmbientEffect ( name ) ? . defaults ?? {}) as Record < string , unknown >) } ;
params = { ... base , ...( COLOR_SEED [ name ] ?? {}) } ;
}
$effect(() => {
seed(active);
});
// ── Control heuristics ────────────────────────────────────────────────
type ControlKind = 'number' | 'boolean' | 'colors' | 'string';
const kindOf = (v: unknown): ControlKind =>
typeof v === 'number' ? 'number'
: typeof v === 'boolean' ? 'boolean'
: Array.isArray(v) ? 'colors'
: 'string';
const numBounds = (v: number) => {
const abs = Math.abs(v);
const max = v === 0 ? 1 : Math.max(abs * 3, abs + 1);
const min = v < 0 ? -max : 0 ;
const step = max < = 2 ? 0.01 : max < = 50 ? 0.1 : 1;
return { min , max , step } ;
};
const isColorish = (k: string) =>
/color|bg|stop|tint|glow|gradient|line/i.test(k);
const entries = $derived(Object.entries(params));
function setParam(key: string, value: unknown) {
params = { ... params , [ key ] : value } ;
}
function setColorList(key: string, csv: string) {
setParam(key, csv.split(',').map((s) => s.trim()).filter(Boolean));
}
// ── Snippet ───────────────────────────────────────────────────────────
const stripDefault = $derived.by(() => {
// Only include params that differ from the effect's own default.
const out: Record< string , unknown > = {} ;
for (const [k, v] of Object.entries(params)) {
if (JSON.stringify(v) !== JSON.stringify(defaults[k])) out[k] = v;
}
return out;
});
const snippet = $derived(
[
"< script lang = 'ts' > " ,
` import { Ambient } from '$packs/ambient';`,
` import '$packs/ambient/effects/${ active } '; // import = opt-in`,
'< /' + 'script>',
'',
Object.keys(stripDefault).length === 0
? `< Ambient effect = "$ { active } " style = "height: 320px" /> `
: `< Ambient \ n effect = "$ { active } " \ n params = { $ { JSON . stringify ( stripDefault )}} \n style = "height: 320px" \ n /> `
].join('\n')
);
< / script >
< div data-uix-canvas-inner >
@ -185,28 +168,115 @@
< div data-uix-eyebrow > Packs · Ambient< / div >
< h1 data-uix-page-title > Ambient< / h1 >
< p data-uix-page-lede >
Fondos animados como pack encapsulado sobre < code > arts/scene< / code > — colores por token del
tema (cambia el modo del shell y se re-tintan), reduced-motion y pausa fuera de vista de
serie. Contrato P: < code > src/packs/ambient/README.md< / code > .
Fondos animados como pack encapsulado sobre < code > arts/scene< / code > — 32 efectos.
Elige uno, ajusta cada parámetro en vivo (los controles se generan del
< code > defaults< / code > del efecto), cambia el fondo del escenario y copia el snippet.
Colores por token del tema (P-4): cambia el modo del shell y los efectos token-aware se
re-tintan. Contrato P: < code > src/packs/ambient/README.md< / code > .
< / p >
< div data-uix-page-meta >
< span data-uix-meta-pill > < span data-uix-meta-key > effects< / span > 32< / span >
< span data-uix-meta-pill >< span data-uix-meta-key > driver</ span > { driver } </ span >
< span data-uix-meta-pill > < span data-uix-meta-key > tier< / span > pack< / span >
< / div >
< / header >
< div style = "display:flex; flex-wrap: wrap; gap: var(--space-2); margin-block: var(--space-4)" >
{ #each effects as name ( name )}
< button
type="button"
data-button
data-selected={ active === name ? '' : undefined }
onclick={() => ( active = name )}
>
{ name }
< / button >
{ /each }
< / div >
<!-- Live stage -->
< Ambient
effect={ active }
params={ params ByEffect[ active ] as never }
style="height: 420px; border-radius: var(--radius-lg); overflow: hidden; background: var(--color-surface-default)"
params={ params as never }
style={ `height: 420px; border-radius: var(--radius-lg); overflow: hidden; background: ${ STAGE_BG [ stageBg ] } ` }
/>
<!-- Effect selector -->
< section data-uix-section >
< h2 data-uix-section-title > Efecto< / h2 >
{ #each GROUPS as group ( group . heading )}
< div data-uix-subsection-head > { group . heading } </ div >
< div data-uix-chips role = "radiogroup" style = "flex-wrap: wrap;" >
{ #each group . names as name ( name )}
< button data-uix-chip data-active = { active === name } onclick= {() => ( active = name )} > { name } </ button >
{ /each }
< / div >
{ /each }
< / section >
<!-- Auto - generated live controls -->
< section data-uix-section >
< h2 data-uix-section-title > Parámetros < span data-uix-tab-count > { entries . length } </ span ></ h2 >
< p data-uix-section-desc >
Generados del < code > defaults< / code > del efecto. Los campos de color aceptan tokens
(< code > --color-*< / code > → resueltos vía < code > eidos.resolveToken< / code > ) o cualquier color CSS.
< button data-uix-chip onclick = {() => seed ( active )} style="margin-inline-start: var ( --space-2 );" > ↺ reset</ button >
< label style = "margin-inline-start: var(--space-3);" >
fondo:
< span data-uix-chips role = "radiogroup" >
{ #each [ 'surface' , 'black' , 'white' , 'transparent' ] as const as bg ( bg )}
< button data-uix-chip data-active = { stageBg === bg } onclick= {() => ( stageBg = bg )} > { bg } </ button >
{ /each }
< / span >
< / label >
< / p >
< div data-uix-controls >
{ #each entries as [ key , value ] ( key )}
{ @const kind = kindOf ( value )}
< label data-uix-control >
< span data-uix-control-label >
{ key }
{ #if isColorish ( key )} < span data-uix-control-hint > token o color CSS</ span > { /if }
< / span >
{ #if kind === 'number' }
{ @const b = numBounds ( defaults [ key ] as number )}
< span style = "display:flex; align-items:center; gap: var(--space-2);" >
< input
type="range"
min={ b . min }
max={ b . max }
step={ b . step }
value={ value as number }
oninput={( e ) => setParam ( key , Number ( e . currentTarget . value ))}
/>
< input
type="number"
step={ b . step }
value={ value as number }
oninput={( e ) => setParam ( key , Number ( e . currentTarget . value ))}
style="inline-size: 6rem;"
/>
< / span >
{ :else if kind === 'boolean' }
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { value === true } onclick= {() => setParam ( key , true )} > true</ button >
< button data-uix-chip data-active = { value === false } onclick= {() => setParam ( key , false )} > false</ button >
< / span >
{ :else if kind === 'colors' }
< input
type="text"
value={( value as string []). join ( ', ' )}
oninput={( e ) => setColorList ( key , e . currentTarget . value )}
style="inline-size: 24rem; max-inline-size: 100%;"
/>
{ : else }
< input
type="text"
value={ value as string }
oninput={( e ) => setParam ( key , e . currentTarget . value )}
style="inline-size: 16rem;"
/>
{ /if }
< / label >
{ /each }
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > pack< / span >
< span > import = opt-in (tree-shaken) · params por token< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { snippet } </ code ></ pre >
< / div >
< / section >
< / div >