FlatInk

The FlatInk Script language

FlatInk Script stays deliberately small. No infinite loop, no hidden evaluation, no surprise. You describe what happens, and the editor keeps it all in plain sight.

Events

An event triggers a block of actions. They come in three families.

when loaded { ... }      // once, at startup
every frame { ... }      // on every frame

when clicked { ... }     // on an object, on click
when hovered { ... }     // hover enter
when unhovered { ... }   // hover exit

when pressed { ... }     // pointer pressed on the object
when dragged { ... }     // moved while it is held
when released { ... }    // let go
when held { ... }        // held still (long press)

Actions

play
pause
go to frame 10 and play
go to "intro" and pause

score = score + 1          // set a variable
grid[i] = 0                // write into an array

if lives > 0 { ... } else { ... }
repeat 8 times { ... }
repeat i from 0 to 5 { ... }

launch()                   // call a procedure
send "win", score          // emit an event to the host page
sound "ding"               // play a sound

Expressions

Values are numeric (zero is false). Expressions drive an animation channel, or compute inside an action.

rotation = sin(time) * 0.5
x = mouse.x
opacity = lives > 0 ? 1 : 0.3

Available variables: time (in seconds), frame, value (the channel’s base value), mouse.x, mouse.y, mouse.dx, mouse.dy, and keys.Space, keys.ArrowLeft, and so on.

Constants: PI, TAU, E.

Functions: sin cos tan, sqrt pow exp log, floor ceil round, abs min max sign hypot, clamp(x, min, max), lerp(a, b, t), mod(a, b), random().

The channels you can drive are x, y, scaleX, scaleY, rotation and opacity. An expression on a channel takes priority over keyframe interpolation.

Referencing an object by name

Any object on the stage is readable by its name: Name.x, Name.y, Name.rotation, Name.scaleX, Name.scaleY, Name.opacity give its on-screen position (and state) at the current frame. And self refers to the current object (its own channels), inside its bindings — no more hand-kept mirror variables to locate yourself or track another object.

object "Ball" {
  rotation = atan2(Target.y - self.y, Target.x - self.x)          // aims at "Target", wherever it is
  opacity  = hypot(Target.x - self.x, Target.y - self.y) < 30 ? 0 : 1
}

These values are read-only (to move an object, drive its own channels or a variable). self only exists inside an object’s channel binding. If two objects share a name, the first one answers — to act on every instance of a symbol, use each (below). The name must be an identifier (no spaces) for the Name.x form.

Dragging an object

drag makes an object draggable with the mouse and writes its position into your variables (which you then bind to x/y). No more hand-written when pressed/when dragged.

object "Piece" {
  drag px, py                 // follows the pointer → writes px, py
  when dropped on Target {    // released over the named zone "Target"
    px = Target.x
    py = Target.y
  }
  x = px
  y = py
}

Frames: your space (local) vs the world

Two spaces, as in any animation tool:

At the scene root, local = world → nothing to think about, they coincide (the common case).

If your object is nested inside a group and you want to relate it to a world position, convert with toLocalX/Y (in a binding or a when …):

object "Cursor" {                    // nested inside a panel
  x = toLocalX(mouse.x, mouse.y)     // follows the mouse, brought into your local space
  y = toLocalY(mouse.x, mouse.y)
}

(At the root it’s the identity — you don’t need it. And drag does the conversion for you, even nested.)

Variables and arrays

let score = 0
let bricks = fill(32, 1)      // an array of 32 cells set to 1
let colors = [0, 1, 2]

An array variable is read with bricks[i] and written with bricks[i] = 0.

Functions

fn distance(ax, ay, bx, by) = hypot(ax - bx, ay - by)   // returns a value
fn launch() {                                            // a procedure
  lives = 3
  go to frame 1 and play
}

Packages

use "name" brings in ready-made functions.

use "collision"   // boxHit, dist, near
use "gesture"     // snap, snapTo, railX, railY, angle, inZone
use "easing"      // easeIn, easeOut, easeInOut, smooth

See the drag-and-drop mini-game for a concrete use of collision and gesture.

Binding every instance of a symbol

When a scene holds many instances of the same symbol, each binds them all at once, with an index per instance.

each "Brick" as i {
  opacity = bricks[i]
}

Instances are matched by the symbol’s name, not by where it sits in the library. Library folders only tidy the list — a symbol’s name stays its unique, global handle, so filing it in a folder never changes how each or Name.x reach it.