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.
- Everything a script changes is one step in the undo history, named after the script, so Ctrl+Z takes the whole run back. A script that opens another document and changes it makes a step in that document too.
- A script that takes more than a second shows a dialog. Stop, or Esc, ends it; what it changed up to then stays, as one step you can undo.
- When a script stops on an error, the Output panel says which file and line, and why.
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.
- Script › Run (Ctrl+Enter) saves the script and runs it on the document in front, as Run Command... would. A script you have not saved yet runs as it is, without being saved. What it traces, and the error it stops on, go to the Output panel.
- Script › Check Syntax checks an ActionScript file.
- Type a dot for code hints, as in the Actions panel:
fl.'s methods, and what documents, timelines and elements have; Ctrl+Space for every word that fits. - The editor colors the language, keeps your indents, and finds and replaces (Ctrl+F, Ctrl+H). Ctrl+G goes to a line, and Ctrl+/ comments lines out and back in. It is the same editor as the Actions panel's.
- Closing the window, or Phoenix, asks about scripts with unsaved changes.
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.
- The package's commands join the Commands menu at once; its tools join the toolbox the next time Phoenix starts.
- Every file in the package is kept in the extension's own folder, beside Phoenix's settings (Open Folder in the dialog shows it).
- Remove takes away the files the extension added. A command or tool you have changed since it was installed is yours, and stays.
- Installing an extension that is already there replaces it.
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.
- Scripts. A panel's
evalScriptruns JSFL. Each panel has a script context of its own, loaded with the panel's script file (its manifest'sScriptPath), so the functions and values it keeps stay between calls. Each call that changes a document is one undo step, named after the panel. The result comes back as text, orEvalScript error.when the script fails; the error itself goes to the Output panel. - What a panel can ask of Phoenix.
CSInterfaceworks as panels expect: the host environment and theme colors, system paths, other extensions, opening and closing panels, the window title, the panel menu (the ≡ button at the panel's top right) and context menus, and opening links in your browser.cep.fsreads and writes files and shows file dialogs. - Events. Panels can send events to each other with
dispatchEvent. Phoenix sendscom.adobe.csxs.events.ThemeColorChangedwhen the theme changes, andcom.adobe.events.flash.followed by the JSFL event name (documentChanged,selectionChanged,timelineChangedand the rest) whenever scripts' listeners hear one. - Starting. A panel whose manifest starts it with the application (
<StartOn>withcom.adobe.csxs.events.ApplicationActivate) runs out of sight from when Phoenix starts until it is opened. - Not there. Panels have no Node.js (
require,cep_node) and cannot start processes (cep.process); installing one that asks for Node.js says so.
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.
| Option | Meaning |
|---|---|
--timeout SECONDS | Stops 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")
fl,FLfile,alert,confirmandpromptare ready in every script. Modules your script imports reach them withimport phoenix(phoenix.fl,phoenix.FLfile).- Every property and method also has a snake_case name:
fl.get_document_dom(),layer.frame_count,timeline.add_new_layer("Notes"). - Methods take keyword arguments for the settings the JSFL reference names:
timeline.insert_frames(5, all_layers=True). A setting left out is left as the method's default. fl.runScript("tools/rename.jsfl", "renameAll", "bg_")runs a JSFL script, or one function in it, and gives back what it returns.printwrites to the Output panel, or to standard output withrun-script. Lists come back as Python lists, plain objects as dicts, whole numbers asint, and nothing asNone.- A run is one step to undo, Stop ends it, and an error names the file and line, as with JSFL. Errors from Phoenix are
phoenix.Error, which a script can catch. A misspelled property is anAttributeErrorrather than a new property. - Each run starts with fresh globals, and the objects it got from
flstop working when it ends: get them again in the next run. Modules stay imported between runs, so a pipeline library loads once.
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"))
- Phoenix is found from the
PHOENIX_EXEenvironment variable, the Phoenix the module came with, or where Phoenix installs. It runs your own Python inside it, so use Python 3.8 or later, as for Phoenix's Python scripts. - Each property and call is a request to that Phoenix, which keeps running until the session ends:
with phoenix.Session() as ph:, orphoenix.shutdown()(it also ends when your program does). ph.python(code)andph.jsfl(code)run code inside Phoenix and give back its last value;ph.run_script(path)runs a.jsflor.pyfile there;ph.call(name, **settings)runs one of Phoenix's own operations.- What Phoenix prints, traces or alerts is printed by your program (or kept in
ph.outputwithSession(echo=False)). Python's own errors come back as themselves (AttributeError,TypeError); others arephoenix.Error. phoenix.export_movie(document, "out.mp4")renders a movie in a Phoenix of its own.
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.
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:
| Object | What works |
|---|---|
fl | The 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. |
FLfile | Reading, 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. |
| Document | Stage 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). |
| Timeline | Layers (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 Frame | Name, 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. |
| Element | Kind, 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 items | Every 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.
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.
| Event | When |
|---|---|
documentNew | A new document is made. |
documentOpened | A document is opened. |
documentChanged | Another document is shown: opened, made, or picked from its tab. |
documentSaved | A document is saved. |
documentClosed | A document is closed. |
timelineChanged | You go into a symbol or back out, or to another scene. |
layerChanged | Another layer becomes current. |
frameChanged | The playhead moves, playing too. |
selectionChanged | The selection on the stage changes. |
prePublish, postPublish | Before and after 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:
configureTool()once, as Phoenix starts. It names the tool throughfl.tools.activeTool:setToolName,setMenuString(the name the Tools panel shows),setToolTip,setIcon(a PNG beside the script) andsetOptionsFile.activate()anddeactivate()as the tool is picked and put down.mouseDown(),mouseMove(),mouseUp()andmouseDoubleClick()as you work, andkeyDown()for a key.fl.tools.penLocis where the pointer is,fl.tools.penDownLocwhere the button went down, andmouseIsDown,shiftIsDown,altIsDownandctlIsDownsay what is held.fl.tools.constrainPoint(from, to)keeps a drag to the nearest 45°, andfl.tools.getKeyDown()gives the key.setCursor(), to choose the pointer withfl.tools.setCursor.notifySettingsChanged(), when the tool's settings change.
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.