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

live2d-companion

v1.0.4

Published

Live2D Companion component for web and React using PixiJS v8

Readme

live2d-companion

A drop-in Live2D character companion for the web. Built on PixiJS v8 and the untitled-pixi-live2d-engine.

Ships a framework-agnostic core class and a ready-made React component. Works with Next.js App Router out of the box.


Features

  • Model rendering — Point at any .model3.json or Cubism 2 .model.json and the character appears on a transparent canvas.
  • Seamless Model Switching — Change models on-the-fly via React props or .loadModel() without WebGL context loss or DOM flickering.
  • Dynamic Canvas Resizing — Canvas & renderer automatically resize according to model metadata presets or explicit layout props.
  • Companion Metadata Registry — Auto-configures canvas dimensions, scale, anchor points, and hit area maps when a model JSON defines a companion block.
  • Touch interactions — Tap hit areas on the model to trigger motion animations.
  • Eye & head tracking — The character follows the user's cursor.
  • Speech bubbles — Prop-driven dialogue appears above the character on interaction.
  • Audio playback — Plays voice clips when the character reacts (optional).
  • Idle behavior — The character periodically plays idle animations with random remarks when you provide idle dialogue.
  • Event bus — Trigger motions from anywhere in your app with companionBus.

Installation

npm install live2d-companion pixi.js untitled-pixi-live2d-engine

pixi.js and untitled-pixi-live2d-engine are peer dependencies. You must install them alongside this package.

React projects

# React 18 or 19 both work
npm install live2d-companion pixi.js untitled-pixi-live2d-engine react react-dom

Quick Start

React (Next.js / Vite / CRA)

'use client'; // Required for Next.js App Router

import { Live2DCompanion } from 'live2d-companion/react';
import "live2d-companion/react.css";

export default function MyCompanion() {
  return (
    <div style={{ position: 'fixed', bottom: 0, right: 0, zIndex: 50 }}>
      <Live2DCompanion
        modelUrl="https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json"
      />
    </div>
  );
}

That's it. The character renders, tracks your cursor, and responds to clicks. If you want speech bubbles or idle remarks, pass them through dialogueData.

Tip: modelUrl accepts both local paths (/models/character/model.model3.json) and full CDN URLs. Using a CDN link is the fastest way to get started — no model files to download.

Dynamic Model Switching (React)

You can dynamically change characters simply by updating state. The companion will reuse the WebGL context and adjust canvas size automatically:

import { useState } from 'react';
import { Live2DCompanion } from 'live2d-companion/react';
import "live2d-companion/react.css";

const MODELS = [
  'https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Nina/model.json',
  'https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json',
];

export default function CompanionSwitcher() {
  const [modelIndex, setModelIndex] = useState(0);

  return (
    <div>
      <button onClick={() => setModelIndex((prev) => (prev + 1) % MODELS.length)}>
        Switch Character
      </button>
      <Live2DCompanion modelUrl={MODELS[modelIndex]} />
    </div>
  );
}

Adding your own conversations

<Live2DCompanion
  modelUrl="/models/character/model.model3.json"
  dialogueData={{
    interactions: {
      flick_head: ["Hey, that's a little tickly!", "Please be gentle~"],
      tap_face: ["My face is right here.", "A little warning next time?"],
    },
    ui_elements: {
      shake: ["Let's go!", "Ready when you are."],
    },
    idleRemarks: ["I'm waiting here for your next command.", "What should we explore next?"],
  }}
/>

There are no built-in conversation strings in the package anymore. Everything shown in the bubble comes from the props you pass in.

Vanilla TypeScript / JavaScript

import { Live2DCompanionCore } from 'live2d-companion';
import "live2d-companion/react.css";

const canvas = document.getElementById('live2d-canvas') as HTMLCanvasElement;

const companion = new Live2DCompanionCore(canvas, {
  audioBaseUrl: '/audio',
  volume: 0.5,
});

companion.onLoaded = () => console.log('Model ready');
companion.onResize = (w, h) => console.log(`Canvas resized to ${w}x${h}`);
companion.onBubbleMessage = (msg) => {
  // msg is a string when showing, null when hiding
  document.getElementById('bubble')!.textContent = msg ?? '';
};

// Initial model load
companion.init('/models/my_character/model.model3.json');

// Switch models dynamically at any time without destroying the renderer
// companion.loadModel('/models/another_character/model.model3.json');

// Clean up when done
// companion.destroy();

Preparing Your Model

Option A: Use a CDN (fastest)

You can skip downloading model files entirely by pointing modelUrl to a CDN-hosted model. GitHub repos containing open-source Live2D models can be served directly via jsDelivr:

// Kiana
<Live2DCompanion modelUrl="https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json" />

Browse available models:

Note: CDN-hosted models load all assets (textures, motions, physics) relative to the model.json URL. As long as the entire model directory is in the same repo, it works automatically.

Option B: Self-host model files

For production use or if you need custom models, host them yourself. A typical model directory looks like this:

public/
└── models/
    └── my_character/
        ├── my_character.model3.json   ← Point modelUrl here
        ├── my_character.moc3
        ├── my_character.physics3.json
        ├── my_character.cdi3.json
        ├── textures/
        │   └── texture_00.png
        └── motions/
            ├── shake.motion3.json
            ├── flick_head.motion3.json
            ├── tap_face.motion3.json
            └── idle.motion3.json

Place this folder in your project's public/ directory (or any static file server) so it's accessible via URL.

Where to get models

Important: Respect model licenses. Many models are free for personal use but require attribution or prohibit commercial use.

Hit areas

Hit areas are defined inside the model's model.json file under HitAreas. Common names are head, face, breast, belly, and leg. This package maps each hit area to a motion animation via the hitAreaMap option.

If your model uses different hit area names, override the mapping:

<Live2DCompanion
  modelUrl="/models/custom/model.json"
  hitAreaMap={{
    'Head':  'nod',        // When user taps the "Head" hit area, play the "nod" motion
    'Body':  'wave',       // "Body" → "wave"
    'Skirt': 'surprised',  // Custom hit areas work too
  }}
/>

Motion names

Motion names correspond to the motion group names in your model.json. Open the file and look for the "Motions" section:

{
  "Motions": {
    "shake": [{ "File": "motions/shake.motion3.json" }],
    "flick_head": [{ "File": "motions/flick_head.motion3.json" }],
    "idle": [{ "File": "motions/idle.motion3.json" }]
  }
}

Use these exact group names when configuring hitAreaMap or calling companionBus.emit().

Customizing a model with props

You can tune the model's look and behavior without changing the library code:

<Live2DCompanion
  modelUrl="/models/character/model.model3.json"
  bodyMode="partialBody"
  canvasWidth={420}
  canvasHeight={560}
  modelScale={1.05}
  modelAnchor={[0.5, 1]}
  hitAreaMap={{
    head: 'flick_head',
    face: 'tap_face',
    breast: 'tap_breast',
    belly: 'tap_belly',
    leg: 'tap_leg',
  }}
  dialogueData={{
    interactions: {
      flick_head: ["Hey!", "That tickles a little."],
    },
    ui_elements: {
      shake: ["Let's begin!"],
    },
    idleRemarks: ["I'll be here when you're ready."],
  }}
/>

For model authors, the packaged metadata can also provide a companion block inside the model JSON to override canvas size, scale, anchor, and hit-area mapping at runtime. This is useful when shipping a custom model registry with opinionated defaults.


Configuration Reference

Every option has a sensible default. Pass only what you want to override.

React Component Props

<Live2DCompanion
  // Required
  modelUrl="/models/character/model.model3.json"

  // Layout (React wrapper only)
  width="100%"               // Container width — default: '100%'
  height="100%"              // Container height — default: '100%'
  className=""               // CSS class on the outer container div
  bubbleClassName=""          // CSS class on the speech bubble div
  bubbleStyle={{}}            // Replace the default bubble inline styles entirely

  // Canvas & Model
  canvasWidth={300}           // Internal canvas pixel width — default: 300
  canvasHeight={400}          // Internal canvas pixel height — default: 400
  modelScale={0.1}            // Model scale factor — default: 0.1
  modelAnchor={[0.5, 1]}      // [x, y] anchor point — default: [0.5, 1] (bottom-center)

  // Interaction
  hitAreaMap={{                // Map hit area names → motion group names
    head: 'flick_head',       //   default mapping shown here
    face: 'tap_face',
    breast: 'tap_breast',
    belly: 'tap_belly',
    leg: 'tap_leg',
  }}

  // Audio
  audioBaseUrl="/companion"   // Base path for audio files — default: '/companion'
  volume={0.6}                // Audio volume (0.0 to 1.0) — default: 0.6
  audioPathResolver={         // Custom function to resolve audio file paths
    (base, category, motion, index) =>
      `${base}/${category}/${motion}/${index}.mp3`
  }

  // Dialogue (opt-in)
  dialogueData={{             // Only used when you want speech bubbles
    interactions: {           //   Shown when user taps the model directly
      flick_head: ["Ouch!", "Hey!"],
    },
    ui_elements: {            //   Shown when triggered via companionBus
      shake: ["Let's go!"],
    },
    idleRemarks: [            //   Shown during idle animations
      "Still there?",
      "...",
    ],
  }}

  // Timing
  idleIntervalMinMs={30000}   // Min idle interval (ms) — default: 30000
  idleIntervalMaxMs={60000}   // Max idle interval (ms) — default: 60000
  motionDebounceMs={500}      // Min time between event bus motions — default: 500

  // Cubism SDK
  cubism2CoreUrl="https://..."  // CDN URL for Cubism 2 runtime
  cubism4CoreUrl="https://..."  // CDN URL for Cubism 4 runtime
/>

Vanilla Class Constructor & Methods

const companion = new Live2DCompanionCore(canvas, {
  // Same options as above, minus: width, height, className, bubbleClassName, bubbleStyle
  // (those are React-only layout props)
});

companion.onLoaded = () => { /* model is ready */ };
companion.onResize = (width, height) => { /* canvas dimensions updated */ };
companion.onBubbleMessage = (msg: string | null) => { /* render your own bubble UI */ };

await companion.init('/models/character/model.model3.json');

// Update options or dialogue dynamically
companion.updateOptions({ volume: 0.8 });

// Switch model dynamically without context loss
await companion.loadModel('/models/another_character/model.model3.json');

// Later:
companion.destroy();

Event Bus

The companionBus lets any part of your app trigger character animations. It uses DOM CustomEvent under the hood, so it works across any framework.

import { companionBus } from 'live2d-companion/react';
// or
import { companionBus } from 'live2d-companion';

companionBus.emit(motion, priority?)

Triggers a full interaction: animation + audio + speech bubble.

<button onClick={() => companionBus.emit('shake')}>
  Click me
</button>

// With priority (higher = more likely to interrupt current animation)
companionBus.emit('flick_head', 5);

companionBus.emitOnHover(motion, priority?)

Triggers animation only (no audio), with a built-in 30% probability gate. Useful for hover effects so the character doesn't react to every mouse movement.

Automatically skipped on touch devices.

<div
  onMouseEnter={() => companionBus.emitOnHover('tap_face')}
>
  Hover over me
</div>

Supported motions

The built-in motion type includes: 'shake', 'flick_head', 'tap_face', 'tap_breast', 'tap_belly', 'idle'.

You can also pass any custom string — it will be forwarded directly to model.motion(). As long as the motion group exists in your model's .model3.json, it will play.

companionBus.emit('my_custom_dance'); // Works if the model has a "my_custom_dance" motion group

Audio Setup (Optional)

Audio is optional. The companion works fine without any audio files — it will just show the speech bubble text silently.

If you want voice clips, organize them to match this directory structure:

public/
└── companion/              ← This is your audioBaseUrl
    ├── interactions/        ← Played when user taps the model
    │   ├── flick_head/
    │   │   ├── 1.mp3
    │   │   ├── 2.mp3
    │   │   └── 3.mp3
    │   ├── tap_face/
    │   │   ├── 1.mp3
    │   │   └── 2.mp3
    │   └── tap_belly/
    │       └── 1.mp3
    └── ui_elements/         ← Played when triggered via companionBus
        ├── shake/
        │   ├── 1.mp3
        │   └── 2.mp3
        └── flick_head/
            └── 1.mp3

The file names (1.mp3, 2.mp3, etc.) correspond to the index of the dialogue text array. If your dialogueData.interactions.flick_head has 3 entries, you need 1.mp3, 2.mp3, and 3.mp3.

Custom audio path format

If your audio files follow a different naming convention, provide a custom audioPathResolver:

<Live2DCompanion
  modelUrl="/model.json"
  audioBaseUrl="/voices"
  audioPathResolver={(base, category, motion, index) =>
    `${base}/${motion}_${category}_${index}.wav`
    // Resolves to: /voices/flick_head_interactions_1.wav
  }
/>

Styling the Speech Bubble

Using CSS classes

The speech bubble div gets the class live2d-bubble by default. Add your own class to style it:

<Live2DCompanion
  modelUrl="/model.json"
  bubbleClassName="my-custom-bubble"
/>
.my-custom-bubble {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  padding: 12px 20px;
  border-radius: 20px;
  font-family: 'Comic Sans MS', cursive;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}

Replacing inline styles entirely

Pass bubbleStyle to completely override the default dark-theme inline styles. When bubbleStyle is provided, the default tail triangle is also removed so you have full control:

<Live2DCompanion
  modelUrl="/model.json"
  bubbleStyle={{
    position: 'absolute',
    top: '-2rem',
    left: '50%',
    transform: 'translateX(-50%)',
    background: 'white',
    color: '#333',
    padding: '10px 18px',
    borderRadius: '12px',
    boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
    fontSize: '14px',
  }}
/>

Examples

Minimal (just a model, no audio)

<Live2DCompanion modelUrl="/models/haru/haru.model3.json" />

Custom character personality

<Live2DCompanion
  modelUrl="/models/robot/robot.model3.json"
  dialogueData={{
    interactions: {
      flick_head: ["Sensor array nominal.", "Scanning..."],
      tap_face: ["Facial recognition active.", "Identity confirmed."],
    },
    ui_elements: {
      shake: ["Executing protocol.", "Acknowledged."],
    },
    idleRemarks: [
      "Awaiting instructions...",
      "All systems nominal.",
      "Running diagnostics...",
    ],
  }}
/>

Large canvas with custom scale

<Live2DCompanion
  modelUrl="/models/character/model.model3.json"
  canvasWidth={600}
  canvasHeight={800}
  modelScale={0.2}
  modelAnchor={[0.5, 0.9]}
/>

Trigger from a navigation bar

import { companionBus } from 'live2d-companion/react';

function NavLink({ href, label }: { href: string; label: string }) {
  return (
    <a
      href={href}
      onMouseEnter={() => companionBus.emitOnHover('flick_head')}
      onClick={() => companionBus.emit('shake')}
    >
      {label}
    </a>
  );
}

Peer Dependencies

| Package | Version | |---|---| | pixi.js | ^8.0.0 | | untitled-pixi-live2d-engine | ^1.3.0 | | react | ^18.0.0 \|\| ^19.0.0 | | react-dom | ^18.0.0 \|\| ^19.0.0 |

react and react-dom are only required if you use the live2d-companion/react entry point.


License

MIT