lfo-toolkit
v0.1.0
Published
Bounded, deterministic LFO waveforms for gamedev and creative coding.
Downloads
152
Maintainers
Readme
lfo-toolkit
Bounded, deterministic LFO waveforms for gamedev and creative coding.
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-toolkitimport { sine } from "lfo-toolkit";
const fn = sine({ yMin: 0, yMax: 3, yStart: 2, period: 10 });
fn(0); // 2Every 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) = yStartin both directions whenphaseis 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 — neverNaN. - Invalid options (
yMin > yMax, non-positiveperiod, out-of-rangeyStartordutyCycle) 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
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
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
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
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
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
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
