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

@three-ws/walk

v0.3.0

Published

A diverse, animated 3D avatar that walks and talks over your web pages — corner companion + full-page playground, with a built-in avatar picker. Powered by Three.js.

Readme

@three-ws/walk

A diverse, animated 3D avatar that walks and talks over your web pages — a corner companion plus a full‑page playground, with a built‑in avatar picker. Powered by Three.js.

Drop a character onto any site. It idles in the corner, turns to follow the cursor, waves when the visitor navigates, and greets each page. Click it and it detaches into a full‑page playground the visitor can steer with the keyboard or an on‑screen d‑pad — strolling across the page from a gentle aerial view, or platforming across your real DOM with gravity and jumps. Walk onto a link and it opens like a doorway into the next page.

Every visitor can choose who walks with them from a built‑in roster of avatars — a robot mascot, humanoids, photoreal people, a fox, dancers, showpieces — or you can supply your own GLB. Whatever they pick is animated correctly whether the rig ships its own clips or needs the shared retargeting library, so nothing ever freezes in a bind/T‑pose.

This is the engine behind the walking companion on three.ws.


Install

npm install @three-ws/walk three

three is a peer dependency — bring your own copy (>= 0.150).

You also need to serve two sets of assets from your origin (or a CDN you point assetBase at):

  • the avatar GLBs the roster references (e.g. /avatars/*.glb), and
  • the shared animation manifest + clips (/animations/manifest.json + clips), used to retarget motion onto rigs that ship no locomotion.

The defaults match the three.ws asset layout. Override them with assetBase, apiBase, and manifestUrl, or pass your own avatars roster.

Don't want to host the assets? Point both at the three.ws CDN — assetBase: 'https://three.ws' and manifestUrl: 'https://three.ws/animations/manifest.json' (CORS is open). Relative clip URLs inside the manifest resolve against the manifest's origin, so a cross-origin manifest just works (v0.1.1+).


Quick start

The whole experience, app‑style (auto‑mounts when enabled, honors ?walk= deep links, resumes the playground after a "dive"):

import { createWalkCompanion } from '@three-ws/walk';

const walk = createWalkCompanion();
walk.bootstrap();

Or drive it yourself:

const walk = createWalkCompanion({ defaultAvatarId: 'fox' });

walk.enable(); // mount the corner companion
walk.openPicker(); // let the visitor choose an avatar
walk.setAvatar('michelle'); // swap the live avatar
walk.disable(); // remove it

createWalkCompanion is side‑effect free on import — nothing touches the DOM until you call enable() or bootstrap().


How avatars are animated

Each roster entry declares a rig strategy so it always moves:

| rig | Used for | How it animates | | ---------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | embedded | self‑animated or non‑humanoid GLBs (robot, fox) | plays the clips baked into the GLB; falls back to the first clip so it never freezes | | shared | humanoids with no locomotion (or only a T‑pose) | retargets the shared clip library (idle/walk/run/wave/jump) onto the rig |

The same unified loader powers both the corner companion and the playground, so adding an avatar makes it work everywhere at once.

Emotes

In the playground, big round buttons pinned to the right edge make the character perform on command: dance, punch (a muay thai combo), backflip, and wave, with the number keys 1-4 as shortcuts. Only emotes the current rig can actually play get a button: shared rigs resolve them against the animation manifest (DEFAULT_EMOTES, overridable per entry with emotes), embedded rigs match their baked clip names (Dance, Punch, Wave, …). Looping clips perform one capped pass, then the character settles back into whatever it was doing. The rail rebuilds automatically when the visitor swaps avatars.

// Give a custom shared-rig avatar its own dance:
{ id: 'mascot', rig: 'shared', asset: '/brand/mascot.glb', source: 'static',
  emotes: { dance: 'av-dance-shuffle' } }

// Or drive emotes programmatically from a loaded controller:
controller.emotes(); // e.g. ['dance', 'punch', 'wave']
controller.playEmote('dance'); // true if it played, false if unsupported

Custom roster

import { createWalkCompanion, WALK_AVATARS } from '@three-ws/walk';

createWalkCompanion({
  avatars: [
    ...WALK_AVATARS,
    {
      id: 'mascot',
      name: 'Our Mascot',
      category: 'Brand',
      asset: '/brand/mascot.glb',
      source: 'static',
      rig: 'shared', // retarget the shared library onto it
      accent: '#ff0066',
    },
  ],
  defaultAvatarId: 'mascot',
}).bootstrap();

User‑generated avatars served by a GLB proxy (/api/avatars/<id>/glb) work via makeApiAvatarEntry(id) and resolve at runtime — no need to list them.


API

createWalkCompanion(options?) → control

| option | default | description | | ----------------- | ---------------------------- | -------------------------------------------------------- | | avatars | WALK_AVATARS | roster shown in the picker and resolvable by id | | defaultAvatarId | 'robot' | avatar loaded when none is chosen/stored | | assetBase | '' | base prepended to static GLB paths (e.g. a CDN origin) | | apiBase | '' | base prepended to the /api/avatars/<id>/glb proxy | | manifestUrl | '/animations/manifest.json'| shared animation manifest | | excludedRoutes | full‑screen 3D routes | path prefixes where the companion never mounts | | enablePicker | true | show the avatar picker button | | lookAt | true | companion's chest/neck/head follow the visitor's cursor | | greeting | built‑in | (path) => string \| null to customise the page greeting | | docsUrl | null | optional "make your own" link in the picker footer | | storagePrefix | 'walk' | localStorage/sessionStorage key prefix |

The returned control object:

control.isEnabled(): boolean
control.enable(): void
control.disable(): void
control.toggle(): void
control.setAvatar(idOrEntry): void   // persist + hot-swap the live avatar
control.openPicker(): void
control.bootstrap(): void            // app-style auto-mount + ?walk= deep links
control.instance                     // the live companion (or null)
control.config                       // the resolved options, storage keys included

Roster helpers

import {
  WALK_AVATARS, DEFAULT_AVATAR_ID, DEFAULT_SHARED_CLIPS,
  getAvatar, defaultAvatar, listCategories,
  makeApiAvatarEntry, resolveAvatarUrl,
} from '@three-ws/walk';

Playground (driven for you by the companion, exported for direct use)

import {
  launchPlayground, exitPlayground, switchPlaygroundMode,
  getPlaygroundMode, shouldDropIn, consumeDropIn, playgroundState,
} from '@three-ws/walk';

Checkpoint quests

Both movement modes accept an optional checkpoint quest — ordered target elements the visitor must steer the character to (this is what powers @three-ws/tour's Explore and Platformer modes). Reaching the active checkpoint freezes the character and hands control to your onReach; call resume() to unfreeze and light up the next one. During a quest the link-diving mechanic is suppressed, and a live mode switch (the M key / mode pill, or switchPlaygroundMode()) carries the checkpoints and progress across — only the physics change. window fires walk-playground:mode with { detail: { mode } } on every switch.

launchPlayground({
  mode: 'platformer',                        // or 'stroll'
  checkpoints: [{ el: hero }, { el: pricing }, { el: faq }],
  onReach: (i, resume) => narrate(i).then(resume),
  onComplete: () => console.log('all checkpoints found'),
});

Picker UI

import { createAvatarPicker } from '@three-ws/walk';

const picker = createAvatarPicker({
  avatars: WALK_AVATARS,
  currentId: 'robot',
  onSelect: (entry) => console.log('chose', entry.id),
});
picker.show();

Low‑level loader

import { loadWalkAvatar, getAvatar } from '@three-ws/walk';

const { model, controller } = await loadWalkAvatar(getAvatar('xbot'));
scene.add(model);
controller.playEmote('punch'); // one-shot performance, then back to base
// each frame:
controller.setState('walk'); // 'idle' | 'walk' | 'run' | 'jump'
controller.update(dt);

URL controls

The companion honours these query params (via bootstrap()):

  • ?walk=1 — force the companion on
  • ?walk=0 — force it off
  • ?walk=play — deep‑link straight into the full‑page playground
  • ?avatar=<id> — load a specific roster avatar (or a user avatar id)

Accessibility & performance

  • One shared WebGL context budget — the companion and playground never run two contexts at once, and the budget coordinates with other Three.js viewers on the page so a busy site never hits the browser's context limit.
  • Reduced motion — respects prefers-reduced-motion; the avatar calms to a steady idle and page "dives" skip the animation.
  • Keyboard — playground steers with arrow keys / WASD; the picker is fully keyboard‑navigable; every control has a visible focus ring.
  • Lean & lazycreateWalkCompanion() is side‑effect free: no DOM, renderer, avatar, or animation work happens until you call enable() / bootstrap(). The full‑page playground is instantiated only on the first detach (loaded through a dynamic import()), so a page that only ever shows the corner companion never runs playground code. On three.ws the playground is additionally delivered as its own lazily‑fetched chunk.
  • Never a T‑pose — a shared‑rig avatar whose GLB turns out not to be a retargetable humanoid (no skinned skeleton) falls back to its own baked clips, then to the default rig — it never freezes in a bind/T‑pose.

License

All rights reserved. See LICENSE.