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

@taipeistudio/pixel-rig

v0.6.0

Published

Procedural, animated pixel-art characters from a pose rig. Deterministic, no image assets.

Readme

pixel-rig

Procedural, animated pixel-art characters from a pose rig.

No image files, no AI services: every frame is drawn by code.

npm version license types PixiJS v8


pixel-rig renders side-view humanoid sprites from two plain-JSON inputs: a look (what the character looks like) and a pose (joint angles). Because every frame comes from the same rig, any combination of looks and animations stays visually consistent, and the same input always produces the same pixels.

Contents

Features

  • Consistent shading: 5-tone hue-shifted colour ramps, lambert shading, crease shadows and selective outlines
  • Configurable humanoid rig: gender, build, stature, skin, 7 hair styles, eyes, facial hair, handedness, headband and outfit colours (top, trim, bottom, shoes, accent, shorts/skort/trousers)
  • Football kits: shirt patterns (stripes, hoops, halves, sash), sock colours, knee-high socks, long sleeves and goalkeeper gloves
  • Basketball kits: sleeveless jerseys, knee-length shorts, crew socks, a shooting sleeve, and a free height scale for very tall players
  • Pose system: pose blending and animations, with a generic set included (idle, run, jump, lunge, dive, cheer, slump)
  • Rear-view rig for 2.5D games: a rider on a motorbike seen from behind (five bike kinds, lean, steering, punches, kicks, weapon swings, helmets), in the same style and scale as the side view
  • On-foot front and back views of the same riders, with a run cycle
  • Top-down vehicles from solids: motorbikes (with riders, pillion passengers, parcel towers, blinkers, fallen) and cars (sedan, hatch, MPV, angkot, pickup, van) seen from a high angle at any heading, built from little 3D boxes, balls and cylinders that are ray-cast into pixel art: the low-poly look in pixels
  • Art styles: the default look plus a cozy farm-sim style (Stardew Valley-like), NES, Game Boy and PICO-8 looks, or your own; a PixiJS filter styles flat scenery to match
  • Finer pixels on request: scale: 2 renders any rig with four times the detail
  • Pluggable held items: racket() and sword() included, or write your own
  • Props: pre-rendered rotations for small sprites such as projectiles, balls and shuttlecocks
  • Deterministic and save-friendly: looks are plain JSON; the same input always yields the same pixels
  • Integrations: canvas helpers (portraits, sprite-sheet PNG export) and an optional PixiJS v8 adapter
  • Zero runtime dependencies, ESM only, fully typed

Installation

npm install @taipeistudio/pixel-rig

pixel-rig ships as ES modules and is intended to be used with a bundler (Vite, webpack, esbuild, etc.). pixi.js is an optional peer dependency, needed only for the @taipeistudio/pixel-rig/pixi entry point:

npm install pixi.js

Quick start

Canvas

import { ANIMS, DEFAULT_LOOK, racket, renderAnims, toScaledCanvas } from "@taipeistudio/pixel-rig";

const look = { ...DEFAULT_LOOK, hair: "ponytail", gender: "female" } as const;
const frames = renderAnims(look, ANIMS, { item: racket() });

document.body.append(toScaledCanvas(frames.idle[0], { scale: 4 }));

PixiJS v8

import { AnimatedSprite } from "pixi.js";
import { ANIMS, randomLook, sword } from "@taipeistudio/pixel-rig";
import { bakeCharacter } from "@taipeistudio/pixel-rig/pixi";

const sheet = bakeCharacter(randomLook(), ANIMS, { item: sword(), cacheKey: "sword" });

const hero = new AnimatedSprite(sheet.textures.run);
hero.anchor.set(sheet.anchor.x, sheet.anchor.y); // anchored at the feet
hero.animationSpeed = ANIMS.run.fps / 60;
hero.play();

// Characters face +x; mirror with hero.scale.x = -1.

Core concepts

| Concept | Description | | --- | --- | | Look | The character's appearance. Plain JSON, safe to store in save files. randomLook(rng, { gender, outfit, skins, hairColors }) generates casts; pass a seeded rng for reproducible crowds. | | Pose | Absolute joint angles in degrees (0 = down, 90 = forward, 180 = up, 270 = back) for lean, head, item arm (arm), off arm, near/far legs, feet, held-item direction, height off the ground, mouth and hair sway. Feet are planted automatically unless air > 0. | | Anim | Key poses plus steps (in-between frames per segment), fps and loop. | | Frame | RGBA pixels plus anchors: origin (feet), hand, and item (the point a held item returns, e.g. a racket head). Use these to line up hits, muzzle flashes or pick-ups. | | HeldItem | An object that declares its materials and draws itself at the hand. |

Sports kits

Kit fields are optional properties of Outfit. When omitted, a look renders exactly as it did before kits were introduced (0.1).

| Field | Values | Effect | | --- | --- | --- | | pattern | "plain" (default), "stripes", "hoops", "halves", "sash" | Painted on the torso; collar and side seam stay in trim | | patternColor | hex (default: trim) | Colour of the stripes, hoops, front half or sash | | socks | hex (default: off-white) | Sock colour; the stripe near the sock top stays accent | | highSocks | boolean | Knee-high football socks (same as sockHeight: "knee") | | sockHeight | "ankle" (default), "crew", "knee" | Sock length; crew socks reach mid-calf | | sleeves | "short" (default), "long", "none" | Long sleeves in top with a trim cuff at the wrist; none is a sleeveless jersey with a trimmed armhole | | bottomStyle | "shorts", "skort", "long" | long: baggy knee-length basketball shorts | | armSleeve | boolean | Compression sleeve on the item arm, in accent | | gloves | hex | Goalkeeper gloves on both hands (slightly larger hands) | | barefoot | boolean | Bare feet: skin to the toes, shoes and socks ignored |

import { DEFAULT_LOOK, type Look } from "@taipeistudio/pixel-rig";

const striker: Look = {
  ...DEFAULT_LOOK,
  outfit: {
    ...DEFAULT_LOOK.outfit,
    top: "#f4f1ea", pattern: "stripes", patternColor: "#1d1d26",
    bottom: "#1d1d26", socks: "#1d1d26", highSocks: true,
  },
};

const keeper: Look = {
  ...striker,
  outfit: { ...striker.outfit, top: "#f2c230", pattern: "plain", sleeves: "long", gloves: "#34c46a" },
};

Look.height (optional number) scales limbs and torso and overrides stature (short 0.93, average 1, tall 1.07). Above about 1.07, jumping poses need a taller frame than DEFAULT_FRAME, e.g. { width: 112, height: 128, groundY: 124, originX: 56 }.

const centre: Look = {
  ...DEFAULT_LOOK,
  height: 1.12,
  build: "slim",
  outfit: { ...DEFAULT_LOOK.outfit, sleeves: "none", bottomStyle: "long", sockHeight: "crew", armSleeve: true },
};

Stripes are ~2 px bands and hoops ~3 px; in side view, "halves" splits the shirt front and back. Kits add two body material slots (M.pattern, M.glove), so BODY_SLOTS is 14 (previously 12). Held items use BODY_SLOTS + i and are unaffected.

Custom poses and animations

import { anim, pose, POSES } from "@taipeistudio/pixel-rig";

const overheadWind = pose({ lean: -10, head: -14, arm: { a: 205, b: 262 }, item: 330 });
const overheadHit = pose({ lean: 8, arm: { a: 162, b: 150 }, item: 135, mouth: "open" });

const smash = anim([overheadWind, overheadHit, POSES.ready], { fps: 18 });

Custom held items

import { add, dir, ramp, type HeldItem } from "@taipeistudio/pixel-rig";

export const torch: HeldItem = {
  materials: () => [
    { ramp: ramp("#6b4a2b"), outline: true },
    { ramp: ramp("#ffb030"), outline: false },
  ],
  draw({ buf, hand, angle, depth, part, mat }) {
    const v = dir(angle);
    const top = add(hand, v, 9);
    buf.capsule(hand, top, 1, 1, depth, part, () => ({ mat: mat(0), tone: 2 }));
    buf.disc(add(top, v, 2), 2.2, depth + 0.1, part, ({ shade }) => ({ mat: mat(1), tone: shade > 0 ? 4 : 3 }));
    return top; // becomes frame.item
  },
};

Rear view: bikes and riders

For pseudo-3D racers and other 2.5D games, renderRider draws a bike from behind with the same materials, shading and scale as the side-view rig, so a character keeps its look when it gets off the bike.

import { DEFAULT_LOOK, mirrorRider, renderRider, riderPose, RIDER_POSES, type Bike, type Rider } from "@taipeistudio/pixel-rig";

const rider: Rider = {
  look: { ...DEFAULT_LOOK, outfit: { ...DEFAULT_LOOK.outfit, top: "#2b2d42", sleeves: "long" } },
  helmet: "full", // or "half", "none"
  helmetColor: "#d8262f",
  patch: true, // emblem on the back in the outfit accent colour
};
const bike: Bike = { kind: "underbone", body: "#d8262f", accent: "#f4f1ea", exhaust: "racing" };

renderRider(rider, bike); // riding straight
renderRider(rider, bike, riderPose({ roll: 14, steer: 0.7 })); // leaning into a right-hander
renderRider(rider, bike, RIDER_POSES.kick); // kick to the right
renderRider(rider, bike, mirrorRider(RIDER_POSES.kick)); // and to the left
renderRider(null, bike, riderPose({ roll: 85, air: 6 })); // riderless, lying on its side
  • Bikes: underbone, scooter, standard, sport, cruiser (BIKE_KINDS), with body and accent colours, optional seat and plate colours, a racing exhaust and a cargo box.
  • Poses: roll (degrees, about the tyre contact point), tuck, shift, head, steer, air, brake, and four limbs. A limb with hold: 1 stays on its grip or peg; at hold: 0 it follows its angles (0 = down, 90 = out to its own side, 180 = up). RIDER_POSES has ride, tuck, brake, punch, kick, swing, hit, footDown and cheer, all to the right; mirrorRider flips any pose. blendRider, riderAnim, riderFramePoses, renderRiderAnim and RIDER_ANIMS mirror the side rig's animation helpers.
  • Sponsors: rider.badges puts up to two marks (disc, bar or diamond, in any colour) on the back of the jacket and the first on the chest; bike.sticker adds stickers to the tail unit.
  • Gender shows in the rear and on-foot rigs: a woman has narrower shoulders, a waist, wider hips, a bust and a softer face with lashes and lips; a man has brows and a square jaw. Long hair (ponytail, bob, curly) comes out from under any helmet.
  • Held items work unchanged: pass { item } and set pose.item / pose.itemSide.
  • Frame: REAR_FRAME is 128 × 88 px with the rear tyre's contact point at (64, 84).
  • Bikes add material slots after the body's (RM, REAR_SLOTS); held items follow them.

On foot, front and back

renderWalker draws the same Rider off the bike, seen from the front or from behind: for a rider running back to a crashed bike, or anyone walking toward or away from the camera in a 2.5D scene.

import { renderWalker, runCycle, STAND_POSE } from "@taipeistudio/pixel-rig";

renderWalker(rider, STAND_POSE, { facing: "front" }); // standing, looking at the camera
runCycle().map((pose) => renderWalker(rider, pose, { facing: "back" })); // 8-frame run, from behind

A WalkPose is each leg's lift (0 planted to 1 knee high), each arm's swing (-1 back to 1 forward) and a bob. raise: { side, angle } holds one arm straight instead (waving a flag, pointing); pass { item } to put a held item in that hand. It uses the default frame, with the origin on the ground between the feet.

Top-down vehicles and solids

For high-angle 2D games (car parks, towns, traffic) pixel-rig draws vehicles from solids: boxes, balls (ellipsoids) and cylinders in metres, ray-cast pixel by pixel from a fixed oblique camera, shaded from their true normals and then outlined and coloured by the same PixelBuffer.resolve() and art style as the rigs. Any heading works, so turning vehicles stay crisp: pre-render the headings you need (16 is plenty) and pick one with headingIndex.

import { headingIndex, renderCar, renderTopBike } from "@taipeistudio/pixel-rig";

const view = { heading: 90, ppm: 36 };            // 90 = nose down the screen, toward the viewer
const car = renderCar({ kind: "angkot", body: "#2f9e44", accent: "#f2d230" }, view, { signal: "left" });
const bike = renderTopBike(
  { kind: "underbone", body: "#2350a8", accent: "#eeeeee", cargo: "#c8963c" },
  { heading: 0, ppm: 36 },
  { rider: { look, helmet: "half" }, passengers: [], cargoHeight: 1.6, style: "harvest" },
);
const frames = Array.from({ length: 16 }, (_, i) => renderCar(myCar, { heading: i * 22.5, ppm: 36 }));
frames[headingIndex(carHeadingDegrees, 16)];

| Function | Draws | | --- | --- | | renderTopBike(bike, view, o) | A Bike (the rear rig's, same kinds and colours) with an optional rider, up to two passengers, a cargoHeight parcel tower, a signal blinker, or fallen on its side | | renderCar(car, view, o) | A Car: kind (sedan, hatch, mpv, angkot, pickup, van), body, accent, roof, a flag on the wing, a rack load; signal (left, right, both) and brake lamps | | renderSolids(solids, materials, view, o) | Your own model: anything built from Solids | | bikeSolids, carSolids | The solids behind the two above, to extend (add a ring light, swap the scooter for a flying carpet) | | projectSolid(view, x, y, z) | The camera's projection, to place sprites and shadows on your ground |

The camera (SolidView): heading in screen degrees (0 = nose to the right, 90 = toward the viewer), ppm pixels per metre, ground (default 0.7) squashes ground depth, height (default 0.85) scales heights, roll leans the whole model. Model space is metres with x forward, y to the model's left and z up; the frame's origin is the model's origin (the ground under the vehicle's middle).

A Solid is { shape, at, size, turn?, mat, part? }: size holds half extents (a cylinder runs along its own x), turn is yaw, pitch and roll in degrees, and mat is a material index or a painter (hit) => { mat, tone } | null that sees the hit point in the solid's own axes (u, v, w, each -1…1), its normal and a ready shade, so windows, stripes, lamps and patterns are painted per face. Solids with different parts get a crease where one overlaps another, as the rigs' limbs do.

Art styles

Every renderer takes style: a preset name or your own ArtStyle object. A style changes how the drawing is coloured, never the pose, frame or anchors, so styles can be switched at any time (a settings toggle, a flashback level) without touching game logic.

| Style | Look | | --- | --- | | classic (default) | 5-tone lit shading, outlines lighter on lit edges. Pixel-identical to earlier versions | | harvest | Cozy farm-sim, after Stardew Valley: bigger head, flat 3-band cel shading in warm hue-shifted ramps (violet-leaning shadows, sunlit highlights), outlines in a dark shade of each colour rather than black, and dark lines where limbs overlap the body | | retro | 8-bit console: NES palette, 3 bands, black outline and inner lines | | handheld | Original handheld: four greens chosen by brightness | | pico | Fantasy console: PICO-8's 32 colours, coloured outlines |

import { renderAnims, renderRider, renderRotations, ANIMS } from "@taipeistudio/pixel-rig";

renderAnims(look, ANIMS, { style: "harvest" });
renderRider(rider, bike, pose, { style: "retro" });
renderRotations(9, 8, materials, drawBall, "harvest"); // props too
bakeCharacter(look, ANIMS, { style: "pico" });          // pixi: cached per style

A custom style is plain data plus an optional ramp function; spread a preset to tweak it:

import { STYLES, type ArtStyle } from "@taipeistudio/pixel-rig";

const dusk: ArtStyle = { ...STYLES.harvest, id: "dusk", outlineColor: [20, 18, 50], head: 1.1 };

| Field | Effect | | --- | --- | | ramp(r) | Rebuild each material's 5-tone ramp (index 2 is the base colour); cozyRamp is harvest's | | bands | Shading band 0…4 → ramp index, e.g. [0, 1, 2, 2, 3] for flat cel shading | | outline | selective, colored (sel-out, mixed toward outlineColor by outlineMix), solid (outlineColor) or none | | innerLines | Outline-coloured lines where a nearer part overlaps one behind it, instead of a 1-tone crease | | palette, paletteMatch | Lock pixels to a palette: nearest (OKLab), rgb (weighted RGB) or luma (palette ordered dark → light) | | head | Skull scale; the face moves forward with it and features stay pixel-sized | | rimLight, edge | Flat art only (styleFilter): lit top edges, and how different colours must be to get an outline |

Scenery and other flat art

Worlds drawn with plain PixiJS Graphics (backdrops, pitches, buildings) can follow the same style with styleFilter, a post-process that gives flat colours what the rig gives characters: an outline on the darker side of every clear colour edge, a lit top edge (rimLight), and the palette. Apply it to the layers around the characters, not to the characters (already styled):

import { styleFilter } from "@taipeistudio/pixel-rig/pixi";

const f = styleFilter("harvest"); // null for classic: flat art stays as drawn
backdrop.filters = f ? [f] : [];
backdrop.filterArea = new Rectangle(0, 0, 480, 270);

It runs every frame, so animated crowds, flags and text are styled too. It works on WebGL 1 and 2 (not WebGPU) and expects the art at native pixel size (resolution 1, no antialiasing). Two style fields tune it: rimLight (0–1) and edge, the OKLab difference two touching colours need before the darker is outlined (default 0.1; raise it to keep subtle detail unlined). flatStyleParams(style) returns the numbers it uses, for porting to another renderer.

NES_PALETTE, DMG_PALETTE and PICO8_PALETTE are exported. A PixelBuffer takes a style as its fifth argument (buf.style), so custom items and props drawn into it follow the style.

Finer pixels

Every renderer takes scale (default 1): the same drawing with scale pixels per rig unit. { scale: 2 } returns a frame twice as wide and tall with four times the detail; outlines and creases stay one pixel wide. The frame's size, origin and anchors are in the finer pixels. PixelBuffer takes the same factor as its fourth argument, with coordinates and radii left in unscaled units, so custom props and items scale without changes.

Props and low-level drawing

  • Props: renderRotations(size, count, materials, draw) pre-renders a small sprite at count headings; rotationIndex(radians, count) selects the right one each frame.
  • Low level: PixelBuffer (capsule, disc, box, line, put; depth, parts, and resolve() for creases, cleanup and outlines) and ramp() are exported for drawing anything else in the same style.

Frame size

The default frame is 112 × 104 px with the feet at (56, 100): room for a ~62 px tall adult plus held items and jumps. Override it per render:

renderPose(look, pose, { frame: { width, height, groundY, originX } });

Versioning policy

pixel-rig follows Semantic Versioning, with one addition: pixels are part of the API. Changes to ramps, shading or rig proportions alter every consumer's art, so visual changes bump the minor version and API changes bump the major version. Pin a minor range (e.g. ~0.2.0) if your art must not shift.

Development

git clone https://github.com/rosdyana/pixel-rig.git
cd pixel-rig
npm install

npm run dev     # preview app: cast, all animations, item switcher, PNG sheet export
npm test        # feet planted, no frame clipping, determinism, pixel-hash regression, props, rear rig
npm run check   # typecheck + tests
npm run build   # dist/ (ESM + .d.ts)

Issues and pull requests are welcome at github.com/rosdyana/pixel-rig/issues.

License

Released under the MIT License with an attribution requirement. See LICENSE.

You may use, modify, distribute and sell software that includes pixel-rig, including in commercial games, provided that:

  1. the copyright notice and license are kept in all copies and modified versions, and

  2. your product visibly credits the author (credits screen, About page, documentation or store listing), for example:

    pixel-rig by Rosdyana Kusuma - https://github.com/rosdyana/pixel-rig

Copyright © 2026 Rosdyana Kusuma.