익스클루시브

Still Time <mv-still-time>

주변 모션 조절기입니다. 아무 영역이나 감싸면 그 안의 모든 애니메이션(CSS 키프레임, CSS 트랜지션, Web Animations, 음소거된 비디오)이 사용자가 움직이는 만큼만 빠르게 재생됩니다.

문화적 레퍼런스

SUPERHOT, SUPERHOT Team (2016, 게임). 내가 움직일 때만 시간이 흐르고, 가만히 서 있는 동안에는 세상이 멈춥니다. UI에서는 배경 애니메이션이 사용자가 행동하는 만큼만 빠르게 움직이고, 사용자가 가만히 있으면 거의 멈출 정도로 느려집니다.

작동 방식

주변 모션 조절기: 어떤 영역이든 감싸면 그 안의 모든 애니메이션(CSS 키프레임, CSS 트랜지션, Web Animations, 음소거된 동영상)이 사용자가 움직이는 만큼만 빠르게 재생됩니다. 포인터 속도, 휠과 스크롤 속도, 키 입력, 터치 이동이 활동 수준을 만들고, 임계 감쇠 스프링이 이를 부드러운 시간 배율로 바꿉니다. 가만히 있을 때는 거의 멈춘 상태(기본값 ×0.03, idle-freeze를 쓰면 완전 정지)이고, 손이 닿으면 최대 속도(×1)가 됩니다. 배율은 아무것도 다시 시작하지 않고 playbackRate로 적용되며, --mv-time-rate, rate 속성, 배율이 적용된 time 시계, 스로틀링된 mv-timerate 이벤트로 노출되므로 canvas와 셰이더 루프도 따라올 수 있습니다. 정지 상태에서는 조절기가 완전히 잠듭니다. 기본은 차분하게, 손이 닿으면 생생하게.

카테고리배경
유형Web Component (<mv-still-time>)
상태안정
Keywordsexclusive, culture, ambient, motion, playback-rate, time-scale, slow-motion, activity, idle, calm, governor, web-animations, css-animations, background, hero, attention

When to use

  • A landing hero or dashboard has decorative motion that competes with reading and should calm down while visitors read
  • Ambient loops (marquees, orbits, floating cards, shimmer) should feel responsive without distracting idle users
  • A kiosk or wall display should stay nearly still until someone interacts with it
  • Canvas or shader backgrounds need a shared speed signal tied to user activity

Avoid when

  • An animation shows real progress or status (spinner, countdown) that must never slow down: mark it data-still-time-ignore
  • The goal is to cap the total amount of motion on a page for performance or accessibility → use Motion Budget instead
  • Motion should simply follow scroll position rather than activity → use Scroll Velocity instead

설치

node scripts/add.mjs still-time --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["still-time"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/still-time/still-time.js, components/still-time/still-time.css.

사용법

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="st-demo">
  <style>
    #st-demo { display:grid; gap:1rem; width:100%; max-width:68rem; margin-inline:auto; font-size:.875rem }
    #st-demo .st-hero { position:relative; overflow:hidden; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:center; gap:2rem; min-height:27rem; padding:2.5rem 2.5rem 5.5rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-2xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); isolation:isolate }
    #st-demo .st-sky { position:absolute; inset:0; z-index:-1; pointer-events:none; overflow:hidden }
    #st-demo .st-blob { position:absolute; width:26rem; height:26rem; border-radius:50%; opacity:.55; background:radial-gradient(closest-side, color-mix(in oklch, var(--_c) 55%, transparent), transparent 70%); animation:st-drift 18s ease-in-out infinite alternate }
    #st-demo .st-b1 { --_c:var(--mv-accent); left:-6rem; top:-8rem }
    #st-demo .st-b2 { --_c:var(--mv-info); right:-4rem; top:-6rem; animation-duration:22s; animation-delay:-7s }
    #st-demo .st-b3 { --_c:var(--mv-success); right:18%; bottom:-14rem; animation-duration:26s; animation-delay:-13s; opacity:.35 }
    @keyframes st-drift { to { transform:translate(4rem, 3rem) scale(1.15) } }

    #st-demo .st-copy { display:grid; gap:1rem; justify-items:start; max-width:28rem }
    #st-demo .st-copy h2 { margin:0; font-size:clamp(1.75rem, 1.2rem + 1.8vw, 2.5rem); line-height:1.08; letter-spacing:-.03em; font-weight:650 }
    #st-demo .st-copy p { margin:0; color:var(--mv-fg-muted); font-size:.9375rem; line-height:1.55 }
    #st-demo .st-cta { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:.25rem }

    #st-demo .st-visual { position:relative; height:21rem }
    #st-demo .st-orbit { position:absolute; left:50%; top:50%; width:15rem; height:15rem; margin:-7.5rem 0 0 -7.5rem }
    #st-demo .st-ring { position:absolute; inset:0; border-radius:50%; border:1px dashed color-mix(in oklch, var(--mv-fg) 16%, transparent) }
    #st-demo .st-ring-2 { inset:2.5rem; border-style:solid; border-color:color-mix(in oklch, var(--mv-accent) 22%, transparent) }
    #st-demo .st-spin { position:absolute; inset:0; animation:st-rotate 16s linear infinite }
    #st-demo .st-spin-2 { inset:2.5rem; animation-duration:9s; animation-direction:reverse }
    #st-demo .st-dot { position:absolute; left:50%; top:0; width:.75rem; height:.75rem; margin:-.375rem 0 0 -.375rem; border-radius:50%; background:var(--mv-accent); box-shadow:0 0 0 4px color-mix(in oklch, var(--mv-accent) 18%, transparent); transform-origin:50% 7.875rem }
    #st-demo .st-spin-2 .st-dot { width:.5rem; height:.5rem; margin:-.25rem 0 0 -.25rem; transform-origin:50% 5.25rem; background:var(--mv-fg-muted); box-shadow:none }
    #st-demo .st-mark { position:absolute; left:50%; top:50%; display:grid; place-items:center; width:4rem; height:4rem; margin:-2rem 0 0 -2rem; border-radius:1.25rem; color:var(--mv-fg-on-accent); background:linear-gradient(145deg, color-mix(in oklch, var(--mv-accent), white 18%), color-mix(in oklch, var(--mv-accent), black 18%)); box-shadow:0 12px 30px -10px color-mix(in oklch, var(--mv-accent) 70%, transparent); animation:st-breathe 4s ease-in-out infinite }
    #st-demo .st-mark svg { width:1.75rem; height:1.75rem }
    @keyframes st-rotate { to { rotate:360deg } }
    @keyframes st-breathe { 50% { scale:1.06 } }

    #st-demo .st-card { position:absolute; display:grid; gap:.375rem; margin:0; padding:.75rem .875rem; min-width:11.5rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:color-mix(in oklch, var(--mv-surface-raised) 88%, transparent); -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px); box-shadow:var(--mv-shadow-lg); animation:st-float 6s ease-in-out infinite alternate }
    #st-demo .st-card small { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:500; text-transform:uppercase; letter-spacing:.06em }
    #st-demo .st-card b { font-size:1.125rem; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
    #st-demo .st-card .st-up { color:var(--mv-success); font-size:.75rem; font-weight:600 }
    #st-demo .st-row { display:flex; align-items:center; gap:.5rem; font-size:.8125rem; font-weight:500 }
    #st-demo .st-row span:last-child { color:var(--mv-fg-muted); font-weight:400; margin-inline-start:auto; font-size:.75rem }
    #st-demo .st-c1 { left:0; top:1rem; animation-delay:-2s }
    #st-demo .st-c2 { right:0; bottom:4rem; animation-duration:7.5s; animation-delay:-4s }
    #st-demo .st-c3 { left:4%; bottom:.25rem; animation-duration:8.5s; animation-delay:-1s; min-width:13rem }
    @keyframes st-float { from { transform:translateY(-7px) rotate(-.6deg) } to { transform:translateY(7px) rotate(.6deg) } }
    #st-demo .st-spark { width:100%; height:1.75rem; overflow:visible }
    #st-demo .st-spark path { fill:none; stroke:var(--mv-accent); stroke-width:2; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:120; animation:st-draw 5s var(--mv-ease-in-out) -3.4s infinite }
    @keyframes st-draw { 0% { stroke-dashoffset:120 } 55%, 100% { stroke-dashoffset:0 } }
    #st-demo .st-check { display:grid; place-items:center; width:1.25rem; height:1.25rem; border-radius:50%; background:color-mix(in oklch, var(--mv-success) 18%, transparent); color:var(--mv-success) }
    #st-demo .st-check svg { width:.75rem; height:.75rem }
    #st-demo .st-bar { position:relative; height:.375rem; border-radius:var(--mv-radius-full); background:var(--mv-bg-muted); overflow:hidden }
    #st-demo .st-bar i { position:absolute; inset:0 38% 0 0; border-radius:inherit; background:linear-gradient(90deg, var(--mv-accent), color-mix(in oklch, var(--mv-accent), white 30%) 50%, var(--mv-accent)) 0 0 / 200% 100%; animation:st-shimmer 1.8s linear infinite }
    @keyframes st-shimmer { to { background-position:-200% 0 } }

    #st-demo .st-marquee { position:absolute; left:0; right:0; bottom:0; height:3.5rem; display:flex; align-items:center; overflow:hidden; border-top:1px solid var(--mv-border); background:color-mix(in oklch, var(--mv-bg-subtle) 70%, transparent); -webkit-mask:linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); mask:linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent) }
    #st-demo .st-track { display:flex; flex:none; gap:3rem; padding-inline-end:3rem; animation:st-scroll 28s linear infinite; color:var(--mv-fg-subtle); font-weight:600; letter-spacing:-.01em; white-space:nowrap }
    #st-demo .st-track span { display:inline-flex; align-items:center; gap:.5rem }
    #st-demo .st-track span::before { content:""; width:.625rem; height:.625rem; border-radius:.2rem; background:currentColor; opacity:.5 }
    @keyframes st-scroll { to { transform:translateX(-100%) } }

    #st-demo .st-lower { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); gap:1rem }
    #st-demo .st-panel { position:relative; display:grid; gap:1rem; padding:1.25rem 1.25rem 3.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #st-demo .st-panel header { display:flex; align-items:center; gap:.5rem }
    #st-demo .st-panel header strong { font-size:.9375rem }
    #st-demo .st-panel header span { margin-inline-start:auto; color:var(--mv-fg-muted); font-size:.8125rem; font-variant-numeric:tabular-nums }
    #st-demo .st-live { position:relative; width:.5rem; height:.5rem; border-radius:50%; background:var(--mv-success) }
    #st-demo .st-live::after { content:""; position:absolute; inset:0; border-radius:50%; background:var(--mv-success); animation:st-ping 1.6s var(--mv-ease-out) infinite }
    @keyframes st-ping { to { transform:scale(3); opacity:0 } }
    #st-demo .st-eq { display:flex; align-items:flex-end; gap:.375rem; height:6.5rem; padding-bottom:.25rem; border-bottom:1px solid var(--mv-border) }
    #st-demo .st-eq i { flex:1; height:100%; border-radius:.3rem .3rem .1rem .1rem; background:linear-gradient(to top, color-mix(in oklch, var(--mv-accent) 70%, transparent), var(--mv-accent)); transform-origin:bottom; animation:st-eq 1.6s ease-in-out infinite alternate }
    @keyframes st-eq { from { transform:scaleY(.18) } to { transform:scaleY(1) } }
    #st-demo .st-legend { display:flex; justify-content:space-between; color:var(--mv-fg-subtle); font-size:.6875rem; font-variant-numeric:tabular-nums }

    #st-demo .st-side { display:grid; gap:1rem; align-content:start; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
    #st-demo .st-side h3 { margin:0; font-size:.9375rem }
    #st-demo .st-side p { margin:0; color:var(--mv-fg-muted); line-height:1.5 }
    #st-demo .st-readouts { display:grid; gap:.5rem }
    #st-demo .st-readout { display:flex; align-items:center; gap:.75rem; padding:.625rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface) }
    #st-demo .st-readout span { color:var(--mv-fg-muted) }
    #st-demo .st-readout output { margin-inline-start:auto; font:600 .8125rem/1 var(--mv-font-mono); font-variant-numeric:tabular-nums }
    #st-demo .st-readout output small { color:var(--mv-fg-subtle); font-weight:500 }
    #st-demo .st-toggle { display:flex; align-items:center; gap:.625rem; cursor:pointer; font-weight:500 }

    @media (prefers-reduced-motion: reduce) {
      #st-demo .st-blob, #st-demo .st-spin, #st-demo .st-mark, #st-demo .st-card, #st-demo .st-spark path, #st-demo .st-bar i, #st-demo .st-track, #st-demo .st-live::after, #st-demo .st-eq i { animation:none }
    }
    :where([data-motion="reduce"]) #st-demo :is(.st-blob, .st-spin, .st-mark, .st-card, .st-spark path, .st-bar i, .st-track, .st-eq i), :where([data-motion="reduce"]) #st-demo .st-live::after { animation:none }
    @media (max-width: 760px) {
      #st-demo .st-hero { grid-template-columns:1fr; padding:1.5rem 1.25rem 5rem }
      #st-demo .st-lower { grid-template-columns:1fr }
    }
  </style>

  <mv-still-time class="st-hero" id="st-hero" indicator>
    <div class="st-sky" aria-hidden="true"><span class="st-blob st-b1"></span><span class="st-blob st-b2"></span><span class="st-blob st-b3"></span></div>

    <div class="st-copy">
      <span class="mv-badge" data-variant="secondary">New · Focus mode</span>
      <h2>Analytics that move when you do</h2>
      <p>Your dashboard stays calm while you read and comes alive the moment you reach for it. Move the pointer over this panel.</p>
      <div class="st-cta">
        <button class="mv-button">Start free trial</button>
        <button class="mv-button" data-variant="outline">Book a demo</button>
      </div>
    </div>

    <div class="st-visual" aria-hidden="true">
      <div class="st-orbit">
        <span class="st-ring"></span>
        <span class="st-ring st-ring-2"></span>
        <span class="st-spin"><i class="st-dot" style="rotate:20deg"></i><i class="st-dot" style="rotate:140deg"></i><i class="st-dot" style="rotate:260deg"></i></span>
        <span class="st-spin st-spin-2"><i class="st-dot" style="rotate:80deg"></i><i class="st-dot" style="rotate:260deg"></i></span>
        <span class="st-mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V9M10 19V5M16 19v-7M22 19H2"/></svg></span>
      </div>
      <figure class="st-card st-c1">
        <small>Revenue · this week</small>
        <div class="st-row"><b>$48,210</b><span class="st-up">↑ 12.4%</span></div>
        <svg class="st-spark" viewBox="0 0 120 28" preserveAspectRatio="none"><path d="M2 22 18 18 30 20 44 12 58 15 72 8 86 11 100 5 118 3"/></svg>
      </figure>
      <figure class="st-card st-c2">
        <div class="st-row"><span class="st-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>Deploy succeeded<span>42s</span></div>
        <div class="st-row"><span class="mv-spinner" style="--mv-spinner-size:1rem;color:var(--mv-accent)"></span>Warming edge cache<span>3 of 5</span></div>
      </figure>
      <figure class="st-card st-c3">
        <div class="st-row">Syncing 1,284 events<span>62%</span></div>
        <div class="st-bar"><i></i></div>
      </figure>
    </div>

    <div class="st-marquee" aria-hidden="true">
      <div class="st-track"><span>Kestrel</span><span>Acme Logistics</span><span>Halden Studio</span><span>Blue Harbor</span><span>Fernhollow Health</span><span>Vertex Labs</span><span>Orbital Freight</span></div>
      <div class="st-track"><span>Kestrel</span><span>Acme Logistics</span><span>Halden Studio</span><span>Blue Harbor</span><span>Fernhollow Health</span><span>Vertex Labs</span><span>Orbital Freight</span></div>
    </div>
  </mv-still-time>

  <div class="st-lower">
    <mv-still-time class="st-panel" id="st-panel" idle-freeze indicator>
      <header><i class="st-live" aria-hidden="true"></i><strong>Live traffic</strong><span>1,284 visitors now</span></header>
      <div class="st-eq" aria-hidden="true">
        <i style="animation-delay:-.2s"></i><i style="animation-delay:-1.1s"></i><i style="animation-delay:-.6s"></i><i style="animation-delay:-1.4s"></i><i style="animation-delay:-.3s"></i><i style="animation-delay:-.9s"></i><i style="animation-delay:-1.5s"></i><i style="animation-delay:-.5s"></i><i style="animation-delay:-1.2s"></i><i style="animation-delay:-.1s"></i><i style="animation-delay:-.8s"></i><i style="animation-delay:-1.3s"></i><i style="animation-delay:-.4s"></i><i style="animation-delay:-1s"></i>
      </div>
      <div class="st-legend" aria-hidden="true"><span>2:00 PM</span><span>2:30 PM</span><span>3:00 PM</span></div>
    </mv-still-time>

    <div class="st-side">
      <h3>Time follows your hand</h3>
      <p>Keep still: the hero crawls at ×0.03 and the traffic panel freezes completely. Move, scroll or type and both speed up to ×1.00.</p>
      <div class="st-readouts">
        <div class="st-readout"><span>Hero</span><output id="st-hero-rate">×0.03 <small>→ ×1.00</small></output></div>
        <div class="st-readout"><span>Traffic panel</span><output id="st-panel-rate">×0.00 <small>→ ×1.00</small></output></div>
      </div>
      <label class="st-toggle"><input type="checkbox" role="switch" class="mv-switch" id="st-freeze" checked> Freeze the panel completely when idle</label>
    </div>
  </div>
</div>

<script type="module">
  const root = document.getElementById("st-demo");
  if (root) {
    const show = (el, out) => {
      const paint = (rate) => {
        out.textContent = `×${rate.toFixed(2)} `;
        const max = document.createElement("small");
        max.textContent = "→ ×1.00";
        out.append(max);
      };
      el.addEventListener("mv-timerate", (e) => paint(e.detail.rate));
      customElements.whenDefined("mv-still-time").then(() => paint(el.rate ?? 0));
    };
    const hero = root.querySelector("#st-hero");
    const panel = root.querySelector("#st-panel");
    show(hero, root.querySelector("#st-hero-rate"));
    show(panel, root.querySelector("#st-panel-rate"));
    root.querySelector("#st-freeze").addEventListener("change", (e) => {
      panel.idleFreeze = e.target.checked;
    });
  }
</script>

API

Attributes

Name유형DefaultDescription
min-ratenumber0.03Time rate when the user is still (0 … max-rate). Animations crawl at this speed instead of stopping, so the scene never looks broken.
max-ratenumber1Time rate under full activity. Values above 1 make motion faster than authored.
idle-freezebooleanfalseExact 0 when idle: animations hold their current frame (playbackRate 0) and governed videos are paused, then resume where they were.
scoperegion | pageregionWhere activity is measured: only over the element (pointer, touch, wheel, keys and focus inside it, scrolling of its descendants), or over the whole page (including window scroll speed).
sensitivitynumber1Multiplies pointer and scroll speeds before they are mapped to activity (2 = half the speed is needed to reach full time).
videomuted | all | nonemutedWhich <video> descendants follow the rate (clamped to the 0.0625-16 range browsers support). Muted only by default, so audio is never slowed down.
indicatorbooleanfalseShows a small decorative time gauge in the bottom corner (ring on a log scale + “×0.03”), aria-hidden, plus a visually hidden sentence explaining the behavior.
data-still-time-ignoreboolean (on a descendant)Excludes this subtree: its animations and videos keep their own speed (use it for spinners, countdowns and other honest status).

Properties

Name유형Description
ratenumberCurrent time rate (read-only).
timenumber (s)Scaled clock: seconds that advance at the current rate (read-only). Drive a canvas or shader loop with it to follow the governor exactly.
minRate, maxRate, idleFreeze, scope, sensitivity, video, indicatorreflectedMirror the attributes (scope is read when the element connects).

Methods

NameDescription
nudge(amount = 0.6)Feeds activity manually (0 … 1), e.g. from a gamepad, a voice command or a custom gesture.

Events

NameDescription
mv-timerateThrottled to ~10 per second while the rate changes, plus one when it comes to rest. detail: { rate, activity, resting }.

Content structure

NameDescription
(content)Any content with ambient motion: CSS keyframes, transitions, Web Animations (other components included), muted <video>. Script-created animations are picked up within ~1.5 s, CSS ones as soon as they start.

CSS classes

NameDescription
mv-still-time-gaugeIndicator pill (aria-hidden); data-level = frozen | still | moving | live.
mv-still-time-arc / -track / -hand / -valueParts of the gauge: filled ring, its track, the clock hand and the “×0.03” text.

CSS variables

NameDefaultDescription
--mv-time-rate1Set on the element to the current rate (3 decimals) while it changes: read it in calc() for effects that are not animations (blur, glow, transition-duration of your own).
--mv-still-time-colorvar(--mv-accent)Color of the gauge arc.
--mv-still-time-insetvar(--mv-space-3)Distance of the gauge from the bottom and inline-end edges.

Accessibility

Purely presentational: it adds no roles, no focus stops and never moves focus. It only slows motion down, so it reduces distraction for people who read slowly or are sensitive to motion, while keyboard users are treated like pointer users (key presses and focus changes count as activity, so nobody gets a frozen interface because they do not use a mouse). The indicator gauge is aria-hidden; when indicator is set, a visually hidden sentence explains that ambient animations follow your activity (no live announcements, which would be noise). Honest status animations (spinners, countdowns, progress) can be excluded with data-still-time-ignore. Reduced motion (system or data-motion="reduce"): the governor does not react at all and keeps the rate at min-rate (0 with idle-freeze); well-behaved content already stops its own continuous animations in that mode. Performance: the animation set is refreshed on animationstart / transitionrun events plus a 1.5 s sweep (getAnimations({ subtree: true })), never per frame; the frameLoop runs only while the rate is changing and sleeps at rest (no work at all while nobody moves), pauses offscreen and in hidden tabs; playbackRate is updated with updatePlaybackRate (no restart, no jump) only when it actually changed; video rates are updated at ~10 Hz. On disconnect every governed animation and video is restored to its natural rate and resumed if it was paused. Known limit: the visually hidden sentence added with indicator is English only and cannot be overridden yet.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고