Drawing over the game
phantom.flash.overlay is a layer above the running game, owned by the loader, and everything your script draws goes in it. Building interfaces in the game is built on it: phantom.aj.ui is this layer with Animal Jam's look and ready-made widgets, and you should try it first. Use the overlay directly when phantom.aj.ui has no widget for what you want.
Drawing only needs "game": "read" in the manifest. Nothing is sent to Animal Jam and nobody else sees it. See Permissions.
Nodes
Everything you draw is a node. A node is a function: call it to remove the node and anything drawn inside it.
const label = await phantom.flash.overlay.text('watching', {
x: 20,
y: 20,
font: { size: 14, bold: true },
background: { color: 0x000000, alpha: 0.6 },
padding: 4,
});
await sleep(3000);
await label();The kinds are group, rect, circle, line, text, rich, image, marker, world, and four controls the loader draws itself: input, button, panel and list. They all take the same placing and colouring options, and update changes any of them later. phantom.flash.overlay.clear() removes everything your script drew.
A node hears the mouse with on, and only gets the events it listens for:
| Event | When |
|---|---|
click, down, up | pressed and released on it, at stage coordinates |
over, out | the mouse arriving and leaving |
hover | the mouse moving about inside it |
wheel | the wheel turned on it, with how far in delta |
move | a dragged node let go, in its parent's coordinates |
link | a span of rich text with a link clicked |
All of these except hover take the mouse from the game, so a click on your button doesn't also walk the player across the room. hover only watches, so the game still gets the hover underneath.
More controls
Five more controls are built from those nodes: a checkbox, a dropdown, tabs, a scrolling box and a slider.
| Call | What it is |
|---|---|
checkbox | a box and a tick with text beside it, which the player turns on and off |
select | a button showing what is chosen, which opens a list of the rest |
tabs | a row of tabs, each with a group of its own that shows with it |
scroller | a window on a group, scrolled under the wheel |
slider | a track and a knob, dragged in Flash |
const panel = await phantom.flash.overlay.panel({
title: 'Settings',
width: 240,
height: 160,
});
const tabs = await phantom.flash.overlay.tabs({
parent: panel,
x: 8,
y: 8,
tabs: [{ name: 'chat' }, { name: 'view' }],
});
const quiet = await phantom.flash.overlay.checkbox({
parent: tabs.page('chat'),
label: 'Hide trade requests',
checked: true,
});
quiet.onChange((ticked) => console.log('hiding:', ticked));
const zoom = await phantom.flash.overlay.slider({
parent: tabs.page('view'),
min: 50,
max: 200,
value: 100,
step: 5,
});
zoom.onChange((percent) => void phantom.aj.room.camera.zoom(percent));Each one is a group with its pieces drawn inside, so removing or moving it does the same to all of them. Its value is heard with onChange rather than on, because the change happens in your script rather than being posted by the loader. value() returns the current value without asking Flash, and setValue sets it without firing onChange, since the player didn't change it.
A select opens its list inside itself, so one drawn in a panel needs room below it in that panel. Use opens: 'up' for one near the bottom. A scroller is a panel with no title bar, because panels are the only nodes that clip what's drawn in them. Call refresh() after drawing something in its content so it knows how far it can scroll.
Text in several colours
rich takes spans, or markup that parseMarkup turns into spans. It's never HTML: <b> shows as those three characters, so text somebody else wrote can't become formatting.
| Tag | What it does |
|---|---|
[b], [i], [u] | bold, italic, underlined |
[color=#ff9900] | a colour, as #rrggbb or 0xrrggbb |
[size=14] | a size in points, 1 to 127 |
[link=value] | clickable, and on('link') hears value |
[[ | a literal [ |
const line = await phantom.flash.overlay.rich(
'[b][color=#22c55e]localhost[/color][/b]: hello [link=who]who is that?[/link]',
{ x: 20, y: 20, background: { color: 0x000000, alpha: 0.5 }, padding: 4 }
);
line.on('link', (value) => console.log('clicked', value));Tags nest, unknown tags are kept as text, and anything left open closes at the end. Use escapeMarkup(text) for text you didn't write, such as a player's message, so it shows exactly as typed. A list's rows take the same markup, as { markup }.
Ready-made chat tools
phantom.aj.chat.custom has three complete tools built on this layer. Each is one call and returns a function that undoes it.
| Member | What it does |
|---|---|
shortcuts | expands and blocks what the player sends, wherever they send it from |
box | replaces the client's own chat bar and send button with an overlay input |
window | a draggable window of what is said in the room, fed as it is said |
const stop = await phantom.aj.chat.custom.shortcuts({
shortcuts: { '/shrug': 'oh well' },
});
const history = await phantom.aj.chat.custom.window({ x: 20, y: 80 });Their source is worth reading too. Each combines the overlay, override and attach as described on this page, and window is the list node inside a panel.
Measuring text
A label's width depends on the font, not the number of characters. measure asks Flash for the size without drawing anything.
const font = { size: 14, bold: true };
const size = await phantom.flash.overlay.measure('Trading: 3 items', {
font,
padding: 4,
});
console.log(size.width, size.height, size.lines);The numbers are what a text node would take up, including Flash's two-pixel gutter at each edge, so a box made to that size fits. wrapWidth measures it wrapped over as many lines as it needs, and a text node takes wrapWidth too, so the two agree.
A node that's already drawn gives its box, children included, with bounds(), in the same coordinates as its own x and y.
const panel = await phantom.flash.overlay.panel({
title: 'Buddies',
width: 200,
height: 120,
});
const line = await phantom.flash.overlay.text('localhost is here', {
parent: panel,
x: 8,
y: 8,
});
const box = await line.bounds();
console.log('the line ends at', box.x + box.width, 'and is shown:', box.shown);Layout
row and column place nodes one after another based on their size, and return the size of the whole line, so you don't have to work out pixel positions yourself.
const panel = await phantom.flash.overlay.panel({
title: 'Trade',
width: 240,
height: 90,
});
const label = await phantom.flash.overlay.text('Sending:', { parent: panel });
const gems = await phantom.flash.overlay.text('1200 gems', {
parent: panel,
font: { bold: true },
});
const line = await phantom.flash.overlay.row([label, gems], {
x: 8,
y: 8,
gap: 4,
align: 'middle',
});
await panel.update({ height: line.height + 38 });gap is the space between nodes, and align is how shorter ones sit across the line: start, middle or end. Nodes stay in whatever they were drawn in, so to lay out a panel's contents, pass nodes whose parent is the panel.
Nodes are placed by their box rather than their own position, so a centred label or a group with children to the left of it lands where you meant.
place does the same placing without a row or column, for layouts that work out their own positions. Pass nodes and a point for each, and each node's box lands on its point.
const left = await phantom.flash.overlay.text('Mods', { centered: true });
const right = await phantom.flash.overlay.text('Enabled', { centered: true });
await phantom.flash.overlay.place(
[left, right],
[
{ x: 10, y: 10 },
{ x: 10, y: 30 },
]
);A node with no point stays where it is. A script can't do this itself, because a node doesn't report where it was last put, so the overlay's own record is needed. phantom.aj.ui.box uses it to lay out grids of cards, and that's usually what you want.
Aligning to the window
If you read the stage size and position things yourself, your panel ends up off the edge as soon as the window is resized. align says where a node sits in the box it's drawn in, and the loader moves it back whenever that box changes size.
await phantom.flash.overlay.text('paused', {
align: { x: 'center', y: 'middle' },
centered: true,
font: { size: 24, bold: true },
outline: 0x000000,
});x takes left, center or right, and y takes top, middle or bottom. The node's own x and y are then an offset from that point, so align: { x: 'right' } with x: -8 sits eight pixels in from the right edge. The box is the stage for a top-level overlay node, the content of the group or panel it's in, or the client object it replaces. A node can follow an anchor or be aligned, not both.
Dragging
drag lets the player move any node. Flash follows the mouse itself, so nothing is sent while the drag runs, and on('move') tells you where it ended up.
const knob = await phantom.flash.overlay.circle({
x: 40,
y: 400,
radius: 7,
fill: 0xffffff,
hand: true,
drag: { axis: 'x', left: 40, right: 200, step: 4, live: true },
});
knob.on('move', ({ x }) => console.log('now at', x));axisisx,yorboth.left,top,rightandbottomkeep it within those limits, in its parent's coordinates.stepmoves it in whole steps, which is how a slider holds whole values.livesends amovefor each step of the drag instead of only where it stopped.
Saving where it was dragged to is up to you. Nothing puts it back by itself, and a node redrawn with an x goes to that x.
A panel drags itself by its title bar, using the same code.
Following something in the room
anchor pins a node to one of the client's display objects, and the loader moves it there every frame, so a label over an animal keeps up without anything being sent.
const me = phantom.flash.use('avatar.AvatarManager').playerAvatarWorldView;
const label = await phantom.flash.overlay.text('that is you', {
anchor: { place: me, y: -80 },
centered: true,
outline: 0x000000,
});
await sleep(5000);
await label();phantom.aj.ui.tag does this with a ready-made look, one tag per player. phantom.flash.overlay.world(point) and marker place a node at a point in the room rather than on an object.
Drawing inside the client's screens
The overlay sits above the whole game. That's right for a label over an animal but wrong for a badge on a HUD button or a shadow on the floor.
attach(place, node)draws the node inside one of the client's containers, so it moves, scales and hides with it.replace(place, node)hides one of the client's display objects and puts the node in its place, at its size.
const badge = await phantom.flash.overlay.text('3', {
x: 24,
y: -4,
font: { size: 11, bold: true },
background: 0xef4444,
padding: 2,
});
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const remove = await phantom.flash.overlay.attach(hud.$at('buddyListBtn'), badge);A place given by path is looked up again from time to time, so it's found again after the client rebuilds the HUD. phantom.aj.ui.places names the client's screens, so you don't have to search the decompiled client for them.
The remove that attach returns removes the node as well.
Pictures
image(url, options) takes an address on the content origin or a base64 data: URL, so a picture your script drew in a canvas can go straight in.
const wall = await phantom.flash.overlay.image('data:image/png;base64,iVBORw0KGgo=', {
x: 100,
y: 100,
width: 240,
});
await wall.update({ url: 'data:image/png;base64,iVBORw0KGgo=' });Given one size, it keeps the picture's proportions. When you give a node a new address, it keeps showing the old picture until the new one loads, so a picture that changes several times a second doesn't flicker, and one that fails to load leaves the previous one up. onFailed tells you it didn't load.
Loader features
Measuring, aligning and dragging need a loader that lists the feature in phantom.flash.bridge.features, which the one Phantom ships does. A game running an older loader refuses the call with an error that says what to do instead.
can checks first, for when the feature is a nice-to-have. A scroll box checks gestures so that on a game that can't drag, it still draws its bar (just not draggable) instead of refusing to draw at all.