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

@uxfront/scene

v0.4.1

Published

The scroll-driven WebGL particle scene behind the uxfront.com family of homepages: a pluggable formation pipeline, a formation catalog and a framework-agnostic scroll controller.

Readme

@uxfront/scene

The scroll-driven WebGL particle scene behind the uxfront.com family of homepages (UXFront, Open Components, Unframework, Styleframe, Inkline). One GPU particle system turns into a new formation for each section of the page as you scroll. The package has no framework dependency: @uxfront/ui wraps it in Vue components, and any other page can drive it directly.

  • A pipeline. Morphs between formations under curl noise, an intro that rises from a floor line, a cursor lens, a click shockwave, a mirror floor, light trails, bloom and a filmic composite.
  • A catalog. Eight formations: monolith, plates, prism, tokens, ink, corridor, stack and threshold.
  • A controller. createExperience maps the page's sections to formations, reveals copy, steps through highlights and pins DOM labels to the formations.

Pages stay fast. The engine and each formation's GLSL are separate chunks, imported only once the page is idle. Shaders compile without blocking the main thread. Slow GPUs get fewer particles at a lower resolution, and without a capable GPU (no WebGL2, a software rasterizer, a lost context) the state switches to "fallback", so the page can show a static image instead.

Install

pnpm add @uxfront/scene

In a Nuxt app, use @uxfront/layer-ui instead: it wires up everything below.

Usage

Mark up one section per formation, in order, then start the experience on the client:

<canvas id="stage" style="position: fixed; inset: 0; width: 100%; height: 100lvh"></canvas>
<main id="page">
  <section data-stage>…</section>
  <section data-stage data-steps="3">…</section>
  <section data-stage>…</section>
</main>
import { createExperience } from "@uxfront/scene";
import { corridor, monolith, plates } from "@uxfront/scene/formations";

const experience = createExperience({
  root: document.getElementById("page")!,
  canvas: document.querySelector("canvas")!,
  formations: [
    monolith({ label: "Foundation" }),
    plates({ label: "Open Components" }),
    corridor({ label: "Build on it" }),
  ],
});

experience.subscribe(({ chapter, status, motion }) => {
  // Update a HUD, a nav, the fallback…
});
experience.setMotion(false); // pause the clock
experience.destroy();

state holds chapter (the formation held), activeId (its section's id), status (idle, loading, ready or fallback), motion, particles and fps. Motion starts off when the visitor prefers reduced motion.

Page contract

| Markup | Effect | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | [data-stage] under root | One section per formation, in DOM order, about a screen tall. It holds its formation while it fills the screen, give or take HOLD_MARGIN. | | data-steps="n" on a section | While it holds its formation, the section gets data-step="0…n-1" as it is read, to highlight [data-step-item]s, each once it's on screen. | | [data-reveal] under root | Gets .is-in once scrolled into view. | | [data-anchor="key:id"] under anchorRoots | Positioned (transform, opacity, visibility) over the formation's anchor while it shows. | | [data-progress] under root | Gets --ux-progress: page scroll, 0 → 1. |

Sections scroll freely, so don't pin them: the copy should move with the page. The scene holds a formation from a quarter of a screen (HOLD_MARGIN) before its section fills the screen until a quarter after, and morphs into the next formation in between, while both sections share the screen.

Portrait, squarish and small screens use each formation's mobile framing, which leaves the top of the screen to the formation. Lay the copy out with the same breakpoints, exported as STACKED_QUERY.

Catalog

Every factory takes { label, key?, views?, look? }. label is shown in the HUD. views overrides the framing, for example to put a formation on the other side of the copy: tokens({ label, views: { desktop: { shift: [0.42, 0.02] } } }). key is only needed to use the same formation twice.

| Formation | Shows | Anchors | | ----------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | monolith | A 1:4:9 slab of light over a mirror floor, the UXFront mark inlaid in it. | None | | plates | Three glass plates with line art, drifting apart. art swaps the drawings. | plates:0–plates:2, at the plates' edges | | prism | A beam split into seven rails that grow as the chapter is read. | prism:0–prism:6, at the rails' ends | | tokens | A framed token matrix re-themed in waves. next() skips to the next theme. | tokens:theme, writes into [data-theme-name] | | ink | Coloured inks poured into water. Scrolling speeds the pour up, or drives it without motion. | None | | corridor | A corridor of streaking light with long trails. Suits a closing section. | None | | stack | Seven glass cards dealt as the chapter is read, one per rail, each holding the same component. art swaps it. | stack:0–stack:6, in the cards' headers | | threshold | The prism's seven beams cross a soap film of light and converge into one white stream. | threshold:stream, above the stream |

Writing a formation

A formation is a small synchronous description plus a lazily loaded shader:

import { defineFormation } from "@uxfront/scene";

export const rings = (label: string) =>
  defineFormation({
    key: "rings",
    label,
    views: {
      desktop: { eye: [0, 1, 10], target: [0, 0, 0], fov: 30, span: [2, 2], shift: [0.3, 0] },
      mobile: { eye: [0, 1, 10], target: [0, 0, 0], fov: 34, span: [2, 2], shift: [0, 0.5] },
    },
    look: { bloom: 1.1 },
    shader: () => import("./rings.shader").then((m) => m.shader),
  });
// rings.shader.ts
import type { FormationShader } from "@uxfront/scene";

export const shader: FormationShader = {
  glsl: /* glsl */ `
uniform float $spin;

Particle $main(uint id, float sel, Frame f) {
  uint s = seedOf(id, 0x5EEDu);
  float a = rnd(s) * TAU + $spin * f.time;
  Particle P = blank();
  P.pos = vec3(cos(a), sin(a), 0.0) * mix(1.0, 2.0, f.local);
  P.col = vec3(0.7, 0.8, 1.0) * 0.1;
  return P;
}
`,
  setup: ({ gl, get }) => gl.uniform1f(get("spin"), 0.2),
};
  • $name identifiers are private: the scene rewrites them to key_name, so formations never clash.
  • $main returns the position, colour, size and refl (mirror reflectivity) of particle id. sel spreads evenly over [0, 1) across the particles, to split them between the parts of a formation.
  • Frame f holds local (progress while the section holds the formation: 0 before, 1 after), approach (0 → 1 while the section scrolls in) and time.
  • Shared helpers: hashU, rnd, seedOf, gauss, noised, curl, bitangent, bezier3, rrectPoint, shapePoint, PI, TAU and FLOOR_Y.
  • glslFloat, glslVec and glslArray write numbers into the GLSL as constants. packShapes packs line art (segments, rounded rectangles, circles, discs and fills) into rows for shapePoint, which places a particle on it. stack and plates draw their art this way.
  • A formation that follows the prism can take its RAILS and SPECTRUM from @uxfront/scene/formations, so rail i keeps its colour.
  • setup uploads uniforms once. update(uniforms, frame) runs every frame with the formation's local, approach, presence, time, dt, motion and scrolled.
  • anchors pin DOM labels: place(progress, out) writes a world position and returns extra visibility, and render(el, progress) can update the label's content.

composeParticleShader builds the complete vertex shader, handy for testing a formation's GLSL.