Studio pipeline

Scripting with JSFL and Python

Phoenix runs JSFL scripts, the JavaScript files many studio pipelines use to clean up, check and export FLA documents, and Python scripts that use the same objects by the same names. A script reaches the open documents, their timelines, layers, frames, objects and library, and the files on disk.

Run a script

Choose Commands › Run Command... and pick a .jsfl or .py file. The script runs on the document in front.

Write a script

Commands › Script Editor... opens a window for writing scripts, with a tab for each one. Its File menu starts a new JSFL or Python script, or an ActionScript file, and opens and saves script files. It starts in your Commands folder, so a script saved there shows up in the Commands menu.

Your own commands

Scripts you put in your Commands folder appear at the bottom of the Commands menu, by file name, ready to run in one click. Commands › Open Commands Folder shows the folder; Commands › Manage Saved Commands... renames or deletes them.

The Commands folder sits in Phoenix's script configuration folder (what scripts call fl.configURI), beside Phoenix's settings. Scripts often keep their own settings in that folder too.

Extensions

Commands and tools are often shared as an extension package, a .zxp file. Commands › Manage Extensions... installs one: Install Package... for the file, or Install from Folder... for one unpacked into a folder. Dragging a .zxp onto Phoenix, or opening one with it, installs it too.

Phoenix runs an extension's JSFL commands and tools, and shows its HTML panels (CEP panels). Panels made as SWF movies are installed and listed but do not show.

HTML panels

An extension's HTML panels are listed under Window › Extensions. Each opens as a dock like Phoenix's own panels, and one left open opens again the next time Phoenix starts. A panel shows in the web view the system provides: WebView2 on Windows (part of Windows 11, and a free install on Windows 10) and WebKit on macOS. On Linux, panels are listed but do not show.

The Output panel

Window › Output (F2) shows what scripts write with fl.trace and the errors they stop on, in red. It opens by itself when a script writes to it. Clear empties it; Save... writes it to a text file. Scripts can do the same with fl.outputPanel.clear() and fl.outputPanel.save().

alert, confirm and prompt show ordinary dialogs while a script runs.

From the command line

Phoenix tidy.jsfl

Runs the script in the Phoenix already open, as if you had chosen it from the Commands menu, or starts Phoenix to run it. This is also what opening a .jsfl file from your file browser does when it is set to open with Phoenix.

Phoenix run-script check.jsfl shot_010.fla shot_020.fla
Phoenix run-script publish.py shot_010.fla

Opens the documents and runs the script with no window, for render nodes and pipelines. What the script traces goes to standard output; errors go to standard error, and Phoenix exits with 1 when the script fails, so a build can stop on it.

OptionMeaning
--timeout SECONDSStops a script that runs longer than this. 600 seconds unless given; 0 for no limit.

With no one to answer them, the dialogs answer themselves: alert prints its message, confirm answers yes, prompt answers with its default, and a file picker for opening answers with the first document named on the command line. Save the documents the script changed from the script itself, for example with fl.saveDocument(doc, uri).

Phoenix batch --jsfl rename_layers.jsfl episode/ --save

Runs the script on every document in a folder (or on the documents named), each opened on its own, and with --save writes back what it changed. See Command line for this and the other commands.

Python scripts

A .py file runs wherever a .jsfl file does: from the Commands menu, your Commands folder, Phoenix script.py and Phoenix run-script. It sees the same objects under the same names, so the JSFL reference and what you know of it carry over:

doc = fl.getDocumentDOM()
for layer in doc.getTimeline().layers:
    print(layer.name, layer.frameCount)
doc.getTimeline().addNewLayer("Notes")

Phoenix uses your own Python, 3.8 or later, so the packages you installed for it are there to import: your studio's ShotGrid, ftrack or Kitsu library, or anything else. Phoenix finds the newest Python installed for you or for everyone, then one on your PATH. To pick one, set the PHOENIX_PYTHON environment variable to its python program, its folder, or a virtual environment; scripts then import the environment's packages, and sys.executable is its python, so a script can run tools with it. PHOENIX_PYTHON=off turns Python scripts off. Phoenix itself starts and runs JSFL scripts with no Python installed.

Python as a module

Your own Python programs, pipeline tools, notebooks and tests can drive Phoenix too. The phoenix module, in the python folder beside Phoenix, starts Phoenix with no window and hands you its documents as Python objects, under the same names (and snake_case names) scripts inside Phoenix use. Add that folder to PYTHONPATH, or to sys.path:

import phoenix

doc = phoenix.open("episode/sh010.fla")
tl = doc.get_timeline()
for layer in tl.layers:
    print(layer.name, layer.frame_count)
tl.add_new_layer(name="Notes", add_above=True)
tl.insert_frames(12, all_layers=True)
phoenix.export_png(doc, "sh010_first.png", frame=0)
phoenix.save(doc, "episode/sh010_long.fla")
print(phoenix.jsfl("fl.getDocumentDOM().getTimeline().frameCount"))

Another Python package called phoenix (from a different project) installs a module of the same name; if you have it, use a virtual environment or put Phoenix's folder first on the path.

Native extensions

Scripts can call functions written in C or C++. Put a native extension library (.dll on Windows, .dylib or .bundle on macOS, .so on Linux) in the External Libraries folder of your Configuration folder (fl.configURI + "External Libraries"); the next script that runs loads it. Each library becomes an object named after its file, so Sample.dll defining computeSum is called as Sample.computeSum(2, 3).

Libraries are written against mm_jsapi.h, which comes with Phoenix's source in src/jsfl. It keeps the C interface and layout of the extension API Animate documents, so libraries built for it are meant to load unchanged: a library includes the header, puts MM_STATE in one source file, and defines MM_Init, which registers its functions with JS_DefineFunction. A library that cannot be loaded, or has no MM_InitWrapper, is reported in the Output panel and left out.

Only install libraries you trust

Native code runs inside Phoenix, so a faulty library can crash it. To run without them, set the environment variable PHOENIX_NATIVE_EXTENSIONS=off, or pass --no-native-extensions to Phoenix run-script or batch.

What scripts can reach

Phoenix builds the scripting objects in the order real scripts use them. This release covers:

ObjectWhat works
flThe open documents, getDocumentDOM (null with nothing open), opening, creating, saving and closing documents, trace, outputPanel, runScript (including another file, and calling a function in it), scriptURI, configURI, file and folder pickers, XUL dialogs and fl.xmlui (see Dialogs), clipCopyString, fl.Math, publishDocument to publish a document by its file, findObjectInDocByName and findObjectInDocByType (each found object with its keyframe, layer, timeline and parent) and selectElement to go to one, and objectDrawingMode.
FLfileReading, writing and appending text, copying, removing, listing folders (with a *.ext mask), making folders, sizes, dates and attributes, and converting between file:/// URIs and paths. Both file:///C|/ and file:///C:/ work. runCommandLine runs a command as the system's shell would (cmd.exe on Windows) without a console window, waits for it and gives its exit code.
DocumentStage size, frame rate and color; the selection; selecting all or none; grouping, ungrouping, breaking apart, tracing bitmaps (traceBitmap(threshold, minimumArea, curveFit), with curveFit one of "pixels", "very tight", "tight", "normal", "smooth" or "very smooth"), converting to a symbol, swapping a symbol; arranging, aligning, distributing, moving, scaling, rotating and transforming the selection; deleting, duplicating, copying, cutting and pasting; new rectangles, ovals, lines and text; spacing and matching sizes, inverting the selection, skewing and resetting transforms, unlocking everything; union, intersect, punch and crop; distributing to layers or keyframes; text strings and attributes; blend modes and filters (getFilters, setFilters, addFilter, setFilterProperty, enabling, disabling and reordering them); the toolbar's fill and stroke; editing, adding, duplicating, renaming, reordering and deleting scenes; editing symbols; importFile for images, sounds, SVG, Illustrator, EPS, PDF and Photoshop files, to the stage or the library; saving, exporting PNG frames and SWF movies, and publishing as the document's publish settings say; data kept on the document and on the selected objects (addDataToDocument, getDataFromDocument, documentHasData, addDataToSelection and their kin).
TimelineLayers (adding, deleting, reordering, selecting), the playhead and current layer, inserting and removing frames, keyframes and blank keyframes, clearing keyframes, classic tweens, selecting, copying, cutting and pasting frames, copying, cutting, pasting and duplicating whole layers, reversing frames, guides (getGuidelines, setGuidelines), and setting frame and layer properties in bulk.
Layer and FrameName, kind, visible, locked, outline and color; the folder, mask or guide a layer sits in (parentLayer, which scripts can also set); filters on a layer's frames (getFiltersAtFrame, setFiltersAtFrame), kept in the file but not yet drawn; each frame's objects, start and length, label, tween, ease, rotation, sound settings and frame script.
ElementKind, position, size, scale, rotation, skew, matrix, transformation point, name and depth. Symbol instances add their library item, behavior, first frame and looping, color effect, filters, blend mode and visibility. Text adds its characters and their font, size and color. Shapes say whether they are groups, drawing objects or primitives, and list a group's members. A shape's outline reads as vertices, edges and contours: each edge with its control points, its stroke and its two half-edges, each half-edge with its vertex, the next and previous along its contour and the opposite one, each contour with its fill and whether it encloses a region; fills and strokes come as plain objects with their colors, gradients and bitmaps. getCubicSegmentPoints gives the curves as drawn. beginEdit and endEdit around deleteEdge, Vertex.setLocation, Edge.splitEdge and Edge.setControl edit the outline as one step to undo. Every object keeps named data (setPersistentData, getPersistentData, hasPersistentData, removePersistentData), saved with the document.
Library and itemsEvery item; selecting, finding, adding symbols and folders, deleting, renaming, moving into folders, duplicating, editing a symbol, placing an item on the stage, unused items; each item's name, kind and linkage, a symbol's timeline and type, a bitmap's size and smoothing; sounds and videos renamed and deleted like any item (one still in use is not deleted); exportToFile writes a bitmap as PNG (lossless ones too) and a sound as WAV or MP3; named data on symbols and bitmaps (addData, getData, hasData, removeData), saved with the document.

Scripts written for older versions of the JSFL language also run: for each loops, setters with no parameter, class used as a name, a return at the top of a script, and toSource.

E4X XML works too: new XML(text) and XML literals with {expressions}, x.@id to read and set attributes, x.item[0], x..item, x.*, filters such as x.item.(@id == 3), for each over lists, and methods like length(), children(), appendChild() and toXMLString(). An attribute read on its own gives its text, so a.@id == b.@id compares the values; XML namespaces are not supported.

An object a script holds stays tied to what it stands for. If the script deletes the layer, undoes the element or closes the document it came from, using it again stops the script with an error that says so, rather than acting on something else. A frame stands for a frame number on its layer, so it follows the layer as keyframes are added and cleared.

Phoenix's own operations

Scripts can also use the operations Phoenix gives AI assistants, which do in one call what would take many: in-betweens, pose cycles, lip sync, bounces, palettes, tidying the library, color scripts and the rest (see AI assistants). fl.phoenix.call takes an operation's name and an object of its arguments and gives back its result; fl.phoenix.operations() lists them all, with what each does and the arguments it takes. Frames count from 1 here, as they do for assistants. A change made this way is part of the script's one undo step.

fl.phoenix.call("in_betweens", {layer: "Arm", frame: 1, ease: 100, onTwos: true});
var cycle = fl.phoenix.call("pose_cycle", {layer: "Walk", repeats: 4, advance: 120});
fl.trace(cycle.layer + ": " + cycle.frames + " frames");

From Python, call fl.phoenix_call(name, json_text) with the arguments as JSON text, and read its result with json.loads.

Not yet

Panels built on HTML, exporting one symbol as an SWF movie (item.exportSWF), drawing the filters set on layers' frames, and saving History panel steps as a command. Each run starts fresh: functions one script defines are not kept for the next.

Dialogs

A script can ask for its settings in a dialog described in XUL, the XML that JSFL dialogs use. fl.xmlPanelFromString(xml) takes the XML as a string; fl.xmlPanel(uri) and document.xmlPanel(uri) read it from a file. Phoenix lays it out as a dialog with OK and Cancel, waits for the person, and returns an object: dismiss is "accept" or "cancel", and each control with an id adds its value as a string, a checkbox as "true" or "false".

var result = fl.xmlPanelFromString(
    '<dialog title="Shake" buttons="accept, cancel">' +
    '<label value="Strength:" control="amount"/>' +
    '<textbox id="amount" value="20"/>' +
    '<checkbox id="taper" label="Taper off" checked="true"/>' +
    '</dialog>');
if (result.dismiss == "accept")
    fl.trace(result.amount + " " + result.taper);

Layout: vbox, hbox, grid with columns and rows, groupbox, spacer and separator. Controls: label, description, textbox (value, maxlength, multiline, size or width), checkbox, radiogroup with radio, menulist with menuitem (editable too), listbox with listitem, colorchip, popupslider, choosefile, targetlist, button, and property for a value with no control. Embedded SWF panels are not shown.

Handlers run in the script's own context, so they can call its functions: oncreate as the dialog opens, onchange when a text box is edited, oncommand when a button, checkbox, radio button or menu item is used, and <script> blocks before the dialog appears. In a handler, fl.xmlui reads and changes the dialog: get, set, getEnabled, setEnabled, getVisible, setVisible, getControlItemElements, setControlItemElements, accept and cancel. An error in a handler is printed in the Output panel and the dialog stays open.

With no one to answer it (Phoenix run-script, or a window drawn offscreen), the dialog is not shown: its script blocks and oncreate handlers run, then it is accepted with the values its controls start with, and a note is printed.

Events

A script can ask to hear when something happens in Phoenix: fl.addEventListener(type, listener) calls the function each time, and gives back a number that fl.removeEventListener(type, number) takes to stop. A script that listens stays loaded after it ends, with its variables, until its last listener is removed or Phoenix quits.

EventWhen
documentNewA new document is made.
documentOpenedA document is opened.
documentChangedAnother document is shown: opened, made, or picked from its tab.
documentSavedA document is saved.
documentClosedA document is closed.
timelineChangedYou go into a symbol or back out, or to another scene.
layerChangedAnother layer becomes current.
frameChangedThe playhead moves, playing too.
selectionChangedThe selection on the stage changes.
prePublish, postPublishBefore and after File › Publish writes its files.
fl.addEventListener("documentSaved", function () {
    fl.trace("Saved " + fl.getDocumentDOM().name);
});

What a listener changes is one step to undo, named after its script. An error in a listener is printed in the Output panel, and the other listeners still hear the event. Events that a running script's own work causes are held until it ends. A tool script (see Your own tools) can listen too; it is loaded when Phoenix starts, which makes it the place for listeners that should always be there.

Python scripts listen the same way, with fl.addEventListener (or fl.add_event_listener) and any function:

def saved():
    print("Saved", fl.getDocumentDOM().name)

fl.addEventListener("documentSaved", saved)

Inside a listener, fl is always the one of the moment, so the documents it reaches are the ones open now.

Your own tools

A script in the Tools folder (beside the Commands folder) is a tool of its own. Phoenix loads it when it starts and adds its button at the end of the Tools panel; click it, then draw on the stage as with any tool. Unlike a command, a tool stays loaded, so its variables last from one click to the next. Phoenix calls the functions the script has:

While the mouse is down, a tool shows what it will draw on fl.drawingLayer, which is drawn over the stage and never saved: beginDraw and endDraw around the drag, beginFrame and endFrame around each new preview, and moveTo, lineTo, curveTo, cubicCurveTo, frameRect, drawPath and setColor to draw it. To add the shape itself, a tool builds a path with fl.drawingLayer.newPath() (addPoint, addCurve, addCubicCurve, close, newContour) and calls its makeShape(): the shape takes the fill and stroke set in the Properties panel, and each such change is one step to undo, named after the tool. Phoenix's own document.addNewPath(svg, fill, stroke) does the same from SVG path data.

A tool's settings come from its options file, an XML file beside the script listing each setting as a <property> with its name, the variable the tool reads it from on fl.tools.activeTool, its type (Number, Strings with a list, or Boolean), a defaultValue and, for numbers, min and max. Tool Settings... in the Properties panel sets them.

function configureTool() {
    var tool = fl.tools.activeTool;
    tool.setMenuString("Box Tool");
    tool.setOptionsFile("Box.xml");
}
function mouseDown() { fl.drawingLayer.beginDraw(); }
function mouseMove() {
    if (!fl.tools.mouseIsDown) return;
    var a = fl.tools.penDownLoc, b = fl.tools.penLoc;
    fl.drawingLayer.beginFrame();
    fl.drawingLayer.frameRect({left: a.x, top: a.y, right: b.x, bottom: b.y});
    fl.drawingLayer.endFrame();
}
function mouseUp() {
    fl.drawingLayer.endDraw();
    var a = fl.tools.penDownLoc, b = fl.tools.penLoc;
    var path = fl.drawingLayer.newPath();
    path.addPoint(a.x, a.y); path.addPoint(b.x, a.y);
    path.addPoint(b.x, b.y); path.addPoint(a.x, b.y);
    path.close();
    path.makeShape();
}

Errors in a tool's functions are printed in the Output panel. A tool added to the folder appears the next time Phoenix starts.