Plays content made in Adobe Animate (published for HTML5 Canvas) on PixiJS, without CreateJS.
Coming from @tawaship/pixi-animate-container? It plays the same content and is used the same way; see Coming from pixi-animate-container.
Status: under development. What does not work yet is listed under Current limits.
API reference: https://tawaship.github.io/pixi-animate-runtime/docs/
Samples (content published from Animate, played by this package or by CreateJS): https://tawaship.github.io/pixi-animate-runtime/samples/
Convert only content you trust. Converting runs the published JavaScript, frame scripts included, to read it. It runs in a sandbox (QuickJS, a separate JavaScript engine compiled to WebAssembly) that is given nothing of your machine: no files, no commands, no network. Running code is never without risk all the same: a flaw in the sandbox or the engines could let code in the file reach further. Convert content published by you or by people you trust, and treat content you receive (from a contractor, an asset store and so on) as code you would run. In the browser, the runtime content is as safe as the published content would be.
The published files are needed only to convert. Your application ships the runtime content (and the images it uses), not the published files. This example keeps them apart:
my-project/
├─ animate/ what Animate publishes (read when converting; not shipped)
└─ public/ what the application ships
├─ animates/ the runtime content, written by the command
├─ lib/ PixiJS and the player
└─ index.html
Publish from Animate. In Adobe Animate, publish the document for HTML5 Canvas into animate/ (you get
game.js, game.html and an images/ directory).
Install. In the project (Node.js 18 or later):
npm install pixi.js@5 @tawaship/pixi-animate-runtime
Copy node_modules/pixi.js/dist/pixi.min.js and
node_modules/@tawaship/pixi-animate-runtime/dist/pixi-animate-runtime.js into public/lib/ (or import the
player with a bundler).
Convert. From the project's root:
npx --no-install @tawaship/pixi-animate-runtime animate --out-dir public/animates
It lists what it will write and asks; answer y. This writes public/animates/game.js (the runtime
content) and copies the images it uses into public/animates/images/. The list shows the content's
composition id, which the next step needs.
Play. In public/index.html, load PixiJS, the player and the runtime content, and put the symbol the
published HTML showed (named after the published file, here game) on an AnimateContainer:
<script src="lib/pixi.min.js"></script>
<script src="lib/pixi-animate-runtime.js"></script>
<script src="animates/game.js"></script>
<script>
const app = new PIXI.Application({ width: 550, height: 400 });
document.body.appendChild(app.view);
const { loadContentAsync, AnimateContainer } = PIXI.animateRuntime;
loadContentAsync({ id: '<composition id>', basepath: 'animates/' }).then(content => {
const container = new AnimateContainer();
app.stage.addChild(container);
app.ticker.add(container.handleTick, container);
container.addCreatejs(content.create('game'));
});
</script>
The content plays at the frame rate it was published with.
After publishing again from Animate, run step 3 again. The player plays the runtime content, never the published files: a change made in Animate shows only once the runtime content is made again.
The sections below explain each step: Converting published content (options, the Windows batch file, the Node API) and Playing content (loading options, the container, the objects).
pixi.js ^5.3.2) in your applicationpixi.js ^5.3.2). Checked with 5.3.9Turn the published content into runtime content with the command line tool, giving it the directory Animate
published into (the one with the published .js files; .fla files and an images/ directory there are
optional):
npx --no-install @tawaship/pixi-animate-runtime assets/animates
--no-install runs only the copy installed in your project: without it, npx would download and run a package from the npm registry when the command is not installed. In the scripts of your package.json, the command name alone is enough ("animate": "pixi-animate-runtime animate -o public/animates"): npm runs scripts with the commands installed in the project and downloads nothing.
To keep npx from downloading anything in your project at all (also when --no-install is forgotten), put
yes=false in the project's .npmrc. npx then stops with an error when a command is not installed, instead
of asking or, where nobody can answer (CI, scripts), downloading it without asking.
This writes, into assets/animates/generated/, one runtime content file for every published JavaScript file
in the directory, under the same name (game.js to generated/game.js), and copies the images each content
uses next to it at the same relative paths (generated/images/...). Images left over from earlier publishes
that no content uses are not copied, and scripts that are not Animate output are left alone. Copy the whole
generated/ directory into your application's assets.
| Argument / option | Effect |
|---|---|
a published .js file instead of a directory |
Converts that file only |
-o, --out-dir <directory> |
Writes straight into that directory instead of generated/ |
-r, --recursive |
Also converts the published files in the directories below (not in node_modules, nor in directories whose name starts with .). With -o, each is written below that directory at the same relative path (title/title.js to <directory>/title/title.js); without it, into generated/ next to its published file |
-y, --yes |
Writes without asking anything (directories not given take their defaults). Needed when run from a script or CI |
-f, --force |
Writes runtime content even when the content uses features that are not supported |
-h, --help |
Shows the usage |
Before writing anything, the tool lists where it reads from and writes to, then a row for each published file: its composition id, the number of images it uses, and whether it is new, replaces a file already there, or is not written (and why):
From: assets/animates
To: generated/ next to each published file
published composition id images
game.js 2FA8E0C7... 3 new
title.js D6D50362... 1 replace
old.js B3DEAFD3... not written: images it uses are missing: images/old_atlas_1.png
2 to write, 1 not written.
Write these? (y/N):
Composition ids are shortened in this list; the whole id is in the published file (lib.properties.id) and at
the top of the runtime content file.
Nothing is written unless you answer y. Run from a script or CI, where nobody can answer (not a terminal),
it only lists and writes nothing unless -y is given; with -y, it asks nothing and writes. Run in a terminal
without a directory (and without -y), the tool first asks for it, lists the published files it found, and
asks where to write (empty: generated/).
Nothing is ever deleted: files already in the output directory that the run did not write are listed, for you to remove when they are no longer needed.
A content is not written when it uses unsupported features (each one is listed), when an image it uses is
missing, or when the path of an image leads out of the published directory (Animate writes them below it);
the other contents still are, and the command then exits with 1. It exits with 2 when the directory or
file cannot be read.
On Windows, tools/pixi-animate-runtime.bat (in the installed package) does the same: copy it into your
project, then drop the published directory on it, or double-click it to be asked.
The same is available from Node, in two steps, so that a script of your own can show the plan, ask its own questions or choose what to write in between:
const { planGeneration, writeGeneration } = require('@tawaship/pixi-animate-runtime/generator');
planGeneration({ input: 'assets/animates', outDir: 'public/animates', recursive: true }).then(plan => {
plan.contents.forEach(content => {
console.log(content.name, '->', content.out, content.problem || '');
});
const result = writeGeneration(plan);
console.log(result.written.length + ' file(s) written');
});
planGeneration(options) takes what the command line takes (input, outDir, recursive, force, cwd),
makes the runtime content and works out where everything goes, but writes nothing. Each of plan.contents is
one published file: name, id (composition id), out (where it goes), exists (a file is already there),
images (from and to), issues, warnings, and problem, the reason it is not written (null when it
is). writeGeneration(plan) writes the runtime content as the plan made it (a published file changed in
between does not change what is written), copies the images, and returns written (the files it wrote) and
left (the files already in the output directories that it did not write). It deletes nothing. To write only some of the contents, give it a plan with
the others removed from contents.
For one published file without writing anything:
const { generateFromPublishedFile } = require('@tawaship/pixi-animate-runtime/generator');
generateFromPublishedFile('assets/animates/game.js').then(generated => {
require('fs').writeFileSync('game.js', generated.source);
});
generated.id is the content's composition id, generated.data its data, generated.issues lists
unsupported features and generated.warnings what to look at (see Versions). Images are not
copied by this function.
generate(definition) makes the runtime content from a definition you already have, in the form
@tawaship/animate-data-converter extracts it (its compact form). An expanded definition (written with
--expanded, or made with expand()) is refused: turn it back with the converter's compact() first.
Frame scripts are copied into the runtime content as they were published, as code. Nothing is rewritten
(a console.log left in a script stays), and the runtime content needs no eval at run time.
A runtime content file registers itself, when it is loaded, under the content's composition id: the
lib.properties.id of the published content, the id Animate gives a document when the .fla is created. The
player takes the content by this id.
.fla keeps its composition id, so two contents made that way share one id. Create a new document
for a new content (the id can also be changed by editing the XML inside the .fla)Load the runtime content file, load the content by its composition id, and put an instance of a symbol on an
AnimateContainer:
<script src="pixi.js"></script>
<script src="pixi-animate-runtime.js"></script>
<script src="assets/animates/game.js"></script> <!-- a runtime content file -->
const { loadContentAsync, AnimateContainer } = PIXI.animateRuntime;
loadContentAsync({ id: '2FA8E0C7230941478CE2CA3DB82DBEDF', basepath: 'assets/animates/' })
.then(content => {
const container = new AnimateContainer();
app.stage.addChild(container);
app.ticker.add(container.handleTick, container);
container.addCreatejs(content.create('game')); // the symbol the published HTML used as its root
});
This loads the content's images from basepath and shows the game symbol, playing it at the frame rate it
was published with, whatever the display's refresh rate: handleTick turns the ticker's time into the
content's frames.
loadContentAsync: with script tags as above, or by adding
script elements at run time. They register into window.PixiAnimateRuntimeContents and can be loaded
before or after the playerid is the composition id (the command line tool prints it, and it is written at the top of the runtime
content file). When no content is registered under it, the error lists the ones that arebasepath is the directory the content's image paths are relative to: where the runtime content file was
written, ending with /. options: { crossOrigin: false } loads the images without CORS (requested with
CORS, anonymous, by default)options: { handleManifest(manifest) { ... } } is called with the content's images ({ id, src }, src as
published) before they are loaded. Changing an entry's src changes what this load reads: a data: or
blob: URL is used as it is, another path is resolved against basepath. It may return a promise to make the
loading wait{ version }, adds _fv=<version> to every image URL to bypass caches; the query
Animate adds to each image path on every publish stayscreate(id) creates an instance of any symbol, set up the way the published HTML sets up the root it
creates: use it for the instances your application places. content.lib holds the symbol classes;
new content.lib.game() creates an instance the way a frame script does, without that setup (it only
matters for color effects, which the player draws the same either way)content.id is the composition id, and content.properties holds the values of the published
lib.properties (width, height, fps, color and so on; read-only)content.create('Bitmap1').pixi.texturecontainer.speed changes the playback speed (1 is the published frame rate, 0 stops). When the
display runs slower than the content, one call plays several frames; set container.overSpeed = false
to play at most one frame per callcontainer.smooth (true by default) keeps the playing even when the time between frames of the display
varies a little: the frames follow a steady clock (the usual time between frames of the display), so a
display frame coming a little early or late does not leave one call with no frame and the next with two.
The clock keeps to the real time, and frames the display misses are still caught up at once. false
plays by the time of each call as it is, as pixi-animate-container doesremoveCreatejs(instance)
takes it off, and it stops playingtick(instance) plays exactly one frame, for stepping through content by handAnimateController: new AnimateController(container) has the addCreatejs, handleTick, speed,
overSpeed and smooth of AnimateContainer, putting the instances on that containerFor Pixim.js applications, use @tawaship/pixim-animate-runtime: it includes this player and loads contents as assets of a Pixim content.
With a bundler, import the player instead (import { loadContentAsync } from '@tawaship/pixi-animate-runtime');
the runtime content files are still loaded as scripts.
Every instance is a CreateJS-compatible object (what frame scripts see as this), shown by a PixiJS node
in its pixi property. Values the two have in common are kept in one place: instance.x = 100 and
instance.pixi.x = 100 are the same write, and what the timeline does with it afterwards is what
CreateJS would do.
Members whose meaning differs between CreateJS and PixiJS keep the CreateJS meaning on the instance and the PixiJS meaning on its node:
| Member | On the instance | On instance.pixi |
|---|---|---|
rotation |
Degrees | Radians (angle is degrees) |
skewX / skewY |
Degrees, CreateJS skew | skew in radians |
mask, filters, parent, children |
CreateJS objects | PixiJS objects |
Shapes that show the same drawing share one PixiJS geometry, even across symbols. Do not draw into the
pixi node of a shape (graphics.clear(), lineTo() and so on): the change would show in every shape that
shares the drawing.
Rotation and skew combine the way CreateJS combines them (the object is rotated first and then skewed), which matches what the published content shows in a browser.
Dynamic text breaks into lines the way the published content does: a line wider than the text box is broken at spaces only, so a long word, or text without spaces such as Japanese, runs past the box. Animate's own stage breaks such text inside words, so the two can look different; the player follows the published content.
Frame scripts can use the names published code uses: this, cjs / createjs (the player's
CreateJS-compatible classes), and lib (the symbol classes). exportRoot and stage are undefined.
playSound() plays nothing.
pixi-animate-container loads CreateJS and the published JavaScript in the browser, runs them, and shows the CreateJS display objects through PixiJS. This package does the CreateJS part once, at development time:
The way content is put on the screen stays the same, under new names:
| pixi-animate-container | pixi-animate-runtime |
|---|---|
<script> tags for CreateJS and the published .js |
A <script> tag for the runtime content file (no CreateJS) |
PIXI.animate |
PIXI.animateRuntime |
loadAssetAsync(targets), giving lib |
loadContentAsync(targets), giving a content (its classes are in content.lib) |
new lib.game() |
content.create('game') (new content.lib.game() also works, see Playing content) |
PIXI.animate.Container |
AnimateContainer |
addCreatejs, addCreatejsAt, removeCreatejs, handleTick |
The same |
createjsSpeed |
speed |
createjsOverSpeed |
overSpeed |
| (none) | smooth: even playing when the time between frames varies (on by default; false plays as pixi-animate-container does) |
options: { crossOrigin } |
The same |
CreatejsMovieClip, CreatejsShape and the other wrapper classes |
MovieClip, Shape and the other CreateJS-compatible classes; instance.pixi is still the PixiJS node |
What behaves differently:
instance.pixi is always current. The instance and its node share their values (see
Objects and their Pixi nodes), so a frame script reading instance.pixi.x
gets the value just written, not the value of the previous frameoptions.listenFrameEvents (endAnimation, reachLabel), options.useSound (sound is not
played), and CreateJS mouse events on instances (see Current limits)mousedown, click, rollover and so on) are not called. To react to the
pointer, listen to PixiJS events on the instance's pixi node (pointerdown, pointertap,
pointerover and so on, with interactive = true). Masks are never hit by the pointertextBaseline other than top, maxWidth and outline are
not checked yetPublished content defines each symbol as a class whose constructor builds its children and its whole timeline,
for every instance. The command runs the published code once, at development time (in its sandbox, with
@tawaship/animate-data-converter), and records each symbol as data: the display objects it owns and the
value of every property its timeline sets, at every frame. The runtime content holds that data, written
compactly, and the frame scripts as code.
The player builds every instance of a symbol from the same data, so creating many instances at once does not build a timeline for each, and it needs neither CreateJS nor the published code. It plays the frames as CreateJS does: tests play every test content on both, frame by frame (see How correctness is checked).
Runtime content records the version of this package that generated it, and the player checks it when the
content is loaded (loadContentAsync):
| Runtime content generated by | What the player does |
|---|---|
A version in RUNTIME_CONTENT_RANGE, not newer than the player |
Plays it |
A version in RUNTIME_CONTENT_RANGE, newer than the player |
Plays it and warns on the console: what the newer version added may not play. Update the player |
A version outside RUNTIME_CONTENT_RANGE (or none) |
Fails to load (the promise is rejected). The message says whether to update the player or to generate the runtime content again |
The generator checks the same way the definitions it is given: a definition records the version of
@tawaship/animate-data-converter that extracted it. One from a version outside the range this package
depends on is refused, and one from a version newer than the generator was built with is generated with a
warning (in generated.warnings; the command line tool prints it and still writes the runtime content).
This matters when definitions are kept as files (animate-data-converter --out) and generated later.
VERSION is the version of the player, and satisfies(version, range) tests a version against a range
written as npm writes it (0.x || 1.x, ^1.2.0, >=1.0.0 <2.0.0).
The package comes with skills for Claude Code (Anthropic's AI coding tool) in dist-skills/: pixi-animate-runtime-guide, which tells it how to generate runtime content and play it, and animate-data-converter-guide (the converter the generator runs). To
let Claude Code use them in your project, copy them into the project's .claude/skills/:
mkdir -p .claude/skills
cp -r node_modules/@tawaship/pixi-animate-runtime/dist-skills/* .claude/skills/
Copy them again after updating the package. Each skill names the versions it was written for; with another
version, it tells Claude Code to follow the type definitions in node_modules where they disagree.
The player was developed with Claude (Anthropic's AI assistant). Its correctness rests on testing rather than on trust in the author: every test content is played both by CreateJS and by this player, and their states are compared at every frame (positions, playback, display lists, drawing commands, text, filters, masks, buttons), together with what the PixiJS nodes show (their matrices, masks, filters, textures). What is drawn is compared too: every test content is drawn by CreateJS and by this player in the same headless browser, frame by frame, and the pixels are compared, with differences in antialiasing allowed for. Known faults are put into the player on purpose to confirm that the comparisons notice them.
loadContentAsync, AnimateContainer,
AnimateController, with speed, overSpeed and smooth)dist-skills/, published with the packageMIT