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

@wave3d/core

v0.9.0

Published

Framework-agnostic 3D gradient-wave renderer + config model (the engine behind Wave Studio)

Readme

@wave3d/core

The framework-agnostic renderer and config model behind Wave Studio: glossy, twisting 3D gradient waves driven by one JSON config.

A wave is a strip swept along a curve, with a gradient running down its length and a satin sheen. The renderer and every export read from the same WaveConfig, so what you design is exactly what ships.

💡 Fastest way in: design a wave in Wave Studio, hit ⟨⟩ Export code, and copy the snippet. This package is what that snippet imports.

📦 Install

pnpm add @wave3d/core three   # three is a peer dependency

three is a peer dependency (>=0.180 <1); add @types/three for TypeScript.

Drop-in wave

The default entry is poster-first: it shows a poster immediately, then upgrades to live WebGL only when the browser can run it. It falls back to the poster on no-WebGL, Save-Data, reduced motion, or a lost context, and three.js is code-split out of the initial load.

import { createWave } from "@wave3d/core";

const handle = createWave(document.getElementById("wave"), config, {
  poster: "/wave.png",
});
// handle.set(nextConfig) · handle.pause() · handle.play() · handle.snapshot() · handle.destroy()

Capture the live frame as an image you can host and reuse as the poster for reduced-motion, no-WebGL, and Save-Data visitors:

const blob = await handle.snapshot(); // resolves null until running. options: { type?, quality?, transparent? }

Single <script> from a CDN

three is bundled into the standalone build, so this needs nothing else:

<script type="module">
  import { mountWave } from "https://esm.sh/@wave3d/core/standalone";
  mountWave(document.getElementById("wave"), {/* your exported config */});
</script>

Interactive waves

Interactivity is per wave and off by default — omit it and the wave (and its compiled shader) is byte-for-byte unchanged. Each wave takes an optional interaction with three parts:

  • hover — the cursor-follow field: agitate (local churn), push (a ± dome that repels or attracts the surface), wake (a trailing trough behind the moving cursor), thin, hueShift, lighten, and smoothing (this wave's follow-lag — vary it across a stack for a parallax drag).
  • pressripple (rings from a click / tap).
  • bindings — inputs that drive this wave's params: { source, target, from?, to }.
// React — the flat `interaction` prop targets the first wave:
<Wave3D
  interaction={{
    hover: { agitate: 6, thin: 0.4 }, // local churn + strand-thinning under the cursor
    press: { ripple: 6 }, // click ripples
    bindings: [{ source: "hover", target: "displaceAmount", to: 12 }], // taller folds while hovered
  }}
/>

Each binding rests at the authored value (from defaults to it) and moves toward to as its input rises 0→1 — sources: scroll, hover, pointerX / pointerY, pointerSpeed, press, scrollVelocity, appear, tiltX / tiltY, and custom:*.

tiltX / tiltY are the device's orientation sensor, normalized the way a ball would roll on the screen and resting at 0.5 in whatever pose the reader was already holding. Binding either one arms the sensor (no listener is attached otherwise); interaction.tilt tunes it — range, smoothing, invertX / invertY, and pointer: true to let tilt stand in for the cursor a phone doesn't have. iOS gets no tilt on purpose: Safari gates the sensor behind a modal permission dialog and nothing here opens one, so a tilt-bound scene on an iPhone reads 0.5 on both axes and looks exactly like one with no tilt. Treat it as an enhancement some phones don't get. A page that genuinely warrants asking can call handle.enableTilt() from a tap.

Shared inputs (one cursor + scroll) and scene-wide effects live on the scene-level interaction:

createWave(el, {
  interaction: {
    radius: 0.3, // shared pointer falloff (fraction of viewport height)
    touch: false, // follow touch pointers too
    bindings: [{ source: "scroll", target: "timeOffset", to: 40 }], // scrub the whole wave with scroll
  },
  waves: [/* each wave carries its own `interaction` (hover / press / bindings) */],
});

Pair scroll → timeOffset with a low speed for a wave that scrubs with the page instead of drifting on its own. Feed your own signal with a custom: source and setInteractionInput:

<Wave3D
  interaction={{ bindings: [{ source: "custom:audio", target: "displaceAmount", to: 30 }] }}
  onReady={(r) => {
    analyser.onLevel = (v) => r.setInteractionInput("audio", v); // v in 0..1
  }}
/>

Entry points

| Import | What | | ------------------------- | -------------------------------------------------------------- | | @wave3d/core | poster-first shell (createWave / mountWave) + config model | | @wave3d/core/renderer | the raw WaveRenderer and WaveGeometry (static three) | | @wave3d/core/presets | the built-in PRESETS | | @wave3d/core/studio | StudioWaveRenderer + randomizers | | @wave3d/core/standalone | single-file build with three bundled (the CDN entry) |

Framework wrappers

Prefer a component? Reach for the wrapper for your framework:

Credits

Built by Amir Abushanab.

License

MIT