Skip to content

LUA SCRIPTING API

API Reference

The complete specification of the callbacks, ctx objects, coordinate systems, and mt.* utilities available to Mug Typography Lua scripts.

Also available offline

The same API specification is included in the distribution ZIP under scripting-simulator/docs/.

Mug Typography scripting is a feature for controlling text layout and Character/Part animations with Lua 5.4 scripts written in the plugin's Script Text. For development, you can use the VS Code extension "Mug Typography Scripting Simulator" (MT Sim) and the installation-free online simulator available in a browser. Both let you preview and debug scripts without launching the host application.

1. Basic Model ​

A script can define up to three callback functions. All are optional, and callbacks that are not defined are simply skipped.

text
Frame evaluation path (every frame)

  Every frame
    └─ OnPreLayout(ctx)
         Modify base values for layout, Global settings, and camera; declare shapes and custom glyphs
          ↓
       Generate Character and Part layout
          ↓
       OnLayout(ctx)
         Individually adjust the generated Characters, Parts, and Shapes
          ↓
       OnPath(ctx)
         Adjust the outlines of selected Parts and Shapes
          ↓
       Rendering

The important point is that the execution timing and the objects that can be manipulated differ for each callback. Define only the callbacks for the stages appropriate to your purpose.

  • OnPreLayout runs before layout generation on every frame. It changes the generation conditions for Characters and Parts, such as text, font, tracking, Global settings, and camera, and declares shapes or custom glyphs
  • OnLayout runs after Characters and Parts have been generated. It adjusts the position, rotation, color, and other properties of each generated element and shape
  • OnPath runs after individual adjustments and before rendering. It replaces or rebuilds the outlines of selected Parts and Shapes

Later callbacks can read values finalized in earlier stages. However, they cannot go back and rewrite values in a stage that has already been processed. For example, OnLayout can read ctx.global and ctx.camera, but cannot modify them.

Changes Are Valid Only Within the Frame ​

Every frame, ctx is passed to callbacks as state recreated from the host's Inspector values. Values written by callbacks are used only to render that frame, and the next frame starts again from the Inspector values. They are never written back to the Inspector or keyframes.

For example, ctx.global.rotation = ctx.global.rotation + 1.0 does not mean "continue rotating by one degree every frame"; it means "always use the Inspector value + 1 degree." To advance the rotation while preserving the Inspector setting, add a frame-independent delta derived from ctx.time, as in ctx.global.rotation = ctx.global.rotation + ctx.time * 20.0. Direct assignment such as ctx.global.rotation = ctx.time * 20.0 should be used only when intentionally replacing the Inspector rotation completely.

2. Frame Independence Requirement (Stateless Execution) ​

The host does not necessarily evaluate frames in chronological order. The result must be the same during scrubbing, reverse playback, caching, and parallel rendering. The result of each frame must be determined only by the current input values and must not depend on the previous frame's result or the order in which frames are evaluated. In this document, this property is called frame independence.

  • If you modify a global variable or a local variable declared outside a function, do so only to pass a value between callbacks within the same frame, and overwrite it from the current input values on every frame. Do not carry forward or accumulate values from the previous frame
  • Do not retain references such as ctx or ctx.chars[index] after the callback ends
  • Do not assume fixed values for the character count, Part count, or the shape of a particular font
  • Derive the value for the frame directly from ctx.time instead of using a sequential simulation
  • Always use ctx.char_count / ctx.part_count when iterating over Characters or Parts

Script Metadata ​

Writing @<name> <value> as a Lua comment at the beginning of a script file lets you declare the required API, MT Sim behavior, initial settings, and Script Inputs. Metadata is not Lua API executed inside callbacks, but the host plugin also interprets @api_level, @input, and @time_independent.

Script Recognition and Execution Settings ​

DeclarationDescription
@MugTypographyWhen placed in the leading uninterrupted -- single-line comment block starting at line 1 (or within the first 5 lines in MT Sim), the file is recognized as a Mug Typography script. It is required for code completion, simulator integration, and host @input parsing, and is always included on line 1 of distributed samples. @mt or @mug can also be used
@duration <seconds>Sets the simulator's initial playback duration. Example: @duration 5.0
@required 3dEnables the 3D projection setting (Enable 3D projection) when the simulator starts. 3dprojection can also be used

Use @recommend <setting> only when a script's presentation requires specific initial conditions. Keep specifications to the minimum necessary. Specifying text, position, background, and other values indiscriminately makes it difficult to tell whether the displayed result comes from the script or from the Inspector's initial settings. Do not use it for scripts that work with arbitrary settings.

DeclarationDescription
@recommend bg <specification>Initial background value. Specify a solid color with #RRGGBB / #RRGGBBAA, a checkerboard background with checkerboard / checker, or solid to turn the checkerboard off explicitly. A color and a mode can be written on the same line. Example: @recommend bg checkerboard #ffffff
@recommend fill <color>Initial Global fill color. Specify it as #RRGGBB / #RRGGBBAA. Example: @recommend fill #d8d4f2
@recommend gradient <color>Initial Gradient End Color. A script can use it as a secondary color even when the gradient mode is none. Example: @recommend gradient #ff4f9a
@recommend pos <X> <Y>Initial Global text position. Larger X values move right, and larger Y values move up. Examples: @recommend pos 0.5 0.5 for the center, or @recommend pos 0.5 0.18 for the lower part of the screen
@recommend text "<string>"Initial Text value. Enclose it in "..." or '...'. \n becomes a line break, while \" \' \\ become the respective literal characters. Example: @recommend text 'swaying\ntext'. "" starts with empty text, for full-screen backgrounds that draw without body text
@recommend vertical <boolean>Initial writing direction: true for vertical, false for horizontal. Applied on script load; later Inspector changes are respected
@recommend lang <language code>Automatically selects a font that supports the language. Example: @recommend lang ja. It can be omitted when the language can be inferred from text, and an explicit value takes precedence over automatic detection

These recommended initial settings are also applied when the simulator's Reset button is pressed. The VS Code extension and online simulator show a color swatch for the bg, fill, and gradient hex values. Select the swatch to change the value with the editor's standard color picker.

Script Inputs ​

Script Inputs are available in API Level 8 and later. @input declares the slot, Inspector label, and optional initial value shown under Script Inputs. Its syntax is:

text
@input <type> <index> "<label>" [default=<value>]
lua
-- @input number 1 "Amplitude" default=15.0
-- @input number 2 "Speed" default=1.0
-- @input color 1 "Primary Color" default={1.0, 0.4, 0.2, 1.0}
-- @input text 1 "Caption" default="Hello"
-- @input boolean 1 "Enable Glow" default=true
typeindexLua accessWhen default is omitted
number1-14ctx.inputs.numbers[index]0.0
color1-8ctx.inputs.colors[index]{1.0, 1.0, 1.0, 1.0}
text1-4ctx.inputs.texts[index]Empty string
boolean1-8ctx.inputs.booleans[index]false

boolean requires API Level 10 or later. Its default accepts only true or false.

  • Types are ASCII case-insensitive. Slots are displayed in Number, Color, Text, Boolean index order, not declaration order
  • Labels must contain 1-64 UTF-8 bytes and support only \" and \\ escapes
  • A Number default must be finite and within -1000000.0-1000000.0; a Color default has exactly four RGBA channels within 0.0-1.0; a Text default is a single-line quoted string without NUL and no longer than 4096 UTF-8 bytes; a Boolean default is true or false
  • Duplicate type/index pairs, unknown types, out-of-range indexes, malformed quotes, escapes or defaults, and trailing tokens are metadata errors. An error preserves the last valid Inspector presentation, reports Input line <N>: <reason> in Script Status, and does not prevent Lua compilation or execution
  • Only declared slots appear in the Inspector. With no declarations, the complete Script Inputs group is hidden. Lua always reads the fixed-length arrays, including undeclared slots
  • When Script Text changes, an initial value is applied only to a newly declared slot or one whose effective default changed. A label-only change preserves its value and keyframes
  • A default does not overwrite a slot with keyframes. Project loading preserves stored values and keyframes without reapplying metadata defaults

Time Independence ​

-- @time_independent takes no value. It allows the host to reuse rendered overlays across frames with unchanged Inspector values and excludes this script from frame-varying notification.

Place -- @time_independent in the leading uninterrupted -- comment block, before any blank or indented line, code, or block comment. Omission retains time-dependent behavior. This is an author contract, with no automatic analysis: do not declare it when output depends on ctx.time, ctx.frame, timeline information, call counts, or external state. Changes to Inspector values (including animated @input slots), script source, or render dimensions still invalidate reuse. Other time-varying effects such as Pulse still require rendering and notification. The simulator parses the declaration but continues evaluating preview frames.

Sample Browser Display Information ​

This information is displayed in the web simulator's sample browser. When omitted, the file name is used as the title, and the author, version, and API level are not displayed.

DeclarationDescription
@title <display name>Title displayed on the card. Example: @title Callback Flow Basics
@author <author name>Author name. Example: @author Mug
@version <version>Script version. Example: @version 1.0
@api_level <number>API level required by the script. Example: @api_level 6. A plugin that does not support the declared level refuses to load the script
@animation_idID of a bundled sample (its genre's three capital letters plus a three-digit number), e.g. FAD002, displayed and searchable in the browser. Your own scripts do not need it
@descriptionMulti-line description inside a block comment. The line break immediately after @description is ignored
@description.<locale>Localized sample description, e.g. @description.ja or @description.en. The browser falls back to @description when a translation is missing and searches all translations

Example ​

lua
-- @MugTypography
-- @duration 5
-- @title Callback Flow Basics
-- @author Mug
-- @version 1.0
-- @api_level 8
-- @input number 1 "Amplitude" default=15.0
--[[ @description
Reference: callback execution timing and the frame-independent
scripting contract.
]]

Placement and Parsing Range ​

@MugTypography and @duration / @required / @recommend / @input / @title / @author / @version / @time_independent / @api_level must be placed within the run of consecutive -- single-line comments starting at line 1. The parsing range ends when an @description block comment or a line that is not a comment (including a blank or indented line) is reached. Therefore, write single-line metadata before @description. Multiple @recommend lines and multiple settings on one line are both supported. In generated code, put @MugTypography on line 1 and the other metadata and description immediately after it.

3. Callbacks ​

CallbackInvocation timingMain purpose
OnPreLayout(ctx)Before layout generation (every frame)Change the generation conditions for Characters and Parts and the Global settings; declare shapes and custom glyphs
OnLayout(ctx)After layout generation (every frame)Individually adjust generated Characters, Parts, and Shapes
OnPath(ctx)After OnLayout (every frame)Replace or rebuild the outlines of selected Parts and Shapes

For fields readable and writable in OnPreLayout / OnLayout, see the ctx reference. Fields for OnPath are described in §3.3.

3.1 OnPreLayout(ctx) ​

ItemDescription
Signaturefunction OnPreLayout(ctx)
Argumentctx: table. Context before layout generation
Return valueNone. Any returned value is ignored
Invocation timingEvery frame, before Character and Part layout generation
Main modification targetsctx.global / ctx.camera / ctx.shapes:declare / ctx.shape_cameras:declare / ctx.glyphs:declare
On errorOn the host, skips later callbacks and does not render that frame (MT Sim continues previewing with the original values)

Receives a working copy of the Global settings created from host parameters and modifies values before layout generation.

TIP

Design rule for scale adjustment: When scaling text to fit the frame based on text length or character count, adjust ctx.global.scale in OnPreLayout rather than shrinking each character's character.scale inside OnLayout. That keeps the overall text size steady instead of shifting every time a character appears during a typewriter-style reveal.

See ctx.timeline for time-axis design rules (real seconds vs. progress).

3.2 OnLayout(ctx) ​

ItemDescription
Signaturefunction OnLayout(ctx)
Argumentctx: table. Context after layout generation
Return valueNone. Any returned value is ignored
Invocation timingEvery frame, after Character and Part layout generation
Main modification targetsctx.chars[index] / ctx.parts[index] / ctx.shapes[index] / ctx.shape_cameras[index] / ctx.bounding_box / ctx.output
On errorOn the host, skips OnPath and does not render that frame (MT Sim continues previewing with the unmodified layout)

Adjusts Characters, Parts, and Shapes generated from the finalized Global settings. The Character and Part counts are finalized at this point.

3.3 OnPath(ctx) ​

ItemDescription
Signaturefunction OnPath(ctx)
Argumentctx: table. Context for path editing
Return valueNone. Any returned value is ignored
Invocation timingEvery frame, after OnLayout and before deformation, projection, and rendering
Main modification targetThe path of a Part or Shape obtained from ctx.paths
On errorOn the host, does not render that frame (MT Sim continues previewing with the original glyph outlines)

Use part / character / select / shape on ctx.paths to obtain target Parts and edit their path. For details of the retrieval methods and editing API, see the ctx reference.

3.4 Complete Script Example ​

lua
-- @MugTypography
-- @api_level 8

-- Base phase: values that determine layout and global inheritance.
function OnPreLayout(ctx)
    ctx.global.tracking = mt.wave(ctx.time, 0.25, 0.0) * 0.05
    ctx.global.rotation = mt.wave(ctx.time, 0.1, 0.0) * 5.0
end

-- Layout phase: adjust only the generated characters and parts.
function OnLayout(ctx)
    for index = 1, ctx.char_count do
        local character = ctx.chars[index]
        local progress = mt.stagger(ctx.time, index, 0.06, 0.35)
        character.offset_y = 0.5 + (1.0 - mt.ease.out_cubic(progress)) * 0.15
        character.rotation = character.rotation + mt.wave(ctx.time, 0.5, index * 0.25) * 3.0
    end

    for index = 1, ctx.part_count do
        local part = ctx.parts[index]
        part.scale = part.scale * (1.0 + mt.random(42, index) * 0.05)
    end
end

For more practical examples, see the bundled samples/. samples/reference/ contains short, topic-grouped lessons for the scripting model and individual APIs, while samples/showcase/ is a collection of complete animations.

4. Types and Coordinate Systems ​

4.1 Definitions of Coordinates and Units ​

Public number fields belong to one of the following unit systems.

X components use canvas width as 1, and Y components use canvas height as 1. Positive Y is upward (§4.3).

canvas normalized position ​

Represents a position on the canvas. The value 0.5 is the canvas center on the corresponding axis.

FieldDefinition
ctx.global.position_x / position_yGlobal position. Writable (OnPreLayout)
ctx.global.pivot_x / pivot_yDisplacement of the pivot for Global transformations relative to global.position. Writable (OnPreLayout)
canvas coordinates in geometry (bounds_center_*, etc.)Read-only positions in the pre-transformation layout. Different for each Character and Part

canvas normalized displacement (neutral value 0.5) ​

Represents displacement relative to the natural position determined by the layout. It is not an absolute position. The neutral value is 0.5, and the rendered displacement is interpreted as follows:

text
Displacement (canvas ratio) = value - 0.5

The displacement is based on canvas width or height, not character size or geometry.advance_*.

FieldDefinition
Character/Part offset_x / offset_yTranslation from the natural position. At 0.5, displacement is 0 and the item remains at the position determined by the layout (its center before transformations is geometry.bounds_center_*). Values greater than 0.5 translate right/up. Use mt.layout.place_2d to place an item at a target 2D canvas position
global.pivot_x / global.pivot_yDisplacement of the rotation/scale pivot from global.position. At 0.5, the pivot matches global.position
Character/Part pivot_x / pivot_yDisplacement of the rotation/scale pivot from the natural center. At 0.5, the pivot matches the natural center (for Characters, geometry.bounds_center; for Parts, the natural position)

Relationship between position and displacement:

TypeTargetsMeaning of valueNeutral / center
Positionglobal.position_*, canvas coordinates in geometryCoordinate relative to the canvas0.5 = canvas center
Displacementglobal.pivot_*, Character/Part offset_* / pivot_*Difference relative to global.position, the natural position, or the natural center0.5 = zero difference

Other Unit Systems ​

Unit systemFieldsDefinition
emtracking, margins, stroke.width, stroke.gap, shadow.distanceRatio to font size. 1.0 = 1 em. Strokes are multiplied by fontSize during rendering
fontSize percentagebounding_box.width / margin_* / corner_radius / fill_offset_* / stroke_offset_*100.0 = the same length as fontSize. Conversion to px is value * fontSizePx / 100
Multiplierscale, stretch_*, zoom1.0 is the original size
0-1opacity, each color component, write_on_start / write_on_end, bounding_box.fill_opacity, etc.Value interpreted as a closed interval or progress
degreerotation, yaw, pitch, and various angle fieldsAngle. See §4.3 for signs
Perspective strengthcamera.perspectiveUnitless. Internally mapped as distancePx = f(perspective) * canvas.height (larger values move the camera closer)
pxcanvas.width / heightPixels

NOTE

shadow.distance is in em, like stroke.width, and is measured in each part's own space: the shadow keeps the same distance from the glyph however the part is scaled (global, character and part scale and stretch_*), rotated or seen in perspective. The direction set by shadow.angle stays fixed on the canvas.

4.2 color ​

lua
{ r = 1.0, g = 0.5, b = 0.0, a = 1.0 }

r / g / b / a are numbers in the range 0-1. You only need to include the channels you want to change (for example, { a = 0.5 }); any omitted keys keep their existing values on the target color. Adding any key other than r / g / b / a raises an error.

4.3 Coordinates ​

  • See §4.1 for the unit definitions of position, offset, and pivot

  • X is based on canvas width, and Y is based on canvas height

  • For global.position, Character/Part offset / pivot, and geometry, positive Y is upward (increasing the Lua Y value moves upward on screen)

  • For 3D depth, positive z points deeper (larger values move farther from the camera and place the item deeper into the screen. Negative values move it closer to the viewer)

  • global.pivot and Character/Part offset / pivot are canvas normalized displacements (§4.1). Geometry canvas coordinates and global.position are canvas normalized positions

  • For geometry canvas positions, the lower-left of the canvas is 0,0, and the center is 0.5,0.5

  • Geometry local positions and advances are displacement amounts, so they have no center offset and are represented as Y-up

  • Drawing coordinates use Y-down; placement coordinates, offsets and character metrics use Y-up

  • rotation, yaw, pitch, and angle are all in degrees

    • 2D rotation (rotation): Positive (+) is clockwise (right rotation), and negative (-) is counterclockwise (left rotation) (90 turns what pointed up so it points right).
    • 3D object rotation (yaw, pitch): For both Individual and Global, rotation occurs around the individual pivot.
      • yaw: Positive (+) rotates the object's right edge toward the viewer (-Z) and its left edge deeper (+Z).
      • pitch: Positive (+) rotates the object's top edge toward the viewer (-Z) and its bottom edge deeper (+Z).
    • Camera orbit (yaw, pitch): The camera orbits around the look-at point.
      • yaw: Positive (+) moves the camera around to the right of the look-at point (the object on screen appears to rotate left).
      • pitch: Positive (+) moves the camera upward around the look-at point (producing a downward-looking angle).
    • Gradient angle (angle): At 0 degrees it progresses from left to right (positive X direction), and increasing (positive) angles rotate it clockwise.
    • Shadow angle (angle): At 0 degrees it extends rightward (positive X direction), and increasing (positive) angles rotate it clockwise (90 degrees points straight down).

WARNING

The 2D angle system consistently follows "screen intuition" (clockwise is positive, 90 degrees = straight down), which feels opposite to the coordinate system (Y-up). While positive offset_y points up, positive rotation / gradient.angle / shadow.angle rotates clockwise, and a shadow angle of 90 points straight down.

Deriving the sign from the coordinate system gives the opposite answer. Y-up normally implies counterclockwise-positive, which is what the mathematical convention says and what Fusion's own Transform node does. This effect is the exception, so an angle taken from either source needs its sign flipped, including when you combine this effect with a Transform node and want them to turn together.

Read the sign off a clock face. rotation = 90 turns what pointed up so it points right (12 o'clock → 3 o'clock). Watch the lower half, where the clock and the screen stop agreeing: a shape hanging down swings left under a positive value, since 6 o'clock advances toward 7 and 8.

When you turn an angle into a direction vector and feed it to offset_y, use mt.polar_offset_2d. Its input stays in the angle convention (90 degrees points down on screen), while the result uses the position convention (positive Y is upward), so it can be added directly to both offset_x and offset_y.

lua
local dx, dy = mt.polar_offset_2d(angle, radius)
character.offset_x = character.offset_x + dx
character.offset_y = character.offset_y + dy

TIP

Use mt.layout.radial_distance for distances in circular and radial effects. The canvas aspect ratio is corrected inside the function, so you do not need to transform the X coordinate manually.

lua
local distance = mt.layout.radial_distance(
    ctx,
    geometry.bounds_center_x,
    geometry.bounds_center_y
)

To calculate a signed distance or dot product in a diagonal direction, multiply the X difference by ctx.canvas.aspect_ratio before the calculation. Also multiply by ctx.canvas.pixel_aspect_ratio when accounting for the physical display ratio of non-square pixels.

pivot Is a Displacement from the Reference Point ​

  • ctx.global.pivot_x/y is a displacement from global.position. At 0.5, 0.5, the pivot matches global.position
  • Character/Part pivot_x/y is a displacement from the item's own reference point. At 0.5, 0.5, a Character rotates and scales around its natural bounding-box center (equivalent to geometry.bounds_center), while a Part rotates and scales around its natural position (equivalent to geometry.canvas_center)
  • Therefore, do not assign a canvas coordinate from geometry directly to a Character or Part pivot. To rotate around a point shifted 10% to the right from the item's own center, use pivot_x = 0.5 + 0.1
  • To use a Character's edge as its pivot, displace it from the natural center by half of its size (bounds_width / bounds_height are canvas-axis ratios and use the same unit system as pivot displacement):
    • Bottom edge: pivot_y = 0.5 - geometry.bounds_height * 0.5
    • Top edge: pivot_y = 0.5 + geometry.bounds_height * 0.5
    • Left edge: pivot_x = 0.5 - geometry.bounds_width * 0.5
    • Right edge: pivot_x = 0.5 + geometry.bounds_width * 0.5

4.4 Assignment Rules ​

The current value of a writable field contains the Inspector setting and any value finalized by an earlier callback. Unless a complete override is part of the effect's design, compose animation over that current value.

  • Add animated deltas to position, offset, pivot, rotation, and depth values
  • Multiply scale, stretch, and opacity by animated factors
  • Direct assignment is appropriate when replacing a style such as a fill or stroke color is the effect's explicit purpose
  • If several calculations need an original value, save it to a local variable before the first write
lua
local inspectorRotation = character.rotation
local inspectorScale = character.scale

character.rotation = inspectorRotation + math.sin(ctx.time) * 10.0
character.offset_y = character.offset_y + math.sin(ctx.time * 2.0) * 0.05
character.scale = inspectorScale * (1.0 + math.sin(ctx.time) * 0.1)
character.opacity = character.opacity * fade

-- Assign directly only when replacing the Inspector setting is intentional
character.rotation = ctx.time * 20.0

The following operations cause errors:

  • Assigning to a non-writable field
  • Adding an unknown field
  • Type mismatch (such as assigning a string where a number is expected)
  • Replacing or changing the number of elements in read-only containers such as ctx.chars / ctx.parts
  • Assigning an invalid string to an enumerated field

5. Inheritance Mechanism ​

Roles of use / opacity / fill.transparent ​

SettingEffect on rendering
fill.use / stroke.use / shadow.useWhen true, uses the individual values set on the target Character or Part; when false, inherits the parent settings. Does not toggle visibility
opacity (Global / Character / Part)Multiplies the opacity of the fill, stroke, shadow, and other values determined after inheritance. 0 is transparent, and 1 preserves the original opacity
global.fill.transparentMakes only the fill transparent. Does not affect the stroke or shadow

The color and opacity used for rendering are determined in the following order.

  1. Determine color: If fill.use is true, use the individual color set on the Character or Part; if false, use the gradient (when enabled) or the Global fill color
  2. Apply opacity: Multiply the opacity of the determined color by global.opacity × character.opacity × part.opacity

When use = false, modifying the individual color of a Character or Part does not make that color affect rendering. Changing only opacity changes only the opacity without changing the color or gradient being used.

Values used for rendering are determined in the following order.

text
Global finalized in OnPreLayout
  ↓
Character: if each group's use is true, use the Character's individual value; if false, use the Global value
  ↓
Part: if each group's use is true, use the Part's individual value; if false, use the value selected at the Character stage

When gradient.color_space is anything other than none, only the areas for which the Global value was selected in this order (where both the Character and Part fill.use values are false) are filled with the gradient. Characters or Parts with individual colors enabled are filled with their individual solid colors.

A Part's stroke / shadow follows the same order as fill when selecting the value to use, except that the host has no corresponding parameter groups for Parts (they always begin unused/with default values).

Characters and Parts passed to OnLayout contain individual settings before parent settings are applied. Lua can also modify the use field of each group, so an individual value can be modified and enabled within the same callback.

lua
local character = ctx.chars[index]
character.fill.use = true
character.fill.color = mt.color.from_hsv(index / ctx.char_count, 0.8, 1.0)

Transform fields (offset / scale / rotation, etc.) have no use_* field and always affect rendering. Script modifications are confined to that frame and are never written back to the host Inspector or keyframes.

6. Execution Environment, Errors, and Limits ​

6.1 Frame Handling on Errors ​

On the host, an error anywhere in a callback causes that frame not to be rendered. Changes made before the failure are not guaranteed to be rolled back, but an incomplete result is never used for rendering.

To keep debugging convenient, MT Sim reports the error in the Output panel and continues previewing with values from before the failed callback. This differs from the host's frame-abort behavior.

Failure locationResult
Script loadingThe host does not render; MT Sim reports the error and retains the last valid script
OnPreLayoutThe host skips later callbacks and does not render; MT Sim previews the original values
OnLayoutThe host skips OnPath and does not render; MT Sim previews the unmodified layout
OnPathThe host does not render; MT Sim previews the original glyph outlines

6.2 Available Standard Libraries ​

The Lua standard libraries are limited to a safe subset.

  • Available: Safe basic functions, math, string, table, utf8
  • Unavailable: os, io, package, require, dofile, loadfile, load, debug, collectgarbage, rawget, rawset, getmetatable, setmetatable

Files, networks, and processes cannot be accessed.

6.3 Execution Limits ​

  • 5,000,000 instructions per callback invocation
  • Memory limit of 128 MiB (during host execution)

A callback that exceeds a limit is treated as failed and follows the behavior described in §6.1.

6.4 print(...) and Frame Dump ​

Lua's standard print(...) can be used to output diagnostic information to the Output panel in MT Sim.

Frame Dump ​

Clicking the Bug icon (Frame Dump) button on the simulator toolbar evaluates the marked timeline range (or all frames) in one pass and dumps scene context (resolution, character/part count, font, etc.) along with each frame's print(...) output and errors to the log.

  • === SIMULATOR CONTEXT ===: Scene context including canvas size, fps, character count, part count, font, and layout mode
  • === FRAME LOGS ===: Per-frame [<frame>] <text> output (frames that print nothing are omitted)
  • === DUMP COMPLETE ===: Evaluation summary
  • You can narrow down the verification range by dragging the timeline in/out handles.

6.5 Checking Errors ​

Script error details cannot be checked in the host. First run the script in MT Sim, then check and fix the error message and stack trace displayed in the Output panel.

Target API level: 15

ctx is the context that Mug Typography passes to each callback. It contains the information needed to process the current frame, including the current time, canvas information, Global settings, and the generated characters and parts.

The available fields and write permissions differ by callback. This document first lists availability for each callback, then describes the fields of ctx in order. For the callback execution order and purposes, see Callback Overview.

1. List of ctx Fields ​

The fields directly under ctx are themselves read-only and cannot be replaced with other values. However, objects such as ctx.global and ctx.chars[index] contain fields that can be modified depending on the callback. ○ indicates that a field can be read in that callback.

FieldTypeOnPreLayoutOnLayoutOnPathDescription
ctx.timenumber○○○Current time in seconds, with the beginning of the clip as 0 (§1.1)
ctx.framenumber○○○Frame number with the beginning of the clip as 0. May include subframes (§1.1)
ctx.fpsnumber○○○Frame rate (§1.1)
ctx.canvastable○○○Canvas and project pixel information (§1.2)
ctx.fontsstring[]○○○1-based array of font names that can be assigned to global.font_name (§1.3)
ctx.timelinetable○○○Clip time range and current progress (§1.4)
ctx.fonttable-○○Metrics (dimensional information) for the font in use (§1.5)
ctx.textstring○○-Deprecated text reference (§1.6)
ctx.char_countinteger-○○Number of valid text characters; shapes are not counted (§1.6)
ctx.part_countinteger-○○Number of valid text parts; shapes are not counted (§1.6)
ctx.shape_countinteger-○○Number of declared shapes (§1.17)
ctx.metatable○○○API version and feature information (§1.7)
ctx.inputstable○○○Script inputs configured in the Inspector (§1.16)
ctx.globaltable○○○Global settings. Writable in OnPreLayout and read-only thereafter (§1.8)
ctx.cameratable○○○Camera settings. Writable in OnPreLayout and read-only thereafter (§1.9)
ctx.charstable[]-○○1-based array of generated text characters (§1.10)
ctx.partstable[]-○○1-based array of generated text parts (§1.11)
ctx.bounding_boxtable-○○Bounding-box drawing settings (§1.12)
ctx.outputtable-○○Output filtering and drawing-order settings (§1.13)
ctx.pathstable--○Context for retrieving and editing part paths (§1.14)
ctx.glyphstable○--Context for declaring custom glyphs (§1.15)
ctx.shapestable○○○Context for declaring shapes and reading them by number (§1.17)
ctx.shape_camerastable○○-Context for declaring the cameras that view shapes and moving them by number (§1.17)

1.1 ctx.time / ctx.frame / ctx.fps ​

These fields express the current evaluation position relative to the beginning of the clip. ctx.time is in seconds, and ctx.frame is in frames. Because ctx.frame can include subframes, it is not necessarily an integer. ctx.fps is the number of frames per 1 second.

1.2 ctx.canvas ​

Canvas and project pixel information. All fields are read-only.

FieldTypeUnitDescription
width / heightnumberpxCanvas width and height
aspect_rationumber-Canvas width divided by height (width / height)
pixel_aspect_rationumber-Pixel aspect ratio of the host project

1.3 ctx.fonts ​

A read-only, 1-based array containing the names of installed fonts recognized by the host. Its contents match the Font list in the host UI, and in OnPreLayout its elements can be assigned to ctx.global.font_name.

Neither the array elements nor the number of elements can be changed.

1.4 ctx.timeline ​

FieldTypeUnitDescription
availableboolean-Whether a finite clip range could be obtained
start_frame / end_framenumberframeStart/end of the range (clip-local; the start is always 0)
duration_framesnumberframeend_frame - start_frame
duration_secondsnumbersecondDuration converted to seconds
progressnumber0-1Current position clamped to 0-1

Clip Range ​

The clip range is the clip's actual range on the timeline, including trimming and extensions before or after it. ctx.time, ctx.frame, and ctx.timeline receive values on the same time axis, with the beginning of the clip as 0. When applied to a video clip as a filter, they also follow that clip's range.

When available is false, the other values are 0, so provide a fallback.

NOTE

Limitation of extending the beginning in generator use: When the beginning of a generator clip (with no input connected) is extended on the timeline, the host treats that interval as a still image of the first frame, so script time does not advance until the extension boundary (the intro appears frozen in its initial state). To change the duration, change the length of the generator clip itself instead of extending it. This limitation does not apply when used as a filter.

Time-Axis Design Rules (Anchor × Speed) ​

Animation timing is designed along 2 axes: "(1) where it is fixed (anchor)" and "(2) how fast it advances (speed)." The default speed is real seconds (based on ctx.time), and progress, which scales proportionally with clip length, should be used only for the "full-span" pattern described below.

PatternAnchorHow to Write It
Intro (entrance)Beginning of clipUse ctx.time directly (real seconds). For a combined intro + outro, use mt.timeline.intro_outro_seconds (§10.9)
Outro (exit)End of clipmt.timeline.remaining(ctx) (real seconds remaining until the end; §10.9)
Typewriter, stagger, spring, etc.Beginning of clipctx.time (real seconds; specify a physical speed such as N seconds per character)
Full-span pattern (drift, sweep, or reveal across the entire duration)Entire clipprogress (the expected behavior is for the movement to lengthen when the duration is extended)

If a transition is written as a fraction of progress, extending the clip makes the fade or typing run in slow motion and breaks the intended behavior. An outro is fundamentally about aligning with the end rather than making its speed proportional to the duration, so write it in real seconds anchored to the end.

lua
-- NG: typing speed changes when the clip length changes
local visible = math.floor(mt.timeline.progress(ctx) * ctx.char_count)

-- OK: constant 0.08s per character regardless of clip length
local visible = math.floor(ctx.time / 0.08)

-- OK: outro fade over the last 0.5s, anchored to the clip end
local fade = mt.clamp(mt.timeline.remaining(ctx) / 0.5, 0.0, 1.0)

-- OK: 0.8s intro + 0.5s outro at fixed real-time speed
-- (compressed automatically only when the clip is shorter)
local intro, outro = mt.timeline.intro_outro_seconds(ctx, 0.8, 0.5)

1.5 ctx.font - Metrics for the Font in Use ​

Metrics (dimensional information) for the font in use, resolved after layout generation.

FieldTypeUnitDescription
em_sizenumbercanvas height ratioSize of one em before transforms
ascentnumbercanvas height ratioAscent upward from the baseline
descentnumbercanvas height ratioDescent downward from the baseline (positive value, expressed as a downward amount)
line_heightnumbercanvas height ratioLine height
units_per_emnumberfont design unitNumber of design units in the font's em square

NOTE

The final on-screen size can be predicted from the product of scale-related fields (2D presets only). The host internally calculates font size as a fixed ratio of canvas.height, so the same ratio is preserved even when the canvas resolution changes. For 2D presets, multiplying this base size by character.scale × character.stretch_x/y, part.scale, and ctx.global.scale × ctx.global.stretch_x/y gives the actual on-screen size (because all of these transformations are composed multiplicatively).

For 3D presets, this product is only an approximation. Camera projection performs perspective division according to depth (including z, apparent depth changes caused by yaw/pitch, and the camera's perspective / zoom), so on-screen size changes nonlinearly with depth, and a tilted character is distorted differently at each vertex and cannot be represented by a single scale factor. ctx.font and character.geometry contain pre-projection values, and the post-projection on-screen size cannot be obtained from OnLayout.

1.6 ctx.text / ctx.char_count / ctx.part_count ​

ctx.text is deprecated. Use ctx.global.text to read the current text, and assign to ctx.global.text in OnPreLayout to rewrite it.

ctx.char_count and ctx.part_count are the number of valid text characters and parts, respectively, after layout generation. Shapes (§1.17) are not counted; use ctx.shape_count. When the text or font changes, these values also change according to the shaping result.

1.7 ctx.meta ​

FieldTypeUnitDescription
ctx.meta.api_levelinteger-Lua API compatibility level
ctx.meta.plugin_versionstring-Running plugin version (for display)
ctx.meta.capabilitiestable-Information about currently available features. Read-only
ctx.meta.capabilities.has_3dboolean-Whether 3D Projection is enabled for the current preset
ctx.meta.limitstable-Limits applied to this instance. Read-only
ctx.meta.limits.max_charactersinteger-Maximum number of characters this instance can handle
ctx.meta.limits.max_partsinteger-Maximum number of parts this instance can handle
ctx.meta.limits.max_shapesinteger-Maximum number of shapes this instance can handle. Shapes use no character or part limit, so it is the same for every preset
ctx.meta.instance_seedinteger-Stable random seed unique to this instance (between 0 and 2^31-1 inclusive). It remains the same throughout the lifetime of a given instance and differs between instances

Feature Detection ​

Determine feature availability using ctx.meta.api_level >= required level and capabilities. Even when has_3d is false, assigning to Camera and yaw / pitch / z does not cause an error, but it does not affect rendering.

Uses of instance_seed ​

Using instance_seed as the seed for mt.random gives each instance a different random sequence, even with the same script.

1.8 ctx.global ​

FieldTypeUnitDescription
textstring-UTF-8 text. Changing it reshapes the text and regenerates the character and part counts
font_namestring-Font family name (one of ctx.fonts). Changing it changes the font in use and reshapes the text
trackingnumberemCharacter spacing added to the trailing advance of each character. 1.0 is 1 em
line_spacingnumbermultiplierAdvance between lines / vertical-writing columns
verticalboolean-When true, enables vertical writing and changes the shaping and placement direction
single_part_glyphboolean-true combines each font glyph into one part, preventing darker translucent contour intersections and internal stroke edges. Custom glyphs and color emoji are unaffected. Writable in OnPreLayout; changing it rebuilds the layout. Initialized from the Inspector each frame (API level 15).
h_alignstring-Horizontal alignment. left / center / right
v_alignstring-Vertical alignment. top / center / bottom / baseline
marginsnumber[]emTrailing advance for each character. Uses the same unit as tracking
emojitable-Emoji placement and font settings (table below)
position_x / position_ynumbercanvas normalized coordinateGlobal position. 0.5 is the center; positive X is right and positive Y is up
rotationnumberdegreeGlobal 2D rotation. Positive (+) rotates right (clockwise); negative (-) rotates left (counterclockwise)
yaw / pitchnumberdegreeGlobal 3D orientation. Affects rendering only in 3D presets. Positive yaw rotates the right edge toward the viewer; positive pitch rotates the top edge toward the viewer
scalenumbermultiplierGlobal uniform scale. Clamped to a minimum of 0.0001 when written
stretch_x / stretch_ynumbermultiplierGlobal nonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted
pivot_x / pivot_ynumbercanvas normalized displacementPivot for Global transformations. 0.5 means displacement 0; larger values move it right/up from global.position
opacitynumber0-1Global opacity. The default is 1.0. The host clamps out-of-range values to 0-1 immediately before rendering. It is multiplied as alpha after colors have been determined for fill, gradient, stroke, shadow, and bounding box (it does not participate in fill.use resolution. See Roles of use / opacity / fill.transparent). No corresponding keyframe parameter exists on the host; it can be written only from scripts
filltable-Global fill settings (table below)
gradienttable-Gradient settings (table below)
stroketable-Global stroke settings (table below)
shadowtable-Global shadow settings (table below)
texturetable-Global texture settings (table below)
blurtable-Global blur of the text itself (table below)

Inspector text input forms \n, <br>, <br/>, and <br /> become actual line-feed characters before OnPreLayout. CRLF becomes LF as well. Escaped input \\n and \<br> becomes literal \n and <br>. ctx.text reads the same interpreted text.

Text assigned in OnPreLayout is not interpreted again. In Lua, use "A\nB" or a multiline string for an actual line break, and "A\\nB" for literal \n. Character arrays are generated after custom glyph declaration and insertion; line breaks themselves are not entries in ctx.chars.

ctx.global.margins ​

Indexing and Calculation ​

margins is a 1-based array keyed by character number in layout order. This number is the same as the index later assigned in ctx.chars. margins[i] is added in em units, together with tracking, to the advance after the i-th character.

text
(margins[i] + tracking) * designUnitsPerEm
Array Format ​

Keys must be integers from 1 and values must be numbers. Non-integer, string, zero, or negative keys and non-number values cause an error. Unset indices add no spacing, so assigning only margins[2], or a table such as { [2] = 0.5 }, is valid and leaves margins[1] at 0.

In OnPreLayout, ctx.global.margins has ctx.meta.limits.max_characters entries, so #ctx.global.margins is not the character count. Indices above that limit can never reach a character and are ignored.

Character Count ​

ctx.char_count is undefined in OnPreLayout. utf8.len(ctx.global.text) returns the number of UTF-8 code points, which may differ from the number of characters after shaping because of combining marks, ligatures, grapheme clusters, and similar constructs. Perform operations requiring the exact count in OnLayout.

Merging Subobjects ​

Assigning an entire table, as in ctx.global.shadow = { enabled = true, distance = 0.8 }, performs a per-field merge. Omitted fields retain their current values.

ctx.global.fill ​

NOTE

The color Type

color is a table with r (red), g (green), b (blue), and a (alpha). Each field is a number from 0-1; a = 0 means fully transparent, and a = 1 means fully opaque.

Fields can be assigned directly. When assigning an entire table, you can specify only the fields to change. Omitted fields retain their current values.

lua
ctx.global.fill.color.r = 1.0
ctx.global.fill.color = { a = 0.5 }

Fields other than r / g / b / a, and values other than numbers, cause an error.

FieldTypeUnitDescription
colorcolorRGBA 0-1Global fill color. Also serves as the gradient start color
transparentboolean-When true, hides the fill while preserving strokes and effects

Characters whose chars[index].fill.use is false use fill.color. For the inheritance order including parts, see "Inheritance Mechanism".

ctx.global.gradient ​

FieldTypeUnitDescription
color_spacestring-Gradient / Crayon mode. The values are the enumeration below
end_colorcolorRGBA 0-1Gradient end color
midpointnumber0-1Midpoint of color interpolation
anglenumberdegreeGradient direction. At 0 degrees it progresses from left to right, and positive (+) rotates right (clockwise)
start_position / end_positionnumberPosition relative to the transformed Global boundsGradient interval. The standard range is 0.0-1.0. For details, see Gradient Range and Direction

Values of gradient.color_space:

  • none
  • okhsv
  • okhsl
  • oklab
  • linear_rgb
  • oklch_short - API Level 11. The shorter OKLCH hue path.
  • oklch_long - API Level 11. The longer OKLCH hue path.
  • crayon

ctx.global.texture ​

The material layered over the fill. p1 to p4 are shared generic controls whose meaning follows the selected type.

FieldTypeUnitDescription
useboolean-Whether this element uses its own texture instead of the inherited one. Ignored on ctx.global; it only matters on ctx.chars[i] / ctx.parts[i]
typestring-Material kind. The values are the enumeration below
p1numbermaterial-dependentFirst control
p2numbermaterial-dependentSecond control
p3numbermaterial-dependentThird control
p4numbermaterial-dependentFourth control
p5numbermaterial-dependentFifth control. Only halftone, halftone_diamond, colored_pencil, prism_foil, iridescent and metallic read it: as the fade direction in degrees for the halftones (0 thickens the dots downwards), as the direction the light comes from on screen for colored_pencil (0 is the right, 90 the bottom), as the direction the tiles run for prism_foil (0 along the part's x axis), as the film variation from 0 to 1 for iridescent (0 is an even film), and as the hairline angle in degrees for metallic (0 along the part's x axis); other materials ignore it. Defaults to 0 (API level 14)
colorcolorRGBA 0-1Colors what the material adds over the fill: the lead for stained_glass, the metal colour for glitter, the stripes for stripe, the dots for halftone, the foil for prism_foil (white is silver), the light for the others. White leaves the light as the material makes it, and paints white lead (iridescent ignores it)

Values of texture.type, and what p1 to p4 mean for each:

typep1p2p3p4
none----
water_dropletThicknessRoundnessGlossLight angle
air_cushionInflationSeal widthWrinklesLight angle
glitterFlake sizeHighlight ratioFlake amountLight angle
iridescentFilm thicknessHue rangeStrengthLight angle
facetedFacet countBevel depthPolishLight angle
stained_glassPiece sizeLead widthHue rangeGlow
metallicPolishReliefReflection contrastLight angle
metallic_2PolishReliefExposureLight angle
stripeStripe density (count)Stripe widthStrengthStripe angle (degrees)
colored_pencilScratch densityScratch lengthLight amountStroke angle (degrees)
halftone, halftone_diamondDot density (count)Dot sizeFade amountGrid angle (degrees)
prism_foilTile sizeHue rangeStrengthLight angle (degrees on screen)

Flake Amount controls all glitter flakes: 0 removes flakes and glow, 1 keeps full density. Hue spread is fixed at the former Hue Range 1.0.

Lit flakes have a near-white core and a soft glow. Glow stays within the text outline and follows Highlight Ratio.

p1 to p4 are all generic slots whose range and unit the material decides. Most read 0 to 1, but a light angle, for one, reads degrees. Each material folds the value it receives into the range it can use (a control that turns repeats each cycle, one with a limit rests against its end), so writing outside that range never rewrites the stored value or its keyframes.

Texture is inherited across three levels: global, character, and part. Setting use = true on ctx.chars[i].texture / ctx.parts[i].texture swaps in a different material for that element alone. The character and part values are script-only (no host parameter backs them), so they always read as unused unless a script sets them.

ctx.global.blur ​

The blur of the text itself: fill, gradient, texture, stroke and crayon together. The shadow is not blurred by this setting; it has its own shadow.blur_x / blur_y. A bounding box is never blurred.

FieldTypeUnitDescription
useboolean-Whether this element uses its own blur instead of the inherited one. Ignored on ctx.global, which has no use; it only matters on ctx.chars[i] / ctx.parts[i]
amount_xnumberemHorizontal blur, as a Gaussian sigma. 1.0 = 1 em; 0 leaves the horizontal edges sharp. Clamped to 0-10 when written
amount_ynumberemVertical blur, with the same unit and range as amount_x
glownumber-Glow strength: brightens the blurred text into a glow, and a saturated core turns toward white. 0 = no glow. No effect while the text is not blurred. Clamped to 0-10 when written

The blur is measured in each part's own space along the canvas axes, so it follows the scale, stretch and perspective of the part like shadow.distance does. Blur is inherited across three levels: global, character, and part. Setting use = true on ctx.chars[i].blur / ctx.parts[i].blur gives that element its own blur. The character and part values are script-only (no host parameter backs them), so they always read as unused and equal to the inherited value until a script sets them. The shadow blur follows the same shadow.use as the shadow color, so overriding only the blur of one part means writing the inherited color back; mt.shadow.resolve(ctx, target, { level = "character" }) returns it.

ctx.global.stroke ​

FieldTypeUnitDescription
widthnumberem (1.0 = font size 1 em. Multiplied by fontSize during rendering)Stroke width
gapnumberem (1.0 = font size 1 em)Gap between the fill and stroke
colorcolorRGBA 0-1Stroke color
outer_widthnumberem (1.0 = font size 1 em)Width of the Outer Stroke adjacent to the outside of the Inner Stroke. Active only when width > 0
outer_colorcolorRGBA 0-1Outer Stroke color
orderstring-Stacking order of fill and stroke. fill_over_stroke / stroke_over_fill / fill_over_stroke_per_character
joinstring-Join style for stroke corners. miter_clip / round / bevel / miter_round / miter_bevel
miter_limitnumbermultiplierMaximum length of sharp miter corners

fill_over_stroke_per_character produces the same stacking as fill_over_stroke (fill above stroke), but completes it one character at a time. Every pass finishes for one run of adjacent items sharing a character index before the next run starts, so each character is finished with its own stroke already hidden under its own fill. Where characters overlap, a later character's stroke therefore never lands on an earlier character's fill. Items are never resorted by character index, so a draw order of A A B A forms three runs. When a gap is present, its erasure is confined to its own run and cannot cut into an earlier character. Frames with 3D projection enabled always draw per character regardless of order. Crayon is still drawn in one batch after the normal passes, so it is outside the scope of this setting.

The simulator's canvas preview does not reproduce order compositing. Selecting, saving, and reading or writing the value from a script all work, but confirm the final stacking in the plugin itself.

Characters whose chars[index].stroke.use is false use the Global stroke settings above. For the inheritance order including parts, see "Inheritance Mechanism".

ctx.global.shadow ​

NOTE

The shadow is a copy of the character or part with the same shape, offset to the position specified by distance / angle, and blurred by blur_x / blur_y (both 0 by default, which leaves its outlines sharp). Its size is always identical to the original, so at distance = 0 and no blur it overlaps exactly behind the main text and the shadow is not visible at all (it does not become visible through shrinkage or transparency).

FieldTypeUnitDescription
enabledboolean-Whether the Global shadow is enabled
distancenumberemShadow distance. 1.0 = 1 em. Measured in each part's own space, so the shadow keeps its distance from the glyph under any scale, stretch, rotation or perspective; the direction set by angle stays fixed on the canvas. Positive values move away in the angle direction. Maximum on write: 10.0
anglenumberdegreeShadow direction. 0 degrees points right, and positive (+) rotates right (clockwise) (90 degrees points straight down)
blur_x / blur_ynumberemBlur of the shadow, as a Gaussian sigma. 1.0 = 1 em. Same range as blur.amount_x / amount_y: clamped to 0-10 when written
glownumber-Glow strength: brightens the blurred shadow into a glow, and a saturated core turns toward white. 0 = no glow. No effect while the shadow is not blurred. Clamped to 0-10 when written
colorcolorRGBA 0-1Shadow color

Characters whose chars[index].shadow.use is false use shadow.color. For the inheritance order including parts, see "Inheritance Mechanism".

WARNING

Enabling the shadow increases drawing cost and reduces rendering performance. The impact is especially large at high resolutions or with complex text, so use it only when needed.

The gradient fills only areas where both the character and part fill.use values are false. An individual color with fill.use set to true takes precedence over the gradient and becomes a solid color. For the separation of stages between fill.use and opacity, see "Inheritance Mechanism".

ctx.global.emoji ​

Corresponds to the host's Emoji group in Text Settings. Writable in OnPreLayout and read-only in OnLayout and OnPath.

FieldTypeUnitDescription
offset_x / offset_ynumberem (1.0 = 1 em of the font size)How far emoji move from where the font places them. Positive Y is up. The advance is unchanged
scalenumbermultiplierEmoji are set as if in a font this many times the text size, advance included. Clamped to a minimum of 0.001 on write
use_custom_fontboolean-true uses the font_name font; false uses the system emoji font (Windows: Segoe UI Emoji, macOS: Apple Color Emoji)
font_namestring-Custom emoji font, one of ctx.fonts. Kept but ignored while use_custom_font is false. An unknown name falls back to the system emoji font
use_outlineboolean-true draws emoji from the custom font's outlines. Takes effect only while use_custom_font is true

WARNING

Changing any of these values rebuilds the layout (and, for deformed emoji, their images). Varying them every frame slows rendering considerably, so use fixed values.

Emoji drawn with use_outline are ordinary characters: fill color, gradient, texture, stroke and shadow apply as they do to text. A glyph without an outline, such as a color-only glyph, becomes a character with no parts and draws nothing.

The simulator shows emoji in a simplified way: each emoji sequence is one character with one part, placed with offset_x / offset_y / scale, and drawn with the browser's emoji font. It does not load the custom emoji font or draw use_outline outlines, and in the 3D preset it shows only the emoji's square. Check the final look in the plugin.

1.9 ctx.camera ​

The camera for 3D presets. Assignments do not cause errors in 2D presets, but affect rendering only in 3D presets.

FieldTypeUnitDescription
target_x / target_ynumbercanvas normalized coordinateCamera look-at point. Positive X is right and positive Y is up. When the look-at point moves, the object appears to move in the opposite direction
yaw / pitchnumberdegreeCamera orbit. Positive yaw moves the camera around to the right; positive pitch moves it upward (looking down)
perspectivenumber-Strength representing camera distance / perspective. Internally mapped to camera distance as distancePx = f(perspective) * canvas.height (f is a monotonic mapping from strength to distance coefficient; larger values move the camera closer and strengthen perspective). Held between 0 and 1. Every camera field must be finite; writing NaN or inf is an error
zoomnumbermultiplierProjection magnification. Clamped to a minimum of 0.001 when written
lens_shift_x / lens_shift_ynumbercanvas ratioPrincipal point offset. Positive lens_shift_x shifts everything right; positive lens_shift_y shifts everything up

1.10 ctx.chars[index] ​

ctx.chars is a 1-based array, as is standard in Lua. When the text or font changes, the number of characters and the index of each element also change.

When use for fill / stroke / shadow is false, the setting is not hidden; it inherits the Global value. For details, see "Inheritance Mechanism".

FieldTypeUnitDescription
indexinteger-1-based character index. Read-only
textstring-Original text of this character cluster. Read-only
part_start / part_countinteger-1-based starting global index and count of owned parts. Read-only
line_indexinteger-1-based index of the line it belongs to. Read-only
geometrytable-Placement information before transformations (below). Read-only
offset_x / offset_ynumbercanvas normalized displacementCharacter displacement. 0.5 means displacement 0; larger values move right/up
pivot_x / pivot_ynumbercanvas normalized displacementPivot for character transformations. At 0.5, the pivot equals the natural center; larger values move it right/up
scalenumbermultiplierUniform scale. Clamped to a minimum of 0.001 when written
stretch_x / stretch_ynumbermultiplierNonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted
rotationnumberdegree2D rotation. Positive (+) is clockwise (right rotation); negative (-) is counterclockwise (left rotation)
znumbercanvas height ratioDepth. Positive (+) moves deeper (away from the camera); negative (-) moves forward (toward the camera). Affects rendering only in 3D presets
yaw / pitchnumberdegreeIndividual 3D rotation. Positive yaw rotates the right edge toward the viewer; positive pitch rotates the top edge toward the viewer. Affects rendering only in 3D presets
opacitynumber0-1Opacity of this character. The default is 1.0, and the host clamps out-of-range values immediately before rendering. Multiplied by ctx.global.opacity (does not participate in fill.use resolution. See division of roles)
z_indexnumber-Paint order key for shapes (ctx.shapes) only. A text character ignores it (a write is accepted but does not affect the paint order)
filltable-Individual fill (table below)
stroketable-Individual stroke (table below)
shadowtable-Individual shadow color (table below)
texturetable-Individual texture. Same structure as ctx.global.texture, plus use
blurtable-Individual blur of the text. Same structure as ctx.global.blur, plus use

chars[index].fill ​

FieldTypeUnitDescription
useboolean-When true, uses fill.color
colorcolorRGBA 0-1Individual color used when use is true

chars[index].stroke ​

FieldTypeUnitDescription
useboolean-When true, uses the individual stroke settings, including Inner and Outer
widthnumberem (1.0 = font size 1 em)Used when use is true
gapnumberem (1.0 = font size 1 em)Used when use is true
colorcolorRGBA 0-1Used when use is true
outer_widthnumberem (1.0 = font size 1 em)Width of the Outer Stroke adjacent to the outside of the Inner Stroke. Active only when width > 0
outer_colorcolorRGBA 0-1Outer Stroke color

chars[index].shadow ​

FieldTypeUnitDescription
useboolean-When true, uses shadow.color, blur_x, blur_y and glow
colorcolorRGBA 0-1Individual color used when use is true
blur_x / blur_ynumberemIndividual shadow blur used when use is true. Reads the inherited blur until set. Clamped to 0-10 when written
glownumber-Individual shadow glow strength used when use is true. Reads the inherited value until set. Clamped to 0-10 when written

Assigning an entire table to a subobject follows the same merge rules as §1.8.

chars[index].geometry ​

Represents the layout from before character script transformations, Deform, and 3D projection.

Positions and dimensions use canvas normalization. Internal pixel coordinates are converted with X as 0.5 + px/width and Y as 0.5 - px/height.

FieldTypeUnitDescription
canvas_origin_x / canvas_origin_ynumbercanvas normalized coordinateTypesetting origin (horizontal writing: placement origin corresponding to the cell's left edge × baseline)
vertical_origin_x / vertical_origin_ynumbercanvas normalized coordinateVertical-typesetting origin (column center axis × cell top)
bounds_center_x / bounds_center_ynumbercanvas normalized coordinateCenter of the area occupied by the character outline before transformations (ink bounds)
bounds_widthnumbercanvas width ratioInk bounds width / canvas width
bounds_heightnumbercanvas height ratioInk bounds height / canvas height
advance_xnumbercanvas width ratioFont horizontal advance (px) / canvas width. Does not include tracking / margins
advance_ynumbercanvas height ratioFont vertical advance (px) / canvas height (sign-inverted to Y-up). Downward advance is negative. Does not include tracking / margins

A glyph-less character such as a space has collapsed ink bounds, so the font-defined character cell is reported instead. Along the layout flow axis (X in horizontal writing, Y in vertical writing) the character's own advance provides the extent, and the cross axis uses the cell height (horizontal) or the column width (vertical). A space's bounds_center_y therefore does not sit half a cell below its neighboring glyphs.

1.11 ctx.parts[index] ​

ctx.parts is a 1-based array, as is standard in Lua. index is not a number within each character, but a part number across the entire frame. When the text or font changes, the number of parts and the index of each element also change.

FieldTypeUnitDescription
indexinteger-1-based global part index. Read-only
character_indexinteger-1-based index of the owning character. Read-only
index_in_characterinteger-1-based part index within the character. Read-only
geometrytable-Placement information before transformations (table below). Read-only
offset_x / offset_ynumbercanvas normalized displacementPart displacement. 0.5 means displacement 0; larger values move right/up
pivot_x / pivot_ynumbercanvas normalized displacementPivot for part transformations. At 0.5, the pivot equals the natural position; larger values move it right/up
scalenumbermultiplierUniform scale. Clamped to a minimum of 0.001 when written
rotationnumberdegree2D rotation. Positive (+) is clockwise (right rotation); negative (-) is counterclockwise (left rotation)
stretch_x / stretch_ynumbermultiplierNonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted
znumbercanvas height ratioDepth. Positive (+) moves deeper (away from the camera); negative (-) moves forward (toward the camera). Affects rendering only in 3D presets
yaw / pitchnumberdegreeIndividual 3D rotation of the part. Applied around the part pivot inside the owning character's rotated space. Positive yaw rotates the right edge toward the viewer; positive pitch rotates the top edge toward the viewer. Affects rendering only in 3D presets
opacitynumber0-1Opacity of this part. The default is 1.0, and the host clamps out-of-range values immediately before rendering. Multiplied by the owning character's opacity (and further by ctx.global.opacity) (does not participate in fill.use resolution. See division of roles)
filltable-Individual fill with use / color. Has the same structure as the character's fill; when use is true, uses color
stroketable-Individual stroke with use / width / gap / color / outer_width / outer_color. Has the same structure as the character's stroke; when use is true, uses the individual values
shadowtable-Individual shadow color with use / color. Has the same structure as the character's shadow; when use is true, uses color
texturetable-Individual texture. Has the same structure as the character's texture; when use is true, uses that material
blurtable-Individual blur of the text. Has the same structure as the character's blur; when use is true, uses amount_x / amount_y / glow

Inheritance from the Character ​

A part's fill, stroke, and shadow are determined by their respective use values as follows.

Part Settinguse = falseuse = true
fillInherits the fill selected at the owning character levelUses the part's fill.color
strokeInherits the Inner / Outer settings selected at the owning character levelUses the part's stroke settings
shadowInherits the shadow.color selected at the owning character levelUses the part's shadow.color

If use = false on the owning character as well, resolution continues up to the ctx.global settings. For details, see "Inheritance Mechanism".

Individual Settings Not in the Inspector ​

There are no parameters for a part's stroke and shadow in the Inspector, so they begin unused/with default values. When a script sets use to true, the configured individual values take precedence.

There are also no yaw / pitch parameters in the Inspector, and their initial values are 0. They are independent of the owning character's rotation and are applied as additional rotation within the character's rotation space.

parts[index].geometry ​

Represents the layout before script transformations, Deform, and 3D projection. Positive Y is up.

FieldTypeUnitDescription
local_center_x / local_center_ynumberdisplacement in canvas-axis ratiosPart center relative to the owning character's pre-transformation outline bounds (ink bounds) center as the origin. No neutral offset (0 is at the character center). The Y component is Y-up
canvas_center_x / canvas_center_ynumbercanvas normalized coordinateCanvas coordinates of the part's natural position (center after layout)
bounds_center_x / bounds_center_ynumbercanvas normalized coordinateCenter of the area occupied by the part outline before transformations (ink bounds)
bounds_width / bounds_heightnumberrespective canvas-axis ratiosWidth and height of the ink bounds

1.12 ctx.bounding_box ​

When enabling a box that is disabled in the Inspector, explicitly set every box field the effect needs, including edges, width, margins, colors, fill, opacity, caps, and offsets. Native does not load current Inspector values for disabled box settings; do not use those fields as a style snapshot.

Base Rectangle ​

In 2D, the base rectangle encloses transformed fill bounds measured before Global rotation and rotates with the text. In 3D, it is authored on the global layout plane and then projected. Margins are added outside it, and fill and stroke have independent offsets.

Dimension Reference ​

The following dimensional fields are percentages of fontSize (rendering: value * fontSizePx / 100; 100.0 = the same length as fontSize).

FieldTypeUnitDescription
enabledboolean-Enables drawing the frame
draw_top / draw_bottom / draw_left / draw_rightboolean-Whether to draw the stroke on each side
colorcolorRGBA 0-1Frame line color
widthnumberfontSize percentageFrame line thickness
fill_enabledboolean-Enables filling inside the frame
fill_colorcolorRGBA 0-1Interior fill color (its alpha is multiplied by fill_opacity)
fill_opacitynumber0-1Opacity multiplier for the interior fill
start_cap / end_capstringenumeration belowCaps for open path endpoints
margin_x / margin_ynumberfontSize percentageMargin outside the text bounds (added to each side)
corner_radiusnumberfontSize percentageCorner radius when all four sides are enabled (capped at half the rectangle size)
fill_offset_x / fill_offset_ynumberfontSize percentageTranslation of the fill rectangle.
stroke_offset_x / stroke_offset_ynumberfontSize percentageTranslation of the frame line (independent of the fill)

Values of start_cap / end_cap: butt / square / round / round_rev / triangle / triangle_rev

1.13 ctx.output ​

Output filtering and drawing-order controls read and written in OnLayout. Separation-related fields are read-only.

FieldTypeUnitDescription
separation_enabledboolean-Whether Separation output is enabled. Read-only
render_modestring-Current output slot. default / separation_1 / separation_2. Read-only
targets_1 / targets_2string-Target specification string assigned to each Separation output. Read-only
write_on_modestring-Write-on unit. character or part
write_on_startnumber0-1Start of the visible interval (normalized progress)
write_on_endnumber0-1End of the visible interval. No filter (everything visible) when start = 0 and end = 1
reorder_partsboolean-When true, uses writing order (stroke order) for part drawing order and reveal order. Has no visual effect on single-part characters
manual_order_enabledboolean-When true, overrides reveal / drawing order with manual_order_text
manual_order_textstring-Manual order specification (syntax below)
force_disable_3d_projectionboolean-When true, makes the final decision to disable 3D projection for that frame. Defaults to false (API Level 7)

2D-only Scripts ​

Set ctx.output.force_disable_3d_projection = true to use the 2D rendering path even in a 3D-capable preset. This ignores all Camera, Global, Character, and Part 3D controls, including Inspector settings and keyframes. Do not set it merely because the current 3D values happen to be neutral; use it only when the effect is explicitly intended to be 2D-only.

lua
function OnLayout(ctx)
    -- This effect intentionally guarantees 2D output.
    ctx.output.force_disable_3d_projection = true
end

Write-on ​

Visible elements are limited to those whose 1-based rank within the unit lies in [startLimit, endLimit].

text
startLimit = floor(write_on_start * N) + 1
endLimit   = ceil(write_on_end * N)

N is the number of characters when write_on_mode == "character" and the number of parts when it is "part".

How ranks are determined:

modeDefault RankWhen manual_order_enabledreorder_parts Only
characterCharacter index in layout order (1..N)Character-level manual reveal orderCharacter rank remains in layout order
partctx.parts[index].index (1..N)Manual reveal order of partsReveal order in stroke order

manual_order_text Syntax ​

Token Interpretation ​

Tokens are separated by commas or spaces. Invalid tokens are ignored, and if the same part is specified multiple times, only the first specification is used.

TokenMeaning
c<n> / C<n>1-based character n. Expands all parts of that character in the part order in effect at the time (reorder_parts true uses stroke order; false uses layout order)
p<n> / P<n>1-based global part n
<n>No prefix. Interpreted as a part index
<a>-<b>Inclusive range. Both endpoints must be of the same type (both characters or both parts). If only one endpoint has a prefix, the other inherits its type. Expands in ascending order

Examples: "3,1,2" (parts 3→1→2), "c1-c3" (characters 1-3), "p2-p5" (parts 2-5).

Effect on Ordering ​

Specified elements are moved to the front in the specified order. Unlisted parts are not hidden; they follow afterward while preserving their relative order from the base ordering.

The order is determined from the undeformed layout and the specification string; it is not recomputed each frame from spatial positions after animation. write_on_start/end determines the visible range, while manual order changes only the order of elements.

In 2D, paint order follows manual order directly. In 3D, each part's depth remains primary; manual order, stroke order, then layout order break equal-depth ties. Write-on reveal order always follows manual order in both 2D and 3D.

1.14 ctx.paths ​

Because ctx.paths retrieves targets lazily, paths for parts that are not retrieved are not copied to the Lua side. ctx.paths is valid only during the OnPath call that received it; keeping it in a variable and using it in a later call raises an error.

FieldTypeUnitDescription
units_per_emnumberpath unit/emPath coordinate scale. Always 1000
part(index)function-Retrieves 1 part by its 1-based global part number. Returns nil if it does not exist
character(index)function-Retrieves all parts owned by the 1-based character number as an array ordered by global part number
select(selector)function-Retrieves parts matching the selection syntax as an array ordered by global part number
shape(number)function-Retrieves the part of the shape with the 1-based shape number, as a one-element array; an empty array if there is no such shape. part, character and select reach the text only

Selection Syntax for select(selector) ​

c denotes a character number, p denotes a global part number, and ! denotes exclusion. Multiple conditions can be specified with commas, and ranges such as c3-5 and p8-10 are supported.

lua
local parts = ctx.paths:select("c1, p8-10, !p9")

When the same part is retrieved multiple times, the same mutable object is returned within a frame.

Retrieved Parts ​

FieldTypeUnitDescription
indexinteger-1-based global part number across the frame
character_indexinteger-1-based number of the character it belongs to
index_in_characterinteger-1-based part number within the character it belongs to
line_indexinteger-1-based number of the line it belongs to
textstring-Text represented by the character it belongs to
pathpath-Drawing path to modify

Editing path ​

path is a mutable object representing the part's drawing outline. You can clear or replace the original outline and add straight lines or Bézier curves. For details on the methods and coordinate system, see Drawing Path Editing API.

1.15 ctx.glyphs ​

API level: 9+

ctx.glyphs:declare(declaration) ​

ctx.glyphs:declare(declaration) declares a custom glyph with a path and character metrics, then returns a one-character placeholder string that can be inserted into ctx.global.text. Because the declaration must be finalized before layout generation, this method is available only in OnPreLayout.

This declaration spans two coordinate spaces. The shape-drawing path uses Y-down like the existing drawing API, while the shape-placement advance_* and bounds use Y-up like the existing geometry API. Therefore, a 900-unit downward advance in vertical writing is specified as advance_y = -900.

FieldTypeDefaultUnit and description
pathMtDrawingPathEmpty pathInitial shape. Part-center origin, Y-down, 1000 units = 1 em
advance_xnumber0.0Horizontal advance. Positive is right, 1000 units = 1 em
advance_ynumber0.0Vertical advance. Positive is up, 1000 units = 1 em; downward vertical flow uses a negative value
boundsnumber[4]Derived from path{minimumX, minimumY, maximumX, maximumY}. Baseline origin, Y-up, 1000 units = 1 em

When bounds is omitted, the Part center's X is placed at advance_x / 2, and the path's bottom edge is aligned to the baseline. The derived bounds are { advance_x / 2 + pathMinimumX, 0, advance_x / 2 + pathMaximumX, pathMaximumY - pathMinimumY }. SVG icons normalized around the origin with mt.svg_path and view_box can therefore be reused without repeating their size. An empty path produces {0,0,0,0} bounds. advance_y remains an advance metric and does not affect automatic placement in horizontal writing.

In vertical writing, the ink center is placed at the center of the cell created by advance_y, independent of its horizontal-writing baseline placement. tracking and per-character margins are trailing cell spacing and do not shift the ink center. With advance_y = 0, the ink center is placed on the current vertical pen position.

bounds with neither width nor height (for example {0,0,0,0}) declares a glyph that draws but does not measure: it is excluded from the alignment box that h_align / v_align work against. Appending advance-less decoration markers to the text therefore leaves the line at its alignment edge. Because alignment is decided by bounds rather than advance, cancelling the advance with a negative margins entry is not a substitute for this exclusion.

When bounds is explicit, its center becomes the Part center. Values below the baseline are negative, and values above it are positive. For example, use { -500, 0, 500, 700 } for a shape extending 700 units above the baseline and 500 units to each side. Path coordinates remain Y-down, matching the existing drawing-path API.

lua
local icon = mt.svg_path("M 2 2 L 22 2 L 22 22 L 2 22 Z", {
    view_box = { 0, 0, 24, 24 },
    em_scale = 0.8,
})

function OnPreLayout(ctx)
    local marker = ctx.glyphs:declare({
        path = icon,
        advance_x = 1000,
        advance_y = -1000,
    })
    ctx.global.text = "A" .. marker .. "B"
end

The path is snapshotted at declaration time. Later changes to the source path do not alter an existing declaration. Omitting path while specifying bounds creates an invisible character carrying only the declared advance and bounds. To place the same custom glyph more than once, insert the returned marker at multiple positions in the text. advance_x and advance_y are pure glyph advances that exclude tracking and per-character margins. As with a normal character, the host adds tracking and the corresponding margins entry to the trailing advance. A glyph with advance_x = 0 therefore retains that spacing in horizontal layout.

The host assigns placeholders in declaration order from Unicode private-use code point U+E002. U+E000 and U+E001 are reserved for internal newline processing. Custom glyphs consume the same character and part limits as normal glyphs. If the hook or commit fails, every declaration from that frame is discarded.

1.16 ctx.inputs ​

ctx.inputs is a read-only object that exposes the Inspector's Script Inputs values to every callback. Each array is 1-based and has a fixed length.

FieldTypeLengthDescription
numbersnumber[]14Unitless numeric inputs
colorsMtColor[]8RGBA color inputs
textsstring[]4Single-line text inputs
booleansboolean[]8Checkbox boolean inputs
lua
local amount = ctx.inputs.numbers[1]
local primary = ctx.inputs.colors[1]
local caption = ctx.inputs.texts[1]
local enabled = ctx.inputs.booleans[1]

ctx.inputs, each array, and every color channel are read-only. An out-of-range index returns nil.

1.17 ctx.shapes ​

ctx.shapes handles shapes placed apart from the text (backgrounds, frames, decorations). A shape is not part of ctx.global.text and takes no part in the layout flow: advance widths, alignment and wrapping. ctx.chars, ctx.char_count, ctx.parts and ctx.part_count count the text only; shapes are reached through ctx.shapes[number] and ctx.shape_count.

Declare shapes in OnPreLayout, then read them by their 1-based shape number in OnLayout and OnPath.

ctx.shapes:declare(declaration) ​

Declares one shape and returns its 1-based shape number (in declaration order). It can only be called in OnPreLayout. Unknown fields and invalid values raise an error.

fieldtypeDefaultDescription
pathMtDrawingPathempty pathThe outline of the shape. Its origin is the shape position, Y-down, 1000 units = 1 em (the same coordinate system as a custom glyph's path)
layerstring"back""back" draws behind the text, "front" in front of it
parentintegernoneThe number of the shape to use as parent. It must be a shape declared earlier (a value an earlier declare returned). Omit it for no parent
cameraintegerthe parent's camera, else noneThe number of the camera that projects this shape in 3D. It must be one declared earlier with ctx.shape_cameras:declare(). A shape with a parent takes its parent's camera, and naming a different one is an error (a child of a shape with no camera cannot name one either). A shape with no camera stays fixed to the canvas as before

path is a snapshot taken at declaration. Shapes of the same layer overlap from the lowest z_index, and in declaration order when z_index is equal.

A shape with a parent is placed relative to it: its position, rotation, scale and stretch work inside the parent's transformed space.

  • With offset_x / offset_y at 0.5, the child sits on the parent's position (the origin of the parent's path). The offset is affected by the parent's scale, rotation and stretch.
  • The child's rotation, scale, stretch_* and pivot_* work about the child's own position, in the parent's space. Under an uneven parent stretch a rotated child looks sheared.
  • The parent's opacity, fill, stroke, shadow, blur and texture are not inherited. Each shape sets its own.
  • A child can be a parent too (the chain is followed up to the grandparents). A parent set to opacity = 0 can leave only its children visible.
  • When the parent's scale reaches 0, the child loses its size too and nothing is drawn. The chain's scale is accumulated in double precision; a shape whose own drawing matrix would exceed the range of a float is not drawn either (a shape in the middle may be out of range while a later one, whose scale comes back, is drawn).
  • Only a shape can be a parent; a text character cannot.
lua
local rectangle = mt.svg_path("M -500 -300 L 500 -300 L 500 300 L -500 300 Z")

function OnPreLayout(ctx)
    local background = ctx.shapes:declare({ path = rectangle })
    local frame = ctx.shapes:declare({ path = rectangle, layer = "front" })
end

Shape settings ​

ctx.shapes[number] can be read in OnLayout and OnPath. In OnLayout you can set the following fields with the same rules as ctx.chars[index].

FieldTypeDescription
indexintegerThe shape number, the same value ctx.shapes:declare returned
offset_x / offset_ynumberPosition offset. 0.5 means no offset. A shape you leave alone appears at the center of the canvas
pivot_x / pivot_y / scale / stretch_x / stretch_y / rotation / z / yaw / pitchnumberThe same transforms as ctx.chars[index]. A shape turns and scales about its position (the origin of its path), and pivot_* is measured from there
opacitynumberOpacity multiplier (0 to 1)
parentintegerRead-only. The parent shape number given to ctx.shapes:declare, 0 when there is no parent
cameraintegerRead-only. The number of the camera that projects this shape (taken from its parent when it names none), 0 when it has none
z_indexnumberPaint order key (default 0). Within a layer shapes are drawn from the lowest (a higher value is in front), and equal values keep declaration order. It never crosses the back/front layers of layer. +inf and -inf rank as the extremes, and assigning NaN raises an error. It is independent of the parent relation
fill / stroke / shadow / texture / blurtableIndividual appearance, the same fields as ctx.chars[index]
pathpathThe outline of the shape, in OnLayout only. It starts as the declared path and is edited in place, for example with path:assign(p). It returns to the declared path every frame

path cannot be assigned. shape.path:assign(p) replaces the outline with a copy of the path you pass. Only a path you edited is used for that frame. In OnPath, ctx.paths:shape(number) lets you edit the path further, starting from what OnLayout left.

lua
function OnLayout(ctx)
    local shape = ctx.shapes[1]
    shape.path:assign(drawLights(ctx))
    shape.opacity = 0.8
    shape.fill.use = true
    shape.fill.color = { r = 1.0, g = 0.5, b = 0.7, a = 1.0 }
end

ctx.shape_cameras:declare() ​

Declares one camera for shapes and returns its 1-based camera number (in declaration order). It can only be called in OnPreLayout, and at most 16 can be declared. A camera for shapes is a camera of its own, apart from the text's camera (ctx.camera): the text's camera, the Global and the text's 3D pose do not reach shapes, and a camera for shapes does not reach the text.

ctx.shape_cameras[number] can be read and set in OnLayout. Its values start every frame at the defaults of the text's camera, so write them in each frame in which you want them to move.

FieldTypeDefaultDescription
target_x / target_ynumber0.5 / 0.5The camera's look-at point (canvas fractions). target_y is positive upward
yaw / pitchnumber0The camera's orbit angles in degrees, in the same sense as ctx.camera
perspectivenumber0.5Perspective strength; held between 0 and 1
zoomnumber1Zoom multiplier
lens_shift_x / lens_shift_ynumber0Lens shift (canvas fractions). lens_shift_y is positive upward

The numbers must be finite; writing NaN or inf is an error. A shape whose pose or camera can no longer be carried out in numbers (z = 1e308, for example) is not drawn.

A shape that names a camera (ctx.shapes:declare({ camera = number })) has its own pose, yaw, pitch and z included, in 3D, and is projected by that camera.

  • Each shape turns about its own position (the origin of its path). A parent's pose carries its children: with the parent tilted by pitch, a child offset up the parent's height moves in depth.
  • One parent-and-child tree is projected by one camera. A child that omits camera uses its parent's camera, and naming a different one is an error.
  • On a shape with no camera, yaw, pitch and z have no visible effect.
  • scale acts across a shape's plane (x and y) only and does not reach depth, so a rotated child under a parent whose scale is not 1 shears. To keep the form of what hangs from a rotated shape, leave scale at 1 and put the size in the path's dimensions and in the child's offset (the parent's length).
  • The paint order is decided by the layer and then z_index; inside one layer and one z_index, only the shapes of the same camera are put in depth order, farthest first. Shapes of another camera and shapes with no camera keep their places.
  • The stroke width and the size of the shadow and blur follow the depth scale at the shape's position.
lua
function OnPreLayout(ctx)
    local view = ctx.shape_cameras:declare()
    local card = ctx.shapes:declare({ path = rectangle, camera = view })
end

function OnLayout(ctx)
    ctx.shape_cameras[1].pitch = 20
    ctx.shapes[1].yaw = ctx.time * 90
end

How shapes differ from the text ​

  • They take no part in the layout flow (advance widths, alignment, wrapping).
  • Write On, Gradient, Bounding Box, Deform, Crayon and selection in the viewer do not apply to them.
  • Global transforms and the text's 3D camera do not reach them. A shape is fixed to the canvas, and only a shape that names a camera from ctx.shape_cameras is projected in 3D, by that camera.
  • A shape's number cannot be written in the manual order (manual_order_text). The paint order follows layer, z_index and the order of declaration. The text cannot be placed between shapes.
  • They use no character or part limit. The limit on shapes is ctx.meta.limits.max_shapes, the same for every preset. A declaration beyond it raises an error.
  • Output Separation does apply to them.
  • Limitation: the text's Crayon strokes (an experimental feature) are drawn in front of layer = "front" shapes.
  • mt.layout.place_2d, mt.layout.get_canvas_position_2d and mt.layout.canvas_to_offset_2d take a shape like a character: its anchor is its position. For a shape with a parent they work through the parent's transform, and place_2d and canvas_to_offset_2d return the offset in the parent's space (an error when the parent's scale is 0).
  • The appearance set for the text (text color, stroke, shadow, blur, glow, texture) applies to shapes too. To override a setting you do not want, set it on ctx.shapes[number] in OnLayout.

Target API level: 10

mt is a namespace containing utilities used for animation calculations, color manipulation, layout, paths, time control, and more.

NOTE

For time or t, normally pass the elapsed time since the animation began. Use ctx.time when starting at the beginning of the clip, or ctx.time - startTime when starting partway through it.

Index by Purpose ​

PurposeAPI
Value constraints and range mappingmt.clamp / mt.saturate / mt.remap / mt.wrap
Interpolation and keyframesmt.lerp / mt.inverse_lerp / mt.lerp_angle / mt.smoothstep / mt.keyframes
Progress distribution (domino, cascade, typewriter)mt.distribute / mt.stagger / mt.stagger_progress / mt.stagger_pattern
Manual drawing and Write On ordermt.order_text / mt.CHAR / mt.PART / mt.NOT
Distance-based influence (wavefront, shine, ripple, shockwave)mt.falloff
Deterministic random values and noise (scatter, camera shake, tremor)mt.random / mt.random_range / mt.distributed_random / mt.noise1 / mt.noise2 / mt.wiggle
Cycles and springs (floating, harmonic wave, damped oscillation)mt.cycle / mt.pingpong / mt.wave / mt.wave_square / mt.wave_triangle / mt.wave_sawtooth / mt.spring
Displacement from polar coordinates (radial scatter, orbit, explosion)mt.polar_offset_2d
Rotation from a direction vector, with optional aspect correctionmt.direction_to_rotation_2d
Physical motion (ballistic trajectory, ground bounce, friction decay)mt.bounce_y / mt.bounce_x / mt.bounce_ground / mt.bounce_wall / mt.impact_squash / mt.projectile_2d / mt.friction_decay
Color (hue shift, interpolation, perceptually uniform)mt.color.resolve_fill / mt.color.from_* / mt.color.to_* / mt.color.lerp* / mt.color.with_alpha
Resolved shadow, blur, stroke and texture of an element (read the value inherited from a higher tier)mt.shadow.resolve / mt.blur.resolve / mt.stroke.resolve / mt.texture.resolve
Easingmt.ease.*
Reflow and line-based groupingmt.layout.reflow / mt.layout.group_by_line
Canvas position, distance, and pivotmt.layout.place_2d / mt.layout.get_canvas_position_2d / mt.layout.canvas_to_offset_2d / mt.layout.radial_distance / mt.layout.pivot_at_2d
Bounds after 2D transformsmt.layout.measure_bounds_2d
Alignment on pathsmt.layout.queue_on_path
Motion pathsmt.path.bezier / mt.path.catmull_rom / mt.path.arc_length
Path creation and editingmt.svg_path / path:*
UTF-8 text processingmt.text.slice / mt.text.classify
Character and part selection stringsmt.target.chars / mt.target.parts
Clip timemt.timeline.progress / mt.timeline.duration / mt.timeline.remaining / mt.timeline.intro_outro_seconds / mt.timeline.window_progress / mt.timeline.window_ctx / mt.timeline.chain
Deprecated compatibility APIsmt.storage / mt.polar_offset / mt.layout.retypeset / mt.layout.canvas_to_offset / mt.layout.set_canvas_position / mt.layout.group_bounds / mt.timeline.intro_outro

1. Value Constraints and Range Mapping ​

mt.clamp(value, low, high) ​

API level: 1+

Constrains a value to the closed interval [low, high].

Arguments ​

ArgumentTypeUnitDefaultDescription
valuenumber-RequiredValue to constrain
lownumber-RequiredLower bound
highnumber-RequiredUpper bound

Return Value ​

TypeUnitDescription
number-Constrained value

mt.saturate(value) ​

API level: 1+

Shorthand for mt.clamp(value, 0.0, 1.0). Use it to keep progress or alpha values within 0-1.

Arguments ​

ArgumentTypeUnitDefaultDescription
valuenumber-RequiredValue to constrain to 0-1

Return Value ​

TypeUnitDescription
number0-1Value constrained to 0-1

mt.remap(value, in_low, in_high, out_low, out_high, clamped?) ​

API level: 1+

Maps a value from an input interval to an output interval. Use it for general interval conversions, such as converting seconds into progress.

Arguments ​

ArgumentTypeUnitDefaultDescription
valuenumber-RequiredValue to convert
in_low / in_highnumber-RequiredInput interval
out_low / out_highnumber-RequiredOutput interval
clampedboolean-falseIf true, constrains the input position to 0-1 before conversion

Return Value ​

TypeUnitDescription
number-Converted value

Constraints and Errors ​

ConditionDescription
in_low == in_highCannot be specified because the input interval would have zero width

Example ​

lua
-- Move the Y offset from 0.7 to 0.5 between 0.5 and 1.2 seconds
character.offset_y = mt.remap(ctx.time, 0.5, 1.2, 0.7, 0.5, true)

mt.wrap(value, boundaryA, boundaryB) ​

API level: 1+

Wraps a value within the half-open interval [min(boundaryA, boundaryB), max(boundaryA, boundaryB)). The boundaries may be specified in either order. Use it to make angles or hues periodic.

Changed in API level 6: The boundaries are now treated as boundaryA / boundaryB instead of low / high. Reversed boundaries now wrap over the interval from the smaller boundary to the larger boundary.

Arguments ​

ArgumentTypeUnitDefaultDescription
valuenumber-RequiredValue to wrap
boundaryAnumber-RequiredOne boundary that defines the interval
boundaryBnumber-RequiredThe other boundary that defines the interval

Return Value ​

TypeUnitDescription
number-Wrapped value

Constraints and Errors ​

If both boundaries are equal, the function returns that boundary value.

2. Interpolation and Keyframes ​

mt.lerp(from, to, t) ​

API level: 1+

Linearly interpolates from from to to.

Arguments ​

ArgumentTypeUnitDefaultDescription
fromnumber-RequiredValue when t = 0
tonumber-RequiredValue when t = 1
tnumber-RequiredInterpolation factor. It is not clamped, so values outside 0-1 produce extrapolation

Return Value ​

TypeUnitDescription
numberSame as the input valuesfrom + (to - from) * t

mt.inverse_lerp(from, to, value) ​

API level: 1+

The inverse of mt.lerp. Returns the position of value within the interval [from, to] as an interpolation factor.

Arguments ​

ArgumentTypeUnitDefaultDescription
fromnumber-RequiredStart of the interval corresponding to a factor of 0
tonumber-RequiredEnd of the interval corresponding to a factor of 1
valuenumber-RequiredValue whose position to calculate

Return Value ​

TypeUnitDescription
number-(value - from) / (to - from). Not clamped

Constraints and Errors ​

Returns 0 if from == to.


mt.lerp_angle(from, to, t) ​

API level: 1+

Interpolates angles in degrees along the shortest direction. Use it to avoid taking the long way around, as with mt.lerp(350, 10, t) (interpolates 350°→10° along the +20° direction).

Arguments ​

ArgumentTypeUnitDefaultDescription
fromnumberdegreeRequiredStarting angle
tonumberdegreeRequiredTarget angle
tnumber-RequiredInterpolation factor. Not clamped

Return Value ​

TypeUnitDescription
numberdegreeInterpolated angle

mt.smoothstep(edge0, edge1, value) ​

API level: 1+

Returns a value that transitions smoothly from 0 to 1 as value crosses from edge0 to edge1 (Hermite interpolation, with zero velocity at both ends).

Arguments ​

ArgumentTypeUnitDefaultDescription
edge0number-RequiredStart of the transition, where the result is 0
edge1number-RequiredEnd of the transition, where the result is 1
valuenumber-RequiredValue to evaluate

Return Value ​

TypeUnitDescription
number0-1Smooth transition value. Clamped outside the interval

Constraints and Errors ​

ConditionDescription
edge0 == edge1Cannot be specified because the transition interval would have zero width

mt.keyframes(keys, time) ​

API level: 1+

Evaluates keyframes spanning multiple intervals. Because it can be called directly with ctx.time, you can write multistage choreography such as falling → bouncing → stopping without manually combining clamp and inverse_lerp.

Arguments ​

ArgumentTypeUnitDefaultDescription
keystable[]-RequiredArray of keyframes. Each element has the form { t = <time>, v = <value>, ease = <optional> }, ordered by ascending t
timenumbersecondsRequiredTime to evaluate

Each key's v must be either a number or a color table of the form { r, g, b, a }; the two types cannot be mixed within one keys array. ease is optional (linear by default) and is applied to the interval leading up to that key. You can pass a string naming an easing function, such as mt.ease.out_bounce, or any easing function in the form function(t) ... end.

If time is before the first key, the first value is returned; if it is after the last key, the last value is returned (clamping).

Return Value ​

TypeUnitDescription
number | colorSame as the key valuesValue interpolated within the interval

Example ​

lua
-- Fall with a bounce from 0.0 to 0.3 seconds, remain still from 0.3 to 0.5 seconds,
-- then slowly exit toward the end of the clip after 0.5 seconds
local y = mt.keyframes({
    { t = 0.0, v = 1.2 },
    { t = 0.3, v = 0.5, ease = "out_bounce" },
    { t = 0.8, v = 0.5 },
    { t = 1.3, v = -0.2, ease = "in_cubic" },
}, ctx.time)

-- Colors can also be interpolated directly
local tint = mt.keyframes({
    { t = 0.0, v = { r = 1.0, g = 1.0, b = 1.0, a = 1.0 } },
    { t = 1.0, v = { r = 1.0, g = 0.4, b = 0.1, a = 1.0 } },
}, ctx.time)
character.fill.color = tint

3. Progress Distribution ​

mt.distribute(index, count) ​

API level: 1+

Distributes a 1-based index evenly across 0-1. The first index maps to 0, and the last maps to 1.

Arguments ​

ArgumentTypeUnitDefaultDescription
indexinteger-Required1-based index, such as an index for ctx.chars[index]
countinteger-RequiredTotal count, such as ctx.char_count

Return Value ​

TypeUnitDescription
number0-1(index - 1) / (count - 1) constrained to 0-1. Returns 0 if count <= 1

Example ​

lua
-- Arrange hues in a rainbow according to character position
local hue = mt.distribute(index, ctx.char_count)
character.fill.use = true
character.fill.color = mt.color.from_okhsv(hue, 0.8, 1.0)

mt.order_text(entries) ​

API level: 8+

Converts encoded character or global-part indices into validated ctx.output.manual_order_text. Input order and duplicates are preserved.

Use a plain 1-based index or mt.CHAR | index for a character, and mt.PART | index for a global part. Each index must be an integer from 1 through 1024. mt.NOT is rejected because manual order has no exclusion syntax.

Arguments ​

ArgumentTypeUnitDefaultDescription
entriesinteger[]-RequiredEncoded character and global-part indices in output order

Return Value ​

TypeUnitDescription
string-Valid manual-order text representing the input sequence; an empty array returns an empty string

Example ​

lua
local order = {}
for index = ctx.char_count, 1, -1 do
    order[#order + 1] = mt.CHAR | index
end
ctx.output.manual_order_enabled = true
ctx.output.manual_order_text = mt.order_text(order)

mt.CHAR ​

Value: 0x0000. Character-kind flag; combine it with a 1-based character index. Because the value is zero, a plain index is equivalent.

mt.PART ​

Value: 0x10000. Global-part-kind flag; combine it with a 1-based global part index. The low 16 bits are reserved for the index.

mt.NOT ​

Value: 0x20000. Exclusion flag reserved for target syntax. Do not pass it to mt.order_text.


mt.each_char(ctx, options?) ​

API level: 6+

Returns an iterator over shaped characters with optional filtering. index always addresses ctx.chars, so it can be passed straight to ctx.chars[index] or to the mt.layout.* helpers. The counter within the filtered subset is info.n.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout / OnPath context
options.frominteger-1First character index to visit
options.tointeger-ctx.char_countLast character index to visit, inclusive
options.lineinteger-No filterKeep only characters with this line_index
options.orderstring-"asc"How info.progress is assigned: "asc" / "desc" / "center" / "random"
options.seedinteger-0Seed selecting the order when order = "random"

Return Value ​

TypeDescription
functionIterator yielding index (into ctx.chars), character and info

Fields of info:

FieldTypeDescription
progressnumberNormalized position in 0-1 following order
ninteger1-based counter within the filtered subset
countintegerNumber of elements in the filtered subset (at least 1)
first / lastbooleanWhether this is the first or last element of the filtered subset

order only remaps progress; the visit order is always ascending by index.

Valueprogress sequence (5 elements)
"asc"0, 0.25, 0.5, 0.75, 1
"desc"1, 0.75, 0.5, 0.25, 0
"center"1, 0.5, 0, 0.5, 1
"random"A duplicate-free shuffle determined by the seed

"center" is the normalized distance from the midpoint. With an even count no element sits on the midpoint, so the minimum is not 0.

Note: info is the same table on every iteration. Copy the values out if they need to outlive the loop.

Example ​

lua
-- Open the second line from the centre outwards
for index, character, info in mt.each_char(ctx, { line = 1, order = "center" }) do
    local delay = info.progress * 0.4
    character.opacity = mt.saturate((ctx.time - delay) * 4.0)
end

mt.each_part(ctx, options?) ​

API level: 6+

Returns an iterator over shaped parts with optional filtering. It follows the same rules as mt.each_char, and additionally resolves the owning character on info and can filter by it.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout / OnPath context
options.frominteger-1First part index to visit
options.tointeger-ctx.part_countLast part index to visit, inclusive
options.from_charinteger-No filterKeep only parts whose owning character index is at or above this
options.to_charinteger-No filterKeep only parts whose owning character index is at or below this
options.lineinteger-No filterKeep only parts with this line_index
options.orderstring-"asc"How info.progress is assigned, as in mt.each_char
options.seedinteger-0Seed selecting the order when order = "random"

from / to (a range of part indices) and from_char / to_char (a range of owning character indices) are separate axes. When both are given, only parts satisfying both pass.

Return Value ​

TypeDescription
functionIterator yielding index (into ctx.parts), part and info

info carries every field from mt.each_char plus:

FieldTypeDescription
charMtCharacterThe character owning this part (an element of ctx.chars)
char_indexintegerIndex of the owning character in ctx.chars
index_in_charintegerPosition within the owning character

info.progress spans the whole filtered set across 0-1. It does not restart on character boundaries. To span 0-1 per character, nest mt.each_char and mt.each_part.

Example ​

lua
-- Lift the parts of characters 3 through 5 in one continuous run
for index, part, info in mt.each_part(ctx, { from_char = 3, to_char = 5 }) do
    part.offset_y = info.progress * 0.05
    part.rotation = info.char.rotation * 0.5
end

mt.stagger(time, index, delay, duration) ​

API level: 1+

Returns animation progress with a different start time for each index (domino cascade, typewriter, sequential entrance). This is the standard function for character-by-character animation. The animation for index i begins delay * (i - 1) seconds later and progresses from 0 to 1 over duration seconds.

Arguments ​

ArgumentTypeUnitDefaultDescription
timenumbersecondsRequiredElapsed time since the animation began
indexinteger-Required1-based index
delaynumbersecondsRequiredDelay before the next index begins
durationnumbersecondsRequiredAnimation duration for each index

Return Value ​

TypeUnitDescription
number0-1Progress clamped to 0-1

Constraints and Errors ​

ConditionDescription
duration <= 0Specify a value greater than 0

Example ​

lua
for index = 1, ctx.char_count do
    local progress = mt.stagger(ctx.time, index, 0.08, 0.4)
    ctx.chars[index].offset_y = 0.5 + (1.0 - mt.ease.out_cubic(progress)) * 0.2
end

mt.stagger_progress(progress, position, span) ​

API level: 6+

Adds a position-dependent start delay to an existing 0-1 normalized progress for synchronized group cascade within a shared window. The element at position = 0 starts with the shared progress, while the element at position = 1 starts when the shared progress reaches span. Every element finishes when the shared progress reaches 1.

Use mt.stagger when constructing progress from time in seconds, an index, and an individual animation duration. Use mt.stagger_progress when distributing progress already obtained from a helper such as mt.timeline.intro_outro_seconds.

Arguments ​

ArgumentTypeUnitDefaultDescription
progressnumber0-1RequiredShared progress to distribute. Values outside the range are accepted, and the result is clamped to 0-1
positionnumber0-1RequiredNormalized element position. Values outside the range are clamped to 0-1. Usually info.progress
spannumberless than 1RequiredFraction of the shared window across which start times are distributed

Return Value ​

TypeUnitDescription
number0-1Progress for the element at position

The result is calculated as follows:

lua
mt.saturate((progress - mt.saturate(position) * span) / (1.0 - span))

Constraints and Errors ​

ConditionDescription
progress or position is not finiteError
span is not finiteError
span < 0 or span >= 1Error

Example ​

lua
local introProgress, outroProgress =
    mt.timeline.intro_outro_seconds(ctx, 0.5, 0.5, 5.0)

for _, character, info in mt.each_char(ctx, { order = "center" }) do
    local appear = mt.ease.out_back(
        mt.stagger_progress(introProgress, info.progress, 0.35))
    local disappear = mt.ease.in_cubic(
        mt.stagger_progress(outroProgress, info.progress, 0.25))

    character.scale = appear
    character.opacity = appear * (1.0 - disappear)
end

Changing the order of mt.each_char or mt.each_part changes only the start time assignment, not the loop's visit order.


mt.stagger_pattern(time, index, count, pattern, delay, duration, seed?) ​

API level: 1+

Returns staggered progress with a configurable starting direction or order (ascending, descending, center-out ripple, duplicate-free random).

Arguments ​

ArgumentTypeUnitDefaultDescription
timenumbersecondsRequiredTime to evaluate
indexinteger-Required1-based index
countinteger-RequiredTotal count, such as ctx.char_count
patternstring-RequiredProgression pattern: "asc" / "desc" (alias "right_to_left") / "center" / "random"
delaynumbersecondsRequiredStart delay
durationnumbersecondsRequiredAnimation duration
seedinteger-42Random seed when pattern = "random"

"random" assigns a duplicate-free shuffled rank to each index. Ranks are always exactly one apart, so the delays stay evenly spaced.

Changed in API level 6: delays used to come from an independent random per index, which let several characters land on nearly the same delay while other stretches stayed empty. It now uses a permutation, so the same seed produces a different order than before.

Return Value ​

TypeUnitDescription
number0-1Progress clamped to 0-1

Constraints and Errors ​

ConditionDescription
duration <= 0Specify a value greater than 0

4. Distance-Based Influence ​

mt.falloff(distance, radius) ​

API level: 3+

Returns an influence weight that is strongest at the center and decreases smoothly with distance (wavefront, passing shine, ripple, shockwave, localized emphasis). The return value is 0-1.

Arguments ​

ArgumentTypeUnitDefaultDescription
distancenumberInput coordinate systemRequiredDistance from the center of the effect
radiusnumberInput coordinate systemRequiredSpread of the falloff. At this distance, the weight is approximately 0.37 (e^-1)

This is a standard choice for concentrating an effect around a particular position, such as a wave crest, a moving highlight, or emphasis focused on one location.

Choosing between it and mt.smoothstep: smoothstep becomes exactly 0 outside its range, creating a cutoff, whereas falloff has a tail that continues indefinitely. As a result, a moving crest or light appears to pass through continuously. It also avoids the corner produced by linear falloff such as (1 - d/r).

Return Value ​

TypeUnitDescription
number0-1Distance-based influence weight

Example ​

lua
-- Raise only the characters currently being passed by the waveFront
local distance = characterRadius - waveFront
character.offset_y = 0.5 + mt.falloff(distance, 0.25) * 0.1

NOTE

Passing 0 for radius returns 0 to avoid division by zero.

5. Deterministic Random Values and Noise ​

mt.random(seed, index, channel?) ​

API level: 1+

A stable random value determined solely by the (seed, index, channel) tuple. Because it does not depend on call order, it can safely be used to vary individual characters or parts (math.random depends on call order and cannot be used safely for this purpose).

TIP

Name each use with channel, such as "angle" or "distance", to obtain independent random values without manually salting the seed or index.

Added in API level 6: The optional string channel was added. Omitting it preserves the previous results.

Arguments ​

ArgumentTypeUnitDefaultDescription
seedinteger-RequiredValue selecting the random sequence. Use the same seed for the same purpose
indexinteger-RequiredPosition within the sequence, such as a character index
channelstring-OmittedName separating one use from another.

Return Value ​

TypeUnitDescription
number0-1Random value in [0, 1). The same arguments always produce the same value

Constraints and Errors ​

The UTF-8 bytes of channel are converted with a fixed 32-bit hash. Unicode normalization is not performed, so ASCII names such as "angle" or "color.hue" are recommended. The 32-bit hash can theoretically collide and is not suitable for cryptographic use.


mt.random_range(seed, index, low, high, channel?) ​

API level: 1+

Returns the result of mt.random mapped to [low, high). An optional string channel separates random sequences by purpose.

Arguments ​

ArgumentTypeUnitDefaultDescription
seed / indexinteger-RequiredSame as mt.random
low / highnumber-RequiredOutput range
channelstring-OmittedSame as mt.random.

Return Value ​

TypeUnitDescription
numberSame as the output rangeStable random value in [low, high)

mt.distributed_random(seed, index, count, low, high, channel?) ​

API level: 8+

Assigns count evenly spaced values in [low, high) to 1-based indices through a deterministic permutation. Unlike independent calls to mt.random_range, the complete set has no clusters or duplicate values. Use info.n and info.count with mt.each_char or mt.each_part so filtered indices remain contiguous.

Arguments ​

ArgumentTypeUnitDefaultDescription
seedinteger-RequiredValue selecting the permutation and common offset
indexinteger-RequiredAssignment position from 1 through count
countinteger-RequiredNumber of values; must be at least 1
low / highnumber-RequiredFinite output bounds with low < high
channelstring-OmittedName separating independent assignments

Return Value ​

TypeUnitDescription
numberSame as the output rangeStable assigned value in [low, high); adjacent sorted values differ by (high - low) / count

Example ​

lua
for _, character, info in mt.each_char(ctx) do
    local phase = mt.distributed_random(
        ctx.meta.instance_seed, info.n, info.count, 0.0, math.pi * 2.0, "phase")
    character.offset_y = 0.5 + mt.wave(ctx.time, 0.5, phase) * 0.03
end

mt.noise1(x, seed?) ​

API level: 1+

Deterministic one-dimensional value noise. Returns a fluctuation that varies smoothly and continuously with x. Passing time as x produces temporal fluctuation; passing a position produces spatial fluctuation.

Arguments ​

ArgumentTypeUnitDefaultDescription
xnumber-RequiredSample position. Values are smoothly interpolated between lattice points at integer intervals
seedinteger-0Value that changes the fluctuation pattern

Return Value ​

TypeUnitDescription
number-1-1Continuous value that is always the same for the same arguments

Example ​

lua
-- Handheld-camera-style shake with a different phase for each character
character.rotation = mt.noise1(ctx.time * 0.8, index) * 4.0

mt.noise2(x, y, seed?) ​

API level: 1+

Deterministic two-dimensional value noise. Returns smooth fluctuation at position (x, y). Passing time on one axis and character position on the other can create fluctuation that "flows through space."

Arguments ​

ArgumentTypeUnitDefaultDescription
x / ynumber-RequiredSample position
seedinteger-0Value that changes the fluctuation pattern

Return Value ​

TypeUnitDescription
number-1-1Continuous value that is always the same for the same arguments

mt.wiggle(time, frequency, amplitude, octaves?, seed?) ​

API level: 1+

Computes deterministic organic tremor (camera shake, wandering float, handheld jitter).

Arguments ​

ArgumentTypeUnitDefaultDescription
timenumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of fluctuations per second
amplitudenumberSame as the output valueRequiredAmplitude of the first octave. This is not the maximum sum across multiple octaves
octavesinteger-1Number of noise octaves to layer
seedinteger-0Random seed

Return Value ​

TypeUnitDescription
numberSame as amplitudeFluctuation value

When octaves > 1, the amplitudes are halved as they are added, so the theoretical absolute upper bound is amplitude * (2 - 2^(1 - octaves)), approaching 2 * amplitude as the number of octaves increases.

6. Cycles and Springs ​

mt.cycle(t, period) ​

API level: 1+

Repeating 0 to 1 linear sawtooth ramp (looping animation, continuous timer, beacon sweep).

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
periodnumbersecondsRequiredLength of one cycle

Return Value ​

TypeUnitDescription
number0-1Progress greater than or equal to 0 and less than 1

Constraints and Errors ​

ConditionDescription
period <= 0Specify a value greater than 0

mt.pingpong(t, period) ​

API level: 1+

Repeating 0 to 1 to 0 back-and-forth motion (pendulum, shuttle loop).

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
periodnumbersecondsRequiredLength of one round trip

Return Value ​

TypeUnitDescription
number0-1Oscillating value from 0 to 1

Constraints and Errors ​

ConditionDescription
period <= 0Specify a value greater than 0

mt.wave(t, frequency, phase?) ​

API level: 1+

Computes a sine wave harmonic oscillation (floating, bobbing, undulating, breathing). Shorthand for sin(2π × frequency × t + phase).

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset. It can also offset the wave for each index

Return Value ​

TypeUnitDescription
number-1-1Waveform value

mt.wave_square(t, frequency, phase?) ​

API level: 1+

Computes a square wave periodic toggle between -1 and 1 (blink, strobe, flashing).

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

TypeUnitDescription
number--1.0 or 1.0

mt.wave_triangle(t, frequency, phase?) ​

API level: 1+

A triangle wave.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

TypeUnitDescription
number-1-1Value that travels linearly back and forth between -1.0 and 1.0

mt.wave_sawtooth(t, frequency, phase?) ​

API level: 1+

A sawtooth wave.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

TypeUnitDescription
number-1-1Repeating value that rises linearly from -1.0 to less than 1.0

mt.spring(t, frequency, damping) ​

API level: 1+

Computes a damped harmonic oscillation (elastic recoil, jiggle, wobble, overshoot settling). It begins at 1 when t = 0 and converges toward 0 while oscillating.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredElapsed time since the motion began. Returns the initial value when t <= 0
frequencynumberHzRequiredNumber of oscillations per second
dampingnumber1/secondRequiredDamping strength. Larger values converge faster

Return Value ​

TypeUnitDescription
number-Damped oscillation value converging from the initial value 1 toward 0

For t > 0, it calculates exp(-damping * t) * cos(2π * frequency * t).

WARNING

The return value is not confined to 0-1. During damping, it can take negative values (and, depending on the phase, overshoot within a range that does not slightly exceed 1). This is the overshoot effect itself, analogous to mt.ease.out_back / elastic extending outside 0-1.

  • Suitable uses: A residual displacement such as offset, rotation, or scatter amount (for example, 0.5 + settle * 0.1)
  • Unsuitable uses: Direct assignment to progress, opacity, scale, or stretch, or use as the t argument to mt.lerp. If you need progress, use mt.ease.* or mt.saturate, and do not multiply a scale factor directly by a negative value (stretch has no host-side lower-bound clamp and can reach rendering while still negative. scale is clamped to a positive lower bound, and opacity is clamped to 0-1 immediately before rendering)

Example ​

lua
-- Use as deviation from the target position (maximum at t=0 and converges over time; negative intervals are overshoot)
local settle = mt.spring(ctx.time - start_time, 3.0, 4.0)
character.offset_y = 0.5 + settle * 0.1

7. Displacement from Polar Coordinates ​

mt.polar_offset_2d(angleDegrees, radius) ​

API level: 5+

Converts a polar direction (degrees) and radius into a Y-up canvas offset pair (radial scatter, orbital motion, circular explosion). It lets you express motion such as scattering, gathering, and orbiting without writing cos / sin.

Arguments ​

ArgumentTypeUnitDefaultDescription
angleDegreesnumberdegreeRequiredDirection. As with coordinates, 0 degrees points right and positive values rotate clockwise
radiusnumbercanvas normalized displacementRequiredDistance in that direction

Return Value ​

TypeUnitDescription
number, numbercanvas normalized displacementTwo values: offsetX, offsetY

The input angle uses the same screen-based convention as rotation / shadow.angle (90 degrees points down), but the returned values are Y-up. They can be added directly to offset_x / offset_y. mt.projectile_2d uses the same convention.

Example ​

lua
-- Scatter characters radially
local angle = mt.distribute(index, ctx.char_count) * 360.0
local dx, dy = mt.polar_offset_2d(angle, 0.3 * scatterAmount)
character.offset_x = 0.5 + dx
character.offset_y = 0.5 + dy

mt.direction_to_rotation_2d(dx, dy, aspectRatio?) ​

API level: 15+

Returns the clockwise-positive rotation angle in degrees for a Y-up vector. dx and dy are required numbers; with the third argument omitted, both axes use the same scale (for example, both in pixels). Right is 0, down is 90, up is -90. The result is in [-180, 180]; a zero vector returns 0. This is the inverse direction conversion of mt.polar_offset_2d for a positive radius.

The optional aspectRatio argument scales X and defaults to 1. For deltas measured as canvas width/height ratios, pass ctx.canvas.aspect_ratio as the third argument. The function does not transform between local and global spaces. Use the same coordinate space as the target rotation. The returned heading assumes the object's unrotated forward direction is +X; add an orientation offset for artwork facing another direction.

lua
local dx, dy = mt.polar_offset_2d(45, 0.1)
character.rotation = mt.direction_to_rotation_2d(dx, dy) -- 45 degrees

-- Heading from normalized canvas deltas, with equally scaled axes:
local heading = mt.direction_to_rotation_2d(deltaX, deltaY, ctx.canvas.aspect_ratio)

8. Physical Motion ​

mt.bounce_y(config) / mt.bounce_x(config) ​

API level: 3+

Calculates and returns the position, velocity, Squash & Stretch, and contact-position correction for a one-dimensional ballistic bounce against a floor (Y axis) or wall (X axis). It does not modify characters or parts directly. Both a configuration-table form and a positional-argument form are supported.

Arguments ​

ArgumentTypeUnitDefaultDescription
configtable-RequiredSettings for time, collision surface, starting position, gravity or acceleration, restitution, initial velocity, and Squash & Stretch

Shared config ​

FieldTypeUnitDefaultDescription
tnumberseconds0.0Elapsed time since the animation began
restitutionnumber-0.5Coefficient of restitution. Larger values produce a stronger rebound after impact
start_velocitynumbercanvas normalized displacement/second0.0Initial velocity along the axis
squash / squash_strengthnumberfactor0.15Strength with which the contact axis is compressed according to impact velocity
stretch / stretch_strengthnumberfactor0.02Strength with which the direction-of-motion axis is extended according to movement velocity
flexibilitynumberradian/second16.0Oscillation speed when returning to the original shape after impact
dampingnumber1/second7.0Rate at which post-impact oscillation settles

When an item has multiple names, the leftmost name takes precedence. Names to the right are compatibility aliases.

config Specific to mt.bounce_y ​

FieldTypeUnitDefaultDescription
ground_ynumbercanvas normalized position0.0Y coordinate of the floor to collide with
start_ynumbercanvas normalized position0.0Starting Y coordinate
gravitynumbercanvas normalized displacement/second²9.81Magnitude of acceleration toward the floor

config Specific to mt.bounce_x ​

FieldTypeUnitDefaultDescription
wall_x / ground_xnumbercanvas normalized position0.0X coordinate of the wall to collide with. ground_x is a compatibility alias
start_xnumbercanvas normalized position0.0Starting X coordinate
acceleration / accel_x / gravitynumbercanvas normalized displacement/second²9.81Magnitude of acceleration toward the wall. accel_x and gravity are compatibility aliases

Positional-Argument Form ​

Instead of a configuration table, you can pass each value in the following order.

lua
mt.bounce_y(t, groundY, startY, gravity, restitution, startVelocity,
    squashStrength, stretchStrength, flexibility, damping)

mt.bounce_x(t, wallX, startX, acceleration, restitution, startVelocity,
    squashStrength, stretchStrength, flexibility, damping)

Each argument can be omitted and uses the same default as the corresponding config field.

Return Value (one of) ​

TypeUnitDescription
table-In configuration-table form, a table containing position, velocity, deformation, and collision state
Multiple number valuesAs appropriate for each valueIn positional-argument form, returns the calculation result as eight values

Return-Value Table ​

FieldTypeUnitDescription
posnumbercanvas normalized positionCalculated coordinate along the axis
ynumbercanvas normalized positionCalculated Y coordinate for mt.bounce_y. Same value as pos
xnumbercanvas normalized positionCalculated X coordinate for mt.bounce_x. Same value as pos
velocitynumbercanvas normalized displacement/secondVelocity along the axis at that time
stretch_x / stretch_ynumbermultiplierX- and Y-axis stretch factors incorporating Squash & Stretch
offset_ynumberRatio of bounds_heightCorrection used by mt.bounce_y to preserve the contact position
offset_xnumberRatio of bounds_widthCorrection used by mt.bounce_x to preserve the contact position
last_impact_timenumber / nilsecondsTime of the previous impact. nil if no impact has occurred yet
next_impact_timenumber / nilsecondsTime of the next impact. nil if the object has already settled
impact_countintegerimpactsNumber of impacts so far
settledboolean-Whether the bounce has ended and the object has settled

The table's [1] through [5] entries also provide, in order, the axis coordinate, stretch_x, stretch_y, contact-position correction, and velocity.

In positional-argument form, the function returns the axis coordinate, stretch_x, stretch_y, contact-position correction, velocity, last_impact_time, next_impact_time, and impact_count, in that order. settled is available only in configuration-table form.

Notes ​

  • Normally, specify a restitution value greater than or equal to 0 and less than 1.
  • The sign of gravity or acceleration is ignored; its absolute value is treated as the acceleration magnitude.

Example ​

lua
-- Precisely calculate Y-axis bounce physics with a detailed configuration table
local b = mt.bounce_y({
    t = ctx.time,
    ground_y = 0.1,
    start_y = 0.6,
    gravity = 9.81,
    restitution = 0.5,
    squash = 0.2,
    stretch = 0.03,
    flexibility = 18.0,
    damping = 6.0,
})

-- Apply the calculation results flexibly
local targetY = b.y + b.offset_y * char.geometry.bounds_height
local _, calcOffsetY = mt.layout.canvas_to_offset_2d(ctx, char, curX, targetY)
char.offset_y = calcOffsetY
char.stretch_x = b.stretch_x
char.stretch_y = b.stretch_y

mt.bounce_ground(ctx, item, groundY, config?) ​

API level: 3+

A simple convenience function that drops a character or part item toward the specified ground Canvas coordinate groundY (for example, 0.1), then makes it bounce and land with a springy motion. offset_y, stretch_x, and stretch_y are applied automatically.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredCharacter or part to modify
groundYnumbercanvas normalized coordinateRequiredY coordinate at which to land
configtable-nilSettings such as starting position, contact reference, and physical parameters

Return Value ​

TypeUnitDescription
table-Calculated bounce state

Changes ​

FieldTypeUnitDescription
item.offset_ynumbercanvas normalized displacementSets the calculated Y displacement
item.offset_xnumbercanvas normalized displacementWhen ground_mode = "absolute", sets the displacement that preserves the current X position on the canvas
item.stretch_x / item.stretch_ynumbermultiplierSets the Squash & Stretch from the bounce state

config ​

FieldTypeUnitDefaultDescription
tnumbersecondsctx.timeTime to evaluate
delaynumberseconds0.0Waiting time before motion begins
start_modestring-"absolute" when start_y is specified; otherwise "relative"Reference for the starting position. "relative" starts drop_height above the current position; "absolute" uses start_y
start_ynumbercanvas normalized positionnilStarting position used when start_mode = "absolute"
drop_heightnumbercanvas normalized displacement0.3Distance above the current position when start_mode = "relative"
ground_modestring-Same as start_modeReference for the landing position. "absolute" treats groundY as a fixed position on the canvas; "relative" follows transforms higher in the hierarchy
align_tostring-"bottom"Position to place on the ground. "bottom" / "bounds_bottom" means the bottom edge of the ink bounds, "baseline" means the character's horizontal baseline, and "center" means the center of the ink bounds. For a part, which has no baseline, "baseline" falls back to the bottom edge
gravitynumbercanvas normalized displacement/second²4.0Magnitude of acceleration toward the ground
restitutionnumber-0.45Coefficient of restitution
start_velocitynumbercanvas normalized displacement/second0.0Initial velocity along the Y axis
squashnumberfactor0.15Strength with which the contact axis is compressed according to impact velocity
stretchnumberfactor0.02Strength with which the direction-of-motion axis is extended according to movement velocity
flexibilitynumberradian/second16.0Oscillation speed when returning to the original shape after impact
dampingnumber1/second7.0Rate at which post-impact oscillation settles

When ground_mode = "absolute", the landing position is calculated using the canvas-space bounding rectangle after rotation and scale have been applied.

Example ​

lua
-- Drop every character vertically from a common fixed screen height (0.85) and land on the BB edge
mt.bounce_ground(ctx, ctx.chars[i], 0.1, { start_mode = "absolute", start_y = 0.85, align_to = "bottom" })

-- Drop from a position a fixed distance (0.3) above each character and align to the baseline
mt.bounce_ground(ctx, ctx.chars[i], 0.1, { start_mode = "relative", drop_height = 0.3, align_to = "baseline" })

-- Even with global.rotation applied, drop from screen position 0.85 and land precisely at screen position 0.1
mt.bounce_ground(ctx, ctx.chars[i], 0.1, {
    start_mode = "absolute", start_y = 0.85,
    ground_mode = "absolute", align_to = "bottom"
})

NOTE

When "absolute" is specified, both offset_x and offset_y are written to place the item precisely at the specified Canvas position (rotation couples the two axes, so changing only one cannot reach the target coordinate). The offset on the non-bounce axis acts as a correction that preserves the current screen position.


mt.bounce_wall(ctx, item, wallX, config?) ​

API level: 3+

A simple convenience function that makes a character or part item collide with and bounce off the specified wall Canvas coordinate wallX (for example, 0.9). offset_x, stretch_x, and stretch_y are applied automatically.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredCharacter or part to modify
wallXnumbercanvas normalized coordinateRequiredX coordinate of the collision target
configtable-nilSettings such as starting position, collision reference, and physical parameters

Return Value ​

TypeUnitDescription
table-Calculated bounce state

Changes ​

FieldTypeUnitDescription
item.offset_xnumbercanvas normalized displacementSets the calculated X displacement
item.offset_ynumbercanvas normalized displacementWhen config.wall_mode = "absolute", sets the displacement that preserves the current Y position on the canvas
item.stretch_x / item.stretch_ynumbermultiplierSets the Squash & Stretch at impact

config ​

FieldTypeUnitDefaultDescription
tnumbersecondsctx.timeTime to evaluate
delaynumberseconds0.0Waiting time before motion begins
start_modestring-"absolute" when start_x is specified; otherwise "relative"Reference for the starting position. "relative" starts travel_distance away from the current position in the direction opposite the wall; "absolute" uses start_x
start_xnumbercanvas normalized positionnilStarting position used when start_mode = "absolute"
travel_distancenumbercanvas normalized displacement0.3Distance from the current position in the direction opposite the wall when start_mode = "relative"
wall_modestring-Same as start_modeReference for the collision position. "absolute" treats wallX as a fixed position on the canvas; "relative" follows transforms higher in the hierarchy
align_tostring-Determined from the direction of travelPosition to make contact. Defaults to "right" when approaching from the left and "left" when approaching from the right. "bounds_right", "bounds_left", and "center" can also be specified
accel / accelerationnumbercanvas normalized displacement/second²4.0Magnitude of acceleration toward the wall. accel takes precedence
restitutionnumber-0.45Coefficient of restitution
start_velocitynumbercanvas normalized displacement/second0.0Initial velocity along the X axis
squashnumberfactor0.15Strength with which the contact axis is compressed according to impact velocity
stretchnumberfactor0.02Strength with which the direction-of-motion axis is extended according to movement velocity
flexibilitynumberradian/second16.0Oscillation speed when returning to the original shape after impact
dampingnumber1/second7.0Rate at which post-impact oscillation settles

Example ​

lua
-- Launch every part from a common fixed screen position (0.1) and bounce it off the wall (wallX = 0.9)
mt.bounce_wall(ctx, ctx.parts[i], 0.9, { start_mode = "absolute", start_x = 0.1, delay = i * 0.05 })

-- Launch from a position a fixed distance (0.25) behind each part
mt.bounce_wall(ctx, ctx.parts[i], 0.9, { start_mode = "relative", travel_distance = 0.25 })

mt.impact_squash(config) ​

API level: 3+

Calculates a Squash & Stretch impulse that compresses and rebounds at the instant of impact. It uses the damped oscillation sin(ωt) * exp(-γt), compressing most strongly immediately after impact and returning to the original shape while oscillating.

Because mt.bounce_* already applies Squash & Stretch internally, use this function when you want to represent an impact at timing you control yourself, such as colliding with another character or hitting something other than the ground.

Arguments ​

ArgumentTypeUnitDefaultDescription
configtable-RequiredSettings for impact time and deformation characteristics

config ​

FieldTypeUnitDefaultDescription
tnumberseconds0.0Current time. Before impact_time, returns values with no deformation
impact_timenumberseconds0.0Time of impact. You can pass last_impact_time from mt.bounce_*
squashnumberratio0.15Maximum compression amount. With 0.2, the height is 80% at maximum compression
stiffnessnumberradian/second25.0Speed of return to the original shape
dampingnumber1/second12.0Rate at which oscillation settles

Return Value (multiple) ​

TypeUnitDescription
numbermultiplierstretchX. X-axis stretch factor
numbermultiplierstretchY. Y-axis stretch factor. It is the reciprocal of stretchX to preserve volume
numberRatio of bounds_heightoffsetCorrection. Corrects the contact-position shift caused by compression. Multiply it by bounds_height and add it to offset_y

The name and meaning of squash are shared with the squash setting of mt.bounce_*.

Example ​

lua
-- Choose the impact time yourself and apply compression
local sx, sy, correction = mt.impact_squash({
    t = ctx.time,
    impact_time = landTime,
    squash = 0.2,
})
character.stretch_x = sx
character.stretch_y = sy
character.offset_y = character.offset_y + correction * character.geometry.bounds_height

TIP

To vary the amount of compression according to impact velocity, calculate squash itself. There is no argument that accepts velocity directly. With a method that simply multiplies by velocity, the compression reaches its lower limit after only a modest increase in velocity, so increasing the speed further no longer changes the image. Explicitly specifying "the velocity at which maximum compression occurs," as below, keeps the result within the intended range.

lua
local squashAmount = 0.3 * mt.saturate(impactSpeed / kFullSquashSpeed)
local sx, sy, correction = mt.impact_squash({ t = ctx.time, impact_time = landTime, squash = squashAmount })

mt.projectile_2d(config) / mt.projectile_2d(t, speed, angle, gravity?, spin?, drag?) ​

API level: 3+

Closed-form 2D ballistic flight from launch velocity under gravity (cannon launch, falling debris, ballistic trajectory). While mt.bounce_y / mt.bounce_x model motion that strikes a surface and bounces, this function is for motion that is thrown and does not land (scattered debris, explosions, and tosses).

Arguments ​

ArgumentTypeUnitDefaultDescription
configtable-RequiredSettings such as elapsed time since launch, initial velocity, direction, and gravity

config ​

FieldTypeUnitDefaultDescription
tnumberseconds0.0Elapsed time since launch. Negative values are treated as 0
speednumbercanvas normalized displacement/second0.0Initial speed
anglenumberdegrees0.0Launch direction. 0 points right, and the positive direction is clockwise. -90 points straight up
gravitynumbercanvas normalized displacement/second²4.0Downward acceleration
spinnumberdegrees/second0.0Angular velocity. The returned rotation can be assigned directly to the field of the same name
dragnumber1/second0.0Exponential decay rate for air resistance. 0 means no decay
start_x / start_ynumbercanvas normalized position0.0Launch position. If omitted, returns displacement relative to the origin

Positional-Argument Form ​

ArgumentTypeUnitDefaultDescription
tnumberseconds0.0Elapsed time since launch. Negative values are treated as 0
speednumbercanvas normalized displacement/second0.0Initial speed
anglenumberdegrees0.0Launch direction
gravitynumbercanvas normalized displacement/second²4.0Downward acceleration
spinnumberdegrees/second0.0Angular velocity
dragnumber1/second0.0Exponential decay rate for air resistance

Because the launch position cannot be specified in positional-argument form, this form returns displacement relative to the origin.

Return Value (one of) ​

TypeUnitDescription
table-In table form, returns a result containing x, y, rotation, velocity_x, and velocity_y. The same values are also available as [1] through [5]
Multiple number valuesAs appropriate for each valueIn positional-argument form, returns x, y, rotation, velocity_x, and velocity_y, in that order

IMPORTANT

The angle is screen-based (clockwise), but the returned y uses Canvas coordinates (positive upward). With angle = -90 (launched straight up), y increases and then begins decreasing under gravity. It can be added directly to offset_y.

Example ​

lua
-- Scatter fragments in a fan and make them fall while rotating
local angle = mt.random_range(seed, index, -140.0, -40.0, "angle")
local speed = mt.random_range(seed, index, 0.6, 1.2, "speed")
local flight = mt.projectile_2d({
    t = ctx.time - burstTime,
    speed = speed,
    angle = angle,
    gravity = 3.0,
    spin = mt.random_range(seed, index, -360.0, 360.0, "spin"),
})
part.offset_x = 0.5 + flight.x
part.offset_y = 0.5 + flight.y
part.rotation = flight.rotation

Keeping the arc readable ​

The returned x / y are always a true parabola. When a flight looks wrong, the trajectory is rarely the cause; something layered on top of it is:

  • Do not touch pivot_*. Leave it at 0.5. Adding the flight displacement to the pivot as well makes the fragment orbit a distant point instead of following its arc. See "Pivot reference points".
  • Keep the tumble bounded. spin does not bend the path, but a fragment turning several times per second reads as rolling in place and hides the arc. For 3D yaw / pitch, prefer a total angle that eases to a settled attitude over a rate multiplied by elapsed time, which accumulates without limit.
  • A purely radial throw barely arcs. On a single line of text the outward direction is nearly horizontal, so there is little rise to fall back from. Blend the direction toward straight up (-90) to give the debris real lift.

mt.friction_decay(t, speed, friction) ​

API level: 3+

Calculates exponential friction deceleration from initial velocity (sliding stop, braking inertia). It lets you control motion that "rushes in and glides to a stop" using the physical quantities initial velocity and coefficient of friction, rather than arrival time.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredElapsed time. Negative values are treated as 0
speednumbercanvas normalized displacement/secondRequiredInitial speed
frictionnumber1/second0.0Decay rate. Larger values stop sooner

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementdistance. Distance traveled so far
numbercanvas normalized displacement/secondcurrentSpeed. Speed at that time

The final travel distance converges to speed / friction. Conversely, if the stopping distance is known, you can set speed = distance * friction.

Choosing between it and mt.ease.out_expo: easing represents progress from 0→1 over a fixed duration, whereas this function treats initial velocity and friction as independent physical quantities. This allows adjustments such as changing only the initial velocity while preserving the character of the deceleration.

Example ​

lua
-- Slide in from the left and stop through friction
local travelled = mt.friction_decay(ctx.time - startTime, 2.0, 4.0)
character.offset_x = 0.5 - 0.5 + travelled

NOTE

When friction is extremely small (1e-4 or less), motion is treated as uniform linear motion. This avoids numerical cancellation and is effectively equivalent to frictionless motion in practical use.

9. Color ​

See "color" in Concepts for the color format. RGBA is the color storage format. When calculating colors, select the color space that matches the intent.

mt.color.resolve_fill(ctx, target, options?) ​

API level: 6+

Resolves the fill applied to a character or part and returns its base color, fill mode, and object opacity. It is available in OnLayout and OnPath. With no options it returns the final fill the target is drawn with; options.level stops the inheritance at an earlier tier (see Style inheritance).

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredCurrent OnLayout or OnPath context
targetcharacter | part-RequiredAn element of ctx.chars or ctx.parts
optionstable-nil(12+) { level = "global" | "character" | "part" }

Return Values ​

ValueTypeUnitDescription
colorcolor-Copy of the resolved solid color, or of ctx.global.fill.color, which supplies the starting color for a Global gradient or Crayon
modestring-"solid" / "gradient" / "crayon" / "transparent"
opacitynumber0-1Product of the Global, owning-character, and target-part opacity values, clamped to 0-1

For a part, resolution checks part.fill.use, then the owning character's fill.use, then Global. For a character, it checks only the character and Global; individual colors on descendant parts are not included. When an individual color is selected, mode is always "solid".

When the fill reaches Global, the mode is "transparent" if fill.transparent is true, "solid" if gradient.color_space is "none", "crayon" if it is "crayon", and "gradient" for every other gradient mode.

The returned opacity does not include the color's own a component. The returned color is a new table, so modifying it does not affect the original fill.color.

lua
for _, part in mt.each_part(ctx) do
    local color, mode, opacity = mt.color.resolve_fill(ctx, part)
    if mode == "gradient" and opacity > 0.0 then
        -- This part is filled by the Global gradient.
        print(color.r, ctx.global.gradient.end_color.r)
    end
end

Color construction, decomposition, and interpolation ​

The functions below return new tables and do not modify their arguments. from_* constructs a color from components, to_* decomposes a color into a component table, and lerp* constructs a color from two colors and t. An omitted alpha, or a missing a in a color passed to to_*, becomes 1.0. hue uses one full turn as its unit; out-of-range input wraps to [0, 1). No interpolation function clamps t. OKHSV/OKHSL saturation, value, and lightness inputs are constrained to 0-1. lerp_oklch / lerp_okhsv / lerp_okhsl take the shortest hue path by default and inherit the colored endpoint's hue when the other endpoint is achromatic. An exact half-turn preserves the sign of the hue difference. lerp_oklch(from, to, t, "long") takes the longer hue path; "short" or omission keeps the shorter path. Equal hues make one increasing revolution with "long", including hues inherited from a colored endpoint. Other hueMethod values are errors.

API level: 1+

  • mt.color.lerp(from, to, t) - Linearly interpolates RGBA components.
  • mt.color.from_hsv(hue, saturation, value, alpha?) - Constructs a color from normalized HSV.
  • mt.color.with_alpha(color, alpha) - Preserves RGB and replaces only alpha.
  • mt.color.from_oklch(lightness, chroma, hue, alpha?) - Constructs a color from OKLCH. Chroma is normally about 0-0.4, and RGB output is constrained to 0-1.

API level: 8+

  • mt.color.from_oklab(lightness, aAxis, bAxis, alpha?) - Creates an RGBA color from OKLab components. RGB output is constrained to 0-1.
  • mt.color.to_oklab(color) - Converts to { lightness, a, b, alpha }. Input RGB is not clamped.
  • mt.color.to_oklch(color) - Converts to { lightness, chroma, hue, alpha }. Achromatic hue is 0.
  • mt.color.from_okhsv(hue, saturation, value, alpha?) - Creates a color from gamut-normalized OKHSV.
  • mt.color.to_okhsv(color) - Converts to { hue, saturation, value, alpha }. Achromatic hue is 0.
  • mt.color.from_okhsl(hue, saturation, lightness, alpha?) - Creates a color from gamut-normalized OKHSL.
  • mt.color.to_okhsl(color) - Converts to { hue, saturation, lightness, alpha }. Achromatic hue is 0.
  • mt.color.lerp_oklab(from, to, t) - Interpolates in Cartesian OKLab coordinates.
  • mt.color.lerp_oklch(from, to, t, hueMethod?) - Interpolates in OKLCH. hueMethod is "short" (default) or "long".
  • mt.color.lerp_okhsv(from, to, t) - Interpolates in OKHSV along the shortest hue path.
  • mt.color.lerp_okhsl(from, to, t) - Interpolates in OKHSL along the shortest hue path.

10. Style Inheritance ​

Fill, stroke, shadow, blur and texture are inherited through three tiers: ctx.global, the owning character, and the part. An element's use flag replaces the whole group it inherits, so shadow.use switches the shadow color and its blur together. The resolve functions return the group an element is drawn with: mt.color.resolve_fill, mt.shadow.resolve, mt.blur.resolve, mt.stroke.resolve and mt.texture.resolve.

With no options they return the final value of the target: for a part this includes its own override, and for a character it does not include the overrides of its parts. options.level stops the inheritance at an earlier tier:

levelReturns
"global"The frame's value, ctx.global
"character"The value after the owning character's override
"part"The value after the part's override (a part's default)

A character's default level is "character", and a level below the target's own tier is an error. To change one property of an overridden group, read the tier above with level, set use, and write the values back:

lua
function OnLayout(ctx)
    for _, part in mt.each_part(ctx) do
        local shadow = mt.shadow.resolve(ctx, part, { level = "character" })
        -- Add blur to this part's shadow only; keep the inherited color.
        part.shadow.use = true
        part.shadow.color = shadow.color
        part.shadow.blur_x = shadow.blur_x + 0.2
        part.shadow.blur_y = shadow.blur_y + 0.2
    end
end

NOTE

A part with shadow.use = true casts a shadow even while the global shadow is off, as with an individual shadow color.

All of them are available in OnLayout and OnPath, and the returned colors are new tables, so modifying them does not affect the original settings.

Arguments of every resolve function:

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredCurrent OnLayout or OnPath context
targetcharacter | part-RequiredAn element of ctx.chars or ctx.parts
optionstable-nil{ level = "global" | "character" | "part" }

API level: 12+

mt.shadow.resolve(ctx, target, options?) ​

Resolves the shadow. Returns { color, blur_x, blur_y, glow }: the shadow color, the horizontal and vertical blur in em, and the glow strength.

mt.blur.resolve(ctx, target, options?) ​

Resolves the blur of the text itself. Returns { amount_x, amount_y, glow }: the blur in em and the glow strength.

mt.stroke.resolve(ctx, target, options?) ​

Resolves the stroke. Returns { width, gap, color, outer_width, outer_color }. order, join and miter_limit exist only on ctx.global.stroke.

mt.texture.resolve(ctx, target, options?) ​

Resolves the texture. Returns { type, p1, p2, p3, p4, color }.

11. Easing ​

API level: 1+

mt.ease.<name>(t) accepts progress t (normally 0-1) and returns eased progress. A typical use is to add character to the linear progress returned by mt.stagger or mt.cycle. Except for mt.ease.cubic_bezier, the input t is not clamped automatically. To constrain it to 0-1, pass it through mt.saturate first.

Names combine a curve type and a direction.

  • in_ - Acceleration (starts slowly and ends quickly). Suitable for exits
  • out_ - Deceleration (starts quickly and stops slowly). Suitable for entrances
  • in_out_ - Accelerates in the first half and decelerates in the second
CurveWhen to use
LinearConstant speed (no change)
QuadGentlest acceleration and deceleration
CubicStandard acceleration and deceleration. Start here when unsure
QuartSteeper than Cubic
SineExtremely gentle easing derived from a sine wave
CircCircular-arc curve that rises sharply at the endpoint
ExpoExtremely strong exponential easing
BackOvershoots once and then returns (extends outside 0-1)
ElasticOscillates like a spring before settling (extends outside 0-1)
BounceBounces as if against a floor

Note that Back and Elastic can return values outside the 0-1 range; that excursion is the essence of their effect.

Every function below takes one argument t (number, progress, not clamped) and returns a number, except mt.ease.cubic_bezier, whose signature is listed with it.

FunctionCharacteristicsReturns
mt.ease.linear(t)Constant-speed easing that returns the input progress unchanged.Same value as the input t
mt.ease.in_quad(t)Quadratic easing that starts slowly and gradually accelerates.Progress calculated as t²
mt.ease.out_quad(t)Quadratic easing that starts quickly and gradually decelerates.Progress calculated as 1 - (1 - t)²
mt.ease.in_out_quad(t)Quadratic easing that accelerates in the first half and decelerates in the second.Progress that symmetrically combines in_quad and out_quad around t = 0.5
mt.ease.in_cubic(t)Cubic easing that starts slowly and gradually accelerates.Progress calculated as t³
mt.ease.out_cubic(t)Cubic easing that starts quickly and gradually decelerates.Progress calculated as 1 - (1 - t)³
mt.ease.in_out_cubic(t)Cubic easing that accelerates in the first half and decelerates in the second.Progress that symmetrically combines in_cubic and out_cubic around t = 0.5
mt.ease.in_quart(t)Quartic easing that starts slowly and accelerates more steeply than Cubic.Progress calculated as t⁴
mt.ease.out_quart(t)Quartic easing that starts quickly and decelerates more steeply than Cubic.Progress calculated as 1 - (1 - t)⁴
mt.ease.in_out_quart(t)Quartic easing that accelerates in the first half and decelerates in the second.Progress that symmetrically combines in_quart and out_quart around t = 0.5
mt.ease.in_sine(t)Sine-wave-derived easing that accelerates extremely gently.Progress calculated as 1 - cos(t × π / 2)
mt.ease.out_sine(t)Sine-wave-derived easing that decelerates extremely gently.Progress calculated as sin(t × π / 2)
mt.ease.in_out_sine(t)Sine-wave-derived easing that accelerates in the first half and decelerates in the second.Progress that symmetrically combines in_sine and out_sine around t = 0.5
mt.ease.in_circ(t)Circular-arc easing that accelerates sharply toward the endpoint.Progress calculated as 1 - √(1 - t²)
mt.ease.out_circ(t)Circular-arc easing that advances sharply immediately after the start and then decelerates.Progress calculated as √(1 - (1 - t)²)
mt.ease.in_out_circ(t)Circular-arc easing that accelerates in the first half and decelerates in the second.Progress that symmetrically combines in_circ and out_circ around t = 0.5
mt.ease.in_expo(t)Exponential easing that accelerates extremely strongly toward the endpoint.0 when t <= 0; otherwise progress calculated as 2^(10t - 10)
mt.ease.out_expo(t)Exponential easing that advances extremely strongly immediately after the start and then decelerates.1 when t >= 1; otherwise progress calculated as 1 - 2^(-10t)
mt.ease.in_out_expo(t)Exponential easing that accelerates extremely strongly in the first half and decelerates extremely strongly in the second.Progress that symmetrically combines in_expo and out_expo around t = 0.5
mt.ease.in_back(t)Easing that moves slightly backward in the negative direction immediately after the start, then accelerates toward the endpoint.Progress that overshoots below 0 near the start
mt.ease.out_back(t)Easing that passes slightly beyond the endpoint and then decelerates while returning.Progress that overshoots above 1 near the endpoint
mt.ease.in_out_back(t)Easing that overshoots near both the start and the endpoint.Progress that symmetrically combines in_back and out_back around t = 0.5
mt.ease.in_elastic(t)Easing that accelerates toward the endpoint while oscillating with increasing amplitude.Progress that oscillates outside the 0-1 range. Returns t unchanged when t <= 0 or t >= 1
mt.ease.out_elastic(t)Easing that decelerates and converges while oscillating around the endpoint.Progress that oscillates outside the 0-1 range. Returns t unchanged when t <= 0 or t >= 1
mt.ease.in_out_elastic(t)Easing that accelerates while oscillating in the first half, then decelerates and converges while oscillating in the second.Progress that symmetrically combines in_elastic and out_elastic around t = 0.5
mt.ease.in_bounce(t)Easing that bounces at the starting side before moving toward the endpoint.Time-reversed out_bounce progress
mt.ease.out_bounce(t)Easing that decelerates toward the endpoint with multiple bounces as if against a floor.Progress representing bounces with a piecewise quadratic function
mt.ease.in_out_bounce(t)Easing that bounces at both the starting and ending sides.Progress that symmetrically combines in_bounce and out_bounce around t = 0.5
mt.ease.cubic_bezier(x1, y1, x2, y2, t)Evaluates a cubic Bézier curve compatible with the CSS cubic-bezier() timing function. Use it to specify arbitrary easing numerically when the predefined curves above are insufficient.Progress on the specified Bézier curve

The three circ functions contain a square root, so normally constrain t to 0-1 before use.

mt.ease.cubic_bezier arguments ​

ArgumentTypeUnitDefaultDescription
x1 / y1number-RequiredFirst control point
x2 / y2number-RequiredSecond control point
tnumber-RequiredProgress. Fixed at 0 when t <= 0 and at 1 when t >= 1

When using it as a CSS-compatible monotonic timing curve, keep x1 and x2 within 0-1.

Example ​

lua
-- Same curve as CSS ease-in-out
local eased = mt.ease.cubic_bezier(0.42, 0.0, 0.58, 1.0, progress)

12. Reflow and Line-Based Grouping ​

mt.layout.reflow(ctx, gap?, config?) ​

API level: 2+

For horizontal writing, uses geometry.canvas_origin_*; for vertical writing, uses geometry.vertical_origin_*. It applies the current scale * stretch to the advance while preserving spacing derived from shaping, kerning, tracking, and margins based on differences between adjacent natural origins. Per-character rotation and pivot are also included in origin alignment. This function does not equalize the distance between visible ink outlines.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
gapnumberRatio of canvas width or height0.0Extra spacing added to adjacent advances. Positive values increase spacing in the direction of progression
configtable-nilSettings for reflow direction, targets, and fixed position

config ​

FieldTypeUnitDefaultDescription
modestring-Determined from ctx.global.verticalDirection to reflow: "horizontal" or "vertical"
targetstable-All charactersArray of 1-based character indices to reflow. If characters between targets are skipped, their advances are included in the calculation, but their coordinates are not changed
anchorinteger-First character in each line/columnCharacter index whose current transformed typesetting origin is fixed while characters are arranged on both sides

When targets is omitted, all characters are reflowed with each line_index treated as an independent line/column, and the first character of each is fixed. When config.anchor is specified, only the line containing that character uses it as the fixed character.

Return Value ​

There is no return value.

Changes ​

Rewrites offset_x and offset_y for the target characters. Call it after setting size, rotation, and pivot animations, as the final layout operation for the frame.

Constraints and Errors ​

  • Specify "horizontal" or "vertical" for mode.
  • Indices in targets must be within the valid range, strictly ascending, and free of duplicates.
  • All characters included in targets must belong to the same line_index.
  • When targets is specified, anchor must identify a character included in that array.
  • When targets is omitted, anchor must be a valid character index.

Example ​

lua
mt.layout.reflow(ctx, 0.02, {
    targets = {3, 4, 7, 8}, -- Include 5 and 6 in the advance, but do not move them
    anchor = 3,
})

mt.layout.group_by_line(ctx) ​

API level: 4+

Groups shaped characters by line_index and returns them in the host's reading order. The groups represent lines in horizontal writing and columns in vertical writing. It does not modify layout or character properties.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context

Return Value ​

TypeUnitDescription
table-Array containing one group per line_index, ordered by typeset line/column order

Each group has the following fields.

FieldTypeUnitDescription
line_indexinteger-Index of the line/column corresponding to the group
item_countintegeritemsNumber of characters in the group
itemstable-Array of characters in reading order

Example ​

lua
local columns = mt.layout.group_by_line(ctx)
for columnOrder, column in ipairs(columns) do
    for indexInColumn, character in ipairs(column.items) do
        local start = (columnOrder - 1) * 0.2 + (indexInColumn - 1) * 0.05
        character.opacity = mt.saturate((ctx.time - start) / 0.4)
    end
end

13. Canvas Position, Distance, and Pivot ​

mt.layout.place_2d(ctx, item, canvasX, canvasY) ​

API level: 2+

Precisely places the anchor of a character or part at a canvas normalized position before 3D, camera, projection, and Deform are applied. It composes and inverts the offset / pivot / rotation / scale / stretch of Global, the owning character, and the target itself using the same hierarchy and the same pixel coordinates for a non-square canvas as Native rendering.

  • Character anchor: natural ink bounds center geometry.bounds_center_x/y
  • Part anchor: part-local natural center (the local origin of the rendered path)
  • Shape anchor: the shape's position, the origin of its path (the canvas center until it is offset). Only the shape's own offset / pivot / rotation / scale / stretch count, because the Global does not reach a shape

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredTarget character, part or shape
canvasX / canvasYnumbercanvas normalized positionRequiredDestination coordinates

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementThe assigned offsetX
numbercanvas normalized displacementThe assigned offsetY

Changes ​

Directly updates the target's offset_x and offset_y.

Constraints and Errors ​

If scale * stretch is 0 anywhere in the transform hierarchy, the transform cannot be inverted and an error occurs.

Example ​

lua
mt.layout.place_2d(ctx, ctx.chars[1], 0.8, 0.8)
mt.layout.place_2d(ctx, ctx.parts[1], 0.5, 0.5)

mt.layout.get_canvas_position_2d(ctx, item) ​

API level: 3+

Gets the current Canvas coordinates (canvasX, canvasY) of a character or part anchor before 3D transforms, in the normalized Y-up coordinate system from 0.0 to 1.0. The function automatically determines whether item is a character (ctx.chars[i]) or a part (ctx.parts[j]).

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredTarget character or part

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized positionAnchor canvasX
numbercanvas normalized positionAnchor canvasY

Example ​

lua
-- Get the Canvas position of a character
local charX, charY = mt.layout.get_canvas_position_2d(ctx, ctx.chars[1])

-- Get the Canvas position of a part (detected automatically)
local partX, partY = mt.layout.get_canvas_position_2d(ctx, ctx.parts[1])

mt.layout.match_pose(ctx, destination, source, options?) ​

API level: 9+

Synchronizes the current pose of source to a destination of the same kind. It accepts two Characters, or two Parts owned by the same Character. It is available only in OnLayout. The application order is size, rotation, depth, opacity, then position.

OptionTypeDefaultDescription
positionbooleantrueSynchronize the pre-3D canvas position
position_delta_x / position_delta_ynumber0.0Untransformed-em displacements along the source-local X/Y axes
rotationbooleantrueSynchronize 2D rotation
rotation_offsetnumber0.0Degrees added after synchronization
sizestring"copy""none" / "copy" / "match_bounds"
size_scale_x / size_scale_ynumber1.0Positive per-axis multipliers
depthbooleantrueSynchronize z / yaw / pitch
z_offsetnumber0.0Canvas-normalized displacement added to Z after synchronization
yaw_offset / pitch_offsetnumber0.0Angles in degrees added after synchronization
opacitybooleanfalseSynchronize opacity
opacity_scalenumber1.0Non-negative opacity multiplier

match_bounds applies the ratio of the natural bounds widths and heights to stretch. When a source ink-bounds axis is zero, it uses the logical character cell: horizontal advance for X in horizontal writing, vertical advance for Y in vertical writing, and one em for a zero advance or the cross axis. Parts have no advance and use one em. A zero destination-bounds axis keeps its existing effective scale while the other axis and pose still synchronize. The destination pivot is not changed.

Position deltas use 1.0 as one untransformed em on both axes and do not use screen-fixed X/Y axes. For a Character they pass through the Global and source Character rotation, scale, and stretch. For a Part they additionally pass through the owning Character and source Part pose. Rotating or scaling the source therefore changes the delta direction and length. rotation_offset and size_scale adjust the destination and are not included in the delta transform.

lua
mt.layout.match_pose(ctx, ctx.chars[4], ctx.chars[1], {
    size = "match_bounds",
    size_scale_x = 1.15,
    size_scale_y = 1.2,
    position_delta_y = -0.75,
})

mt.layout.canvas_to_offset_2d(ctx, item, canvasX, canvasY) ​

API level: 3+

Calculates and returns the offset_x and offset_y required to place a character or part at the specified Canvas coordinates (canvasX, canvasY) before 3D transforms, without rewriting the properties themselves. The type is detected automatically from the character or part passed as item.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredTarget character or part
canvasX / canvasYnumbercanvas normalized positionRequiredDestination coordinates

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementRequired offsetX
numbercanvas normalized displacementRequired offsetY

The target's fields are not modified.

Constraints and Errors ​

An error occurs if scale * stretch is 0 in the parent hierarchy being inverted.

Example ​

lua
-- Invert the transform to obtain the offset needed to place a character at Target Canvas coordinates (0.5, 0.8)
local cox, coy = mt.layout.canvas_to_offset_2d(ctx, ctx.chars[1], 0.5, 0.8)

-- Invert the transform to obtain the offset needed to place a part at Target Canvas coordinates (0.2, 0.3) (detected automatically)
local pox, poy = mt.layout.canvas_to_offset_2d(ctx, ctx.parts[1], 0.2, 0.3)

mt.layout.radial_distance(ctx, canvasX, canvasY, centerX?, centerY?) ​

API level: 3+

Returns the distance from the center of a radial effect to the specified Canvas coordinates. If centerX / centerY are omitted, the center of the screen (0.5, 0.5) is used.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext containing canvas information
canvasX / canvasYnumbercanvas normalized positionRequiredCoordinates to measure
centerX / centerYnumbercanvas normalized position0.5Center of the effect

Return Value ​

TypeUnitDescription
numberRatio of canvas heightAspect-ratio-corrected distance from the center

IMPORTANT

The aspect ratio is corrected. In normalized Canvas coordinates, a physical length of 1.0 differs between the horizontal and vertical axes. If distance is calculated simply as sqrt(dx² + dy²), intended concentric circles become squashed into ellipses in a wide project such as 16:9. This function corrects the horizontal direction by multiplying it by ctx.canvas.aspect_ratio, so the returned distance is always based on true circles.

Example ​

lua
-- A ripple expanding from the center. The crest passes each character according to its distance
for index = 1, ctx.char_count do
    local character = ctx.chars[index]
    local geometry = character.geometry
    local radius = mt.layout.radial_distance(ctx, geometry.bounds_center_x, geometry.bounds_center_y)
    local crest = mt.falloff(radius - waveFront, 0.2)
    character.offset_y = character.offset_y + crest * 0.08
end

mt.layout.pivot_at_2d(ctx, item, anchor) ​

API level: 4+

Automatically determines whether the target is a character or part and sets its pivot to a semantic 2D position. It corrects offset_x/y by the difference between the local 2D matrices before and after the change, so the current pose before 3D application from rotation, scale, and stretch does not move. Global is not supported. Preserving the visual result after yaw, pitch, z, camera, or projection is outside its scope.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredTarget character or part
anchorstring-RequiredName of the semantic 2D position to set

Return Value ​

There is no return value.

Changes ​

Sets the target's pivot_x and pivot_y, then corrects offset_x and offset_y so the pose does not move.

anchor ​

Characters and parts share these anchors:

  • "center": center of the ink bounds
  • "top" / "bottom": horizontal center at the top/bottom edge of the ink bounds
  • "left" / "right": left/right edge at the vertical center of the ink bounds
  • "top_left" / "top_right" / "bottom_left" / "bottom_right": corresponding corner of the ink bounds

Characters additionally support these anchors:

  • "baseline_left" / "baseline" / "baseline_right": left edge/center/right edge of the ink bounds on the horizontal baseline
  • "typesetting_origin": character-cell left edge on the horizontal baseline
  • "vertical_start" / "vertical_center" / "vertical_end": vertical column axis at the character-cell start/center/end

Existing pivot values are not 0-1 ratios within the bounds. 0.5 is the natural center; X is displacement as a ratio of canvas width, and Y is displacement as a ratio of canvas height. This function converts the bounds dimensions, character typesetting origin, and differing natural centers of characters and parts into the existing pivot units.

Example ​

lua
mt.layout.pivot_at_2d(ctx, character, "top")
character.rotation = math.sin(ctx.time * 3.0) * 8.0

14. Bounds After 2D Transforms ​

mt.layout.measure_bounds_2d(ctx, targets?, targetType?, space?) ​

space (level 12+) defaults to "canvas". "before_global_rotation" removes only Global rotation around position + pivot while measuring; scale, stretch, individual transforms, and layout are preserved. Results remain Canvas-normalized Y-up coordinates. To draw in that frame, rotate the resulting geometry back around the same Global rotation center. Context and Inspector values are never modified.

API level: 2+

Transforms the natural bounding rectangles of the specified targets through the complete Global→character→part 2D hierarchy, then returns canvas-axis-aligned bounds enclosing all their vertices. When characters are specified, the natural bounds of their owned parts are combined; characters with no parts use the character's natural bounds. Call it after writing the target fields.

These bounds are before 3D, camera, projection, and Deform are applied. They do not include stroke or shadow overhang, nor path-tight bounds for outlines containing whitespace within their natural bounding rectangle. For this stage and definition, the result is not an approximation: it incorporates rotation, pivot, negative stretch, parent-character transforms, and Global transforms.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
targetstable | nil-AllArray of 1-based character or part indices. nil selects every target of the requested type
targetTypestring-"character"Target type: "character" or "part"
spacestring-"canvas""canvas" or "before_global_rotation"

Return Value (one of) ​

TypeUnitDescription
table-Bounds enclosing all valid targets
nil-If there are no valid targets

The bounds table has the following fields. All coordinate systems are Y-up.

FieldTypeUnitDescription
left / rightnumbercanvas normalized positionLeft and right edges
bottom / topnumbercanvas normalized positionBottom and top edges
center_x / center_ynumbercanvas normalized positionCenter of the bounds
width / heightnumbercanvas normalized displacementWidth and height of the bounds

Constraints and Errors ​

Selecting all targets by omitting targets is available. targetType = "character" selects every character, while "part" selects every part. An empty table {} does not select all; as before, it selects nothing and returns nil.

All-character selection includes characters with no parts and uses their natural character bounds. All-part selection includes only elements present in ctx.parts. Visibility, opacity, and write-on state do not affect target selection; this function measures the specified geometry.

Example ​

lua
local bounds = mt.layout.measure_bounds_2d(ctx, {1, 2, 3})
if bounds then
    mt.layout.place_2d(ctx, ctx.chars[4], bounds.center_x, bounds.top + 0.05)
end

local allCharacterBounds = mt.layout.measure_bounds_2d(ctx)
local allPartBounds = mt.layout.measure_bounds_2d(ctx, nil, "part")

15. Alignment on Paths ​

mt.layout.queue_on_path(ctx, path, options?) ​

API level: 3+

Arranges characters (or parts) on a path created by mt.path.arc_length in reading order, and returns the distance ratio (0-1) occupied by each element.

Spacing is derived from the origin spacing that the host actually typeset, using the same method as mt.layout.reflow. Parameter settings such as tracking, per-character margins, and kerning are therefore reflected directly in the character advance along the path. options.gap is additional spacing added on top of that, so when gap is 0, elements are arranged according to the host's character advance.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
pathtable-RequiredReturn value of mt.path.arc_length
optionstable-nilPlacement settings
options.alignstring-"end"Position at which to place the string. Specify "end", "start", or "center"
options.gapnumberRatio of canvas width0.0Additional spacing added to the host's character advance
options.modestring-"horizontal"Host typesetting direction: "horizontal" or "vertical"
options.target_typestring-"character"Target type: "character" or "part"
options.targetstable-All elementsArray of target indices in reading order

Return Value ​

TypeUnitDescription
table0-1slots array indexed like ctx.chars or ctx.parts, providing the distance ratio occupied by each element on the path

slots represents positions on the path, not animation progress.

Direction of Travel and Reading Order ​

Slots are assigned in ascending distance along the direction of travel. The reference is the path's direction, not left and right on the screen. If targets is omitted, slots are assigned by ascending index; therefore, if the path travels from left to right, this directly produces screen reading order.

IMPORTANT

On a path traveling from right to left, ascending distance corresponds to decreasing x. Therefore, when targets is omitted, character 1 is placed farthest right and character N farthest left, making the string appear reversed on screen. To produce a left-to-right-readable arrangement, pass targets in reverse reading order.

lua
-- Preserve screen reading order on a path traveling from right to left
local targets = {}
for index = ctx.char_count, 1, -1 do
    targets[#targets + 1] = index
end
local slots = mt.layout.queue_on_path(ctx, path, { targets = targets })

This is a tradeoff: the following two properties cannot both be satisfied on a reversed path. Choose according to the use case.

targetsOn-Screen ArrangementFront of the Queue
Omitted (ascending index)Reversed (read right→left)Character 1
Reverse reading orderCorrect (read left→right)Last character

align is likewise relative to the direction of travel. "end" places the end of the string at the end of the path, so on a path traveling right to left, the end of the string appears on the left side of the screen.

Path DirectionPosition of the String End with align = "end"
Left→rightRight side of the screen
Right→leftLeft side of the screen

Example ​

lua
-- Use case 1: Arrange text along a path (stationary)
local path = mt.path.arc_length(kPathPoints, ctx.canvas.aspect_ratio)
local slots = mt.layout.queue_on_path(ctx, path, { gap = 0.004 })
for index = 1, ctx.char_count do
    local x, y, heading = path:at_distance(slots[index])
    ctx.chars[index].rotation = heading
    mt.layout.place_2d(ctx, ctx.chars[index], x, y)
end
lua
-- Use case 2: Travel along the path and stop at each character's own slot (the queue brakes)
local path = mt.path.arc_length(kPathPoints, ctx.canvas.aspect_ratio)
local slots = mt.layout.queue_on_path(ctx, path, { align = "end" })
local progress = mt.ease.out_cubic(mt.saturate(ctx.time / 3.2))
for index = 1, ctx.char_count do
    -- math.min implements the queue's stopping behavior itself: the leader reaches the end, while each follower stops at its own position
    local x, y, heading = path:at_distance(math.min(progress, slots[index]))
    ctx.chars[index].rotation = heading
    mt.layout.place_2d(ctx, ctx.chars[index], x, y)
end

NOTE

If the string's total length exceeds the path length, distance ratios are clamped to 0 and 1, causing characters to overlap. When placing long text on a short path, use path:length() to confirm that it fits.

16. Motion Paths ​

These curve-evaluation APIs represent motion along control points, such as "flowing in" or "landing along an arc."

The coordinate system is not fixed. Points are plain { x, y } tables. The functions accept whichever coordinate system the caller passes, such as normalized offset coordinates, canvas pixels, or character-local coordinates, and return values in the same coordinate system.

NOTE

When assigning returned values directly to offset_x / offset_y, specify the points in offset units as well (the canvas normalized displacement whose neutral value is 0.5). In the example below, the landing point is { x = 0.5, y = 0.5 } (zero displacement), so character.offset_x = x lands directly at the natural position. If absolute canvas coordinates or zero-based local coordinates are assigned to offset unchanged, the character will move to an unintended position.

Every function returns four values: x, y, tangentX, tangentY. tangentX / tangentY are raw, unnormalized derivatives that also include travel speed. To obtain the direction angle, use math.deg(math.atan(tangentY, tangentX)); to obtain a unit vector, divide by local length = math.sqrt(tangentX^2 + tangentY^2).

mt.path.bezier(p0, p1, p2, p3, t) ​

API level: 1+

Returns the coordinates and tangent on a cubic Bézier curve defined by four points.

Arguments ​

ArgumentTypeUnitDefaultDescription
p0tablecoordinatesRequiredStarting point { x, y }
p1tablecoordinatesRequiredControl point near the start { x, y }
p2tablecoordinatesRequiredControl point near the end { x, y }
p3tablecoordinatesRequiredEnd point { x, y }
tnumber0-1RequiredPosition on the curve. Values outside the range are not clamped and are extrapolated unchanged

Return Value (multiple) ​

TypeUnitDescription
numberAs defined by the input coordinatesx on the curve
numberAs defined by the input coordinatesy on the curve
numberAs defined by the input coordinatesTangent tangentX
numberAs defined by the input coordinatesTangent tangentY

Example ​

lua
-- Make the character follow an arc and land at its own natural position
local p0 = { x = 0.5, y = 0.2 }
local p1 = { x = 0.3, y = 0.6 }
local p2 = { x = 0.5, y = 0.5 }
local p3 = { x = 0.5, y = 0.5 }
local progress = mt.saturate(mt.stagger(ctx.time, index, 0.05, 0.6))
local x, y = mt.path.bezier(p0, p1, p2, p3, mt.ease.out_cubic(progress))
character.offset_x = x
character.offset_y = y

mt.path.catmull_rom(points, t) ​

API level: 1+

Returns the coordinates and tangent on a Catmull-Rom spline passing through any number of control points. Unlike a Bézier curve, this curve passes exactly through every point in the list.

Arguments ​

ArgumentTypeUnitDefaultDescription
pointstablecoordinatesRequired1-based array containing at least two { x, y } points through which the curve passes
tnumber0-1RequiredProgress across the entire curve. Clamped to 0-1

Return Value (multiple) ​

TypeUnitDescription
numberAs defined by the input coordinatesx on the curve
numberAs defined by the input coordinatesy on the curve
numberAs defined by the input coordinatesTangent tangentX
numberAs defined by the input coordinatesTangent tangentY

Constraints and Errors ​

points must contain at least two points.

Example ​

lua
-- Flow the character along a trajectory passing through three predetermined points
local waypoints = {
    { x = 0.1, y = 0.8 },
    { x = 0.5, y = 0.3 },
    { x = 0.9, y = 0.8 },
}
local progress = mt.saturate(ctx.time / 1.5)
local x, y = mt.path.catmull_rom(waypoints, progress)
character.offset_x = x
character.offset_y = y

mt.path.arc_length(points, aspectRatio?, options?) ​

API level: 3+

Constructs an arc-length-parameterized path from control points. The two functions above evaluate using curve parameter t, but equal intervals of t are not equal distances. Samples bunch together where the curve bends, so uses that require spacing by distance, such as queues, character advance along a path, or constant-speed motion, require a cumulative-length table. This function constructs that table once and provides distance-based lookup.

Passing ctx.canvas.aspect_ratio as aspectRatio multiplies X differences by it to equalize the meaning of distance on the horizontal and vertical axes. If omitted, distance is measured directly in normalized units, causing spacing to expand or contract on a non-square canvas.

Arguments ​

ArgumentTypeUnitDefaultDescription
pointstablecoordinatesRequired1-based control-point array. Specifying canvas normalized coordinates allows use with mt.layout.place_2d and mt.layout.queue_on_path
aspectRationumber-1.0Aspect ratio multiplied into X-axis distances. Normally pass ctx.canvas.aspect_ratio
optionstable-nilSettings specifying the curve type
options.kindstring-"catmull_rom""catmull_rom" or "bezier"

Return Value ​

TypeUnitDescription
table-Path object providing distance-based lookup

The path object has the following methods.

MethodReturn ValueUnitDescription
path:length()numberAs defined by the input coordinatesTotal path length corrected for aspect ratio
path:at_distance(distanceRatio)Three number valuesAs defined by the input coordinates, degreesx, y, and headingDegrees at distance ratio 0-1

Constraints and Errors ​

  • points must contain at least two points.
  • With options.kind = "bezier", points must contain exactly four points.
  • Specify "catmull_rom" or "bezier" for options.kind.

NOTE

The third value returned by at_distance is a direction in degrees, already converted to the screen coordinate system (where canvas Y points downward). It can be assigned directly to rotation. Unlike the raw tangents returned by mt.path.bezier / catmull_rom, you do not need to reverse the sign yourself.

The direction follows the direction of travel. On a path traveling from right to left, it is approximately 180 degrees, so assigning it directly to rotation turns characters upside down. Add 180.0 to keep them upright while preserving the slope.

lua
-- Keep characters upright on a path traveling from right to left
character.rotation = heading + 180.0

Example ​

lua
-- Place at equal distance intervals (equal intervals of t bunch up around bends)
local path = mt.path.arc_length(kWaypoints, ctx.canvas.aspect_ratio)
for index = 1, ctx.char_count do
    local ratio = mt.distribute(index, ctx.char_count)
    local x, y, heading = path:at_distance(ratio)
    ctx.chars[index].rotation = heading
    mt.layout.place_2d(ctx, ctx.chars[index], x, y)
end

17. Path Creation and Editing ​

mt.svg_path(source, optionsOrSourceUnitsPerEm?) ​

API level: 5+

Converts a string in the same format as an SVG d attribute into a reusable, read-only path template. As a simplified SVG-compatible implementation, it supports absolute and relative M / L / H / V / Q / T / C / S / Z commands and consecutive coordinate sets. Elliptical arcs A / a are not supported.

Arguments ​

ArgumentTypeUnitDefaultDescription
sourcestring-RequiredSVG path data
optionsOrSourceUnitsPerEmnumber / table-nilNumber of source-data units per em, or { view_box, em_scale }

Return Value ​

TypeUnitDescription
MtDrawingPath-Read-only path template that can be passed to path:assign(template)

Normalization Settings ​

When a number is specified, it is treated as the number of source-data units per em and normalized to 1000 units (for example, use mt.svg_path(source, 24) to treat 24 units as 1 em).

When view_box = { minX, minY, width, height } is specified, the viewBox center is moved to the origin, and the longest side is scaled to em_scale em while preserving the aspect ratio. The default em_scale is 1.0.

Constraints and Errors ​

  • A numeric units-per-em value must be a finite positive number.
  • view_box must contain four finite numbers, and its width and height must be positive.
  • em_scale must be a finite positive number.
  • units_per_em cannot be specified in the configuration table. Use the numeric form or em_scale.

Example ​

lua
local icon = mt.svg_path(svgPathFrom24PxIcon, {
    view_box = { 0, 0, 24, 24 },
    em_scale = 0.7,
})

function OnPath(ctx)
    for _, part in ipairs(ctx.paths:select("c1, p8-10, !p9")) do
        part.path:assign(icon)
    end
end

IMPORTANT

em_scale is the rendered size, expressed as a multiple of 1 em. With em_scale = 0.5, the result is half-sized. In many fonts, the actual glyph face is smaller than 1 em, so the default 1.0 can cause an icon to overlap neighboring characters. In that case, lower em_scale or increase the character advance in OnLayout.

Notes ​

For a static SVG string, convert it with mt.svg_path outside the function and retain it in a local variable to avoid reparsing it every frame.


mt.drawing_path() ​

API level: 9+

Creates an empty mutable MtDrawingPath. It is available at top level and in every hook. Coordinates use Y-down part-local space where 1000 units equal 1 em. Build a shape with the existing construction methods and pass it to APIs such as ctx.glyphs:declare({ path = path }).

lua
local diamond = mt.drawing_path()
diamond:move_to(0, -500)
diamond:line_to(500, 0)
diamond:line_to(0, 500)
diamond:line_to(-500, 0)
diamond:close()

Drawing Path Editing API ​

API level: 5+

The path of a part obtained through ctx.paths can be edited with the following methods.

MethodDescription
path:clear()Delete every command in the path
path:assign(template)Replace with a template created by mt.svg_path
path:set_svg(source)Parse SVG path data and replace the path
path:move_to(x, y)Add the starting point of a subpath
path:line_to(x, y)Add a line
path:quad_to(cx, cy, x, y)Add a quadratic Bézier curve
path:cubic_to(cx1, cy1, cx2, cy2, x, y)Add a cubic Bézier curve
path:close()Close the current subpath
path:bounds()Get tight bounds including curve extrema
path:map_points(callback)Transform every point without changing topology

Coordinate System ​

Path coordinates use normalized Y-down local coordinates with the part center as the origin, where 1000 units equal 1 em. They are converted automatically to the actual font size at the API boundary and therefore do not depend on Canvas resolution.

Applying Changes ​

Because ctx.paths is reconstructed every frame, apply path changes every frame. Parts to which changes are not applied revert to the original glyph outline.


path:clear() ​

API level: 5+

Deletes all commands in the path.

Return Value ​

There is no return value.

Changes ​

Empties the target path and resets its coordinate-normalization settings to their initial values.


path:assign(template) ​

API level: 5+

Replaces the path with the contents of a template created by mt.svg_path.

Arguments ​

ArgumentTypeUnitDefaultDescription
templateMtDrawingPath-RequiredSource path template to copy

Return Value ​

There is no return value.

Changes ​

Replaces the target's path data and coordinate-normalization settings with the template contents.


path:set_svg(source) ​

API level: 5+

Parses SVG path data and replaces the target path.

Arguments ​

ArgumentTypeUnitDefaultDescription
sourcestring-RequiredPath data in the same format as an SVG d attribute

Return Value ​

There is no return value.

Changes ​

Replaces the target path with the specified SVG path and resets its coordinate-normalization settings to their initial values.


path:move_to(x, y) ​

API level: 5+

Adds a new subpath beginning at the specified coordinates.

Arguments ​

ArgumentTypeUnitDefaultDescription
x / ynumberPath-local coordinatesRequiredStarting point of the subpath

Return Value ​

There is no return value.

Changes ​

Adds a move command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


path:line_to(x, y) ​

API level: 5+

Adds a straight line from the current position to the specified coordinates.

Arguments ​

ArgumentTypeUnitDefaultDescription
x / ynumberPath-local coordinatesRequiredEnd point of the line

Return Value ​

There is no return value.

Changes ​

Adds a line command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


path:quad_to(cx, cy, x, y) ​

API level: 5+

Adds a quadratic Bézier curve from the current position to the specified end point.

Arguments ​

ArgumentTypeUnitDefaultDescription
cx / cynumberPath-local coordinatesRequiredControl point
x / ynumberPath-local coordinatesRequiredEnd point of the curve

Return Value ​

There is no return value.

Changes ​

Adds a quadratic Bézier command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


path:cubic_to(cx1, cy1, cx2, cy2, x, y) ​

API level: 5+

Adds a cubic Bézier curve from the current position to the specified end point.

Arguments ​

ArgumentTypeUnitDefaultDescription
cx1 / cy1numberPath-local coordinatesRequiredControl point near the current position
cx2 / cy2numberPath-local coordinatesRequiredControl point near the end point
x / ynumberPath-local coordinatesRequiredEnd point of the curve

Return Value ​

There is no return value.

Changes ​

Adds a cubic Bézier command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


path:close() ​

API level: 5+

Closes the current subpath by connecting it to its starting point.

Return Value ​

There is no return value.

Changes ​

Adds a close command to the target path.


path:bounds() ​

API level: 9+

Returns tight bounds including curve extrema as four values: minimumX, minimumY, maximumX, maximumY. An empty path returns one nil. Values use the path's Y-down space where 1000 units equal 1 em. It works wherever the path can be referenced, on both read-only templates and mutable paths.


path:map_points(callback) ​

API level: 9+

Replaces anchors and control points in command order with the two coordinates returned by callback(x, y, point). Command kinds and counts, contours, and close state are preserved. The candidate is committed only after every callback succeeds. A standalone mutable path can use it wherever that path is retained; a ctx.paths target can use it in OnPath.

point is a reused read-only view with index, command_index, contour_index, role (anchor / control1 / control2), and command (move / line / quad / cubic). Limits are 4096 points per path and 16384 points per script execution. Re-entering or modifying the same receiver inside its callback, or returning non-finite coordinates, raises an error and leaves the path unchanged.

lua
path:map_points(function(x, y, point)
    return x, y + math.sin(point.index * 1.7 + ctx.progress * math.pi * 2) * 25
end)

18. UTF-8 Text Processing ​

mt.text.slice(text, startChar, endChar?) ​

API level: 1+

Slices a string at UTF-8 code-point boundaries. Unlike Lua's standard byte-based string.sub, it does not cut through an individual UTF-8 code point. Because it does not operate on grapheme clusters, it can split combining characters or ZWJ-joined emoji in the middle of what appears to be one character. ctx.chars[index].text is a cluster after shaping and does not use the same index concept as this function.

Arguments ​

ArgumentTypeUnitDefaultDescription
textstring-RequiredTarget string
startCharinteger-RequiredStarting code-point index. 1-based and inclusive
endCharinteger-EndEnding code-point index. 1-based and inclusive

Return Value ​

TypeUnitDescription
string-String sliced to the specified range. An empty string if the range is empty or invalid

mt.text.classify(text) ​

API level: 4+

Classifies the first Unicode code point of a text cluster into a character-category name convenient for animation.

Arguments ​

ArgumentTypeUnitDefaultDescription
textstring-RequiredText cluster to classify

Return Value ​

TypeUnitDescription
string-One of "kanji", "hiragana", "katakana", "punctuation", "latin", "digit", "space", or "other"

Example ​

lua
for _, character in ipairs(ctx.chars) do
    if mt.text.classify(character.text) == "kanji" then
        character.scale = 1.2
    end
end

Notes ​

This is lightweight classification based on the first code point, not language detection across the entire grapheme cluster. Empty strings, emoji, combining marks, and anything else outside the defined ranges are classified as "other".

19. Character and Part Selection ​

mt.target.chars(ctx, specification, options?) ​

API level: 13+

Resolves a selection string to character numbers. The string uses the same grammar as the character and part target settings of the host, so a script can offer the same notation as an input.

  • Tokens are separated by commas or whitespace.
  • c3 is character 3, p2 is global part 2, and a bare number is a character in this function.
  • a-b is an inclusive range. c1-c3, p2-p5 and 2-4 are all valid. A bare end takes the kind of the other end, a reversed range is swapped, and a range whose ends have different kinds is ignored.
  • A leading ! excludes. An exclusion wins over every include, whatever the order.
  • Numbers outside the text, malformed tokens and 0 are ignored.

A character is listed when at least one of its parts is selected, so p5 lists the character that owns part 5.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout or OnPath context
specificationstring-RequiredSelection string
optionstable-nil{ empty = "none" | "all" }. What a string that selects nothing resolves to. "none" (default) selects nothing and "all" selects every character. Exclusions still apply to "all"

Return Value ​

TypeUnitDescription
integer[]-Ascending character numbers (the ctx.chars indices), ready for ipairs

Example ​

lua
function OnLayout(ctx)
    for _, characterNumber in ipairs(mt.target.chars(ctx, "1-3, 6, !c2")) do
        ctx.chars[characterNumber].scale = 1.2
    end
end

mt.target.parts(ctx, specification, options?) ​

API level: 13+

Resolves a selection string to global part numbers. The grammar is the one described for mt.target.chars, except that a bare number is a part. A character selector such as c3 selects all of that character's parts.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout or OnPath context
specificationstring-RequiredSelection string
optionstable-nil{ empty = "none" | "all" }. What a string that selects nothing resolves to. "none" (default) selects nothing and "all" selects every part. Exclusions still apply to "all"

Return Value ​

TypeUnitDescription
integer[]-Ascending global part numbers (the ctx.parts indices), ready for ipairs

Example ​

lua
function OnLayout(ctx)
    for _, partNumber in ipairs(mt.target.parts(ctx, "c1 p9")) do
        ctx.parts[partNumber].opacity = 0.5
    end
end

20. Clip Time ​

mt.timeline.progress(ctx, fallbackDuration?) ​

API level: 1+

Gets the current progress from 0 to 1.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext
fallbackDurationnumberseconds4.0Virtual clip length used for looping when the timeline is unavailable

Return Value ​

TypeUnitDescription
number0-1Current progress

Constraints and Errors ​

When the timeline is unavailable, specify a positive value for fallbackDuration.


mt.timeline.duration(ctx, fallbackDuration?) ​

API level: 6+

Returns a positive host clip duration in seconds. When the host timeline is unavailable, or its duration in seconds is not finite and positive, it returns the requested fallback duration.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext
fallbackDurationnumberseconds4.0Virtual clip length used when the timeline is unavailable

Return Value ​

TypeUnitDescription
numbersecondsHost clip duration or fallback duration

Constraints and Errors ​

An explicit fallbackDuration must be a finite positive number. When passed a derived context returned by window_ctx, this function returns the duration of that local time window.

lua
local clipDuration =
    mt.timeline.duration(ctx, kFallbackClipLength)

mt.timeline.remaining(ctx, fallbackDuration?) ​

API level: 1+

Returns the actual number of seconds remaining until the end of the clip. This is a basic building block for writing an outro (exit animation) in real time anchored to the endpoint (see the design rules for ctx.timeline).

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext
fallbackDurationnumberseconds4.0Virtual clip length used for looping when the timeline is unavailable

Return Value ​

TypeUnitDescription
numbersecondsTime remaining until the end of the clip

Example ​

lua
-- Fade out during the final 0.5 seconds, regardless of clip length
local fade = mt.clamp(mt.timeline.remaining(ctx) / 0.5, 0.0, 1.0)

mt.timeline.intro_outro_seconds(ctx, introSeconds, outroSeconds, fallbackDuration?) ​

API level: 1+

Returns progress for an intro anchored to the beginning of the clip and an outro anchored to the end, at a fixed speed over the specified real number of seconds. Changing the clip length does not change the transition speed. Only when the clip is shorter than introSeconds + outroSeconds are both intervals compressed proportionally to fit within the clip (automatic fast-forwarding).

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext
introSecondsnumbersecondsRequiredIntro length. Negative values are treated as 0
outroSecondsnumbersecondsRequiredOutro length. Negative values are treated as 0
fallbackDurationnumberseconds4.0Virtual clip length used when the timeline is unavailable

Return Value (multiple) ​

TypeUnitDescription
number0-1introProgress. Intro progress
number0-1outroProgress. Outro progress

Example ​

lua
local intro, outro = mt.timeline.intro_outro_seconds(ctx, 0.8, 0.5)
character.offset_y = 0.5 + (1.0 - mt.ease.out_cubic(intro)) * 0.2
character.scale = character.scale * (1.0 - mt.ease.in_cubic(outro))

mt.timeline.window_progress(ctx, start, duration) ​

API level: 6+

Converts a time window in the supplied context to linear progress from 0 to 1. It returns exactly 0.0 before start and exactly 1.0 at or after start + duration. Within the window, it returns (ctx.time - start) / duration.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext from which time is read
startnumbersecondsRequiredStart time within the context
durationnumbersecondsRequiredLength of the time window

Return Value ​

TypeUnitDescription
number0-1Linear progress through the window, clamped before and after it

Constraints and Errors ​

  • ctx.time must be a number.
  • start must be a finite number.
  • duration must be a finite positive number.

Example ​

lua
local appear =
    mt.timeline.window_progress(ctx, 0.5, 0.8)
local disappear =
    mt.timeline.window_progress(ctx, 4.0, 0.5)

character.opacity = appear * (1.0 - disappear)

Notes ​

Use this when only progress is needed. Use mt.timeline.window_ctx when time and frame must also be localized before passing the context to time-dependent APIs.


mt.timeline.window_ctx(ctx, start, duration) ​

API level: 4+

Creates a derived ctx for a specified time window from the original ctx. start is the starting time in seconds within the original ctx, and duration is a positive window length. The derived ctx's time is ctx.time - start clamped to 0-duration; frame, timeline.duration_*, and timeline.progress are also converted to the same local time basis. All other values, including canvas, Global, chars, and parts, are inherited from the original ctx.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOriginal context
startnumbersecondsRequiredStart time within the original context
durationnumbersecondsRequiredLength of the time window

Return Value ​

TypeUnitDescription
table-Derived context whose time-related values are remapped to the specified window

Constraints and Errors ​

  • ctx.time must be a number.
  • start must be a finite number.
  • duration must be a finite positive number.

Example ​

lua
local characterCtx =
    mt.timeline.window_ctx(ctx, (index - 1) * 0.08, 0.5)

local bounce = mt.bounce_y({ t = characterCtx.time })
local opacity = mt.ease.out_cubic(characterCtx.timeline.progress)

Notes ​

This API does not copy the original ctx or shared characters and parts. It is a shallow derived context that replaces only time-related values.


mt.timeline.chain(ctx, initialValue, segments, options?) ​

API level: 4+

Evaluates pure functions for time intervals in sequence and passes the final return value of each completed interval to the next interval. An evaluate before the current time is evaluated with that interval's end context, while only the current interval is evaluated with its interval-local ctx. The return value may be any Lua value, including a number, color, or table combining coordinates and transforms.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOriginal context
initialValueAny-RequiredInitial value passed to the first interval
segmentstable-RequiredArray of animation or hold intervals
optionstable-nilSettings used when the timeline is unavailable
options.fallback_durationnumberseconds4.0Virtual clip length

Return Value ​

TypeUnitDescription
Any-Value of the last interval evaluated through the current time

Example ​

lua
local result = mt.timeline.chain(ctx, initialValue, {
    {
        duration = 1.0,
        evaluate = function(segmentCtx, previousValue)
            return enterAnimation(segmentCtx, previousValue)
        end,
    },
    { hold = 0.5 },
    {
        duration = 1.2,
        evaluate = function(segmentCtx, previousValue)
            return bounceAnimation(segmentCtx, previousValue)
        end,
    },
})

Interval Context ​

segmentCtx inherits canvas, Global, chars, parts, and other values from the caller's ctx, replacing only time-related values with those of the current interval. It can be passed like a normal ctx to existing Level 3 APIs that read ctx, such as mt.timeline.progress.

FieldTypeUnitDescription
segmentCtx.timenumbersecondsTime since the interval began
segmentCtx.framenumberframeNumber of frames since the interval began
segmentCtx.fpsnumberfpsSame frame rate as the caller
segmentCtx.timeline.duration_secondsnumbersecondsLogical duration of the interval
segmentCtx.timeline.duration_framesnumberframeLogical duration of the interval converted to frames
segmentCtx.timeline.progressnumber0-1Progress within the interval

Interval Forms ​

Each interval uses one of the following forms.

IntervalSpecificationBehavior
Fixed-length animation{ duration = seconds, evaluate = function }Uses the return value of evaluate(segmentCtx, previousValue)
Fixed-length hold{ hold = seconds }Retains the preceding value without calling a function
Variable-length animation{ duration = "remaining", evaluate = function }Uses the remaining clip duration after fixed intervals are excluded
Variable-length hold{ hold = "remaining" }Retains the preceding value for the remaining clip duration

"remaining" can be specified for only one interval within a chain. If a fixed-length outro follows it, only the variable interval absorbs changes in clip length, while the outro always completes at the end of the clip.

lua
local state = mt.timeline.chain(ctx, initialState, {
    { duration = 1.0, evaluate = evaluateIntro },
    { duration = 1.2, evaluate = evaluateBounce },
    { duration = "remaining", evaluate = evaluateIdle },
    { duration = 0.6, evaluate = evaluateOutro },
}, {
    fallback_duration = 6.0,
})

If the total length of fixed intervals exceeds the clip length, all fixed intervals are compressed proportionally. Because segmentCtx.time and segmentCtx.timeline are remapped to the declared duration, every animation reaches its ending state even in a short clip. When the timeline is unavailable, options.fallback_duration (default 4.0 seconds) is used as a virtual looping clip length.

Constraints and Errors ​

  • ctx.time must be a number, and segments and options must be tables.
  • Each interval must specify exactly one of duration or hold.
  • A duration interval requires an evaluate function, which cannot be specified for a hold interval.
  • An interval length must be a finite number greater than or equal to 0, or "remaining".
  • Only one interval may specify "remaining".
  • options.fallback_duration must be a finite positive number.

IMPORTANT

evaluate is reevaluated within the same frame to obtain the ending values of past intervals. Write it as a pure function that returns a value calculated from its arguments without directly modifying ctx or a character, and apply only the final result. Combine value-returning APIs such as mt.bounce_y with segmentCtx.time, rather than using APIs such as mt.bounce_ground that modify an item directly.

21. Deprecated Compatibility APIs ​

mt.storage Deprecated ​

API level: 1+

mt.storage is retained for compatibility with existing scripts. In new scripts, store immutable values in local variables declared outside functions.

TypeDescription
tableRead-only compatibility storage

mt.polar_offset(angleDegrees, radius) Deprecated ​

API level: 3+

Legacy polar-coordinate conversion that uses the angle system's Y direction.

Arguments ​

ArgumentTypeUnitDefaultDescription
angleDegreesnumberdegreesRequiredDirection. 0 degrees points right, and the positive direction is clockwise
radiusnumberdisplacementRequiredDistance in that direction

Return Value (multiple) ​

TypeUnitDescription
numberAs defined by radiusoffsetX
numberAs defined by radiusscreenOffsetY. Positive downward in screen coordinates

WARNING

Deprecated. Use mt.polar_offset_2d. The arguments are the same as for mt.polar_offset_2d, but because the return value uses the angle coordinate system (positive Y points down), its sign had to be reversed before passing it to offset_y. Forgetting that reversal easily caused only the vertical motion to be inverted, so it has been replaced by mt.polar_offset_2d, which returns values in the position coordinate system. The behavior has not changed, so existing scripts continue to work unchanged.

Example ​

lua
-- Old: the caller must reverse the sign
local dx, dy = mt.polar_offset(angle, radius)
character.offset_y = 0.5 - dy

-- New: can be added directly
local dx, dy = mt.polar_offset_2d(angle, radius)
character.offset_y = 0.5 + dy

mt.layout.retypeset(ctx, gap?, config?) Deprecated ​

API level: 1+

CAUTION

This is an API Level 1 compatibility function. Use mt.layout.reflow in new code.

Recalculates character advance in a simplified manner using geometry.advance_x/y, while accounting for each character's individual scale and stretch. It does not equalize gaps between visible ink bounds. Call it after applying per-character size animation, as the final retypesetting operation for the frame. Rotation, individual pivots, tracking, margins, and transformed outlines are not included in advance calculations.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredContext
gapnumberRatio of canvas width or height0.0Extra spacing added between advances
configtable-nilRealignment settings
config.alignstring-"baseline"Cross-axis alignment in horizontal writing: "baseline", "top", "center", or "bottom"
config.modestring-"horizontal""horizontal" or "vertical"
config.targetstable-All charactersArray of 1-based character indices to realign. Characters outside the target set are not changed
config.anchorinteger-Smallest index in targetsCharacter index whose current position on the advance axis is fixed
config.baselinestring-"anchor""anchor" takes the reference line from the current pose; "natural" takes it from the natural pose

With config.baseline = "anchor", changing the anchor character's scale each frame moves the entire line vertically along with the reference line. "natural" fixes the cross axis to the natural position and is suitable when used with a time-varying scale. "anchor" uses the current pose after scale and offset have been applied, while "natural" uses the pose from the Base layout with scale 1 and no offset. The anchor position on the advance axis remains at its current position in either case. config.align is not used by simplified vertical alignment.

Return Value ​

There is no return value.

Changes ​

Directly rewrites offset_x and offset_y for the target characters.

WARNING

When only some characters are targeted with targets, gap can cause the target range to collide with adjacent non-target characters.gap is added to spacing between characters within the target range on every call, expanding the width of the target range as a whole. Because this function never changes coordinates for characters not included in targets (that is, it does not automatically make room based on non-target character positions), if an edge of the target range (the first or last character) was originally adjacent to a non-target character, each added gap expands the range toward and eventually overlaps that non-target character. This is not a problem when all characters are targeted or when sufficient space already exists before and after the target range. When targeting only some characters whose boundary is adjacent to another character, set gap to 0 or manually provide spacing outside the target range with tracking / margins.

Example ​

lua
-- Align only characters at indices 3-6, fixing the position of character 3 as the reference and arranging characters on both sides
mt.layout.retypeset(ctx, 0.02, {
    targets = {3, 4, 5, 6},
    anchor = 3,
    align = "baseline"
})

mt.layout.canvas_to_offset(resolvedGlobal, canvasWidth, canvasHeight, naturalCenterX, naturalCenterY, canvasX, canvasY) Deprecated ​

API level: 1+

CAUTION

This is an API Level 1 compatibility function. It inverts only the 2D Global transform. Use mt.layout.place_2d in new code.

Use this function to calculate the offset that moves the natural center of a character or part to a target canvas position while accounting for the 2D Global transform. offset_x / offset_y represent displacement from the item's own natural position and are in the coordinate system before the overall ctx.global transform (position / rotation / scale / stretch / pivot). Therefore, assigning target canvas coordinates directly, such as offset_x = canvasX, produces the intended screen position only while ctx.global has its initial values. If the whole composition is rotated, moved, or enlarged, the actual position shifts by that transform. This function cancels the 2D transform of ctx.global, then calculates the offset_x / offset_y corresponding to the target canvas position.

Arguments ​

ArgumentTypeUnitDefaultDescription
resolvedGlobaltable-Requiredctx.global resolved in OnPreLayout
canvasWidth / canvasHeightnumberpxRequiredctx.canvas.width and ctx.canvas.height
naturalCenterX / naturalCenterYnumbercanvas normalized positionRequiredNatural center of the target. For characters, geometry.bounds_center_x/y; for parts, geometry.canvas_center_x/y
canvasX / canvasYnumbercanvas normalized positionRequiredDestination coordinates

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementoffsetX
numbercanvas normalized displacementoffsetY

Constraints and Errors ​

This function inverts only position_x/y, rotation, scale, stretch_x/y, and pivot_x/y. It does not invert individual character/part rotation or pivot, the hierarchical transform from an owning character to a part, Global or individual 3D values, camera, projection, or Deform. Width, height, and global.scale * stretch_x/y must be nonzero.

Example ​

lua
-- Even if 2D ctx.global.rotation / position_x and similar values are changed in OnPreLayout,
-- the natural center reaches (0.8, 0.3) on the canvas when there are no individual/3D/Deform transforms
local geometry = character.geometry
character.offset_x, character.offset_y = mt.layout.canvas_to_offset(
    ctx.global, ctx.canvas.width, ctx.canvas.height,
    geometry.bounds_center_x, geometry.bounds_center_y,
    0.8, 0.3
)

Notes ​

For a part, the part's own transform (part.offset_x/y, etc.) is applied inside the resolved transform of its owning character. Therefore, this function cannot place a part at a fixed canvas position while ignoring the character transform; if the character itself moves, the part moves with it. To place a part at a fixed canvas position independently of the character transform, first inspect the character's offset_x/y / rotation / scale and other values, then separately apply their inverse transform to the part's displacement.


mt.layout.set_canvas_position(item, canvasX, canvasY, ctx?) Deprecated ​

API level: 1+

CAUTION

This is an API Level 1 compatibility function. It does not invert a part's owning-character hierarchy or the target's own transforms. Use mt.layout.place_2d in new code.

A helper that writes the offset_x/y corresponding to a specified canvas target position (canvasX, canvasY) to a character object (ctx.chars[i]) or part object (ctx.parts[i]).

It uses geometry.bounds_center_x/y as the natural center for characters and geometry.canvas_center_x/y for parts. Passing ctx as the fourth argument inverts only the same 2D Global transform as canvas_to_offset. Because it does not invert the owning-character transform of a part, 3D, camera, projection, or Deform, it does not guarantee the final screen position when these are active. When ctx is omitted, the natural center matches the target canvas position only when Global is the identity transform.

Arguments ​

ArgumentTypeUnitDefaultDescription
itemtable-RequiredTarget character or part
canvasX / canvasYnumbercanvas normalized positionRequiredDestination coordinates
ctxtable-nilIf specified, inverts only the 2D transform of ctx.global

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementThe assigned offsetX
numbercanvas normalized displacementThe assigned offsetY

Changes ​

Directly updates the target's offset_x and offset_y.

Example ​

lua
-- Move the character's natural center to the upper-right of the 2D screen (0.8, 0.8)
mt.layout.set_canvas_position(ctx.chars[1], 0.8, 0.8, ctx)

-- Move the part's natural center to the center of the screen when its parent character and Global are identity
mt.layout.set_canvas_position(ctx.parts[1], 0.5, 0.5)

mt.layout.group_bounds(ctx, targets, targetType?) Deprecated ​

API level: 1+

CAUTION

This is an API Level 1 compatibility function. It returns an approximation that excludes rotation, pivot, parent characters, and Global. Use mt.layout.measure_bounds_2d in new code.

Returns approximate axis-aligned bounds at an intermediate transform stage for the specified characters or parts.

It applies only each element's offset_x/y, scale, and stretch_x/y to its natural ink bounds. It does not account for the element's rotation/pivot, a part's owning-character transform, ctx.global, 3D, camera, projection, or Deform. It can therefore be used for supporting calculations in 2D layouts with no rotation or hierarchical/Global transforms, but does not guarantee final rendered bounds, physical bounds, or collision detection. The public API does not provide a function for obtaining final projected path bounds. Call it after writing the target elements' offset/scale/stretch.

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredLayout context
targetstable-RequiredArray of 1-based target indices
targetTypestring-"character""character" or "part"

Return Value (one of) ​

TypeUnitDescription
table-Approximate bounds
nil-If there are no valid targets

The bounds table has the following fields.

FieldTypeUnitDescription
left / right / bottom / topnumbercanvas normalized positionApproximate bounds at the transform stage described above
center_x / center_ynumbercanvas normalized positionCenter of the group
width / heightnumbercanvas normalized displacementWidth and height of the entire group

Example ​

lua
-- Get the bounds of the entire parent-character group at indices 1-3
local bounds = mt.layout.group_bounds(ctx, {1, 2, 3})
if bounds then
    -- Use the center coordinates, width, and height of the whole group to place a background plate or ruby text
    print(bounds.center_x, bounds.center_y, bounds.width, bounds.height)
end

mt.timeline.intro_outro(progress, introFraction, outroFraction) Deprecated ​

API level: 1+

Separately extracts and returns progress for the first interval (intro) and last interval (outro) from the overall progress.

WARNING

Deprecated: Percentage-based transitions expand or contract in proportion to clip length, so lengthening the clip makes the fade occur in slow motion. Use the fixed-speed mt.timeline.intro_outro_seconds instead (see the design rules for ctx.timeline).

Arguments ​

ArgumentTypeUnitDefaultDescription
progressnumber0-1RequiredOverall progress. Normally obtained from mt.timeline.progress
introFractionnumber0-1RequiredFraction of the overall duration treated as the intro
outroFractionnumber0-1RequiredFraction of the overall duration treated as the outro

Return Value (multiple) ​

TypeUnitDescription
number0-1introProgress. Intro progress
number0-1outroProgress. Outro progress

This appendix explains issues that can be confusing when creating scripts and how to address them.

1. Adjusting the advance of individually scaled characters ​

1.1 When this is needed ​

This is needed when you change scale or stretch for individual characters in OnLayout and want to rearrange the characters according to their resulting sizes.

1.2 The problem ​

Character advances are determined before individual transformations are applied in OnLayout. Therefore, simply changing scale or stretch for each character can cause the following problems.

  • Enlarged characters overlap adjacent characters.
  • Extra spacing remains on both sides of reduced characters.
  • Character reference positions shift away from the baseline.

This occurs because characters are scaled up or down around geometry.bounds_center while their original advances remain unchanged.

1.3 Solution ​

After changing character size, rotation, and pivot, call mt.layout.reflow at the end of OnLayout.

lua
function OnLayout(ctx)
    for index = 1, ctx.char_count do
        local character = ctx.chars[index]
        character.scale = 0.8 + index * 0.05
    end

    -- Adjust advances to match the character sizes after transformation
    mt.layout.reflow(ctx)
end

If you need additional space between characters, specify the spacing in the second argument. To limit the affected characters or the characters that remain fixed, specify config. For argument details, see mt.layout.reflow.

1.4 How it works ​

mt.layout.reflow uses the horizontal and vertical layout origins to rearrange characters according to their current scale, stretch, rotation, and pivot while preserving the spacing produced by shaping, kerning, tracking, and margins. It does not make the spacing between individual ink outlines uniform.

When the pivot is 0.5 and only scale is changed, a point P in natural coordinates moves according to the following formula. C is geometry.bounds_center, s is scale × stretch, and d is offset - 0.5.

text
P' = C + s × (P - C) + d
d  = P'(target) - C - s × (P - C)

You do not need to calculate this formula directly in the script. mt.layout.reflow aligns the layout origins while accounting for the complete 2D character transformation, including rotation, pivot, vertical writing, and non-square canvases.

2. Adjusting the gradient range and direction ​

2.1 When this is needed ​

Check this section when the gradient start position, end position, or color direction does not match your expectations after changing start_position, end_position, or angle.

2.2 The problem ​

start_position and end_position are not coordinates relative to the entire canvas. The gradient angle is also not calculated using only the width or height of the bounding box.

If you treat these values as canvas coordinates, the position where the colors transition may appear to shift away from the expected location when the text is moved, rotated, or enlarged, or when a diagonal gradient is set.

2.3 Solution ​

Specify start_position and end_position as ratios of the range of the entire transformed text as viewed along the angle direction.

  • To apply the gradient from one edge of the text to the other, set start_position = 0.0 and end_position = 1.0.
  • To transition colors inside the text, increase start_position and decrease end_position. With the standard settings of 0.2 and 0.8, the gradient interval begins 20% inward from each edge.
  • With angle = 0, colors change from left to right; with angle = 90, from top to bottom; and with angle = -90 or 270, from bottom to top.

The fill color is assigned to the start_position side, and end_color is assigned to the end_position side. The direction of angle follows the same definition as coordinates.

2.4 Reference range ​

The gradient uses the bounding box of the entire text after scale, rotation, offset, and other transformations have been applied to characters and parts in OnLayout.

  • During 2D rendering: This is the 2D bounding box on the canvas that encloses the transformed paths of all characters and parts.
  • During 3D projection: This is the bounding box on the 3D layout plane, unaffected by camera projection. Because projected coordinates are calculated for each character, the gradient position does not shift relative to the characters when the camera moves.

2.5 How the reference line is calculated ​

The gradient reference line from 0.0 to 1.0 is the line segment connecting the nearest and farthest extents when the bounding box is projected in the angle direction. For diagonal directions, the boundaries are not based on the rectangle's width or height, but on the position that extends farthest in the specified direction and the position on the opposite side.

Let the center of the bounding box be C, the direction vector of angle be D, and the four vertices be P_i. The projected value of each vertex is calculated using the following formula.

text
proj_i = (P_i - C) · D

If the minimum projected value is minProj and the maximum projected value is maxProj, the start point bbP0 and end point bbP1 of the reference line are as follows.

text
bbP0 = C + D * minProj
bbP1 = C + D * maxProj

If start_position is t_s and end_position is t_e, the actual start point P0 and end point P1 are the following positions on the reference line.

text
P0 = bbP0 + (bbP1 - bbP0) * t_s
P1 = bbP0 + (bbP1 - bbP0) * t_e

3. Passing values between callbacks ​

3.1 When this is needed ​

This is needed when you want to use a value from before it is overridden in OnPreLayout later in OnLayout during the same frame. For example, this applies when using the host's Scale as a factor that determines animation travel rather than character size.

3.2 The problem ​

If you change ctx.global.scale to 1.0 in OnPreLayout, OnLayout can only access the modified value. Because the original host value cannot be restored, you can no longer perform calculations using that value.

In contrast, values that are not overridden in OnPreLayout can be read directly from ctx.global in OnLayout. For example, reference tracking and line_spacing directly where they are used instead of saving them.

3.3 Solution ​

Save the value before it is overridden in a local variable declared outside the functions, and reference it from OnLayout. One Lua execution environment is created for each instance and persists until the script is reloaded, so the same variable can be referenced across callbacks.

lua
local scaleKnob = 1.0

function OnPreLayout(ctx)
    -- Save the host's Scale, then cancel its effect on character size
    scaleKnob = math.max(ctx.global.scale, 0.05)
    ctx.global.scale = 1.0
end

function OnLayout(ctx)
    local travel = 0.07 * scaleKnob
    for index = 1, ctx.char_count do
        ctx.chars[index].offset_y = ctx.chars[index].offset_y + travel
    end
end

Use this method only when you need a value from before it was overridden. Saving unnecessary values increases the script's state and makes the flow of processing harder to understand.

3.4 Do not use this to preserve state between frames ​

Variables declared outside functions retain their values not only between callbacks but also between frames. However, due to seeking, changes in rendering order, parallel rendering, and other factors, the host does not necessarily evaluate frames in chronological order.

If you perform delta calculations or accumulation using values from the previous frame, the results may differ between playback and rendering. Ensure that the result of each frame is determined uniquely from that frame's ctx.time. Outside functions, retain only constants and values passed between callbacks within the same frame.