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

glove-env-motion

v1.0.0

Published

Motion stdlib adapter for glove-working-environment. Renders React scenes — including React Native Reanimated — to deterministic PNG frames, stills and MP4 video, as env:motion. The agent writes a component; a file comes out.

Readme

glove-env-motion

Draft v0.1 — same standing as the other glove-env-* adapters. Published, measured, and byte-deterministic; the API may still move before 1.0.

A stdlib adapter for glove-working-environment. The agent writes a React component; a video, an animated GIF, PNG frames or a still image comes out.

The whole setup

pnpm add glove-env-motion

Works on macOS, Windows and Linux. An installed Chrome, Edge or Chromium is found automaticallynpx playwright-core install chromium is only needed on hosts with no browser at all (CI, bare containers).

import { createWorkingEnvironment } from "glove-working-environment";
import { motion, MOTION_LIMITS } from "glove-env-motion";

const env = await createWorkingEnvironment({
  stdlib: [motion()],
  limits: MOTION_LIMITS,   // raises the per-run ceiling; renders need more than 30s
});

That is all of it. React, the Babel toolchain, and ffmpeg ship with the package; the browser is the only thing a host installs. Want React Native / Reanimated scenes too?

pnpm add react-native-reanimated react-native-web

No Babel config, no bundler config, no plugin wiring — the package carries its own pinned toolchain and applies the worklets transform when Reanimated is present.

Check any host in one command:

$ pnpm exec glove-motion-doctor
✓ browser     /opt/browsers/chromium/chrome
✓ ffmpeg      …/@ffmpeg-installer/linux-x64/ffmpeg
✓ react       bundled with glove-env-motion — no install needed
✓ reanimated  installed with react-native-web and the worklets plugin — React Native motion code renders here

ready — env:motion can render on this host

Every failing line comes with the one command that fixes it. The same checks feed capabilities() (what the agent can call at runtime) and the generated /std/motion/README.md (which tells the agent what this host can do before it spends a render finding out).

MOTION_LIMITS raises the ceiling, not every script's allowance: a run asks for the time it needs with run_script's timeout_ms, and that is clamped to runTimeoutMs. So a long render becomes possible without also handing four minutes to an accidental for(;;).

And if you forget it? The render is refused up front, naming both the timeout_ms to ask for and the ceiling that has to allow it — it does not die mid-run with a generic timeout.

What the agent does

import { render, still } from 'env:motion';

export default async function main() {
  await render('/scenes/intro.jsx', '/out/intro.mp4', { durationSeconds: 4 });
  await still('/scenes/card.jsx', '/out/card.png', { width: 1200, height: 630 });
}

The output extension picks the format: .mp4, .webm, .gif, .png, or an extensionless path for a directory of numbered frames.

Stills are the part people underestimate. A one-frame render is a PNG, so the same component that makes a video makes a chart, a title card, a diagram or a social image — with the whole browser as the drawing surface. That is why this is env:motion and not env:video.

Why this exists

The environment could already produce a PDF, a deck, a workbook and a resized image. It could not produce anything that moves — and the reason was never the encoder, because ffmpeg has been there since glove-env-media. It was that nothing could draw a frame.

The one hard problem

A browser animation is a function of wall-clock time. Screenshot the same scene twice and you get two different pictures; a renderer that fell behind by 4ms emits a frame from the wrong moment. Neither is acceptable for video, where frame N must be exactly N.

So time is replaced, not measured. Before any scene code runs, requestAnimationFrame becomes a queue nobody drains except the renderer, and performance.now() / Date.now() return a number it sets. One advance is one frame.

Measured: two independent runs of the same 60-frame scene produce byte-identical PNGs for every frame. That is what makes a re-render after an edit a real diff, and it is the property everything else here is built on.

Two ways to write a scene — no mode switch

useFrame() — a pure function of the frame number

import { useFrame, useVideoConfig, interpolate, Easing } from 'glove/motion';

export default function Scene() {
  const frame = useFrame();
  const { fps, width } = useVideoConfig();
  const x = interpolate(frame, [0, fps * 2], [0, 400], { easing: Easing.inOut });
  return (
    <div style={{ width, height: 720, background: '#0b0b10', display: 'grid', placeItems: 'center' }}>
      <h1 style={{ color: 'white', transform: `translateX(${x}px)` }}>Q3 Revenue</h1>
    </div>
  );
}

A scene can use any file in the tree as a picture — <img src="/inbox/photo.webp" /> — not just files sitting beside it. Referenced assets are staged for the render; a path the tree does not have becomes a warning naming it, and any image the browser could not decode is reported the same way. A missing picture is the one defect that survives every other check, because the render succeeds and the file is valid.

Easing carries linear, in, out, inOut (also as ease, easeIn, easeOut, easeInOut), quad, cubic, sin, expo, circle, back, bounce, and Easing.bezier(x1, y1, x2, y2) for anything else — plus any (t) => number you write yourself. A name that does not exist throws with the list of ones that do, rather than arriving inside interpolate as undefined is not a function.

Reanimated — real React Native motion code, unchanged

import { View } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';

export default function Scene() {
  const x = useSharedValue(0);
  useEffect(() => { x.value = withTiming(600, { duration: 2000 }); }, []);
  const style = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] }));
  return <Animated.View style={[{ width: 160, height: 160, backgroundColor: '#7c5cff' }, style]} />;
}

The renderer drives both signals on every frame — the frame number for useFrame() scenes, the clock for Reanimated — and each signal is inert for the other kind, so any scene animates with no configuration and the two stay consistent by construction (frame f is always t = f/fps). Earlier versions made the caller pick a mode, and picking wrong produced a valid video of a still image; mode still exists as an override, but nobody has to know that.

Stills got the same treatment: a frame-driven scene is detected and jumped to the requested frame directly, a clock-driven scene is walked there without intermediate screenshots — so spot-checking frame 90 is cheap for either kind, and a Reanimated still captures the animated moment rather than the initial state.

Five findings, all internalised

These cost a diagnostic round each. Every one fails silently — first frame renders, nothing moves, no error, nothing to grep for — which is exactly why none of them is host configuration anymore:

| Finding | Where it lives now | |---|---| | Reanimated's worklets are lifted by a Babel plugin, and esbuild does not run Babel | The transform runs automatically whenever Reanimated is installed | | The plugin requires Babel 7, and a host's Babel 8 fails inside it with a misleading error | @babel/core@^7 is a dependency — the host's Babel never enters the picture | | .web.js must beat .js, or the native runtime bundles and does nothing in a browser | Resolution order is fixed internally | | The synthetic clock must install before the scene's module code runs | Init order is fixed internally | | page.setContent() does not run addInitScript — only navigation does | The page is written to disk and navigated to |

The one silent case that can still occur — Reanimated present but its plugin missing, e.g. a broken partial install — is detected and reported as a render warning naming the reinstall.

Checking the output

render() returns warnings, and an empty array is the good case. The one that matters most says every frame came out identical — the scene is not animating, and the video is technically valid and completely useless. That is exactly the failure a test of the individual pieces would miss.

Then look at it. Pair with env:render's view_image:

const out = await render('/scenes/intro.jsx', '/out/intro.mp4', { keepFrames: '/tmp/frames' });
if (out.warnings.length) throw new Error(out.warnings.join('; '));
view_image({ path: '/tmp/frames/frame-00045.png',
             prompt: 'Is the heading fully inside the frame, and readable against the background?' })

Cost

Every frame is a browser screenshot — roughly a second per 10 frames. A 10-second clip at 30fps is 300 frames. Four guards, in order: a hard frame ceiling per render (default 1800, refused with the number), the up-front budget check (refused with the timeout_ms to ask for and the ceiling that bounds it), a process-wide cap on how many Chromiums exist at once across every environment (maxBrowsers, default 2), and the docs steering agents to iterate on stills before rendering the whole thing.

The browser itself is reused between renders in an environment — a fresh browser context per render keeps the isolation, while the process stays warm — so the second still in a session does not pay the launch again.

Output formats

| Output path | Result | |---|---| | …/x.mp4 | H.264, yuv420p, +faststart, padded to even dimensions | | …/x.webm | VP9 | | …/x.gif | Palette generated across the whole sequence, then applied — one-pass quantisation shimmers | | …/x.png | A single frame | | …/frames (no extension) | A directory of frame-00000.png … |

Render .mp4 for people, .webm for a bare Chromium. H.264 is the format Chrome, Edge, Safari and Firefox all play, so it is the right default for anything a person opens. Chromium builds without proprietary codecs — including the one playwright-core install puts on disk — cannot decode it, and the failure is the usual silent one: a <video> element with working controls, a correct duration, and a black rectangle. canPlayType('video/mp4; codecs="avc1.42E01E"') returns "" on such a build. If your viewer is that kind of Chromium, render VP9 instead; the file itself is fine either way.

Known limits

  • No audio. Add it with env:media — that package owns ffmpeg for the agent, and this one deliberately stops at "frames to a playable file".
  • Frames render in sequence, in one browser. A useFrame() scene is a pure function of the frame and could be split across workers; nothing does that yet.
  • Only system fonts. The browser has no webfont unless the scene embeds one.