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

@terrakin/figure

v0.1.0

Published

Characters for three.js worlds and games: a loader that turns a rigged .glb into a body that plays clips, shows feelings, blinks and looks around, with the body spec and a check for a body file.

Readme

@terrakin/figure

Characters for three.js worlds and games, from Terrakin.

A rigged .glb that follows the body spec becomes a Body: it plays clips, shows feelings on its face, blinks, breathes, and turns its head to look at things. A scene holds a body and never asks what kind it is.

This is an early release. The plan for the rest (a flat body for characters with no model yet, and a mover for games) is RFC 0031. Until 1.0, a minor version can change anything.

Two entries

  • @terrakin/figure has no dependencies. It holds the body spec, the check for a body file, the feeling names, and the pure rules (which clip to play, which face keys to hold). A server can import it.
  • @terrakin/figure/three draws. It needs three (0.160 or later) installed beside it.

Loading a body

import { loadBody } from "@terrakin/figure/three";

const body = await loadBody("/bodies/plush.glb", { height: 0.75, id: "r_42" });
scene.add(body.object);

// Once a frame:
body.update(seconds);

loadBody takes a URL or the file's bytes. height is how tall the body stands in your scene, whatever size the file is. id sets when this body blinks, so a crowd doesn't blink together.

The file is treated as outside data. It is checked against the spec and your caps before anything is drawn, nothing it points at outside itself is fetched, and a file that fails throws a BodyError whose problems are written by this package, never quoted from the file.

For KTX2 textures, pass your own KTX2Loader as ktx2, set up with its transcoder and your renderer. Meshopt compression and WebP textures need nothing.

Using a body

body.play("walk", { rate: 1.2 });      // loops until another clip is asked for
body.play("wave", { loop: false });    // once, then back to what was playing
body.feel("happy");                    // on the face and in the clip, until another feeling
body.lookAt(new Vector3(1, 0.6, 2));   // the head turns, within about 35 degrees
body.lookAt(undefined);                // and back
body.socket("socket_hand_r")?.add(lantern);
body.still = true;                     // reduced motion: no blink, breath, or bob
body.dispose();
  • play can be called every frame. Asking for the clip that is already playing changes nothing.
  • A clip the body lacks falls back. A walk bobs the idle, a hop squashes and stretches in code, and anything else plays idle.
  • feel holds the face keys the body has for that feeling, and falls back when one is missing (a sad face with no sad key half closes its eyes). laugh and surprised hop once. sleepy holds the sleep clip.

Walking

import { drive } from "@terrakin/figure";

const next = drive({ speed, grounded }, body);
body.play(next.clip, { rate: next.rate });

drive picks the clip for how the body is moving and the rate that keeps planted feet planted. It needs body.walkSpeed, which comes from the file (below). It never plays a walk slower than half speed or faster than 2.5 times; past either end the feet slide, so a game that needs to travel faster wants a faster clip.

The body spec

A body is one self-contained .glb.

  • Meters, feet on y = 0, centered on the y axis, facing +z with y up.
  • A skeleton with a Hips and a Head bone. The other shared bone names are in BONES; a body has the ones its shape has.
  • An idle clip. walk, wave, sit, sleep, hop, flutter and fly are used when present, and a body may carry more under its own names.
  • Empty nodes named in SOCKETS (socket_head, socket_hand_l and so on) where things it wears or holds attach.
  • Face morph targets named in extras.targetNames on the mesh that holds them, from EXPRESSIONS.
  • Optionally walk_speed_m_s in asset.extras: how fast the walk covers ground, in meters a second at the file's size, with the clip played in place.
  • No required extension outside EXTENSIONS.

Checking a file

import { checkBody, glbJson } from "@terrakin/figure";

const report = checkBody(glbJson(bytes), { triangles: 12_000, textures: 4 });
if (!report.ok) console.log(report.problems); // ["no idle clip", "more than 12000 triangles"]

glbJson reads the JSON out of a .glb and throws on anything else. checkBody takes that JSON and your limits and returns what the body holds (clips, bones, sockets, expressions, counts, walkSpeed), what keeps it from being drawn (problems) and what a loader falls back from (notes).

Neither throws on a document of the wrong shape, and a report never carries text from the file except names made of letters, digits and underscores. The check reads the JSON alone, so it can't tell which way a body faces or whether its feet are on the ground.

Feelings

FEELINGS is the fixed list of what a figure can show: neutral, happy, laugh, love, shy, surprised, sad, sleepy and thinking. Feeling is its type and isFeeling tells a name on the list from anything else. A feeling is a name, never text, and the list only grows. FEELING_LOOKS is what each one asks of a body.

License

MIT