npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@maravilla-labs/frames

v0.7.0

Published

Tiny vanilla helper for declaring Web Animations API timelines that the Maravilla runtime renderer drives frame-accurately.

Readme

@maravilla-labs/frames

Declare a timeline of animations and video playback for a page. The same declaration works two ways:

  • In a normal browser, each item plays at its scheduled time on its own.
  • When the page is rendered to video by the Maravilla runtime, the timeline is stepped frame by frame to produce a deterministic clip.

Zero dependencies. Bring your own animation library if you want one; nothing is required.

Install

pnpm add @maravilla-labs/frames

Usage

import { defineTimeline } from "@maravilla-labs/frames";

defineTimeline({
  duration: 5000, // total timeline length, ms

  instructions: [
    // Title fades in from t=0 over the first 500 ms.
    {
      at: 0,
      duration: 500,
      selector: "h1",
      props: [{ opacity: 0 }, { opacity: 1 }],
    },

    // Subtitle slides up, starting 300 ms after the title begins.
    {
      at: 300,
      duration: 600,
      selector: ".subtitle",
      props: [
        { opacity: 0, transform: "translateY(20px)" },
        { opacity: 1, transform: "translateY(0)" },
      ],
    },

    // A clip plays from t=2000 ms until t=5000 ms (3 s of video).
    {
      kind: "video",
      at: 2000,
      duration: 3000,
      selector: "video.intro",
    },
  ],
});

Call defineTimeline once, after the elements referenced by every selector exist in the DOM.

How at and duration interact

| Field | Meaning | | ---------- | ------------------------------------------------------------------ | | at | When on the timeline this item starts, in ms | | duration | How long this item runs from at, in ms. Omit for sensible default |

Two animations with the same at start at the same time. Two animations with different at values start at different times. A duration of 500 ms means the animation reaches its end keyframe 500 ms after it started, regardless of how long the total timeline is.

If you omit duration on an animation, it runs from at until the end of the timeline. If you omit duration on a video, it plays for its natural length.

Animations

Each animation instruction maps to a single Animation created via Element.animate(keyframes, { duration, fill: "both" }). Anything the Web Animations API supports as a keyframe is valid in props:

{
  at: 1000,
  duration: 800,
  selector: ".card",
  props: [
    { transform: "scale(0.9)", opacity: 0 },
    { transform: "scale(1)",   opacity: 1 },
  ],
}

CSS-class animations (Animate.css and friends)

Use kind: "css" to schedule an animation defined as CSS @keyframes and triggered by a class — for example anything from Animate.css:

{
  kind: "css",
  at: 1400,
  duration: 1000,    // optional — defaults to the longest keyframes duration on the element
  selector: ".badge",
  classes: ["animate__animated", "animate__bounceInDown"],
}

The helper adds the classes, captures the resulting CSS animation as a WAAPI Animation object via Element.getAnimations(), and drives its currentTime exactly like every other slot. The visual is the class's native CSS animation; the timing is yours to control.

The stylesheet that defines the keyframes must be loaded before the animations are materialised — but you don't need to do anything for that. defineTimeline waits internally for render-blocking stylesheets and fonts to be ready before it reads the keyframes, so you can just call it as soon as your module runs:

defineTimeline({ /* ... */ });

Do not gate defineTimeline behind the window load event (await new Promise(r => addEventListener("load", r))). load is also held open by <video preload> and <img>, which on a media-heavy page can take long enough to blow past the Maravilla renderer's readiness budget and fail the render with "timeline never became available." defineTimeline already waits for exactly what it needs (stylesheets + fonts) and nothing it doesn't (media). If you need that signal yourself, import stylesheetsReady().

This works for any library that exposes its motion as CSS classes plus shipped @keyframes — Animate.css, Magic, your own custom keyframes — not just Animate.css specifically.

When you pass an explicit duration, the helper writes animation-duration inline with !important so it beats any @media (prefers-reduced-motion: reduce) rule the library ships with (Animate.css collapses to 1 ms otherwise). You opted into this timeline; the helper assumes you mean it.

Pre-hiding the element

If you need the element invisible before its at time, use visibility: hidden in your baseline CSS — not opacity: 0. The helper flips visibility: visible automatically when the css instruction fires.

.badge { visibility: hidden; }

Why not opacity: 0? Animate.css's "*In" keyframes (bounceInDown, fadeInUp, etc.) define opacity at the start and middle of the animation but not at 100%. CSS fills missing keyframe values with the element's underlying cascade value — so a baseline of opacity: 0 makes the animation interpolate 1 → 0 at the tail end and the element fades back out as soon as it lands. visibility is not animated by the keyframes, so it stays cleanly out of the way.

Video

Use kind: "video" to schedule a <video> element on the timeline. Optionally seek inside the source file with seek:

{
  kind: "video",
  at: 4000,       // start playing at t=4 s on the timeline
  duration: 6000, // play for 6 s
  seek: 12000,    // start 12 s into the source file
  selector: "video#hero",
}

The <video> element itself stays as you'd write it in HTML — set src, preload="auto", and muted so the browser autoplay policy doesn't block it.

<video class="intro" src="/clips/intro.mp4" preload="auto" muted playsinline></video>

The audio track of an embedded <video> is not captured (browsers don't expose decoded audio to the rendering API). Add a kind: "audio" instruction (below) instead — or, for a single pre-mixed soundtrack, the audio option on FRAMES.render(...).

Audio

Declare time-bound audio tracks right in the timeline. They are mixed into the final video by the renderer (multi-track ffmpeg, server-side) and played in the browser preview by a built-in Web Audio engine — so preview matches render.

defineTimeline({
  duration: 12000,
  instructions: [
    // Background music bed for the whole piece.
    { kind: "audio", at: 0, duration: 12000, role: "music",
      src: "/audio/bed.m4a", key: "audio/bed.m4a", fadeIn: 400, fadeOut: 800 },
    // A voiceover from 3s; music automatically ducks beneath it.
    { kind: "audio", at: 3000, role: "voiceover",
      src: "/audio/vo.m4a", key: "audio/vo.m4a" },
  ],
});

Fields: at / duration / seek (ms, like video); src — a URL the browser preview fetches + decodes; key — the storage key the renderer stages to mix (omit for preview-only tracks); role"voiceover" | "music" | "sfx" (default "music"); gain (linear, default 1); fadeIn / fadeOut (ms); duck — whether a music bed dips under voiceover (default true for role: "music").

Ducking is a static envelope computed from the voiceover clips' positions (not their loudness), so it's deterministic and the preview reproduces the exact dip the export bakes in.

Preview transport. Scrubbing via applyState(t) stays silent (like any NLE); audio is heard during continuous playback. Drive it from your player with window.__mvFrames.playAudio(fromMs) / stopAudio() / setAudioMuted(muted). The renderer reads the tracks it mixes from window.__mvFrames.audio.

Bring your own animation library

Any library that exposes an explicit time value works alongside this helper. Drive its time cursor inside the same applyState-equivalent hook and the renderer samples it deterministically. GSAP, Lottie, and Three.js all fit this shape.

License

MIT