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

lfo-toolkit

v0.1.0

Published

Bounded, deterministic LFO waveforms for gamedev and creative coding.

Downloads

152

Readme

lfo-toolkit

Bounded, deterministic LFO waveforms for gamedev and creative coding.

CI

hero

Background

In game dev, generative art, and creative coding, an oscillator is the go-to tool for animating a parameter over time. Raw Math.sin has some undesirable qualities that most developers end up adjusting in isolated situations:

| Math.sin | lfo-toolkit | Benefits | | ----------------------- | ---------------------------------------------- | ----------------------------------------------------------- | | Period = 2 * PI | Period = period, which defaults to 1. | No longer need to work in radians. Period can be any value. | | Is bounded by [-1, 1] | Is bounded by [yMin, yMax] | Bounds can match the desired min/max value of a parameter. | | f(0) = 0 | f(0) = yStart where yMin <= yStart <= yMax | Starting value and initial direction can be set. | | One shape | Six shapes with one normalized interface | Swap waveforms without rewriting the surrounding code. |

lfo-toolkit supersedes bounded-sine, which solved the first three rows for a single sine wave.

Installation

pnpm add lfo-toolkit
import { sine } from "lfo-toolkit";

const fn = sine({ yMin: 0, yMax: 3, yStart: 2, period: 10 });
fn(0); // 2

Every factory returns a pure function of time: pass in elapsed seconds (or frames, or any unit — period shares it) and get a value back. No internal state, no update calls.

Guarantees

  • Output always stays within [yMin, yMax].
  • f(0) = yStart in both directions when phase is 0 (see the sawtooth note for the one degenerate exception).
  • Fully deterministic: identical options produce identical output, including the seeded random waveforms.
  • Degenerate bounds (yMin === yMax) return a constant — never NaN.
  • Invalid options (yMin > yMax, non-positive period, out-of-range yStart or dutyCycle) throw immediately with a clear message.

Shared options

Every waveform takes a single options object with these common parameters:

| Parameter | Default | Description | | --------- | ------- | ------------------------------------------------------------------------------- | | yMin | -1 | The minimum output value. | | yMax | 1 | The maximum output value. | | period | 1 | The length of one cycle. For noise, the width of one smoothing cell. | | phase | 0 | Offset in cycles: f(t) equals the unshifted waveform at t + phase * period. |

phase vs yStart: on the waveforms that support it, yStart picks where the waveform begins and direction picks which way it initially moves — together they define the base alignment. phase then shifts the whole waveform on top of that. The f(0) = yStart guarantee applies when phase is 0.

Waveforms

sine

sine

import { sine } from "lfo-toolkit";
const fn = sine({ yMin: 0, yMax: 3, yStart: 2, direction: "down" });

| Parameter | Default | Description | | ----------- | ------------------- | -------------------------------------------------------- | | yStart | (yMin + yMax) / 2 | The value of f(0). Must be within [yMin, yMax]. | | direction | "up" | Whether the wave initially rises or falls from yStart. |

triangle

triangle

import { triangle } from "lfo-toolkit";
const fn = triangle({ yMin: 0, yMax: 3, yStart: 2 });

| Parameter | Default | Description | | ----------- | ------------------- | -------------------------------------------------------- | | yStart | (yMin + yMax) / 2 | The value of f(0). Must be within [yMin, yMax]. | | direction | "up" | Whether the wave initially rises or falls from yStart. |

sawtooth

sawtooth

import { sawtooth } from "lfo-toolkit";
const fn = sawtooth({ yMin: 0, yMax: 3, period: 10 });

| Parameter | Default | Description | | ----------- | --------------------------- | --------------------------------------------------- | | yStart | yMin (up) / yMax (down) | The value of f(0). Must be within [yMin, yMax]. | | direction | "up" | Which way the ramp travels. |

Note: yStart at the top of an "up" ramp sits exactly on the discontinuity, which is the same waveform as starting at the bottom — f(0) returns yMin (and vice versa for "down").

square

square

import { square } from "lfo-toolkit";
const fn = square({ yMin: 0, yMax: 3, dutyCycle: 0.25 });

| Parameter | Default | Description | | ----------- | ------- | ---------------------------------------------------------------- | | dutyCycle | 0.5 | Fraction of each cycle spent at yMax. Cycles start high-first. |

Square outputs only yMin and yMax, so it takes no yStart — use phase to shift the transitions.

noise

noise

import { noise } from "lfo-toolkit";
const fn = noise({ yMin: 0, yMax: 3, period: 0.25, seed: 1 });

| Parameter | Default | Description | | --------- | ------- | ----------------------------------------- | | seed | 0 | Seed for the deterministic random source. |

Smooth value noise: a random control value every period, interpolated with a quintic smoothstep. Because it interpolates between random values inside the bounds, output typically stays well inside [yMin, yMax] and rarely touches the exact bounds.

sampleAndHold

sample-and-hold

import { sampleAndHold } from "lfo-toolkit";
const fn = sampleAndHold({ yMin: 0, yMax: 3, period: 0.25, seed: 2 });

| Parameter | Default | Description | | --------- | ------- | ----------------------------------------- | | seed | 0 | Seed for the deterministic random source. |

A stepped random wave: one deterministic value per period-sized cell.

Interactive demo

Clone the repo, then pnpm install && pnpm demo and open http://localhost:3000/demo/ to explore every waveform with live sliders.

License

MIT