@zakkster/lite-noise generates coherent noise for terrain, particles, animations, and procedural art β all deterministic and zero-allocation.
New in v1.1.0: field baking, Quilez-style domain warp, and 3D curl.
It gives you:
- π Simplex 2D and 3D noise
- ποΈ FBM (fractal Brownian motion) with configurable octaves
- β°οΈ
ridged2/billow2multifractals β mountains and clouds, sharing the FBM skeleton - π
noiseLoopβ seamless periodic 1D noise for perfect animation loops - π§©
tileable2β seamless tiling textures (cross-checked bylite-patternforge'sseamlessScore) - π Curl noise (2D and 3D) for smoke, fluid, and volumetric particle movement
- π Quilez-style domain warping over FBM
- πΊοΈ
fillField2β bake a Float32Array heightfield in one call, row-incremental coord stepping, optionalnormalizeto [0,1] - π§΅
tileableField2β bake a whole seamless multifractal field (fbm/ridged/billow) whose opposite edges wrap seamlessly (bit-exact===on a grid-aligned bake β power-of-two width + integer period) - π² Seeded via an inlined Mulberry32 PRNG (deterministic, reproducible, zero runtime dependencies)
- 0οΈβ£ Zero allocation in any hot-path function (unrolled FBM, no rest/spread, no per-cell object synthesis)
- π§Ή Caller-owned output for
curl2/curl3/warp2(no shared reference bugs) - π‘οΈ Zero-alloc claim made falsifiable via
@zakkster/lite-gc-profilergates (npm run torture) - πͺΆ ~2.69 KB minified + gzipped, self-contained (zero dependencies; measured by
npm run bundle-check)
Part of the @zakkster/lite-* ecosystem β micro-libraries built for deterministic, cache-friendly game development.
npm i @zakkster/lite-noiseimport {
seedNoise,
simplex2, simplex3,
fbm2, fbm3,
ridged2, billow2,
noiseLoop, tileable2,
curl2, curl3,
warp2,
fillField2, tileableField2,
} from '@zakkster/lite-noise';
// Seed for reproducibility
seedNoise(42);
// v1.3.0: ridged mountains + billow clouds (share the FBM octave skeleton)
const peak = ridged2(x * 0.01, y * 0.01, 6);
const puff = billow2(x * 0.01, y * 0.01, 6);
// v1.3.0: a perfect animation loop β t sweeps 0..2Ο, closes seamlessly
const wobble = noiseLoop((frame / TOTAL) * Math.PI * 2, 1.5);
// v1.3.0: a seamless tiling texture, period 4 in noise space
const tile = tileable2(u * 4, v * 4, 4, 4); // wraps edge-to-edge
// v1.1.0: bake a heightfield in one call, zero allocation
const heightmap = new Float32Array(256 * 256);
fillField2(heightmap, 256, 256, { scale: 0.01, octaves: 6 });
// v1.5.0: bake a SEAMLESS multifractal field. Grid-aligned here (256 is a power
// of two, period 4 is an integer) so opposite edges wrap bit-exact (===).
const tileField = new Float32Array(256 * 256);
tileableField2(tileField, 256, 256, { model: 'fbm', periodX: 4, periodY: 4, octaves: 6 });
// v1.1.0: Quilez-style domain warp β richer procedural art per byte
const warped = { x: 0, y: 0 };
warp2(x * 0.01, y * 0.01, 1.5, warped);
const value = fbm2(warped.x, warped.y);
// v1.1.0: 3D curl for volumetric smoke (twelve simplex3 samples, caller-owned out)
const flow3d = { x: 0, y: 0, z: 0 };
curl3(px * 0.005, py * 0.005, pz * 0.005, flow3d);
// Fluid particles (zero-GC)
const vel = { x: 0, y: 0 };
curl2(particle.x * 0.005, particle.y * 0.005, vel);
particle.vx += vel.x * 0.5;
particle.vy += vel.y * 0.5;There are two ways to sample, and the difference is ownership of the permutation table.
createNoise(seed) β an independent field. Each instance owns its own table. Two instances are two fields that cannot disturb each other, which is what you want the moment more than one subsystem samples noise on the same page.
import { createNoise } from '@zakkster/lite-noise';
const terrain = createNoise(42); // its own table
const particles = createNoise(7); // a different, independent table
terrain.fillField2(field, w, h, { scale: 0.01, octaves: 6 });
particles.curl2(x * 0.005, y * 0.005, vel);
// Neither call can change what the other samples. Reseed one with
// `particles.seed(99)` and `terrain` is untouched.Every module function has an instance method of the same name: simplex2, simplex3, fbm2, fbm3, curl2, curl3, warp2, fillField2, plus .seed(s) to re-seed in place. An instance at seed S is byte-identical to the module functions after seedNoise(S) β same values, isolated ownership.
The module functions β one shared table. Convenient for a single consumer, but they share one module-scoped table that seedNoise rewrites for everyone:
// terrain module
seedNoise(42);
const h = simplex2(x, y); // uses seed-42 table
// particles module (elsewhere in the same process)
seedNoise(7); // β now everyone samples from seed-7
const p = simplex2(px, py); // seed-7 table
// terrain module samples again
const h2 = simplex2(x, y); // β this changed silentlyUnder a single seed and single consumer this is invisible; the moment two consumers each own "their" seed, it isn't. seedNoise warns once (dev builds) when called more than once, naming createNoise as the fix. When in doubt, give each consumer its own createNoise(seed).
| Library | Size | Seeded | FBM | Warp | Curl 2D | Curl 3D | Field bake | Zero-GC | Install |
|---|---|---|---|---|---|---|---|---|---|
| simplex-noise | ~8 KB | No | No | No | No | No | No | No | npm i simplex-noise |
| noisejs | ~4 KB | Yes | No | No | No | No | No | No | npm i noisejs |
| lite-noise | ~2.69 KBβ | Yes | Yes | Yes | Yes | Yes | Yes | Yes | npm i @zakkster/lite-noise |
β lite-noise's figure is minified + gzipped, self-contained β the full installed footprint with zero runtime dependencies (2,755 B, npm run bundle-check). The other libraries' sizes are their published bundle sizes as listed on npm. And this figure buys more: ridged/billow multifractals, seamless noiseLoop, and tileable2 on top of the columns above.
Every sampler below exists twice: as a module function sharing one table, and as a method on a Noise instance owning its own table. Same signatures, same values at the same seed.
createNoise(seed?) β Noiseβ an independent noise field owning its own permutation table. The way to run two consumers without collision.new Noise(seed?)β the class behindcreateNoise, exported forinstanceof/ typing.noise.seed(seed) β thisβ re-seed an instance in place; affects only that instance.noise.simplex2 / simplex3 / fbm2 / fbm3 / ridged2 / billow2 / noiseLoop / tileable2 / curl2 / curl3 / warp2 / fillField2 / tileableField2β instance methods mirroring the module functions below.
simplex2(x, y) β numberβ 2D Simplex, approx.[-1, 1]simplex3(x, y, z) β numberβ 3D Simplex, approx.[-1, 1]fbm2(x, y, octaves?, lacunarity?, gain?) β numberβ unrolled 2D FBM, zero alloc.octaves β₯ 1;octaves = 0returns0, notNaN.gain β₯ 0(per-octave amplitude decay, the standard FBM domain, typically0..1) β a negative gain alternates the amplitude sign and pushes the output past~[-1, 1]. This domain caveat is shared byridged2/billow2.fbm3(x, y, z, octaves?, lacunarity?, gain?) β numberβ unrolled 3D FBM, same octaves andgain β₯ 0contract.ridged2(x, y, octaves?, lacunarity?, gain?) β numberβ ridged multifractal,(1 β |simplex|)Β²per octave over the shared FBM skeleton. Sharp creases reaching the unit ceiling; range ~[0, 1], skewed high. Same octaves contract. The[0, 1]range holds forgain β₯ 0(the FBM domain); a negative gain voids it (see FBM note below).billow2(x, y, octaves?, lacunarity?, gain?) β numberβ billow,|simplex|per octave. Soft absolute-value fold piling at zero; range ~[0, 1]. Same octaves andgain β₯ 0contract.noiseLoop(t, radius?) β numberβ seamless periodic 1D noise on a circle of radiusradius(default1).noiseLoop(0) === noiseLoop(2Ο)exactly and the derivative matches at the seam β drivetover0..2Οfor a perfect loop.tis reducedmod 2Ο.tileable2(x, y, periodX, periodY) β numberβ tileable 2D noise over[0, periodX) Γ [0, periodY). Opposite edges are byte-identical (tileable2(0, y) === tileable2(periodX, y)), so tiles are seamless by construction. Foursimplex2samples; the blend narrows the extremes slightly inside[-1, 1]. Precondition:periodX, periodY > 0β a period of0divides by zero and returns a non-finite value (NaNorΒ±Infinity) (unguarded on the hot path; a zero tile size is a caller error, not a data value).
curl2(x, y, out) β outβ divergence-free 2D vector.out = { x, y }. Typical magnitude for scale ~0.005 inputs: mean|v| β 3.4. Scale before wiring to particle velocities.curl3(x, y, z, out) β outβ divergence-free 3D vector via Bridson-style offset vector-potential (twelvesimplex3samples).out = { x, y, z }. Typicalmean |v| β 3.8,max β 10.6. Divergence residual ~0.6 % of|v|β finite-difference truncation floor.warp2(x, y, strength, out) β outβ Quilez-style domain warp; writes warped coords intoout = { x, y }. Compose withfbm2(out.x, out.y).strength = 0returns the input unchanged.
fillField2(out, w, h, opts?) β outβ bake aw Γ hFBM heightfield into a caller-suppliedFloat32Array/Float64Array.opts(all optional):scale,octaves,lacunarity,gain,ox,oy,normalize. Row-incremental coord stepping (no per-cell multiplies), zero allocation. The raw fill is amplitude-normalised but ~[-0.84, 0.82]at the defaults;normalize: truedoes a second in-place pass to exact[0, 1](a colour ramp usually wants this).outis caller-owned and written start-to-end β don't alias it with anything read during the call.tileableField2(out, w, h, opts) β outβ bake a seamless, multifractalw Γ hfield into a caller-suppliedFloat32Array/Float64Array. It sums octaves oftileable2at harmonic periods and computes the per-cell coordinate by multiply. Exact-wrap precondition: opposite edges wrap bit-exact (===) when the grid is aligned βw * (periodX/w) === periodXin float64, i.e. a power-of-two width with an integer period (the safe seamless recipe); for non-aligned dims the seam is seamless only to within float epsilon (~1e-14). Grid alignment alone governs this β notlacunarity/gain.opts:model('fbm'|'ridged'|'billow', default'fbm'), requiredperiodX/periodY, plus optionaloctaves,lacunarity,gain,scale,ox,oy,normalize. Fails closed at setup, beforeoutis written: an unknownmodelthrows aRangeErrornaming the valid set, and a non-finite or non-positiveperiodX/periodY(including an omitted required period, orInfinity) throws. Zero allocation once options are read;normalize: trueremaps to exact[0, 1]as infillField2.
seedNoise(seed?)β build the shared module permutation table. Call once, or call again to re-seed. Auto-seeded with0on module load. Warns once in dev builds if called more than once (silent underNODE_ENV === 'production'); for independent fields usecreateNoiseinstead.
The zero-allocation claim on every hot path (simplex2, simplex3, fbm2, fbm3, ridged2, billow2, noiseLoop, tileable2, curl2, curl3, warp2, and the fillField2 / tileableField2 bakes incl. normalize) β across both the module functions and the instance methods β is gated by @zakkster/lite-gc-profiler:
npm run torturePoint samplers use measureOps / checkOps with stabilize: true so heap deltas reflect the surviving-allocation delta (retention), not transient churn. Rules: maxBytesPerOp: 2 (V8's inline-cache / feedback-vector noise floor per the profiler docs) plus maxMajorsPerKOp: 0 β a real allocation crosses both bars, V8 noise crosses neither. Heavy fillField2 bakes are gated on major-GC count and ArrayBuffer retention instead. NOISE_TORTURE_BREAK=1 npm run torture injects a leak and must exit non-zero β proof the gate can bite.
seed 42 produces byte-identical fields across versions unless a kernel change is deliberate. The test suite commits FNV-1a hashes of three baked fields:
- 256Γ256 default
fillField2βddef5970 - 128Γ128
warp2 + fbm2βca4f9f1e - 32Γ32Γ8
curl3slab β1ac7a518 - 128Γ128
ridged2β2342c230 - 128Γ128
billow2βacf96355 - 64Γ64 period-4
tileable2βb6d00662 - 720-sample
noiseLoopβ2cfa58f8 - 64Γ64 period-4
tileableField2fbmβ8f34c3b8,ridgedβe117b1a8,billowβb5d78012
Any change to those numbers is a breaking change and requires a CHANGELOG entry. Regenerate via npm run goldens when the change is intentional.
Runnable, CI-tested integration recipes live in examples/ (not shipped in the tarball). Each adds zero runtime dependency β the peers are dev-only, and each recipe holds its own createNoise instance, so two on one page never collide (the reason the instance API came first).
curl2β@zakkster/lite-particles(curl-advection.mjs) β advect particles through a curl flow field. Needs no API from either side: anonUpdate(p, dt)hook samplescurl2into a pre-allocated{x,y}and writesp.vx/p.vy. Both sides are 0 B/call, so the loop is zero-alloc.fillField2β@zakkster/lite-gl(field-to-gl.mjs) β a bakedFloat32Arrayis a GL instance buffer. Packs oneLAYOUT.POINTper cell (stride 8, andLAYOUT.POINT === lite-particles POINT_STRIDE) and proves the handoff with a bit-exact round-trip. The live GPU upload/readback is lite-gl's own tested territory.- Curl ambient behavior (
ambient-curl.mjs) β aregisterBehavior('CURL', β¦)whosetickadvances particles through a curl field. Registration + physics are CI-tested headless; the canvas render is browser-only. ambient-fx keeps its zero-dep pledge β a recipe, not a dependency. tileableField2β@zakkster/lite-gradient-studio(tileable-to-gradient.mjs) β paint a seamless tile through a gradient LUT. A grid-alignedtileableField2field is bit-exact seamless;bakeGradientToLutβsampleLutis a pure per-cell map, so the coloured tile inherits the exact wrap. Proven headless by a zero colour-seam-gap on the wrap column (fbm/ridged/billow); the returned RGBA-LEUint32ArrayisImageData-ready. The recipe fails closed on a non-grid-aligned bake rather than ship an epsilon-off seam.
N1 composability is asserted at the integration level: two instance-backed flows interleaved in one process produce byte-identical trajectories to each run alone. npm test runs all of this against the installed peers.
npm run benchMeasures three ways to bake a 256Γ256, 6-octave FBM heightfield (naive alloc-per-bake, row-step reused buffer, fillField2), median-of-20 with a 3-rep warmup. Bench output carries a machine stamp (Node version, platform, CPU) so numbers trace back to the hardware they were measured on.
The dominant cost is ~393K simplex2 calls per bake (256 Γ 256 Γ 6 octaves). Saved two-multiplies-per-cell register as ~1.03β1.07Γ against that β the row-step optimisation is numerically clean (drift < Float32 epsilon over 512Γ512) but not a headline speedup. The real win of fillField2 is API surface (one call, no user-written loop) and buffer reuse.
Full declarations in Noise.d.ts β includes Vec2, Vec3, and FillField2Options.
See llms.txt for AI-optimized metadata and usage examples.
MIT. Copyright (c) Zahary Shinikchiev.