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

@forgeax/engine-animation

v0.3.5

Published

Animation graph and playback domain.

Readme

@forgeax/engine-animation

[!IMPORTANT] Ordinary entities and skin joints use the same AnimationTargetId model. A target needs Transform, but does not need Skin or a renderer component.

Engine profiles install animationPayloadsPlugin(lookup) as the resolver Provider and animationRuntimePlugin() as the World consumer. Their provide/inject edge makes the dependency explicit. animationPlugin(lookup?) remains the direct standalone composition for a World that does not need a separately replaceable resolver.

Quick start

import {
  AnimationPlayer,
  AnimationTargetId,
  animationPlugin,
  bindAnimationTargets,
  deriveAnimationTargetId,
} from '@forgeax/engine-animation';
import { createWorldContext, World } from '@forgeax/engine-ecs';
import { ChildOf, Name, Transform } from '@forgeax/engine-scene';
import type { AnimationClip } from '@forgeax/engine-types';

const world = new World();
const context = await createWorldContext(world, [animationPlugin()]);

const targetId = deriveAnimationTargetId(['Root', 'Planet']);
const clip = {
  kind: 'animation-clip',
  duration: 1,
  channels: [{
    targetId,
    property: 'translation',
    sampler: {
      input: new Float32Array([0, 1]),
      output: new Float32Array([0, 0, 0, 4, 0, 0]),
      interpolation: 'LINEAR',
    },
  }],
} satisfies AnimationClip;
const clipHandle = world.allocSharedRef('AnimationClip', clip);
const slots = {
  clips: [clipHandle],
  times: [0],
  weights: [1],
  speeds: [1],
};
const player = world.spawn(
  { component: Transform, data: {} },
  { component: Name, data: { value: 'Root' } },
  { component: AnimationPlayer, data: slots },
).unwrap();
const target = world.spawn(
  { component: Transform, data: {} },
  { component: Name, data: { value: 'Planet' } },
  { component: ChildOf, data: { parent: player } },
  { component: AnimationTargetId, data: { value: targetId } },
).unwrap();

const bound = bindAnimationTargets(world, player, [target]);
if (!bound.ok) throw bound.error;
world.update(1 / 60);
await context.fiber.restart();

For imported scenes, collect targets explicitly from SceneInstance.mapping. Only ENTITY_NULL_RAW is absent from that mapping; entity handle 0 is valid. Adding an AnimationPlayer does not scan the scene tree. AnimationGraph playback uses the same player, target IDs, channels, and update system.

Identity and ownership

  • AnimationTargetId is the stable authored identity. Its wire value is exactly 32 lowercase hexadecimal characters.
  • deriveAnimationTargetId(path) length-prefixes the UTF-8 path segments under a fixed namespace, hashes them with BLAKE3, and writes the UUID v8 version and variant bits before rendering that wire value.
  • AnimatedBy stores the target's one live player owner.
  • AnimationTargets is the ECS-maintained reverse relationship on that player.
  • bindAnimationTargets(world, player, targets) validates the complete explicit batch before assigning missing IDs or ownership. Repeating the same batch is idempotent.
  • An existing ID survives rename or reparent. When binding creates a missing ID, it derives it from the complete Name lineage from the player through the target, including both endpoints.
  • Importers and other authoring tools instead persist IDs derived from the source asset root through each target. A synthetic runtime controller above that asset root is not part of the authored path, so multiple instances of one asset reuse the same target IDs. Importers write those IDs to animated glTF/FBX scene entities and matching clip channels.

Animation channels contain one targetId. The previous targetPath wire was a clean cut: there is no dual read, adapter, or fallback.

Playback and diagnostics

Direct slots and AnimationGraph output both flow through AnimationPlayer and the default animation schedule. Translation, rotation, and scale are blended before one final local Transform write; normal scene propagation then computes world transforms.

Development diagnostics are structured objects with code, hint, and detail. They skip only the malformed channel or target and let valid sibling players continue. console.warn aggregates repeated channel failures by player, target, and reason. subscribeAnimationDiagnostics(listener) separately emits each unique player/clip/channel/target/reason fact so editor diagnostics can group and locate failures without parsing console text. The subscription is a read-only observation seam, not a queue or ECS resource. Production builds silently skip malformed entries without emitting these diagnostics.

Error recovery

Never parse message. Branch on error.code, use error.detail to locate the entity, and follow error.hint.

| Code | Repair | |:--|:--| | animation-target-player-invalid | Keep a live player entity with AnimationPlayer. | | animation-target-invalid | Keep a live target with Transform. | | animation-target-outside-player-root | Parent the target below the selected player. | | animation-target-name-missing | Add Name to every lineage entity before deriving an ID. | | animation-target-id-invalid | Replace the value with a valid derived or imported wire. | | animation-target-id-duplicate | Give distinct targets distinct authored IDs. | | animation-target-player-conflict | Unbind the live owner or bind through that owner. | | animation-target-capacity-reserve-failed | Reduce the batch or free managed-buffer capacity before retrying. | | animation-target-bind-failed | Inspect the ECS failure detail and retry with live entities. |

Example and boundaries

Run pnpm --filter @forgeax/bevy-animated-transform dev or inspect apps/bevy/animated-transform. The demo uses createApp defaults, three hierarchical entities without Skin, direct and graph players, controls, and two isolated instances sharing one clip and target IDs.

This package does not animate arbitrary component properties. It does not provide an animation FSM, masks, IK, a target registry, hidden subtree scanning, or editor UI. Material, light, camera, and custom component field animation require separate features.

Imported animation bounds

The pure @forgeax/engine-animation/animated-bounds subpath serves the glTF and FBX import producers. It encloses influenced vertices after inverse-bind transforms across all imported LINEAR/STEP TRS clips, their convex blends, and the supplied morph-weight envelope. The published six-float AABB is in mesh local coordinates, including every instance using the skeleton.

| Producer fact | Enclosure | |:--|:--| | Translation and scale | Rest pose plus key extrema | | Animated rotation | Complete unit-quaternion orbit, including between-key extrema | | Shared mesh/joint ancestors | Cancel before enclosure propagation | | Deep joint chains | Radius preserved under rotation; no repeated box-to-sphere inflation | | Missing influence facts, invalid hierarchy, or singular inverse mesh transform | No automatic bound |

Explicit source or sidecar bounds remain authoritative. The importer does this work once; the renderer consumes the resulting Skeleton metadata without sampling animation to invent bounds each frame. Bounds certify the imported clip closure. Reimport after editing source clips; external procedural joint motion needs explicit bounds covering that motion. Conservative bounds may be larger than a sampled pose, which trades some culling precision for correctness.