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 |