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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user