Make it yours.
Then make it move.
Live2DRender creates reproducible anime busts from a seed and explicit choices. It uses layered PNG artwork and a transparent WebGL renderer. Node.js generates the model; native browser modules animate it. No frontend build step.
Gender and outfits
Choose male, female or nonbinary with recipe.gender. New characters default to female. Male offers a tailored blazer, relaxed zip hoodie, bomber and crewneck; Female offers the original fitted hoodie and academy vest plus a fitted bomber and crewneck with natural bust and waist shaping. Nonbinary offers both straight and fitted cuts. Hair, face and eye choices are shared.
Change gender through POST /api/v1/models/customize with { model, patch: { gender: 'male' } }. Compatible outfits stay selected; incompatible inherited outfits are replaced while preserving the other choices and colors. Explicit incompatible outfits return INCOMPATIBLE_SELECTION. The studio filters outfit choices, and saved models and overlays retain gender. Older catalogs keep their original appearance; generate a new character to use the setting.
Resting eyes
New characters rest with both eyes 75% open. Catalog 20 places both eyes and eyebrows slightly higher on the face. Set eyeOpenLeft or eyeOpenRight to 1 for a wider look, or 0 to close the eye. Reset restores the resting pose; blinks and winks return to the underlying pose. Unrigged portraits use the same relaxed opening. Saved older models retain their original default.
Perspective head turns
Catalog 22 / rig 5 uses separate perspective shapes for the face, eyes, brows, nose, mouth, ears and hair. setHeadPose({yaw: 0.7, pitch: 0.3, roll: 0}) turns the head while preserving local gaze and eyelid controls. Each value ranges from -1 to 1; positive yaw turns toward screen-right, positive pitch looks up, and roll tilts the head clockwise. The prepared range supports modest turns. The studio provides Head turn, Look up/down and Head tilt sliders. Long and bob hairstyles have a lower hairline. The cheek and chin contours change with the turn. A separate rounded rear skull fills the side and underside behind the face, with human ears layered between them. Both neck layers remain behind the jaw. Their upper attachment follows the rear skull and blends into its underside. Internal face and nape edges blend into the whole head. During upward turns, the underside tapers toward the chin to preserve its definition. A separately driven upper neck and turn-dependent jaw shadow keep the neck from stretching with the face. Saved older models keep their earlier motion.
Generate and mount
This complete example runs on a page served over HTTP(S). Replace the origin with your local server when developing.
<canvas id="character" width="1000" height="1000"
style="width:360px;height:360px;background:transparent"></canvas>
<script type="module">
import { createRenderer } from
'https://l2drenderer.cerberusgamelabs.xyz/js/runtime/index.js';
import { requestModel } from
'https://l2drenderer.cerberusgamelabs.xyz/js/integration.js';
const origin = 'https://l2drenderer.cerberusgamelabs.xyz';
const model = await requestModel({
seed: 'my-character', template: 'anime-bust-v1', mode: 'rigged', gender: 'male',
generatorVersion: '1.1.0', catalogRevision: '22',
appearance: { hair: { color: '#967ac2' } }
}, origin);
const renderer = createRenderer({
assetResolver: asset => new URL(asset.url, origin),
onError: error => console.error(error.code, error.message)
});
const character = await renderer.mount(document.querySelector('canvas'), model);
character.play('idle');
character.setExpression('happy', 0.8);
character.setHeadPose({ yaw: 0.2, pitch: 0, roll: 0.1 });
// On removal: character.dispose(); renderer.dispose();
</script>
Two modes, one appearance
Pass mode: 'unrigged' when generating, then call character.setMouthState('open') or 'closed'. There is no idle loop in this mode. setMouthOpen(value) uses a 0.5 threshold. Other animation controls throw UNSUPPORTED_CONTROL.
Convert an existing model with POST /api/v1/models/mode, sending { model, mode: 'unrigged' }. The response contains a new model and previousModelId. Its appearance is preserved, its identity changes, and its original remains intact. Remount the returned model.
Rigged controls
| Method | Meaning |
|---|---|
lookAt(x, y) | Gaze in [-1, 1]. Positive X points right, positive Y points up. |
setHeadPose({yaw, pitch, roll}) | Normalized [-1, 1] values. Positive yaw right, pitch up, roll clockwise. |
setMouthOpen(value) | Continuous [0, 1]. An external audio source can supply this value. |
setExpression(id, weight, {duration}) | neutral, happy or sad; weight [0, 1], optional duration in milliseconds. |
setParameter(id, value, {duration}) | Manual override. Inspect discoverable parameters below. |
releaseParameter(id) | Let expression and idle tracks own that control again. |
blink({duration}) | One blink, 180 ms default, allowed range 50–1000 ms. |
play('idle') / stop('idle') | Breathing, automatic blinking and gentle hair movement. |
resetPose() | Neutral pose; clears expressions, tracks and manual overrides. |
resize({width,height,pixelRatio}) | Preserves the square portrait within the canvas; resolution capped at 2048 per dimension. |
draw() / evaluateAt(milliseconds) | Draw now, or freeze at a chosen deterministic timeline position. |
dispose() | Stop frames and release this instance's graphics resources. Repeated disposal is safe. |
Composition: neutral → idle → weighted expression → manual overrides → blink overlay → bounded deformation. Finite values are clamped; NaN, infinity and unknown controls throw typed errors. Transition duration is 0–10000 ms. Call methods after mount resolves. onError(error) reports graphics context loss; remount or reload to recover. Instances own their resources independently.
Discoverable parameter ranges and defaults
| Parameter | Minimum, maximum, default |
|---|---|
headYaw | -1, 1, 0 |
headPitch | -1, 1, 0 |
headRoll | -1, 1, 0 |
gazeX | -1, 1, 0 |
gazeY | -1, 1, 0 |
mouthOpen | 0, 1, 0 |
eyeOpenLeft | 0, 1, 0.75 |
eyeOpenRight | 0, 1, 0.75 |
upperLidLeft | 0, 1, 0 |
lowerLidLeft | 0, 1, 0 |
upperLidRight | 0, 1, 0 |
lowerLidRight | 0, 1, 0 |
breath | 0, 1, 0 |
hairSway | -1, 1, 0 |
browRaise | -1, 1, 0 |
mouthSmile | -1, 1, 0 |
Independent eyes and lids
Rig revisions 3 through 5 use independently prepared artwork for sclera, iris/pupil, upper lid and lower lid. The eye color channel affects only the iris, preserving white sclera, black pupil, highlights and lash colors. Gaze moves the iris behind a stencil aperture. Set eyeOpenLeft or eyeOpenRight to 0 to hold one eye closed, or call wink('left') / wink('right') for a timed wink. Each upperLidLeft, lowerLidLeft, upperLidRight and lowerLidRight parameter adds independent closure from 0 to 1. Lids and the white aperture follow the same authored contours throughout closure. Upper-only and lower-only controls leave the opposite boundary fixed until contact. Reset releases all held controls. These controls are Rigged-only.
Human ears are separate layers on an earless head. Choosing cat ears replaces them. Catalogs 1 and 2 retain their original baked-ear/combined-eye artwork for reproducibility; Catalog 3 also remains available with its original eye binding. Generate a new character in catalog 15 to use the prepared artwork and independent contour rig, with compact eyes positioned closer together and aligned brows. Catalogs 4 through 14 preserve their previous proportions and rig behavior.
HTTP API
All JSON POST requests use Content-Type: application/json. Successful operations return HTTP 200. The public service has no accounts or durable model database.
| Endpoint | Input → output |
|---|---|
| GET /api/v1/templates | templates array with slots, palettes, modes, controls and exact version pins |
| GET /api/v1/templates/anime-bust-v1 | Current template's full discoverable contract |
| POST /api/v1/models/generate | { recipe } → { model } |
| POST /api/v1/models/options | { model, slot } → { options: [{id,name}] } |
| POST /api/v1/models/customize | { model, patch } → { model } |
| POST /api/v1/models/mode | { model, mode } → { model, previousModelId } |
| POST /api/v1/models/validate | { model } → { valid, modelId } |
| POST /api/v1/models/export | { model, importHandle? } → character.l2d attachment |
| POST /api/v1/models/import | Raw .l2d bytes → { model, importHandle, expiresAt, assetUrls } |
Import uses application/vnd.live2drender.bundle+json. Use assetResolver: asset => assetUrls[asset.hash] to mount its returned model, resolving those paths against the service origin for cross-origin use. Pass importHandle when re-exporting. Handles expire after 30 minutes or a restart; save the bundle for permanent ownership.
Explicit choices and reproducibility
Seed strings are 1–256 characters, normalized to Unicode NFC; spaces and case matter. Omitted required styles select deterministically; optional ears/horns default to none. Six-digit hex colors are normalized to lowercase. create accepts random choices. Customization takes explicit choices and preserves other resolved fields. Null and unknown fields are rejected. Hair tousled and horns obsidian are incompatible.
// Apply only the fields you want to change:
{ "model": yourModel, "patch": {
"hair": { "style": "short-bob", "color": "#967ac2" },
"accessories": { "ears": "cat", "horns": "none" }
} }
Save the returned resolved manifest or portable bundle. A seed alone is not a permanent identity across catalog or generator updates. Current pins are generator 1.1.0 / catalog 22 / model schema 1 / rig 5. Available archived catalog revisions can be used with explicit pins. Saved older manifests render without regeneration; unsupported customization returns an error rather than silently migrating.
Limits and errors
JSON bodies: 2 MiB; parsed manifests: 1 MiB; bundles: 24 MiB; PNG: 8 MiB and 4096 pixels per dimension; 2–4 textures; 64 layers. The public API permits 600 requests/minute/IP, two concurrent exports and eight temporary imports total. Capacity failures are explicit and imports expire. Public modules, artwork and API allow credential-free cross-origin use; no credentialed CORS. API responses are uncached. The server never fetches a URL from a model.
Errors use {error:{code,message,details}}. Common statuses: 400 invalid input, 404 missing resource, 409 incompatible choices, 413 size/capacity, 422 unsupported version or corrupt content, 429 request/export limit. An imported bundle verifies manifest identity, dependency closure, PNG dimensions and texture hashes without extracting filesystem paths.
Iframe overlays
Copy an overlay link from the studio to preserve the current appearance, mode and version pins. Or use /embed?seed=my-character&mode=unrigged&gender=nonbinary for a simple current-version portrait. Recipe links contain only a small recipe, never full model data or arbitrary texture URLs.
const origin = 'https://l2drenderer.cerberusgamelabs.xyz';
const frame = document.createElement('iframe');
frame.title = 'Character overlay';
frame.style = 'width:360px;height:360px;border:0;background:transparent';
frame.src = origin + '/embed?seed=my-character&mode=unrigged&gender=nonbinary'
+ '&parentOrigin=' + encodeURIComponent(location.origin);
window.addEventListener('message', event => {
if (event.origin !== origin || event.source !== frame.contentWindow) return;
if (event.data?.channel !== 'live2drender' || event.data.version !== 1) return;
if (event.data.type === 'ready') frame.contentWindow.postMessage({
channel: 'live2drender', version: 1, type: 'command', id: 'mouth-1',
command: 'setMouthState', args: ['open']
}, origin);
});
document.body.append(frame);
The adapter accepts messages only from the exact parentOrigin and its parent window. With no parentOrigin, messaging is disabled. Replies are ready, ack or error, with the matching request id for commands. The command allowlist is the pose methods above, excluding lifecycle and rendering methods. type: 'initialize', model: resolvedModel validates and mounts a saved manifest using service assets; custom bundle assets require direct integration with the import resolver. On the embed page itself, await window.Live2D.ready returns the mounted character.
Try the working overlay playground. The protocol checks follow the postMessage origin and source guidance.
Node.js without an HTTP round trip
import { createGenerator, loadCatalog } from './src/generator/index.js';
const generator = createGenerator({ catalog: await loadCatalog() });
const model = await generator.create({
seed: 'my-character', template: 'anime-bust-v1', mode: 'unrigged'
});
console.log(generator.serialize(model));
Install the project with npm ci, then npm start. Default address: http://127.0.0.1:3148. Node 22 or newer is required; Node 24 and Chrome 152 are the verified baseline. HTTPS or localhost is required for browser checksum verification. Other browsers need WebGL, native modules, createImageBitmap and Web Crypto; they are not yet certified. Tracking, microphone analysis and native Cubism export are outside this first release.
Starter artwork
The starter pack includes three hairstyles, two eye shapes, two outfits, cat ears and horns, with 40 compatible part combinations plus recoloring. It uses original AI-assisted source sheets prepared into a shared rig. Preserve the asset provenance notice when sharing the starter assets. This private project does not declare an open-source distribution license.