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.
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
↓
RenderingThe 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.
OnPreLayoutruns 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 glyphsOnLayoutruns after Characters and Parts have been generated. It adjusts the position, rotation, color, and other properties of each generated element and shapeOnPathruns 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
localvariable 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
ctxorctx.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.timeinstead of using a sequential simulation - Always use
ctx.char_count/ctx.part_countwhen 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
| Declaration | Description |
|---|---|
@MugTypography | When 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 3d | Enables the 3D projection setting (Enable 3D projection) when the simulator starts. 3dprojection can also be used |
Recommended Initial Settings
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.
| Declaration | Description |
|---|---|
@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:
@input <type> <index> "<label>" [default=<value>]-- @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| type | index | Lua access | When default is omitted |
|---|---|---|---|
number | 1-14 | ctx.inputs.numbers[index] | 0.0 |
color | 1-8 | ctx.inputs.colors[index] | {1.0, 1.0, 1.0, 1.0} |
text | 1-4 | ctx.inputs.texts[index] | Empty string |
boolean | 1-8 | ctx.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 within0.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 istrueorfalse - 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>inScript Status, and does not prevent Lua compilation or execution - Only declared slots appear in the Inspector. With no declarations, the complete
Script Inputsgroup 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.
| Declaration | Description |
|---|---|
@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_id | ID 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 |
@description | Multi-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
-- @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
| Callback | Invocation timing | Main 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)
| Item | Description |
|---|---|
| Signature | function OnPreLayout(ctx) |
| Argument | ctx: table. Context before layout generation |
| Return value | None. Any returned value is ignored |
| Invocation timing | Every frame, before Character and Part layout generation |
| Main modification targets | ctx.global / ctx.camera / ctx.shapes:declare / ctx.shape_cameras:declare / ctx.glyphs:declare |
| On error | On 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)
| Item | Description |
|---|---|
| Signature | function OnLayout(ctx) |
| Argument | ctx: table. Context after layout generation |
| Return value | None. Any returned value is ignored |
| Invocation timing | Every frame, after Character and Part layout generation |
| Main modification targets | ctx.chars[index] / ctx.parts[index] / ctx.shapes[index] / ctx.shape_cameras[index] / ctx.bounding_box / ctx.output |
| On error | On 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)
| Item | Description |
|---|---|
| Signature | function OnPath(ctx) |
| Argument | ctx: table. Context for path editing |
| Return value | None. Any returned value is ignored |
| Invocation timing | Every frame, after OnLayout and before deformation, projection, and rendering |
| Main modification target | The path of a Part or Shape obtained from ctx.paths |
| On error | On 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
-- @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
endFor 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.
| Field | Definition |
|---|---|
ctx.global.position_x / position_y | Global position. Writable (OnPreLayout) |
ctx.global.pivot_x / pivot_y | Displacement 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:
Displacement (canvas ratio) = value - 0.5The displacement is based on canvas width or height, not character size or geometry.advance_*.
| Field | Definition |
|---|---|
Character/Part offset_x / offset_y | Translation 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_y | Displacement of the rotation/scale pivot from global.position. At 0.5, the pivot matches global.position |
Character/Part pivot_x / pivot_y | Displacement 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:
| Type | Targets | Meaning of value | Neutral / center |
|---|---|---|---|
| Position | global.position_*, canvas coordinates in geometry | Coordinate relative to the canvas | 0.5 = canvas center |
| Displacement | global.pivot_*, Character/Part offset_* / pivot_* | Difference relative to global.position, the natural position, or the natural center | 0.5 = zero difference |
Other Unit Systems
| Unit system | Fields | Definition |
|---|---|---|
| em | tracking, margins, stroke.width, stroke.gap, shadow.distance | Ratio to font size. 1.0 = 1 em. Strokes are multiplied by fontSize during rendering |
| fontSize percentage | bounding_box.width / margin_* / corner_radius / fill_offset_* / stroke_offset_* | 100.0 = the same length as fontSize. Conversion to px is value * fontSizePx / 100 |
| Multiplier | scale, stretch_*, zoom | 1.0 is the original size |
| 0-1 | opacity, each color component, write_on_start / write_on_end, bounding_box.fill_opacity, etc. | Value interpreted as a closed interval or progress |
| degree | rotation, yaw, pitch, and various angle fields | Angle. See §4.3 for signs |
| Perspective strength | camera.perspective | Unitless. Internally mapped as distancePx = f(perspective) * canvas.height (larger values move the camera closer) |
| px | canvas.width / height | Pixels |
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
{ 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/Partoffset/pivot, and geometry, positive Y is upward (increasing the Lua Y value moves upward on screen)For 3D depth, positive
zpoints 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.pivotand Character/Partoffset/pivotare canvas normalized displacements (§4.1). Geometry canvas coordinates andglobal.positionare canvas normalized positionsFor geometry canvas positions, the lower-left of the canvas is
0,0, and the center is0.5,0.5Geometry 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) (90turns 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): At0degrees it progresses from left to right (positive X direction), and increasing (positive) angles rotate it clockwise. - Shadow angle (
angle): At0degrees it extends rightward (positive X direction), and increasing (positive) angles rotate it clockwise (90degrees points straight down).
- 2D rotation (
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.
local dx, dy = mt.polar_offset_2d(angle, radius)
character.offset_x = character.offset_x + dx
character.offset_y = character.offset_y + dyTIP
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.
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/yis a displacement fromglobal.position. At0.5, 0.5, the pivot matchesglobal.position- Character/Part
pivot_x/yis a displacement from the item's own reference point. At0.5, 0.5, a Character rotates and scales around its natural bounding-box center (equivalent togeometry.bounds_center), while a Part rotates and scales around its natural position (equivalent togeometry.canvas_center) - Therefore, do not assign a canvas coordinate from
geometrydirectly to a Character or Part pivot. To rotate around a point shifted 10% to the right from the item's own center, usepivot_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_heightare 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
- Bottom edge:
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
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.0The 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
| Setting | Effect on rendering |
|---|---|
fill.use / stroke.use / shadow.use | When 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.transparent | Makes only the fill transparent. Does not affect the stroke or shadow |
The color and opacity used for rendering are determined in the following order.
- Determine color: If
fill.useistrue, use the individual color set on the Character or Part; iffalse, use the gradient (when enabled) or the Global fill color - 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.
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 stageWhen 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.
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 location | Result |
|---|---|
| Script loading | The host does not render; MT Sim reports the error and retains the last valid script |
OnPreLayout | The host skips later callbacks and does not render; MT Sim previews the original values |
OnLayout | The host skips OnPath and does not render; MT Sim previews the unmodified layout |
OnPath | The 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.
| Field | Type | OnPreLayout | OnLayout | OnPath | Description |
|---|---|---|---|---|---|
ctx.time | number | ○ | ○ | ○ | Current time in seconds, with the beginning of the clip as 0 (§1.1) |
ctx.frame | number | ○ | ○ | ○ | Frame number with the beginning of the clip as 0. May include subframes (§1.1) |
ctx.fps | number | ○ | ○ | ○ | Frame rate (§1.1) |
ctx.canvas | table | ○ | ○ | ○ | Canvas and project pixel information (§1.2) |
ctx.fonts | string[] | ○ | ○ | ○ | 1-based array of font names that can be assigned to global.font_name (§1.3) |
ctx.timeline | table | ○ | ○ | ○ | Clip time range and current progress (§1.4) |
ctx.font | table | - | ○ | ○ | Metrics (dimensional information) for the font in use (§1.5) |
ctx.text | string | ○ | ○ | - | Deprecated text reference (§1.6) |
ctx.char_count | integer | - | ○ | ○ | Number of valid text characters; shapes are not counted (§1.6) |
ctx.part_count | integer | - | ○ | ○ | Number of valid text parts; shapes are not counted (§1.6) |
ctx.shape_count | integer | - | ○ | ○ | Number of declared shapes (§1.17) |
ctx.meta | table | ○ | ○ | ○ | API version and feature information (§1.7) |
ctx.inputs | table | ○ | ○ | ○ | Script inputs configured in the Inspector (§1.16) |
ctx.global | table | ○ | ○ | ○ | Global settings. Writable in OnPreLayout and read-only thereafter (§1.8) |
ctx.camera | table | ○ | ○ | ○ | Camera settings. Writable in OnPreLayout and read-only thereafter (§1.9) |
ctx.chars | table[] | - | ○ | ○ | 1-based array of generated text characters (§1.10) |
ctx.parts | table[] | - | ○ | ○ | 1-based array of generated text parts (§1.11) |
ctx.bounding_box | table | - | ○ | ○ | Bounding-box drawing settings (§1.12) |
ctx.output | table | - | ○ | ○ | Output filtering and drawing-order settings (§1.13) |
ctx.paths | table | - | - | ○ | Context for retrieving and editing part paths (§1.14) |
ctx.glyphs | table | ○ | - | - | Context for declaring custom glyphs (§1.15) |
ctx.shapes | table | ○ | ○ | ○ | Context for declaring shapes and reading them by number (§1.17) |
ctx.shape_cameras | table | ○ | ○ | - | 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.
| Field | Type | Unit | Description |
|---|---|---|---|
width / height | number | px | Canvas width and height |
aspect_ratio | number | - | Canvas width divided by height (width / height) |
pixel_aspect_ratio | number | - | 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
| Field | Type | Unit | Description |
|---|---|---|---|
available | boolean | - | Whether a finite clip range could be obtained |
start_frame / end_frame | number | frame | Start/end of the range (clip-local; the start is always 0) |
duration_frames | number | frame | end_frame - start_frame |
duration_seconds | number | second | Duration converted to seconds |
progress | number | 0-1 | Current 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.
| Pattern | Anchor | How to Write It |
|---|---|---|
| Intro (entrance) | Beginning of clip | Use ctx.time directly (real seconds). For a combined intro + outro, use mt.timeline.intro_outro_seconds (§10.9) |
| Outro (exit) | End of clip | mt.timeline.remaining(ctx) (real seconds remaining until the end; §10.9) |
| Typewriter, stagger, spring, etc. | Beginning of clip | ctx.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 clip | progress (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.
-- 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.
| Field | Type | Unit | Description |
|---|---|---|---|
em_size | number | canvas height ratio | Size of one em before transforms |
ascent | number | canvas height ratio | Ascent upward from the baseline |
descent | number | canvas height ratio | Descent downward from the baseline (positive value, expressed as a downward amount) |
line_height | number | canvas height ratio | Line height |
units_per_em | number | font design unit | Number 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
| Field | Type | Unit | Description |
|---|---|---|---|
ctx.meta.api_level | integer | - | Lua API compatibility level |
ctx.meta.plugin_version | string | - | Running plugin version (for display) |
ctx.meta.capabilities | table | - | Information about currently available features. Read-only |
ctx.meta.capabilities.has_3d | boolean | - | Whether 3D Projection is enabled for the current preset |
ctx.meta.limits | table | - | Limits applied to this instance. Read-only |
ctx.meta.limits.max_characters | integer | - | Maximum number of characters this instance can handle |
ctx.meta.limits.max_parts | integer | - | Maximum number of parts this instance can handle |
ctx.meta.limits.max_shapes | integer | - | 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_seed | integer | - | 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
| Field | Type | Unit | Description |
|---|---|---|---|
text | string | - | UTF-8 text. Changing it reshapes the text and regenerates the character and part counts |
font_name | string | - | Font family name (one of ctx.fonts). Changing it changes the font in use and reshapes the text |
tracking | number | em | Character spacing added to the trailing advance of each character. 1.0 is 1 em |
line_spacing | number | multiplier | Advance between lines / vertical-writing columns |
vertical | boolean | - | When true, enables vertical writing and changes the shaping and placement direction |
single_part_glyph | boolean | - | 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_align | string | - | Horizontal alignment. left / center / right |
v_align | string | - | Vertical alignment. top / center / bottom / baseline |
margins | number[] | em | Trailing advance for each character. Uses the same unit as tracking |
emoji | table | - | Emoji placement and font settings (table below) |
position_x / position_y | number | canvas normalized coordinate | Global position. 0.5 is the center; positive X is right and positive Y is up |
rotation | number | degree | Global 2D rotation. Positive (+) rotates right (clockwise); negative (-) rotates left (counterclockwise) |
yaw / pitch | number | degree | Global 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 |
scale | number | multiplier | Global uniform scale. Clamped to a minimum of 0.0001 when written |
stretch_x / stretch_y | number | multiplier | Global nonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted |
pivot_x / pivot_y | number | canvas normalized displacement | Pivot for Global transformations. 0.5 means displacement 0; larger values move it right/up from global.position |
opacity | number | 0-1 | Global 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 |
fill | table | - | Global fill settings (table below) |
gradient | table | - | Gradient settings (table below) |
stroke | table | - | Global stroke settings (table below) |
shadow | table | - | Global shadow settings (table below) |
texture | table | - | Global texture settings (table below) |
blur | table | - | 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.
(margins[i] + tracking) * designUnitsPerEmArray 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.
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.
| Field | Type | Unit | Description |
|---|---|---|---|
color | color | RGBA 0-1 | Global fill color. Also serves as the gradient start color |
transparent | boolean | - | 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
| Field | Type | Unit | Description |
|---|---|---|---|
color_space | string | - | Gradient / Crayon mode. The values are the enumeration below |
end_color | color | RGBA 0-1 | Gradient end color |
midpoint | number | 0-1 | Midpoint of color interpolation |
angle | number | degree | Gradient direction. At 0 degrees it progresses from left to right, and positive (+) rotates right (clockwise) |
start_position / end_position | number | Position relative to the transformed Global bounds | Gradient interval. The standard range is 0.0-1.0. For details, see Gradient Range and Direction |
Values of gradient.color_space:
noneokhsvokhsloklablinear_rgboklch_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.
| Field | Type | Unit | Description |
|---|---|---|---|
use | boolean | - | 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] |
type | string | - | Material kind. The values are the enumeration below |
p1 | number | material-dependent | First control |
p2 | number | material-dependent | Second control |
p3 | number | material-dependent | Third control |
p4 | number | material-dependent | Fourth control |
p5 | number | material-dependent | Fifth 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) |
color | color | RGBA 0-1 | Colors 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:
type | p1 | p2 | p3 | p4 |
|---|---|---|---|---|
none | - | - | - | - |
water_droplet | Thickness | Roundness | Gloss | Light angle |
air_cushion | Inflation | Seal width | Wrinkles | Light angle |
glitter | Flake size | Highlight ratio | Flake amount | Light angle |
iridescent | Film thickness | Hue range | Strength | Light angle |
faceted | Facet count | Bevel depth | Polish | Light angle |
stained_glass | Piece size | Lead width | Hue range | Glow |
metallic | Polish | Relief | Reflection contrast | Light angle |
metallic_2 | Polish | Relief | Exposure | Light angle |
stripe | Stripe density (count) | Stripe width | Strength | Stripe angle (degrees) |
colored_pencil | Scratch density | Scratch length | Light amount | Stroke angle (degrees) |
halftone, halftone_diamond | Dot density (count) | Dot size | Fade amount | Grid angle (degrees) |
prism_foil | Tile size | Hue range | Strength | Light 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.
| Field | Type | Unit | Description |
|---|---|---|---|
use | boolean | - | 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_x | number | em | Horizontal blur, as a Gaussian sigma. 1.0 = 1 em; 0 leaves the horizontal edges sharp. Clamped to 0-10 when written |
amount_y | number | em | Vertical blur, with the same unit and range as amount_x |
glow | number | - | 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
| Field | Type | Unit | Description |
|---|---|---|---|
width | number | em (1.0 = font size 1 em. Multiplied by fontSize during rendering) | Stroke width |
gap | number | em (1.0 = font size 1 em) | Gap between the fill and stroke |
color | color | RGBA 0-1 | Stroke color |
outer_width | number | em (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_color | color | RGBA 0-1 | Outer Stroke color |
order | string | - | Stacking order of fill and stroke. fill_over_stroke / stroke_over_fill / fill_over_stroke_per_character |
join | string | - | Join style for stroke corners. miter_clip / round / bevel / miter_round / miter_bevel |
miter_limit | number | multiplier | Maximum 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).
| Field | Type | Unit | Description |
|---|---|---|---|
enabled | boolean | - | Whether the Global shadow is enabled |
distance | number | em | Shadow 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 |
angle | number | degree | Shadow direction. 0 degrees points right, and positive (+) rotates right (clockwise) (90 degrees points straight down) |
blur_x / blur_y | number | em | Blur 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 |
glow | number | - | 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 |
color | color | RGBA 0-1 | Shadow 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.
| Field | Type | Unit | Description |
|---|---|---|---|
offset_x / offset_y | number | em (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 |
scale | number | multiplier | Emoji 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_font | boolean | - | true uses the font_name font; false uses the system emoji font (Windows: Segoe UI Emoji, macOS: Apple Color Emoji) |
font_name | string | - | 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_outline | boolean | - | 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.
| Field | Type | Unit | Description |
|---|---|---|---|
target_x / target_y | number | canvas normalized coordinate | Camera 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 / pitch | number | degree | Camera orbit. Positive yaw moves the camera around to the right; positive pitch moves it upward (looking down) |
perspective | number | - | 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 |
zoom | number | multiplier | Projection magnification. Clamped to a minimum of 0.001 when written |
lens_shift_x / lens_shift_y | number | canvas ratio | Principal 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".
| Field | Type | Unit | Description |
|---|---|---|---|
index | integer | - | 1-based character index. Read-only |
text | string | - | Original text of this character cluster. Read-only |
part_start / part_count | integer | - | 1-based starting global index and count of owned parts. Read-only |
line_index | integer | - | 1-based index of the line it belongs to. Read-only |
geometry | table | - | Placement information before transformations (below). Read-only |
offset_x / offset_y | number | canvas normalized displacement | Character displacement. 0.5 means displacement 0; larger values move right/up |
pivot_x / pivot_y | number | canvas normalized displacement | Pivot for character transformations. At 0.5, the pivot equals the natural center; larger values move it right/up |
scale | number | multiplier | Uniform scale. Clamped to a minimum of 0.001 when written |
stretch_x / stretch_y | number | multiplier | Nonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted |
rotation | number | degree | 2D rotation. Positive (+) is clockwise (right rotation); negative (-) is counterclockwise (left rotation) |
z | number | canvas height ratio | Depth. Positive (+) moves deeper (away from the camera); negative (-) moves forward (toward the camera). Affects rendering only in 3D presets |
yaw / pitch | number | degree | Individual 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 |
opacity | number | 0-1 | Opacity 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_index | number | - | Paint order key for shapes (ctx.shapes) only. A text character ignores it (a write is accepted but does not affect the paint order) |
fill | table | - | Individual fill (table below) |
stroke | table | - | Individual stroke (table below) |
shadow | table | - | Individual shadow color (table below) |
texture | table | - | Individual texture. Same structure as ctx.global.texture, plus use |
blur | table | - | Individual blur of the text. Same structure as ctx.global.blur, plus use |
chars[index].fill
| Field | Type | Unit | Description |
|---|---|---|---|
use | boolean | - | When true, uses fill.color |
color | color | RGBA 0-1 | Individual color used when use is true |
chars[index].stroke
| Field | Type | Unit | Description |
|---|---|---|---|
use | boolean | - | When true, uses the individual stroke settings, including Inner and Outer |
width | number | em (1.0 = font size 1 em) | Used when use is true |
gap | number | em (1.0 = font size 1 em) | Used when use is true |
color | color | RGBA 0-1 | Used when use is true |
outer_width | number | em (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_color | color | RGBA 0-1 | Outer Stroke color |
chars[index].shadow
| Field | Type | Unit | Description |
|---|---|---|---|
use | boolean | - | When true, uses shadow.color, blur_x, blur_y and glow |
color | color | RGBA 0-1 | Individual color used when use is true |
blur_x / blur_y | number | em | Individual shadow blur used when use is true. Reads the inherited blur until set. Clamped to 0-10 when written |
glow | number | - | 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.
| Field | Type | Unit | Description |
|---|---|---|---|
canvas_origin_x / canvas_origin_y | number | canvas normalized coordinate | Typesetting origin (horizontal writing: placement origin corresponding to the cell's left edge × baseline) |
vertical_origin_x / vertical_origin_y | number | canvas normalized coordinate | Vertical-typesetting origin (column center axis × cell top) |
bounds_center_x / bounds_center_y | number | canvas normalized coordinate | Center of the area occupied by the character outline before transformations (ink bounds) |
bounds_width | number | canvas width ratio | Ink bounds width / canvas width |
bounds_height | number | canvas height ratio | Ink bounds height / canvas height |
advance_x | number | canvas width ratio | Font horizontal advance (px) / canvas width. Does not include tracking / margins |
advance_y | number | canvas height ratio | Font 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.
| Field | Type | Unit | Description |
|---|---|---|---|
index | integer | - | 1-based global part index. Read-only |
character_index | integer | - | 1-based index of the owning character. Read-only |
index_in_character | integer | - | 1-based part index within the character. Read-only |
geometry | table | - | Placement information before transformations (table below). Read-only |
offset_x / offset_y | number | canvas normalized displacement | Part displacement. 0.5 means displacement 0; larger values move right/up |
pivot_x / pivot_y | number | canvas normalized displacement | Pivot for part transformations. At 0.5, the pivot equals the natural position; larger values move it right/up |
scale | number | multiplier | Uniform scale. Clamped to a minimum of 0.001 when written |
rotation | number | degree | 2D rotation. Positive (+) is clockwise (right rotation); negative (-) is counterclockwise (left rotation) |
stretch_x / stretch_y | number | multiplier | Nonuniform scale. Unlike scale, it has no positive lower bound; 0 and negative values are accepted |
z | number | canvas height ratio | Depth. Positive (+) moves deeper (away from the camera); negative (-) moves forward (toward the camera). Affects rendering only in 3D presets |
yaw / pitch | number | degree | Individual 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 |
opacity | number | 0-1 | Opacity 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) |
fill | table | - | Individual fill with use / color. Has the same structure as the character's fill; when use is true, uses color |
stroke | table | - | 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 |
shadow | table | - | Individual shadow color with use / color. Has the same structure as the character's shadow; when use is true, uses color |
texture | table | - | Individual texture. Has the same structure as the character's texture; when use is true, uses that material |
blur | table | - | 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 Setting | use = false | use = true |
|---|---|---|
fill | Inherits the fill selected at the owning character level | Uses the part's fill.color |
stroke | Inherits the Inner / Outer settings selected at the owning character level | Uses the part's stroke settings |
shadow | Inherits the shadow.color selected at the owning character level | Uses 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.
| Field | Type | Unit | Description |
|---|---|---|---|
local_center_x / local_center_y | number | displacement in canvas-axis ratios | Part 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_y | number | canvas normalized coordinate | Canvas coordinates of the part's natural position (center after layout) |
bounds_center_x / bounds_center_y | number | canvas normalized coordinate | Center of the area occupied by the part outline before transformations (ink bounds) |
bounds_width / bounds_height | number | respective canvas-axis ratios | Width 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).
| Field | Type | Unit | Description |
|---|---|---|---|
enabled | boolean | - | Enables drawing the frame |
draw_top / draw_bottom / draw_left / draw_right | boolean | - | Whether to draw the stroke on each side |
color | color | RGBA 0-1 | Frame line color |
width | number | fontSize percentage | Frame line thickness |
fill_enabled | boolean | - | Enables filling inside the frame |
fill_color | color | RGBA 0-1 | Interior fill color (its alpha is multiplied by fill_opacity) |
fill_opacity | number | 0-1 | Opacity multiplier for the interior fill |
start_cap / end_cap | string | enumeration below | Caps for open path endpoints |
margin_x / margin_y | number | fontSize percentage | Margin outside the text bounds (added to each side) |
corner_radius | number | fontSize percentage | Corner radius when all four sides are enabled (capped at half the rectangle size) |
fill_offset_x / fill_offset_y | number | fontSize percentage | Translation of the fill rectangle. |
stroke_offset_x / stroke_offset_y | number | fontSize percentage | Translation 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.
| Field | Type | Unit | Description |
|---|---|---|---|
separation_enabled | boolean | - | Whether Separation output is enabled. Read-only |
render_mode | string | - | Current output slot. default / separation_1 / separation_2. Read-only |
targets_1 / targets_2 | string | - | Target specification string assigned to each Separation output. Read-only |
write_on_mode | string | - | Write-on unit. character or part |
write_on_start | number | 0-1 | Start of the visible interval (normalized progress) |
write_on_end | number | 0-1 | End of the visible interval. No filter (everything visible) when start = 0 and end = 1 |
reorder_parts | boolean | - | When true, uses writing order (stroke order) for part drawing order and reveal order. Has no visual effect on single-part characters |
manual_order_enabled | boolean | - | When true, overrides reveal / drawing order with manual_order_text |
manual_order_text | string | - | Manual order specification (syntax below) |
force_disable_3d_projection | boolean | - | 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.
function OnLayout(ctx)
-- This effect intentionally guarantees 2D output.
ctx.output.force_disable_3d_projection = true
endWrite-on
Visible elements are limited to those whose 1-based rank within the unit lies in [startLimit, endLimit].
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:
| mode | Default Rank | When manual_order_enabled | reorder_parts Only |
|---|---|---|---|
character | Character index in layout order (1..N) | Character-level manual reveal order | Character rank remains in layout order |
part | ctx.parts[index].index (1..N) | Manual reveal order of parts | Reveal 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.
| Token | Meaning |
|---|---|
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.
| Field | Type | Unit | Description |
|---|---|---|---|
units_per_em | number | path unit/em | Path 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.
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
| Field | Type | Unit | Description |
|---|---|---|---|
index | integer | - | 1-based global part number across the frame |
character_index | integer | - | 1-based number of the character it belongs to |
index_in_character | integer | - | 1-based part number within the character it belongs to |
line_index | integer | - | 1-based number of the line it belongs to |
text | string | - | Text represented by the character it belongs to |
path | path | - | 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.
| Field | Type | Default | Unit and description |
|---|---|---|---|
path | MtDrawingPath | Empty path | Initial shape. Part-center origin, Y-down, 1000 units = 1 em |
advance_x | number | 0.0 | Horizontal advance. Positive is right, 1000 units = 1 em |
advance_y | number | 0.0 | Vertical advance. Positive is up, 1000 units = 1 em; downward vertical flow uses a negative value |
bounds | number[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.
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"
endThe 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.
| Field | Type | Length | Description |
|---|---|---|---|
numbers | number[] | 14 | Unitless numeric inputs |
colors | MtColor[] | 8 | RGBA color inputs |
texts | string[] | 4 | Single-line text inputs |
booleans | boolean[] | 8 | Checkbox boolean inputs |
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.
| field | type | Default | Description |
|---|---|---|---|
path | MtDrawingPath | empty path | The 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) |
layer | string | "back" | "back" draws behind the text, "front" in front of it |
parent | integer | none | The 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 |
camera | integer | the parent's camera, else none | The 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_yat0.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_*andpivot_*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,blurandtextureare 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 = 0can 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.
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" })
endShape 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].
| Field | Type | Description |
|---|---|---|
index | integer | The shape number, the same value ctx.shapes:declare returned |
offset_x / offset_y | number | Position 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 / pitch | number | The 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 |
opacity | number | Opacity multiplier (0 to 1) |
parent | integer | Read-only. The parent shape number given to ctx.shapes:declare, 0 when there is no parent |
camera | integer | Read-only. The number of the camera that projects this shape (taken from its parent when it names none), 0 when it has none |
z_index | number | Paint 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 / blur | table | Individual appearance, the same fields as ctx.chars[index] |
path | path | The 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.
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 }
endctx.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.
| Field | Type | Default | Description |
|---|---|---|---|
target_x / target_y | number | 0.5 / 0.5 | The camera's look-at point (canvas fractions). target_y is positive upward |
yaw / pitch | number | 0 | The camera's orbit angles in degrees, in the same sense as ctx.camera |
perspective | number | 0.5 | Perspective strength; held between 0 and 1 |
zoom | number | 1 | Zoom multiplier |
lens_shift_x / lens_shift_y | number | 0 | Lens 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
camerauses its parent's camera, and naming a different one is an error. - On a shape with no camera,
yaw,pitchandzhave no visible effect. scaleacts 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, leavescaleat 1 and put the size in the path's dimensions and in the child'soffset(the parent's length).- The paint order is decided by the layer and then
z_index; inside one layer and onez_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.
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
endHow 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_camerasis projected in 3D, by that camera. - A shape's number cannot be written in the manual order (
manual_order_text). The paint order followslayer,z_indexand 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_2dandmt.layout.canvas_to_offset_2dtake a shape like a character: its anchor is its position. For a shape with a parent they work through the parent's transform, andplace_2dandcanvas_to_offset_2dreturn theoffsetin the parent's space (an error when the parent's scale is0).- 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]inOnLayout.
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
| Purpose | API |
|---|---|
| Value constraints and range mapping | mt.clamp / mt.saturate / mt.remap / mt.wrap |
| Interpolation and keyframes | mt.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 order | mt.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 correction | mt.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 |
| Easing | mt.ease.* |
| Reflow and line-based grouping | mt.layout.reflow / mt.layout.group_by_line |
| Canvas position, distance, and pivot | mt.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 transforms | mt.layout.measure_bounds_2d |
| Alignment on paths | mt.layout.queue_on_path |
| Motion paths | mt.path.bezier / mt.path.catmull_rom / mt.path.arc_length |
| Path creation and editing | mt.svg_path / path:* |
| UTF-8 text processing | mt.text.slice / mt.text.classify |
| Character and part selection strings | mt.target.chars / mt.target.parts |
| Clip time | mt.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 APIs | mt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
value | number | - | Required | Value to constrain |
low | number | - | Required | Lower bound |
high | number | - | Required | Upper bound |
Return Value
| Type | Unit | Description |
|---|---|---|
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
value | number | - | Required | Value to constrain to 0-1 |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Value 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
value | number | - | Required | Value to convert |
in_low / in_high | number | - | Required | Input interval |
out_low / out_high | number | - | Required | Output interval |
clamped | boolean | - | false | If true, constrains the input position to 0-1 before conversion |
Return Value
| Type | Unit | Description |
|---|---|---|
number | - | Converted value |
Constraints and Errors
| Condition | Description |
|---|---|
in_low == in_high | Cannot be specified because the input interval would have zero width |
Example
-- 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/boundaryBinstead oflow/high. Reversed boundaries now wrap over the interval from the smaller boundary to the larger boundary.
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
value | number | - | Required | Value to wrap |
boundaryA | number | - | Required | One boundary that defines the interval |
boundaryB | number | - | Required | The other boundary that defines the interval |
Return Value
| Type | Unit | Description |
|---|---|---|
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
from | number | - | Required | Value when t = 0 |
to | number | - | Required | Value when t = 1 |
t | number | - | Required | Interpolation factor. It is not clamped, so values outside 0-1 produce extrapolation |
Return Value
| Type | Unit | Description |
|---|---|---|
number | Same as the input values | from + (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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
from | number | - | Required | Start of the interval corresponding to a factor of 0 |
to | number | - | Required | End of the interval corresponding to a factor of 1 |
value | number | - | Required | Value whose position to calculate |
Return Value
| Type | Unit | Description |
|---|---|---|
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
from | number | degree | Required | Starting angle |
to | number | degree | Required | Target angle |
t | number | - | Required | Interpolation factor. Not clamped |
Return Value
| Type | Unit | Description |
|---|---|---|
number | degree | Interpolated 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
edge0 | number | - | Required | Start of the transition, where the result is 0 |
edge1 | number | - | Required | End of the transition, where the result is 1 |
value | number | - | Required | Value to evaluate |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Smooth transition value. Clamped outside the interval |
Constraints and Errors
| Condition | Description |
|---|---|
edge0 == edge1 | Cannot 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
keys | table[] | - | Required | Array of keyframes. Each element has the form { t = <time>, v = <value>, ease = <optional> }, ordered by ascending t |
time | number | seconds | Required | Time 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
| Type | Unit | Description |
|---|---|---|
number | color | Same as the key values | Value interpolated within the interval |
Example
-- 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 = tint3. 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
index | integer | - | Required | 1-based index, such as an index for ctx.chars[index] |
count | integer | - | Required | Total count, such as ctx.char_count |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | (index - 1) / (count - 1) constrained to 0-1. Returns 0 if count <= 1 |
Example
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
entries | integer[] | - | Required | Encoded character and global-part indices in output order |
Return Value
| Type | Unit | Description |
|---|---|---|
string | - | Valid manual-order text representing the input sequence; an empty array returns an empty string |
Example
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout / OnPath context |
options.from | integer | - | 1 | First character index to visit |
options.to | integer | - | ctx.char_count | Last character index to visit, inclusive |
options.line | integer | - | No filter | Keep only characters with this line_index |
options.order | string | - | "asc" | How info.progress is assigned: "asc" / "desc" / "center" / "random" |
options.seed | integer | - | 0 | Seed selecting the order when order = "random" |
Return Value
| Type | Description |
|---|---|
function | Iterator yielding index (into ctx.chars), character and info |
Fields of info:
| Field | Type | Description |
|---|---|---|
progress | number | Normalized position in 0-1 following order |
n | integer | 1-based counter within the filtered subset |
count | integer | Number of elements in the filtered subset (at least 1) |
first / last | boolean | Whether this is the first or last element of the filtered subset |
order only remaps progress; the visit order is always ascending by index.
| Value | progress 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:
infois the same table on every iteration. Copy the values out if they need to outlive the loop.
Example
-- 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)
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout / OnPath context |
options.from | integer | - | 1 | First part index to visit |
options.to | integer | - | ctx.part_count | Last part index to visit, inclusive |
options.from_char | integer | - | No filter | Keep only parts whose owning character index is at or above this |
options.to_char | integer | - | No filter | Keep only parts whose owning character index is at or below this |
options.line | integer | - | No filter | Keep only parts with this line_index |
options.order | string | - | "asc" | How info.progress is assigned, as in mt.each_char |
options.seed | integer | - | 0 | Seed 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
| Type | Description |
|---|---|
function | Iterator yielding index (into ctx.parts), part and info |
info carries every field from mt.each_char plus:
| Field | Type | Description |
|---|---|---|
char | MtCharacter | The character owning this part (an element of ctx.chars) |
char_index | integer | Index of the owning character in ctx.chars |
index_in_char | integer | Position 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
-- 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
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
time | number | seconds | Required | Elapsed time since the animation began |
index | integer | - | Required | 1-based index |
delay | number | seconds | Required | Delay before the next index begins |
duration | number | seconds | Required | Animation duration for each index |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Progress clamped to 0-1 |
Constraints and Errors
| Condition | Description |
|---|---|
duration <= 0 | Specify a value greater than 0 |
Example
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
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
progress | number | 0-1 | Required | Shared progress to distribute. Values outside the range are accepted, and the result is clamped to 0-1 |
position | number | 0-1 | Required | Normalized element position. Values outside the range are clamped to 0-1. Usually info.progress |
span | number | less than 1 | Required | Fraction of the shared window across which start times are distributed |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Progress for the element at position |
The result is calculated as follows:
mt.saturate((progress - mt.saturate(position) * span) / (1.0 - span))Constraints and Errors
| Condition | Description |
|---|---|
progress or position is not finite | Error |
span is not finite | Error |
span < 0 or span >= 1 | Error |
Example
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)
endChanging 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
time | number | seconds | Required | Time to evaluate |
index | integer | - | Required | 1-based index |
count | integer | - | Required | Total count, such as ctx.char_count |
pattern | string | - | Required | Progression pattern: "asc" / "desc" (alias "right_to_left") / "center" / "random" |
delay | number | seconds | Required | Start delay |
duration | number | seconds | Required | Animation duration |
seed | integer | - | 42 | Random 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
seedproduces a different order than before.
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Progress clamped to 0-1 |
Constraints and Errors
| Condition | Description |
|---|---|
duration <= 0 | Specify 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
distance | number | Input coordinate system | Required | Distance from the center of the effect |
radius | number | Input coordinate system | Required | Spread 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
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Distance-based influence weight |
Example
-- 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.1NOTE
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
channelwas added. Omitting it preserves the previous results.
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
seed | integer | - | Required | Value selecting the random sequence. Use the same seed for the same purpose |
index | integer | - | Required | Position within the sequence, such as a character index |
channel | string | - | Omitted | Name separating one use from another. |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Random 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
seed / index | integer | - | Required | Same as mt.random |
low / high | number | - | Required | Output range |
channel | string | - | Omitted | Same as mt.random. |
Return Value
| Type | Unit | Description |
|---|---|---|
number | Same as the output range | Stable 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
seed | integer | - | Required | Value selecting the permutation and common offset |
index | integer | - | Required | Assignment position from 1 through count |
count | integer | - | Required | Number of values; must be at least 1 |
low / high | number | - | Required | Finite output bounds with low < high |
channel | string | - | Omitted | Name separating independent assignments |
Return Value
| Type | Unit | Description |
|---|---|---|
number | Same as the output range | Stable assigned value in [low, high); adjacent sorted values differ by (high - low) / count |
Example
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
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
x | number | - | Required | Sample position. Values are smoothly interpolated between lattice points at integer intervals |
seed | integer | - | 0 | Value that changes the fluctuation pattern |
Return Value
| Type | Unit | Description |
|---|---|---|
number | -1-1 | Continuous value that is always the same for the same arguments |
Example
-- Handheld-camera-style shake with a different phase for each character
character.rotation = mt.noise1(ctx.time * 0.8, index) * 4.0mt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
x / y | number | - | Required | Sample position |
seed | integer | - | 0 | Value that changes the fluctuation pattern |
Return Value
| Type | Unit | Description |
|---|---|---|
number | -1-1 | Continuous 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
time | number | seconds | Required | Time to evaluate |
frequency | number | Hz | Required | Number of fluctuations per second |
amplitude | number | Same as the output value | Required | Amplitude of the first octave. This is not the maximum sum across multiple octaves |
octaves | integer | - | 1 | Number of noise octaves to layer |
seed | integer | - | 0 | Random seed |
Return Value
| Type | Unit | Description |
|---|---|---|
number | Same as amplitude | Fluctuation 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
period | number | seconds | Required | Length of one cycle |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Progress greater than or equal to 0 and less than 1 |
Constraints and Errors
| Condition | Description |
|---|---|
period <= 0 | Specify 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
period | number | seconds | Required | Length of one round trip |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Oscillating value from 0 to 1 |
Constraints and Errors
| Condition | Description |
|---|---|
period <= 0 | Specify 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
frequency | number | Hz | Required | Number of cycles per second |
phase | number | radian | 0 | Phase offset. It can also offset the wave for each index |
Return Value
| Type | Unit | Description |
|---|---|---|
number | -1-1 | Waveform value |
mt.wave_square(t, frequency, phase?)
API level: 1+
Computes a square wave periodic toggle between -1 and 1 (blink, strobe, flashing).
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
frequency | number | Hz | Required | Number of cycles per second |
phase | number | radian | 0 | Phase offset |
Return Value
| Type | Unit | Description |
|---|---|---|
number | - | -1.0 or 1.0 |
mt.wave_triangle(t, frequency, phase?)
API level: 1+
A triangle wave.
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
frequency | number | Hz | Required | Number of cycles per second |
phase | number | radian | 0 | Phase offset |
Return Value
| Type | Unit | Description |
|---|---|---|
number | -1-1 | Value 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Time to evaluate |
frequency | number | Hz | Required | Number of cycles per second |
phase | number | radian | 0 | Phase offset |
Return Value
| Type | Unit | Description |
|---|---|---|
number | -1-1 | Repeating 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Elapsed time since the motion began. Returns the initial value when t <= 0 |
frequency | number | Hz | Required | Number of oscillations per second |
damping | number | 1/second | Required | Damping strength. Larger values converge faster |
Return Value
| Type | Unit | Description |
|---|---|---|
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, orstretch, or use as thetargument tomt.lerp. If you need progress, usemt.ease.*ormt.saturate, and do not multiply a scale factor directly by a negative value (stretchhas no host-side lower-bound clamp and can reach rendering while still negative.scaleis clamped to a positive lower bound, andopacityis clamped to 0-1 immediately before rendering)
Example
-- 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.17. 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
angleDegrees | number | degree | Required | Direction. As with coordinates, 0 degrees points right and positive values rotate clockwise |
radius | number | canvas normalized displacement | Required | Distance in that direction |
Return Value
| Type | Unit | Description |
|---|---|---|
number, number | canvas normalized displacement | Two 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
-- 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 + dymt.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.
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
config | table | - | Required | Settings for time, collision surface, starting position, gravity or acceleration, restitution, initial velocity, and Squash & Stretch |
Shared config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | 0.0 | Elapsed time since the animation began |
restitution | number | - | 0.5 | Coefficient of restitution. Larger values produce a stronger rebound after impact |
start_velocity | number | canvas normalized displacement/second | 0.0 | Initial velocity along the axis |
squash / squash_strength | number | factor | 0.15 | Strength with which the contact axis is compressed according to impact velocity |
stretch / stretch_strength | number | factor | 0.02 | Strength with which the direction-of-motion axis is extended according to movement velocity |
flexibility | number | radian/second | 16.0 | Oscillation speed when returning to the original shape after impact |
damping | number | 1/second | 7.0 | Rate 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
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
ground_y | number | canvas normalized position | 0.0 | Y coordinate of the floor to collide with |
start_y | number | canvas normalized position | 0.0 | Starting Y coordinate |
gravity | number | canvas normalized displacement/second² | 9.81 | Magnitude of acceleration toward the floor |
config Specific to mt.bounce_x
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
wall_x / ground_x | number | canvas normalized position | 0.0 | X coordinate of the wall to collide with. ground_x is a compatibility alias |
start_x | number | canvas normalized position | 0.0 | Starting X coordinate |
acceleration / accel_x / gravity | number | canvas normalized displacement/second² | 9.81 | Magnitude 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.
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)
| Type | Unit | Description |
|---|---|---|
table | - | In configuration-table form, a table containing position, velocity, deformation, and collision state |
Multiple number values | As appropriate for each value | In positional-argument form, returns the calculation result as eight values |
Return-Value Table
| Field | Type | Unit | Description |
|---|---|---|---|
pos | number | canvas normalized position | Calculated coordinate along the axis |
y | number | canvas normalized position | Calculated Y coordinate for mt.bounce_y. Same value as pos |
x | number | canvas normalized position | Calculated X coordinate for mt.bounce_x. Same value as pos |
velocity | number | canvas normalized displacement/second | Velocity along the axis at that time |
stretch_x / stretch_y | number | multiplier | X- and Y-axis stretch factors incorporating Squash & Stretch |
offset_y | number | Ratio of bounds_height | Correction used by mt.bounce_y to preserve the contact position |
offset_x | number | Ratio of bounds_width | Correction used by mt.bounce_x to preserve the contact position |
last_impact_time | number / nil | seconds | Time of the previous impact. nil if no impact has occurred yet |
next_impact_time | number / nil | seconds | Time of the next impact. nil if the object has already settled |
impact_count | integer | impacts | Number of impacts so far |
settled | boolean | - | 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
restitutionvalue greater than or equal to0and less than1. - The sign of
gravityoraccelerationis ignored; its absolute value is treated as the acceleration magnitude.
Example
-- 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_ymt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Character or part to modify |
groundY | number | canvas normalized coordinate | Required | Y coordinate at which to land |
config | table | - | nil | Settings such as starting position, contact reference, and physical parameters |
Return Value
| Type | Unit | Description |
|---|---|---|
table | - | Calculated bounce state |
Changes
| Field | Type | Unit | Description |
|---|---|---|---|
item.offset_y | number | canvas normalized displacement | Sets the calculated Y displacement |
item.offset_x | number | canvas normalized displacement | When ground_mode = "absolute", sets the displacement that preserves the current X position on the canvas |
item.stretch_x / item.stretch_y | number | multiplier | Sets the Squash & Stretch from the bounce state |
config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | ctx.time | Time to evaluate |
delay | number | seconds | 0.0 | Waiting time before motion begins |
start_mode | string | - | "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_y | number | canvas normalized position | nil | Starting position used when start_mode = "absolute" |
drop_height | number | canvas normalized displacement | 0.3 | Distance above the current position when start_mode = "relative" |
ground_mode | string | - | Same as start_mode | Reference for the landing position. "absolute" treats groundY as a fixed position on the canvas; "relative" follows transforms higher in the hierarchy |
align_to | string | - | "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 |
gravity | number | canvas normalized displacement/second² | 4.0 | Magnitude of acceleration toward the ground |
restitution | number | - | 0.45 | Coefficient of restitution |
start_velocity | number | canvas normalized displacement/second | 0.0 | Initial velocity along the Y axis |
squash | number | factor | 0.15 | Strength with which the contact axis is compressed according to impact velocity |
stretch | number | factor | 0.02 | Strength with which the direction-of-motion axis is extended according to movement velocity |
flexibility | number | radian/second | 16.0 | Oscillation speed when returning to the original shape after impact |
damping | number | 1/second | 7.0 | Rate 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
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Character or part to modify |
wallX | number | canvas normalized coordinate | Required | X coordinate of the collision target |
config | table | - | nil | Settings such as starting position, collision reference, and physical parameters |
Return Value
| Type | Unit | Description |
|---|---|---|
table | - | Calculated bounce state |
Changes
| Field | Type | Unit | Description |
|---|---|---|---|
item.offset_x | number | canvas normalized displacement | Sets the calculated X displacement |
item.offset_y | number | canvas normalized displacement | When config.wall_mode = "absolute", sets the displacement that preserves the current Y position on the canvas |
item.stretch_x / item.stretch_y | number | multiplier | Sets the Squash & Stretch at impact |
config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | ctx.time | Time to evaluate |
delay | number | seconds | 0.0 | Waiting time before motion begins |
start_mode | string | - | "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_x | number | canvas normalized position | nil | Starting position used when start_mode = "absolute" |
travel_distance | number | canvas normalized displacement | 0.3 | Distance from the current position in the direction opposite the wall when start_mode = "relative" |
wall_mode | string | - | Same as start_mode | Reference for the collision position. "absolute" treats wallX as a fixed position on the canvas; "relative" follows transforms higher in the hierarchy |
align_to | string | - | Determined from the direction of travel | Position 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 / acceleration | number | canvas normalized displacement/second² | 4.0 | Magnitude of acceleration toward the wall. accel takes precedence |
restitution | number | - | 0.45 | Coefficient of restitution |
start_velocity | number | canvas normalized displacement/second | 0.0 | Initial velocity along the X axis |
squash | number | factor | 0.15 | Strength with which the contact axis is compressed according to impact velocity |
stretch | number | factor | 0.02 | Strength with which the direction-of-motion axis is extended according to movement velocity |
flexibility | number | radian/second | 16.0 | Oscillation speed when returning to the original shape after impact |
damping | number | 1/second | 7.0 | Rate at which post-impact oscillation settles |
Example
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
config | table | - | Required | Settings for impact time and deformation characteristics |
config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | 0.0 | Current time. Before impact_time, returns values with no deformation |
impact_time | number | seconds | 0.0 | Time of impact. You can pass last_impact_time from mt.bounce_* |
squash | number | ratio | 0.15 | Maximum compression amount. With 0.2, the height is 80% at maximum compression |
stiffness | number | radian/second | 25.0 | Speed of return to the original shape |
damping | number | 1/second | 12.0 | Rate at which oscillation settles |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | multiplier | stretchX. X-axis stretch factor |
number | multiplier | stretchY. Y-axis stretch factor. It is the reciprocal of stretchX to preserve volume |
number | Ratio of bounds_height | offsetCorrection. 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
-- 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_heightTIP
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.
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
config | table | - | Required | Settings such as elapsed time since launch, initial velocity, direction, and gravity |
config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | 0.0 | Elapsed time since launch. Negative values are treated as 0 |
speed | number | canvas normalized displacement/second | 0.0 | Initial speed |
angle | number | degrees | 0.0 | Launch direction. 0 points right, and the positive direction is clockwise. -90 points straight up |
gravity | number | canvas normalized displacement/second² | 4.0 | Downward acceleration |
spin | number | degrees/second | 0.0 | Angular velocity. The returned rotation can be assigned directly to the field of the same name |
drag | number | 1/second | 0.0 | Exponential decay rate for air resistance. 0 means no decay |
start_x / start_y | number | canvas normalized position | 0.0 | Launch position. If omitted, returns displacement relative to the origin |
Positional-Argument Form
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | 0.0 | Elapsed time since launch. Negative values are treated as 0 |
speed | number | canvas normalized displacement/second | 0.0 | Initial speed |
angle | number | degrees | 0.0 | Launch direction |
gravity | number | canvas normalized displacement/second² | 4.0 | Downward acceleration |
spin | number | degrees/second | 0.0 | Angular velocity |
drag | number | 1/second | 0.0 | Exponential 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)
| Type | Unit | Description |
|---|---|---|
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 values | As appropriate for each value | In 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
-- 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.rotationKeeping 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 at0.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.
spindoes not bend the path, but a fragment turning several times per second reads as rolling in place and hides the arc. For 3Dyaw/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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
t | number | seconds | Required | Elapsed time. Negative values are treated as 0 |
speed | number | canvas normalized displacement/second | Required | Initial speed |
friction | number | 1/second | 0.0 | Decay rate. Larger values stop sooner |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized displacement | distance. Distance traveled so far |
number | canvas normalized displacement/second | currentSpeed. 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
-- 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 + travelledNOTE
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Current OnLayout or OnPath context |
target | character | part | - | Required | An element of ctx.chars or ctx.parts |
options | table | - | nil | (12+) { level = "global" | "character" | "part" } |
Return Values
| Value | Type | Unit | Description |
|---|---|---|---|
color | color | - | Copy of the resolved solid color, or of ctx.global.fill.color, which supplies the starting color for a Global gradient or Crayon |
mode | string | - | "solid" / "gradient" / "crayon" / "transparent" |
opacity | number | 0-1 | Product 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.
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
endColor 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.hueMethodis"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:
level | Returns |
|---|---|
"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:
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
endNOTE
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:
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Current OnLayout or OnPath context |
target | character | part | - | Required | An element of ctx.chars or ctx.parts |
options | table | - | 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 exitsout_- Deceleration (starts quickly and stops slowly). Suitable for entrancesin_out_- Accelerates in the first half and decelerates in the second
| Curve | When to use |
|---|---|
| Linear | Constant speed (no change) |
| Quad | Gentlest acceleration and deceleration |
| Cubic | Standard acceleration and deceleration. Start here when unsure |
| Quart | Steeper than Cubic |
| Sine | Extremely gentle easing derived from a sine wave |
| Circ | Circular-arc curve that rises sharply at the endpoint |
| Expo | Extremely strong exponential easing |
| Back | Overshoots once and then returns (extends outside 0-1) |
| Elastic | Oscillates like a spring before settling (extends outside 0-1) |
| Bounce | Bounces 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.
The three circ functions contain a square root, so normally constrain t to 0-1 before use.
mt.ease.cubic_bezier arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
x1 / y1 | number | - | Required | First control point |
x2 / y2 | number | - | Required | Second control point |
t | number | - | Required | Progress. 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
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
gap | number | Ratio of canvas width or height | 0.0 | Extra spacing added to adjacent advances. Positive values increase spacing in the direction of progression |
config | table | - | nil | Settings for reflow direction, targets, and fixed position |
config
| Field | Type | Unit | Default | Description |
|---|---|---|---|---|
mode | string | - | Determined from ctx.global.vertical | Direction to reflow: "horizontal" or "vertical" |
targets | table | - | All characters | Array 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 |
anchor | integer | - | First character in each line/column | Character 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"formode. - Indices in
targetsmust be within the valid range, strictly ascending, and free of duplicates. - All characters included in
targetsmust belong to the sameline_index. - When
targetsis specified,anchormust identify a character included in that array. - When
targetsis omitted,anchormust be a valid character index.
Example
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
Return Value
| Type | Unit | Description |
|---|---|---|
table | - | Array containing one group per line_index, ordered by typeset line/column order |
Each group has the following fields.
| Field | Type | Unit | Description |
|---|---|---|---|
line_index | integer | - | Index of the line/column corresponding to the group |
item_count | integer | items | Number of characters in the group |
items | table | - | Array of characters in reading order |
Example
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
end13. 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/stretchcount, because the Global does not reach a shape
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Target character, part or shape |
canvasX / canvasY | number | canvas normalized position | Required | Destination coordinates |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized displacement | The assigned offsetX |
number | canvas normalized displacement | The 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
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Target character or part |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized position | Anchor canvasX |
number | canvas normalized position | Anchor canvasY |
Example
-- 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.
| Option | Type | Default | Description |
|---|---|---|---|
position | boolean | true | Synchronize the pre-3D canvas position |
position_delta_x / position_delta_y | number | 0.0 | Untransformed-em displacements along the source-local X/Y axes |
rotation | boolean | true | Synchronize 2D rotation |
rotation_offset | number | 0.0 | Degrees added after synchronization |
size | string | "copy" | "none" / "copy" / "match_bounds" |
size_scale_x / size_scale_y | number | 1.0 | Positive per-axis multipliers |
depth | boolean | true | Synchronize z / yaw / pitch |
z_offset | number | 0.0 | Canvas-normalized displacement added to Z after synchronization |
yaw_offset / pitch_offset | number | 0.0 | Angles in degrees added after synchronization |
opacity | boolean | false | Synchronize opacity |
opacity_scale | number | 1.0 | Non-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.
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Target character or part |
canvasX / canvasY | number | canvas normalized position | Required | Destination coordinates |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized displacement | Required offsetX |
number | canvas normalized displacement | Required 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
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context containing canvas information |
canvasX / canvasY | number | canvas normalized position | Required | Coordinates to measure |
centerX / centerY | number | canvas normalized position | 0.5 | Center of the effect |
Return Value
| Type | Unit | Description |
|---|---|---|
number | Ratio of canvas height | Aspect-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
-- 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
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
item | table | - | Required | Target character or part |
anchor | string | - | Required | Name 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
mt.layout.pivot_at_2d(ctx, character, "top")
character.rotation = math.sin(ctx.time * 3.0) * 8.014. 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
targets | table | nil | - | All | Array of 1-based character or part indices. nil selects every target of the requested type |
targetType | string | - | "character" | Target type: "character" or "part" |
space | string | - | "canvas" | "canvas" or "before_global_rotation" |
Return Value (one of)
| Type | Unit | Description |
|---|---|---|
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.
| Field | Type | Unit | Description |
|---|---|---|---|
left / right | number | canvas normalized position | Left and right edges |
bottom / top | number | canvas normalized position | Bottom and top edges |
center_x / center_y | number | canvas normalized position | Center of the bounds |
width / height | number | canvas normalized displacement | Width 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
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout context |
path | table | - | Required | Return value of mt.path.arc_length |
options | table | - | nil | Placement settings |
options.align | string | - | "end" | Position at which to place the string. Specify "end", "start", or "center" |
options.gap | number | Ratio of canvas width | 0.0 | Additional spacing added to the host's character advance |
options.mode | string | - | "horizontal" | Host typesetting direction: "horizontal" or "vertical" |
options.target_type | string | - | "character" | Target type: "character" or "part" |
options.targets | table | - | All elements | Array of target indices in reading order |
Return Value
| Type | Unit | Description |
|---|---|---|
table | 0-1 | slots 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.
-- 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.
targets | On-Screen Arrangement | Front of the Queue |
|---|---|---|
| Omitted (ascending index) | Reversed (read right→left) | Character 1 |
| Reverse reading order | Correct (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 Direction | Position of the String End with align = "end" |
|---|---|
| Left→right | Right side of the screen |
| Right→left | Left side of the screen |
Example
-- 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-- 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)
endNOTE
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
p0 | table | coordinates | Required | Starting point { x, y } |
p1 | table | coordinates | Required | Control point near the start { x, y } |
p2 | table | coordinates | Required | Control point near the end { x, y } |
p3 | table | coordinates | Required | End point { x, y } |
t | number | 0-1 | Required | Position on the curve. Values outside the range are not clamped and are extrapolated unchanged |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | As defined by the input coordinates | x on the curve |
number | As defined by the input coordinates | y on the curve |
number | As defined by the input coordinates | Tangent tangentX |
number | As defined by the input coordinates | Tangent tangentY |
Example
-- 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 = ymt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
points | table | coordinates | Required | 1-based array containing at least two { x, y } points through which the curve passes |
t | number | 0-1 | Required | Progress across the entire curve. Clamped to 0-1 |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | As defined by the input coordinates | x on the curve |
number | As defined by the input coordinates | y on the curve |
number | As defined by the input coordinates | Tangent tangentX |
number | As defined by the input coordinates | Tangent tangentY |
Constraints and Errors
points must contain at least two points.
Example
-- 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 = ymt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
points | table | coordinates | Required | 1-based control-point array. Specifying canvas normalized coordinates allows use with mt.layout.place_2d and mt.layout.queue_on_path |
aspectRatio | number | - | 1.0 | Aspect ratio multiplied into X-axis distances. Normally pass ctx.canvas.aspect_ratio |
options | table | - | nil | Settings specifying the curve type |
options.kind | string | - | "catmull_rom" | "catmull_rom" or "bezier" |
Return Value
| Type | Unit | Description |
|---|---|---|
table | - | Path object providing distance-based lookup |
The path object has the following methods.
Constraints and Errors
pointsmust contain at least two points.- With
options.kind = "bezier",pointsmust contain exactly four points. - Specify
"catmull_rom"or"bezier"foroptions.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.
-- Keep characters upright on a path traveling from right to left
character.rotation = heading + 180.0Example
-- 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)
end17. 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
source | string | - | Required | SVG path data |
optionsOrSourceUnitsPerEm | number / table | - | nil | Number of source-data units per em, or { view_box, em_scale } |
Return Value
| Type | Unit | Description |
|---|---|---|
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_boxmust contain four finite numbers, and its width and height must be positive.em_scalemust be a finite positive number.units_per_emcannot be specified in the configuration table. Use the numeric form orem_scale.
Example
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
endIMPORTANT
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 }).
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.
| Method | Description |
|---|---|
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
template | MtDrawingPath | - | Required | Source 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
source | string | - | Required | Path 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
x / y | number | Path-local coordinates | Required | Starting 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
x / y | number | Path-local coordinates | Required | End 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
cx / cy | number | Path-local coordinates | Required | Control point |
x / y | number | Path-local coordinates | Required | End 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
cx1 / cy1 | number | Path-local coordinates | Required | Control point near the current position |
cx2 / cy2 | number | Path-local coordinates | Required | Control point near the end point |
x / y | number | Path-local coordinates | Required | End 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.
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
text | string | - | Required | Target string |
startChar | integer | - | Required | Starting code-point index. 1-based and inclusive |
endChar | integer | - | End | Ending code-point index. 1-based and inclusive |
Return Value
| Type | Unit | Description |
|---|---|---|
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
text | string | - | Required | Text cluster to classify |
Return Value
| Type | Unit | Description |
|---|---|---|
string | - | One of "kanji", "hiragana", "katakana", "punctuation", "latin", "digit", "space", or "other" |
Example
for _, character in ipairs(ctx.chars) do
if mt.text.classify(character.text) == "kanji" then
character.scale = 1.2
end
endNotes
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.
c3is character 3,p2is global part 2, and a bare number is a character in this function.a-bis an inclusive range.c1-c3,p2-p5and2-4are 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
0are ignored.
A character is listed when at least one of its parts is selected, so p5 lists the character that owns part 5.
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout or OnPath context |
specification | string | - | Required | Selection string |
options | table | - | 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
| Type | Unit | Description |
|---|---|---|
integer[] | - | Ascending character numbers (the ctx.chars indices), ready for ipairs |
Example
function OnLayout(ctx)
for _, characterNumber in ipairs(mt.target.chars(ctx, "1-3, 6, !c2")) do
ctx.chars[characterNumber].scale = 1.2
end
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | OnLayout or OnPath context |
specification | string | - | Required | Selection string |
options | table | - | 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
| Type | Unit | Description |
|---|---|---|
integer[] | - | Ascending global part numbers (the ctx.parts indices), ready for ipairs |
Example
function OnLayout(ctx)
for _, partNumber in ipairs(mt.target.parts(ctx, "c1 p9")) do
ctx.parts[partNumber].opacity = 0.5
end
end20. Clip Time
mt.timeline.progress(ctx, fallbackDuration?)
API level: 1+
Gets the current progress from 0 to 1.
Arguments
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context |
fallbackDuration | number | seconds | 4.0 | Virtual clip length used for looping when the timeline is unavailable |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Current 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context |
fallbackDuration | number | seconds | 4.0 | Virtual clip length used when the timeline is unavailable |
Return Value
| Type | Unit | Description |
|---|---|---|
number | seconds | Host 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.
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context |
fallbackDuration | number | seconds | 4.0 | Virtual clip length used for looping when the timeline is unavailable |
Return Value
| Type | Unit | Description |
|---|---|---|
number | seconds | Time remaining until the end of the clip |
Example
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context |
introSeconds | number | seconds | Required | Intro length. Negative values are treated as 0 |
outroSeconds | number | seconds | Required | Outro length. Negative values are treated as 0 |
fallbackDuration | number | seconds | 4.0 | Virtual clip length used when the timeline is unavailable |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | 0-1 | introProgress. Intro progress |
number | 0-1 | outroProgress. Outro progress |
Example
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context from which time is read |
start | number | seconds | Required | Start time within the context |
duration | number | seconds | Required | Length of the time window |
Return Value
| Type | Unit | Description |
|---|---|---|
number | 0-1 | Linear progress through the window, clamped before and after it |
Constraints and Errors
ctx.timemust be a number.startmust be a finite number.durationmust be a finite positive number.
Example
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Original context |
start | number | seconds | Required | Start time within the original context |
duration | number | seconds | Required | Length of the time window |
Return Value
| Type | Unit | Description |
|---|---|---|
table | - | Derived context whose time-related values are remapped to the specified window |
Constraints and Errors
ctx.timemust be a number.startmust be a finite number.durationmust be a finite positive number.
Example
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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Original context |
initialValue | Any | - | Required | Initial value passed to the first interval |
segments | table | - | Required | Array of animation or hold intervals |
options | table | - | nil | Settings used when the timeline is unavailable |
options.fallback_duration | number | seconds | 4.0 | Virtual clip length |
Return Value
| Type | Unit | Description |
|---|---|---|
| Any | - | Value of the last interval evaluated through the current time |
Example
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.
| Field | Type | Unit | Description |
|---|---|---|---|
segmentCtx.time | number | seconds | Time since the interval began |
segmentCtx.frame | number | frame | Number of frames since the interval began |
segmentCtx.fps | number | fps | Same frame rate as the caller |
segmentCtx.timeline.duration_seconds | number | seconds | Logical duration of the interval |
segmentCtx.timeline.duration_frames | number | frame | Logical duration of the interval converted to frames |
segmentCtx.timeline.progress | number | 0-1 | Progress within the interval |
Interval Forms
Each interval uses one of the following forms.
| Interval | Specification | Behavior |
|---|---|---|
| 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.
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.timemust be a number, andsegmentsandoptionsmust be tables.- Each interval must specify exactly one of
durationorhold. - A
durationinterval requires anevaluatefunction, which cannot be specified for aholdinterval. - An interval length must be a finite number greater than or equal to 0, or
"remaining". - Only one interval may specify
"remaining". options.fallback_durationmust 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.
| Type | Description |
|---|---|
table | Read-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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
angleDegrees | number | degrees | Required | Direction. 0 degrees points right, and the positive direction is clockwise |
radius | number | displacement | Required | Distance in that direction |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | As defined by radius | offsetX |
number | As defined by radius | screenOffsetY. 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
-- 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 + dymt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Context |
gap | number | Ratio of canvas width or height | 0.0 | Extra spacing added between advances |
config | table | - | nil | Realignment settings |
config.align | string | - | "baseline" | Cross-axis alignment in horizontal writing: "baseline", "top", "center", or "bottom" |
config.mode | string | - | "horizontal" | "horizontal" or "vertical" |
config.targets | table | - | All characters | Array of 1-based character indices to realign. Characters outside the target set are not changed |
config.anchor | integer | - | Smallest index in targets | Character index whose current position on the advance axis is fixed |
config.baseline | string | - | "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
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
resolvedGlobal | table | - | Required | ctx.global resolved in OnPreLayout |
canvasWidth / canvasHeight | number | px | Required | ctx.canvas.width and ctx.canvas.height |
naturalCenterX / naturalCenterY | number | canvas normalized position | Required | Natural center of the target. For characters, geometry.bounds_center_x/y; for parts, geometry.canvas_center_x/y |
canvasX / canvasY | number | canvas normalized position | Required | Destination coordinates |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized displacement | offsetX |
number | canvas normalized displacement | offsetY |
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
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
item | table | - | Required | Target character or part |
canvasX / canvasY | number | canvas normalized position | Required | Destination coordinates |
ctx | table | - | nil | If specified, inverts only the 2D transform of ctx.global |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | canvas normalized displacement | The assigned offsetX |
number | canvas normalized displacement | The assigned offsetY |
Changes
Directly updates the target's offset_x and offset_y.
Example
-- 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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
ctx | table | - | Required | Layout context |
targets | table | - | Required | Array of 1-based target indices |
targetType | string | - | "character" | "character" or "part" |
Return Value (one of)
| Type | Unit | Description |
|---|---|---|
table | - | Approximate bounds |
nil | - | If there are no valid targets |
The bounds table has the following fields.
| Field | Type | Unit | Description |
|---|---|---|---|
left / right / bottom / top | number | canvas normalized position | Approximate bounds at the transform stage described above |
center_x / center_y | number | canvas normalized position | Center of the group |
width / height | number | canvas normalized displacement | Width and height of the entire group |
Example
-- 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)
endmt.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
| Argument | Type | Unit | Default | Description |
|---|---|---|---|---|
progress | number | 0-1 | Required | Overall progress. Normally obtained from mt.timeline.progress |
introFraction | number | 0-1 | Required | Fraction of the overall duration treated as the intro |
outroFraction | number | 0-1 | Required | Fraction of the overall duration treated as the outro |
Return Value (multiple)
| Type | Unit | Description |
|---|---|---|
number | 0-1 | introProgress. Intro progress |
number | 0-1 | outroProgress. 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.
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)
endIf 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.
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.0andend_position = 1.0. - To transition colors inside the text, increase
start_positionand decreaseend_position. With the standard settings of0.2and0.8, the gradient interval begins 20% inward from each edge. - With
angle = 0, colors change from left to right; withangle = 90, from top to bottom; and withangle = -90or270, 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.
proj_i = (P_i - C) · DIf 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.
bbP0 = C + D * minProj
bbP1 = C + D * maxProjIf 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.
P0 = bbP0 + (bbP1 - bbP0) * t_s
P1 = bbP0 + (bbP1 - bbP0) * t_e3. 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.
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
endUse 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.