feat(labs): visual polish wave — LabFX foundation + 33 sims juiced up

ФУНДАМЕНТ (4 новых файла):
- _fx_core.js: LabFX namespace, glow.drawGlow, glow.pulse, haptic, shake
- _fx_particles.js: пул 1500 объектов, 6 shapes (dot/spark/ring/smoke/splash/dust)
- _fx_motion.js: tween + 12 easings + critically-damped spring
- _fx_sound.js: 9 procedural synth-звуков (click/tick/whoosh/chime/fizz/spark/bounce/pour/drone), Web Audio API
- Sound toggle в шапке lab.html с localStorage-persist

UX МИКРО (CSS + JS):
- Button states: hover scale+brightness, active scale-down, disabled grayscale
- Slider polish: custom thumb с тенью, filled-track gradient, hover/active
- Focus rings через :focus-visible
- Tooltip system .tt-host data-tt= с 400ms hover, fade-in
- Marching ants для selection
- Loading skeleton с shimmer
- Empty state .sim-empty-* паттерн
- Toast: progress bar внизу, icons по типу
- Cursor states utility classes
- View Transitions API для smooth sim-switch, fallback на CSS fade

PHASE 2 — визуальные эффекты для 33 симуляций:

Physics motion: projectile (launch whoosh + landing splash/shake/haptic + target chime), pendulum (max-extension tick + bob glow), collision (bounce + sparks + shake), angrybirds (whoosh/bounce/fizz/chime + confetti), newton (rocket flame trail + scene transitions), forcesandbox (spring glow + impact sparks)

Physics fields: emfield (field-lines glow + particle trail + lightning при high field + Gauss-drag haptic + rod motion sparks), circuit (energized-wire glow ∝ current + LED bloom + short-circuit shake/spark + heat shimmer smoke + place/erase/switch sounds), opticsbench (beam glow на всех режимах + caustics dust у фокуса + TIR/Brewster one-shot sounds)

Thermo+waves: waves (mode-switch whoosh + Mach-cone particles + spectrum harmonic chime + waveform glow), hydrostatics (pour sound + splash при погружении + valve click), isoprocess (PV-trail dust), heatengine (drone цикла + hot/cold reservoir smoke/dust + phase-change ticks), radioactive (Geiger tick throttle + decay sparks + half-life chime + α/β/γ glow)

Chemistry: chemsandbox (pour splash + fizz bubbles/dust/spark по типу реакции + shake при горении), equilibrium/electrolysis/reactions/titration/flask/ionexchange/redox (pour/fizz/spark/chime по событиям, частицы при ключевых моментах), stoichiometry (fizz bubbles + recipe-change click)

Math+geom: geometry (tool-click + object-create tick + locus glow + challenge confetti via LabFX + haptic), triangle (vertex-drop tick + special-point glow), stereo (figure-change whoosh + cross-section chime, no particles — Three.js), trigcircle (drag-haptic + pitch∝angle tick + sin/cos glow), graph/graphtransform/quadratic (slider-tick throttled + curve glow + discriminant-cross chime), probability (bounce + finish-chime burst), normaldist (pulsing shade + bell glow)

Bio+misc: celldivision (phase-change whoosh + prophase dust + anaphase poles spark + cytokinesis chime), photosynthesis (photon tick + ATP chime + glucose sparkle), bohratom (electron-jump chime ∝ levels + photon spark), orbitals/crystal (mode-change whoosh — Three.js, sound only), logic (gate-place + wire-connect + LED bloom + HIGH wire flowing dots + preset chime), gas/brownian/diffusion/states (preset whoosh, throttled tick по событиям)

Все LabFX вызовы обёрнуты в if (window.LabFX) guard — graceful degradation если фундамент не загружен. 47 JS файлов прошли syntax check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-23 13:58:49 +03:00
parent 8b3159b529
commit 6afe928c0d
50 changed files with 2748 additions and 215 deletions
+366
View File
@@ -1111,3 +1111,369 @@
background: rgba(155,93,229,0.18);
font-weight: 700;
}
/* ════════════════════════════════════════════════════════════════════════════
UX MICRO-INTERACTIONS — appended block
All additions are scoped to lab.html UI; individual sim JS files untouched.
════════════════════════════════════════════════════════════════════════════ */
/* ── 1. BUTTON STATES ────────────────────────────────────────────────────────
Snappy hover/active/disabled for all interactive controls in the lab shell.
Uses cubic-bezier(0.16,1,0.3,1) — fast-out-slow-in for a spring-like feel.
─────────────────────────────────────────────────────────────────────────── */
.zoom-btn,
.proj-preset-chip,
.geo-tool-btn,
.sb-tool-btn {
transition: transform .12s cubic-bezier(0.16,1,0.3,1),
filter .12s cubic-bezier(0.16,1,0.3,1),
border-color .12s, color .12s, background .12s,
box-shadow .12s;
}
.zoom-btn:hover,
.proj-preset-chip:hover,
.geo-tool-btn:hover,
.sb-tool-btn:hover {
transform: scale(1.025);
filter: brightness(1.08);
}
.zoom-btn:active,
.proj-preset-chip:active,
.geo-tool-btn:active,
.sb-tool-btn:active {
transform: scale(0.96);
filter: brightness(0.92);
}
.zoom-btn:disabled,
.proj-preset-chip:disabled,
.geo-tool-btn:disabled,
.sb-tool-btn:disabled,
.zoom-btn[disabled],
.proj-preset-chip[disabled],
.geo-tool-btn[disabled],
.sb-tool-btn[disabled] {
opacity: .45;
filter: grayscale(.6);
cursor: not-allowed;
pointer-events: none;
}
/* .sim-card already has transition; add scale micro-interaction */
.sim-card:not(.soon):hover { transform: translateY(-2px) scale(1.012); }
.sim-card:not(.soon):active { transform: scale(0.985); filter: brightness(0.97); }
/* ── 2. SLIDER POLISH ────────────────────────────────────────────────────────
Custom track + thumb for all range inputs inside .proj-panel.
Track: 4px, filled-gradient from left to thumb position via background-size trick.
Thumb: 16px circle, cyan/violet, shadow, scale on hover/active.
─────────────────────────────────────────────────────────────────────────── */
.proj-panel input[type=range],
.param-slider {
-webkit-appearance: none;
appearance: none;
height: 4px;
border-radius: 4px;
background: linear-gradient(
to right,
var(--violet) 0%,
var(--violet) var(--sl-pct, 50%),
rgba(255,255,255,.15) var(--sl-pct, 50%),
rgba(255,255,255,.15) 100%
);
outline: none;
cursor: pointer;
transition: background .12s;
}
.proj-panel input[type=range]::-webkit-slider-thumb,
.param-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px; height: 16px;
border-radius: 50%;
background: var(--violet);
box-shadow: 0 0 0 3px rgba(155,93,229,.25), 0 2px 6px rgba(0,0,0,.35);
cursor: pointer;
transition: transform .12s cubic-bezier(0.16,1,0.3,1),
box-shadow .12s;
}
.proj-panel input[type=range]:hover::-webkit-slider-thumb,
.param-slider:hover::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 4px rgba(155,93,229,.3), 0 3px 8px rgba(0,0,0,.4);
}
.proj-panel input[type=range]:active::-webkit-slider-thumb,
.param-slider:active::-webkit-slider-thumb {
transform: scale(0.95);
}
.proj-panel input[type=range]::-moz-range-thumb,
.param-slider::-moz-range-thumb {
width: 16px; height: 16px;
border-radius: 50%; border: none;
background: var(--violet);
box-shadow: 0 0 0 3px rgba(155,93,229,.25), 0 2px 6px rgba(0,0,0,.35);
cursor: pointer;
transition: transform .12s cubic-bezier(0.16,1,0.3,1);
}
.proj-panel input[type=range]:hover::-moz-range-thumb,
.param-slider:hover::-moz-range-thumb { transform: scale(1.15); }
.proj-panel input[type=range]:active::-moz-range-thumb,
.param-slider:active::-moz-range-thumb { transform: scale(0.95); }
/* ── 3. FOCUS RINGS (accessibility) ─────────────────────────────────────────
:focus-visible only — mouse clicks don't show the ring.
─────────────────────────────────────────────────────────────────────────── */
.zoom-btn:focus-visible,
.proj-preset-chip:focus-visible,
.geo-tool-btn:focus-visible,
.sb-tool-btn:focus-visible,
.sim-card:focus-visible,
.lab-filter:focus-visible,
.gp-btn:focus-visible,
.sim-back:focus-visible,
.proj-launch-btn:focus-visible,
.proj-reset-btn:focus-visible,
.param-slider:focus-visible,
.proj-panel input[type=range]:focus-visible {
outline: 2px solid var(--violet);
outline-offset: 2px;
}
/* ── 4. TOOLTIP SYSTEM (CSS-only, zero JS) ───────────────────────────────────
Usage: add class="tt-host" data-tt="Your tooltip text" to any element.
Tooltip appears above the element after a 400ms delay on hover.
Arrow points downward. Dark overlay with white text, 8px radius.
─────────────────────────────────────────────────────────────────────────── */
.tt-host {
position: relative;
}
.tt-host::after {
content: attr(data-tt);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
white-space: nowrap;
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
background: rgba(20,20,30,.95);
color: #fff;
font-family: 'Manrope', sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
padding: 5px 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,.1);
box-shadow: 0 4px 16px rgba(0,0,0,.4);
pointer-events: none;
opacity: 0;
transition: opacity .15s ease, transform .15s ease;
transition-delay: 0s;
z-index: 9000;
}
.tt-host:hover::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
transition-delay: 400ms;
}
/* arrow */
.tt-host::before {
content: '';
position: absolute;
bottom: calc(100% + 3px);
left: 50%;
transform: translateX(-50%) translateY(4px);
border: 5px solid transparent;
border-top-color: rgba(20,20,30,.95);
pointer-events: none;
opacity: 0;
transition: opacity .15s ease, transform .15s ease;
transition-delay: 0s;
z-index: 9001;
}
.tt-host:hover::before {
opacity: 1;
transform: translateX(-50%) translateY(0);
transition-delay: 400ms;
}
/* ── 5. SIM FADE TRANSITION ──────────────────────────────────────────────────
.sim-fading is toggled by lab-glue.js around openSim() calls.
The sim body wrap fades out (150ms) then new sim fades in (200ms).
─────────────────────────────────────────────────────────────────────────── */
#lab-sim {
transition: opacity .2s ease;
}
#lab-sim.sim-fading {
opacity: 0;
pointer-events: none;
}
/* ── 6. MARCHING ANTS SELECTION ──────────────────────────────────────────────
Opt-in: apply class="sim-selected-ants" to any SVG stroke element.
Downstream sims (geometry, circuit, logic) can use this.
─────────────────────────────────────────────────────────────────────────── */
@keyframes ant-march {
to { stroke-dashoffset: -28; }
}
.sim-selected-ants {
stroke-dasharray: 4 3;
animation: ant-march 800ms linear infinite;
}
/* ── 7. LOADING SKELETON ─────────────────────────────────────────────────────
Opt-in: apply .sim-loading-skel to any container while a sim initialises.
Shimmer moves left-to-right. Remove the class when the sim is ready.
─────────────────────────────────────────────────────────────────────────── */
@keyframes skel-shimmer {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
.sim-loading-skel {
background: linear-gradient(
90deg,
rgba(255,255,255,.04) 25%,
rgba(255,255,255,.10) 50%,
rgba(255,255,255,.04) 75%
);
background-size: 800px 100%;
animation: skel-shimmer 1.4s ease-in-out infinite;
border-radius: 8px;
min-height: 40px;
}
/* ── 8. EMPTY STATE PATTERN ──────────────────────────────────────────────────
Usage:
<div class="sim-empty-state">
<svg class="sim-empty-icon" ...>...</svg>
<div class="sim-empty-title">Title</div>
<div class="sim-empty-desc">Description text</div>
<button class="sim-empty-cta">Action</button> <!-- optional -->
</div>
─────────────────────────────────────────────────────────────────────────── */
.sim-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
color: var(--text-3);
opacity: .72;
}
.sim-empty-icon {
width: 48px; height: 48px;
stroke: var(--text-3);
stroke-width: 1.4;
fill: none;
opacity: .6;
display: block;
}
.sim-empty-title {
font-family: 'Unbounded', sans-serif;
font-size: 0.88rem;
font-weight: 800;
color: var(--text-2);
}
.sim-empty-desc {
font-size: 0.82rem;
line-height: 1.55;
max-width: 280px;
}
.sim-empty-cta {
margin-top: 4px;
padding: 8px 20px;
border-radius: 99px;
border: 1.5px solid var(--violet);
background: transparent;
color: var(--violet);
font-family: 'Manrope', sans-serif;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: background .15s, color .15s;
}
.sim-empty-cta:hover {
background: var(--violet);
color: #fff;
}
/* ── 9. TOAST UPGRADE — progress bar ────────────────────────────────────────
The existing lsToast already slides in from right and stacks.
This adds a thin auto-dismiss progress bar at the bottom of each toast.
JS in lab-glue.js injects .ls-toast-progress with --toast-dur CSS var.
─────────────────────────────────────────────────────────────────────────── */
@keyframes toast-progress {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
.ls-toast {
position: relative;
overflow: hidden;
}
.ls-toast-bar {
position: absolute;
bottom: 0; left: 0;
height: 3px;
width: 100%;
background: rgba(255,255,255,.45);
transform-origin: left center;
animation: toast-progress var(--toast-dur, 3.5s) linear forwards;
border-radius: 0 0 14px 14px;
}
/* ── 10. CURSOR STATES ───────────────────────────────────────────────────────
Default canvas cursor for projector-style sims.
Sims can toggle .cur-* classes on .proj-canvas-outer or canvas itself.
─────────────────────────────────────────────────────────────────────────── */
.proj-canvas-outer canvas { cursor: crosshair; }
.cur-grab { cursor: grab !important; }
.cur-grabbing { cursor: grabbing !important; }
.cur-cell { cursor: cell !important; }
.cur-pointer { cursor: pointer !important; }
.cur-cross { cursor: crosshair !important; }
.cur-move { cursor: move !important; }
/* Draggable canvas elements within sims */
[draggable="true"] canvas,
canvas[data-draggable] { cursor: grab; }
[draggable="true"] canvas:active,
canvas[data-draggable]:active { cursor: grabbing; }
/* ── 11. VIEW TRANSITIONS ────────────────────────────────────────────────────
If browser supports View Transitions API, openSim is wrapped in
document.startViewTransition() in lab-glue.js (see below comment).
::view-transition-* rules control the cross-fade animation.
Fallback: manual .sim-fading class (rule 5 above).
─────────────────────────────────────────────────────────────────────────── */
@supports (view-transition-name: none) {
@view-transition { navigation: none; }
::view-transition-old(sim-view),
::view-transition-new(sim-view) {
animation-duration: 200ms;
animation-timing-function: ease;
}
::view-transition-old(sim-view) {
animation-name: sim-vt-out;
}
::view-transition-new(sim-view) {
animation-name: sim-vt-in;
}
}
@keyframes sim-vt-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes sim-vt-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Assign view-transition-name so only the sim area morphs */
#lab-sim {
view-transition-name: sim-view;
}