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

@nachi-vfx/mesh-fx

v0.2.3

Published

Procedural Three.js VFX meshes and declarative NodeMaterial composition

Readme

@nachi-vfx/mesh-fx

Procedural effect meshes and a declarative Three.js NodeMaterial factory. The package is standalone: it depends on @nachi-vfx/tsl-kit and Three.js, but not @nachi-vfx/core.

import { fxMaterial, polarUV, slashArc } from '@nachi-vfx/mesh-fx';

const material = fxMaterial({
  color: '#66ddff',
  map: effectTexture,
  uv: polarUV().flow({ speed: [0.35, 0] }),
  dissolve: {
    texture: noiseTexture,
    overLife: [
      [0, 0],
      [1, 1],
    ],
    edgeColor: '#ffffff',
    edgeIntensity: 0.8,
    edgeModulate: 'map',
    uv: 'static',
  },
  fresnel: { color: '#2aa8ff', power: 2 },
  blending: 'additive',
});

const arc = slashArc({
  angle: 140,
  radius: 1.2,
  innerRadius: 0.6,
  taper: 0.8,
  segments: 48,
  material,
});

material.fx.setTime(2);
material.fx.setNormalizedLife(0.4);
material.fx.setOpacity(0.6);
scene.add(arc);

Factories include slashArc, ring, open cylinder, open cone, and magicCircle. The matching create*Geometry exports are useful when mesh ownership belongs to an application. slashArc and ring expose angle/radius UVs; cylinder and cone expose circumference/height UVs. magicCircle uses centered Cartesian primary UVs for polarUV() and publishes concentric angle/radius islands as uv1.

fxMaterial creates writable time and normalized-life uniforms when they are omitted. A numeric or omitted opacity likewise creates a writable opacity uniform; a TSL opacity node is composed at compile time instead, and setOpacity() rejects mutation. Pass TSL nodes through time and normalizedLife for standalone externally owned bindings. The @nachi-vfx/timeline adapter drives its effect-local clock through the writable controls, so timeline materials must omit time; no wall clock is read implicitly.

By default, dissolve samples the same authored UV as map, preserving the original composition. Set dissolve.uv: 'static' for the geometry's unmodified UV, or provide a separate polarUV() / uvFlow() authoring value. edgeIntensity scales edge emission, while edgeModulate: 'map' multiplies it by map luminance (map is required); both default to the previous unmodulated edge.

For a stable hold phase, put the dissolve hold threshold below the minimum value of the noise texture, so a bright edge contour does not remain across the mesh. Match noise frequency to mesh scale: large meshes need correspondingly finer noise, while small meshes need coarser noise to keep the same apparent feature size.

Three.js is an exact [email protected] peer. The package is ESM-only and declares sideEffects: false.

Geometry ownership with timeline clones

Procedural mesh factories return an application-owned mesh, geometry, and material. When that mesh is adapted by @nachi-vfx/timeline, timeline clones the mesh object and material controls but keeps the exact same BufferGeometry reference. Geometry is therefore an immutable borrowed resource: attribute, index, group, bounding-volume, or drawRange mutation through either the source or any clone is immediately visible to every other clone.

The application/resource owner must keep the geometry alive while the source definition or any timeline instance can use it, and call geometry.dispose() only after all such instances and any retained prepared object have been released. Timeline release/error cleanup disposes each cloned material, but never the borrowed geometry or the source mesh/material. Do not dispose geometry from an instance lifecycle callback; if mutable per-instance geometry is required, manage separate application-owned resources outside this shared timeline adapter contract.

Blender VAT runtime

applyVat() applies a one-frame-per-row Vertex Animation Texture to any single Three.js NodeMaterial mesh. It creates a writable standalone time uniform when time is omitted, or accepts an externally owned TSL clock for effect-local playback.

import { applyVat } from '@nachi-vfx/mesh-fx';

const vat = applyVat(mesh, {
  positionTexture, // FloatType or HalfFloatType, NoColorSpace
  normalTexture, // optional; Blender normals default to 0..1 encoding
  frameCount: 48,
  fps: 24,
  interpolation: 'linear',
  positionEncoding: 'remapped',
  positionRange: { min: -0.82, max: 1.14 },
});

vat.setTime(0.5);
vat.setFrame(20);

The default compatibility profile follows the Blender Extensions VAT exporter: texture X is the vertex index, texture Y is the frame, frames are ordered top-to-bottom, positions are offsets, the reported normalization range is one global min/max pair, normals decode from [0, 1], and Blender XYZ is sampled as Three.js XZY. This default axisMap: 'xzy' is the exporter's mirrored axis swap; use axisMap: 'xz-y' for the right-handed Z-up to Y-up rotation (x, z, -y). The generated vertex_anim lookup UV is expected as uv1; use the explicit vertexLookup: 'vertex-index' fallback only when mesh and VAT were generated in identical vertex order. frameOrder defaults to 'top-to-bottom'. It describes GPU texel-row order after loading: load VAT textures with texture.flipY = false; if a loader has already flipped rows, use the opposite frameOrder. Set 'bottom-to-top' for an unmodified exporter Y-flip output.

Position VATs must be linear float/half-float textures and their width must exactly match the mesh's position-attribute count. The v1 runtime deliberately rejects wrapped/cropped layouts and normalized PNG position textures; convert them to a non-color float texture or export OpenEXR. Variable topology is not representable. Dynamic VAT bounds disable frustum culling by default; applications may opt out after supplying conservative mesh bounds.

An external numeric VatConfig.time must be finite. loop and disableFrustumCulling are strict booleans for untyped JavaScript input; strings are never interpreted by truthiness. Validation runs before VAT nodes or mesh culling state are mutated.

applyVat() also retains internal, cloneable binding metadata for the timeline adapter. A timeline clone rebuilds graph-reachable VAT layers in order instead of trusting Three's generic NodeMaterial clone, which can alias the source VAT graph. Position reachability starts at the latest absolute layer, or the first layer when all are offsets; normal reachability is the latest layer that supplies a normal texture. Their ordered, de-duplicated union owns the active controls. Package-owned VAT clocks become independent uniforms for the source and every clone; position/normal textures and explicit external TSL clock nodes keep their normal shared-reference ownership. A current package-owned source time is snapshotted when the clone is created, then timeline play() resets the clone to element-local time zero. Detached metadata is compacted without disposing caller-owned textures or controls, and clone rebuild skips detached graphs entirely.

Clone metadata remains active per channel only while the mesh still uses the material and the exact position or normal root installed by applyVat(). Replacing a final root after VAT application makes that channel an externally authored, shared clone binding; timeline neither rebuilds its detached VAT graph nor writes its detached clock. The other channel remains independently active. Replacing only the normal root between calls preserves the accumulated VAT position layers, while replacing the position root starts a new VAT chain from that authored root. Replacing both roots or the material leaves no stale timeline-owned VAT clock. Mutating inside an unchanged root object is not detectable; replace the root reference when transferring ownership.

Standalone behavior is unchanged: controls.setTime() validates a non-looping clip's authored time range, and external numeric/TSL clocks remain non-writable. Timeline uses a package-internal writer that still requires a finite, non-negative element-local time but permits the element to outlive a non-looping clip; the VAT shader then holds its final frame. VAT textures are always caller-owned: timeline release, failed clone/prepare, and retained preparation never dispose them.