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

times-fps

v1.0.0-alpha

Published

A tiny frame-sampling toolkit with timeline, easing, and interpolation utilities.

Downloads

8

Readme

times-fps

times-fps is a tiny TypeScript toolkit for sampling animation frames with timelines, easing, and interpolation utilities.

This library allows you to describe your animation with named phases. times-fps gives you:

  • a Timeline you can use to define animation phases
  • an Interpolator you can use to interpolate between values mapping normalized progress and animation segments to ranges
  • a FrameSampler that generates frames for an FPS and duration
  • a TimelineInspector you can use to ask "has this phase started/finished yet?"

FrameSampler can produce frame values in the form of numbers, strings, arrays, or nested objects.

Installation

npm install times-fps
# or
yarn add times-fps
# or
pnpm add times-fps

The library is written in TypeScript, ships as an ES module, and includes type definitions.

Features

Timeline Phases

Build timelines from named phases and ratios instead of hard‑coded numeric intervals.

import {Timeline} from "times-fps";

const timeline = Timeline.ofPhases(
    ["intro", 1],   // 1 unit of time
    ["main",  2],   // 2 units of time
    ["outro", 1]    // 1 unit of time
);

// Each phase is a Segment with [start, end] in [0, 1]
const phases = timeline.phases;
// phases.intro.start === 0
// phases.main.end   === 0.75
// phases.outro.end  === 1

Under the hood, Timeline builds a Sequence of Segments and scales them to the normal [0, 1] interval.


Interpolator & Segment Mapping

Turn a normalized progress value into numbers and ranges that respect your timeline structure.

import {createFrameSampler, getInterpolator, Timeline} from "times-fps";

const tl = Timeline.ofPhases(
    ["intro", 1],
    ["main", 2],
    ["outro", 1]
);
const p = tl.phases;

const sampler = createFrameSampler(progress => {
    const map = getInterpolator(progress);

    // Map the "intro" phase to a radius range
    const introRadius = map(p.intro).to(0, 50);

    // Map the "main" phase to an opacity range
    const mainOpacity = map(p.main).to(0, 1);

    // Map the whole sequence to anchor values
    const x = map.sequence(tl.sequence).to(0, 100, 200, 300);
    // end-inclusive slicing
    const y = map.sequence(tl.sequence.subsequence("main", "outro")).to(0, 100, 200);

    return {introRadius, mainOpacity, x, y};
});
  • segment(segment).to(start, end): remap the current progress within a specific segment to a numeric range.
  • segment(segment).withEasing(easing).to(start, end): apply custom easing inside that segment.
  • sequence(sequence).to(...anchors): map the full sequence or a subsequence to a list of anchor values, one more than the number of segments.

Easing & Cubic Bézier Curves

Use built‑in easing functions or create custom cubic‑Bézier easings for your frames.

import {cubicBezierEasing, easeIn, easeOut, easeInOut} from "times-fps";

const customEase = cubicBezierEasing(0.55, 0.085, 0.68, 0.53);

// All easing functions are (t: number) => number
customEase(0.5); // eased value in [0, 1]
easeIn(0.3);
easeOut(0.8);
easeInOut(0.4);

Easing functions can be plugged directly into the FrameSampler (to ease global progress) or into the Interpolator when mapping particular segments.


Frame Sampling

Sample your animation at a fixed frames‑per‑second and get back structured frame data.

import {createFrameSampler, type Frame} from "times-fps";

type MyValue = {
    x: number;
    y: number;
    opacity: number;
}

const sampler = createFrameSampler<MyValue>(progress => {
    const t = progress.value; // normalized 0 → 1

    return {
        x: t * 100,
        y: t * 50,
        opacity: t
    };
});

// Lazily iterate frames
for (const frame of sampler.iterate({duration: 2, fps: 60})) {
    // frame: { progress: number; value: MyValue }
}

// Or eagerly collect them
const data = sampler.collect({duration: 2, fps: 60});
// data.duration === 2
// data.fps      === 60
// data.frames   === Frame<MyValue>[]

FrameSampler works with nested data: numbers, strings, arrays, and objects, so you can shape the frame values as you like.


Timeline Inspection

Ask timeline‑related questions at any point in time: “has this phase started yet?”, “is this sequence still active?”.

import {createFrameSampler, getTimelineInspector, Timeline} from "times-fps";

const timeline = Timeline.ofPhases(
    ["intro", 1],
    ["main",  2],
    ["outro", 1]
);

const sampler = createFrameSampler(progress => {
    const inspect = getTimelineInspector(progress);
    const p = timeline.phases;

    const intro = inspect(p.intro);
    const main  = inspect(p.main);

    if (intro.isActive()) {
        // do something during intro
    }
    if (main.hasStarted()) {
        // do something when main starts
    }

    return {};
});

TimelineInspector is built on top of the same normalized progress value, so it stays in sync with your frame sampling and interpolation.


Exporting Frames (Node.js)

Use the FrameExporter helper to write frame data to disk when running in Node.js—for example, to precompute animation data for a game engine or for SVG path animations.

import {createFrameSampler, cubicBezierEasing, Timeline} from "times-fps";
import {FrameExporter} from "times-fps/exporter";

const timeline = Timeline.ofPhases(
    ["intro", 1],
    ["main", 2],
    ["outro", 1]
);

const sampler = createFrameSampler(progress => {
    return { x: progress.value * 100 };
});

await FrameExporter.exportToJson(
    sampler.collect({
        duration: 3,
        fps: 120,
        easing: cubicBezierEasing(0.55, 0.085, 0.68, 0.53)
    }),
    // this path should be relative to `process.cwd()`
    "./json-exports",
    "frames"
);

Note: exportToJson is implemented only for the Node.js build (times-fps/exporter); the browser variant throws to guard against accidental use.


WAAPI & requestAnimationFrame

You can use FrameSampler with Web Animations API:

const sampler = createFrameSampler(progress => {
    return {x: progress.value * 100};
});

const duration = 3;
const frames = sampler.collect({
    duration,
    fps: 120,
    easing: cubicBezierEasing(0.55, 0.085, 0.68, 0.53)
});

const element = document.getElementById("element-id");
element.animate(frames, {duration: duration * 1000});

or requestAnimationFrame:

const sampler = createFrameSampler(progress => {
    return {x: progress.value * 100};
});

const duration = 3;
const generator = sampler.iterate({
    duration,
    fps: 120,
    easing: cubicBezierEasing(0.55, 0.085, 0.68, 0.53)
});

const animate = () => {
    const next = generator.next();
    if (next.done)
        return;

    // use the value
    const value = next.value;

    requestAnimationFrame(animate);
};
requestAnimationFrame(animate);

You could also do:

const resolver = createFrameSampler(progress => {
    return {x: progress.value * 100};
});

const duration = 3;

let progress = 0;
let startTime: number | undefined;

const animate = (time) => {
    if (typeof startTime === "undefined")
        startTime = time;
    progress = (time - startTime) / duration;
    if (progress > 1)
        return;

    // use the value
    const value = sampler.sampleAt(progress);

    requestAnimationFrame(animate);
};
requestAnimationFrame(animate);

Example: Driving an SVG Path Animation

Here is a more complete, end‑to‑end example combining times-fps with another library I authored, svg-path-kit, to precompute SVG path frames:

import {PathBuilder, Point2D, Angle} from "svg-path-kit";
import {
    createFrameSampler,
    getInterpolator,
    Timeline,
    cubicBezierEasing
} from "times-fps";
import {FrameExporter} from "times-fps/exporter";

const timeline = Timeline.ofPhases(
    ["arc1", 1],
    ["arc2", 1]
);
const p = timeline.phases;

const sampler = createFrameSampler(progress => {
    const map = getInterpolator(progress);
    const pb = PathBuilder.m(Point2D.ORIGIN);

    const arc1Radius = map(p.arc1).to(1, 5);
    const arc1EndAngle = map(p.arc1).to(0, 3 * Math.PI / 4);
    pb.bezierCircularArc(arc1Radius, Angle.ZERO, arc1EndAngle);
    pb.l(pb.currentVelocity);

    const arc2Radius = map(p.arc2).to(1, 5);
    const arc2EndAngle = map(p.arc2).to(0, -Math.PI);
    const lineAngle = pb.currentVelocity.angle;
    pb.bezierCircularArc(arc2Radius, Angle.of(lineAngle).halfTurnBackward(), arc2EndAngle);

    return pb.toSVGPathString();
});

await FrameExporter.exportToJson(
    sampler.collect({
        duration: 3,
        fps: 120,
        easing: cubicBezierEasing(0.55, 0.085, 0.68, 0.53)
    }),
    "../json-exports",
    "path-data"
); // This will store the animation frames in a `path-data.json` file

API Overview

The primary entry points you will usually work with are:

  • Timeline: build normalized timelines with named phases.

    • Timeline.ofPhases(...[name, duration]) – create a timeline from labeled durations.
    • .phases – a read‑only map of phase names to Segments.
    • .sequence – the underlying Sequence of segments (useful with the interpolator).
  • createFrameSampler: create a sampler around a progress => value function.

    • .sampleAt(t) – sample a single frame at normalized time (t).
    • .iterate(options) – lazily generate frames.
    • .collect(options) – eagerly collect frames along with duration and fps.
  • getInterpolator: attach an Interpolator to a specific AnimationProgress.

    • map(segment).to(start, end) – map progress in a segment to a numeric range.
    • map.segment(segment).withEasing(easing).to(start, end) – map with easing.
    • map.sequence(sequence).to(...anchors) – map a full sequence to anchor values.
  • getTimelineInspector: derive a TimelineInspector from an AnimationProgress.

    • inspect(segment).hasStarted() / hasFinished() / isActive() – query phase/segment state.
  • Easing helpers:

    • cubicBezierEasing(mX1, mY1, mX2, mY2) – create custom easing functions.
    • easeIn, easeOut, easeInOut – standard easing presets.

All of these are exported from the root times-fps entry point, except for FrameExporter, which is available from times-fps/exporter.

Note from Author

Designing animations often means thinking in terms of timelines and phases—“intro”, “main”, “outro”, loops, beats—but many APIs still expose only raw numbers: milliseconds, pixel deltas, easing parameters. I wrote times-fps to bridge that gap: to let me talk about time in the same structured way I sketch animations, while still giving me precise control over individual segments, easing, and sampling.

My goal is to keep this library small, focused, and composable with geometry and rendering toolkits, so you can use it in GUI apps, data visualizations, or SVG art without pulling in heavy dependencies. If you have ideas, suggestions, or corrections, the project is open‑source and I’d be very happy to hear from you.

Thanks a lot!