Skip to content

Target API level: 10

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

NOTE

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

Index by Purpose ​

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

1. Value Constraints and Range Mapping ​

mt.clamp(value, low, high) ​

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number-Constrained value

mt.saturate(value) ​

API level: 1+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
valuenumber-RequiredValue to constrain to 0-1

Return Value ​

TypeUnitDescription
number0-1Value constrained to 0-1

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

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number-Converted value

Constraints and Errors ​

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

Example ​

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

mt.wrap(value, boundaryA, boundaryB) ​

API level: 1+

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

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number-Wrapped value

Constraints and Errors ​

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

2. Interpolation and Keyframes ​

mt.lerp(from, to, t) ​

API level: 1+

Linearly interpolates from from to to.

Arguments ​

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

Return Value ​

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

mt.inverse_lerp(from, to, value) ​

API level: 1+

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

Arguments ​

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

Return Value ​

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

Constraints and Errors ​

Returns 0 if from == to.


mt.lerp_angle(from, to, t) ​

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
numberdegreeInterpolated angle

mt.smoothstep(edge0, edge1, value) ​

API level: 1+

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

Arguments ​

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

Return Value ​

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

Constraints and Errors ​

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

mt.keyframes(keys, time) ​

API level: 1+

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

Arguments ​

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

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

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

Return Value ​

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

Example ​

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

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

3. Progress Distribution ​

mt.distribute(index, count) ​

API level: 1+

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

Arguments ​

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

Return Value ​

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

Example ​

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

mt.order_text(entries) ​

API level: 8+

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

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

Arguments ​

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

Return Value ​

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

Example ​

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

mt.CHAR ​

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

mt.PART ​

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

mt.NOT ​

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


mt.each_char(ctx, options?) ​

API level: 6+

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

Arguments ​

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

Return Value ​

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

Fields of info:

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

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

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

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

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

Example ​

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

mt.each_part(ctx, options?) ​

API level: 6+

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

Arguments ​

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

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

Return Value ​

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

info carries every field from mt.each_char plus:

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

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

Example ​

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

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

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number0-1Progress clamped to 0-1

Constraints and Errors ​

ConditionDescription
duration <= 0Specify a value greater than 0

Example ​

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

mt.stagger_progress(progress, position, span) ​

API level: 6+

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

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number0-1Progress for the element at position

The result is calculated as follows:

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

Constraints and Errors ​

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

Example ​

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

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

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

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


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

API level: 1+

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

Arguments ​

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

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

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

Return Value ​

TypeUnitDescription
number0-1Progress clamped to 0-1

Constraints and Errors ​

ConditionDescription
duration <= 0Specify a value greater than 0

4. Distance-Based Influence ​

mt.falloff(distance, radius) ​

API level: 3+

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

Arguments ​

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

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

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

Return Value ​

TypeUnitDescription
number0-1Distance-based influence weight

Example ​

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

NOTE

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

5. Deterministic Random Values and Noise ​

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

API level: 1+

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

TIP

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

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

Arguments ​

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

Return Value ​

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

Constraints and Errors ​

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


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

API level: 1+

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

Arguments ​

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

Return Value ​

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

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

API level: 8+

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

Arguments ​

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

Return Value ​

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

Example ​

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

mt.noise1(x, seed?) ​

API level: 1+

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

Arguments ​

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

Return Value ​

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

Example ​

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

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

API level: 1+

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

Arguments ​

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

Return Value ​

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

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

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
numberSame as amplitudeFluctuation value

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

6. Cycles and Springs ​

mt.cycle(t, period) ​

API level: 1+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
periodnumbersecondsRequiredLength of one cycle

Return Value ​

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

Constraints and Errors ​

ConditionDescription
period <= 0Specify a value greater than 0

mt.pingpong(t, period) ​

API level: 1+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
periodnumbersecondsRequiredLength of one round trip

Return Value ​

TypeUnitDescription
number0-1Oscillating value from 0 to 1

Constraints and Errors ​

ConditionDescription
period <= 0Specify a value greater than 0

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

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number-1-1Waveform value

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

API level: 1+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

TypeUnitDescription
number--1.0 or 1.0

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

API level: 1+

A triangle wave.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

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

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

API level: 1+

A sawtooth wave.

Arguments ​

ArgumentTypeUnitDefaultDescription
tnumbersecondsRequiredTime to evaluate
frequencynumberHzRequiredNumber of cycles per second
phasenumberradian0Phase offset

Return Value ​

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

mt.spring(t, frequency, damping) ​

API level: 1+

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

Arguments ​

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

Return Value ​

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

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

WARNING

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

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

Example ​

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

7. Displacement from Polar Coordinates ​

mt.polar_offset_2d(angleDegrees, radius) ​

API level: 5+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
number, numbercanvas normalized displacementTwo values: offsetX, offsetY

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

Example ​

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

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

API level: 15+

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

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

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

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

8. Physical Motion ​

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

API level: 3+

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

Arguments ​

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

Shared config ​

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

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

config Specific to mt.bounce_y ​

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

config Specific to mt.bounce_x ​

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

Positional-Argument Form ​

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

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

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

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

Return Value (one of) ​

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

Return-Value Table ​

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

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

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

Notes ​

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

Example ​

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

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

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

API level: 3+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
table-Calculated bounce state

Changes ​

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

config ​

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

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

Example ​

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

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

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

NOTE

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


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

API level: 3+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
table-Calculated bounce state

Changes ​

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

config ​

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

Example ​

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

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

mt.impact_squash(config) ​

API level: 3+

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

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

Arguments ​

ArgumentTypeUnitDefaultDescription
configtable-RequiredSettings for impact time and deformation characteristics

config ​

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

Return Value (multiple) ​

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

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

Example ​

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

TIP

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

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

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

API level: 3+

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

Arguments ​

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

config ​

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

Positional-Argument Form ​

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

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

Return Value (one of) ​

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

IMPORTANT

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

Example ​

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

Keeping the arc readable ​

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

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

mt.friction_decay(t, speed, friction) ​

API level: 3+

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

Arguments ​

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

Return Value (multiple) ​

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

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

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

Example ​

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

NOTE

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

9. Color ​

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

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

API level: 6+

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

Arguments ​

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

Return Values ​

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

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

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

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

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

Color construction, decomposition, and interpolation ​

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

API level: 1+

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

API level: 8+

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

10. Style Inheritance ​

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

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

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

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

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

NOTE

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

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

Arguments of every resolve function:

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

API level: 12+

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

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

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

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

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

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

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

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

11. Easing ​

API level: 1+

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

Names combine a curve type and a direction.

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

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

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

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

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

mt.ease.cubic_bezier arguments ​

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

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

Example ​

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

12. Reflow and Line-Based Grouping ​

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

API level: 2+

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

Arguments ​

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

config ​

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

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

Return Value ​

There is no return value.

Changes ​

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

Constraints and Errors ​

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

Example ​

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

mt.layout.group_by_line(ctx) ​

API level: 4+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context

Return Value ​

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

Each group has the following fields.

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

Example ​

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

13. Canvas Position, Distance, and Pivot ​

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

API level: 2+

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

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

Arguments ​

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

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementThe assigned offsetX
numbercanvas normalized displacementThe assigned offsetY

Changes ​

Directly updates the target's offset_x and offset_y.

Constraints and Errors ​

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

Example ​

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

mt.layout.get_canvas_position_2d(ctx, item) ​

API level: 3+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
ctxtable-RequiredOnLayout context
itemtable-RequiredTarget character or part

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized positionAnchor canvasX
numbercanvas normalized positionAnchor canvasY

Example ​

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

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

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

API level: 9+

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

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

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

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

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

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

API level: 3+

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

Arguments ​

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

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementRequired offsetX
numbercanvas normalized displacementRequired offsetY

The target's fields are not modified.

Constraints and Errors ​

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

Example ​

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

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

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

API level: 3+

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

Arguments ​

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

Return Value ​

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

IMPORTANT

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

Example ​

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

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

API level: 4+

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

Arguments ​

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

Return Value ​

There is no return value.

Changes ​

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

anchor ​

Characters and parts share these anchors:

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

Characters additionally support these anchors:

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

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

Example ​

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

14. Bounds After 2D Transforms ​

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

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

API level: 2+

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

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

Arguments ​

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

Return Value (one of) ​

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

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

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

Constraints and Errors ​

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

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

Example ​

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

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

15. Alignment on Paths ​

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

API level: 3+

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

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

Arguments ​

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

Return Value ​

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

slots represents positions on the path, not animation progress.

Direction of Travel and Reading Order ​

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

IMPORTANT

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

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

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

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

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

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

Example ​

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

NOTE

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

16. Motion Paths ​

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

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

NOTE

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

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

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

API level: 1+

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

Arguments ​

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

Return Value (multiple) ​

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

Example ​

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

mt.path.catmull_rom(points, t) ​

API level: 1+

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

Arguments ​

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

Return Value (multiple) ​

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

Constraints and Errors ​

points must contain at least two points.

Example ​

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

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

API level: 3+

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

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

Arguments ​

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

Return Value ​

TypeUnitDescription
table-Path object providing distance-based lookup

The path object has the following methods.

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

Constraints and Errors ​

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

NOTE

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

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

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

Example ​

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

17. Path Creation and Editing ​

mt.svg_path(source, optionsOrSourceUnitsPerEm?) ​

API level: 5+

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

Arguments ​

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

Return Value ​

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

Normalization Settings ​

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

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

Constraints and Errors ​

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

Example ​

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

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

IMPORTANT

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

Notes ​

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


mt.drawing_path() ​

API level: 9+

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

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

Drawing Path Editing API ​

API level: 5+

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

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

Coordinate System ​

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

Applying Changes ​

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


path:clear() ​

API level: 5+

Deletes all commands in the path.

Return Value ​

There is no return value.

Changes ​

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


path:assign(template) ​

API level: 5+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
templateMtDrawingPath-RequiredSource path template to copy

Return Value ​

There is no return value.

Changes ​

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


path:set_svg(source) ​

API level: 5+

Parses SVG path data and replaces the target path.

Arguments ​

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

Return Value ​

There is no return value.

Changes ​

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


path:move_to(x, y) ​

API level: 5+

Adds a new subpath beginning at the specified coordinates.

Arguments ​

ArgumentTypeUnitDefaultDescription
x / ynumberPath-local coordinatesRequiredStarting point of the subpath

Return Value ​

There is no return value.

Changes ​

Adds a move command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


path:line_to(x, y) ​

API level: 5+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
x / ynumberPath-local coordinatesRequiredEnd point of the line

Return Value ​

There is no return value.

Changes ​

Adds a line command to the target path.

Constraints and Errors ​

Coordinates must be finite numbers.


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

API level: 5+

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

Arguments ​

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

Return Value ​

There is no return value.

Changes ​

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

Constraints and Errors ​

Coordinates must be finite numbers.


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

API level: 5+

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

Arguments ​

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

Return Value ​

There is no return value.

Changes ​

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

Constraints and Errors ​

Coordinates must be finite numbers.


path:close() ​

API level: 5+

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

Return Value ​

There is no return value.

Changes ​

Adds a close command to the target path.


path:bounds() ​

API level: 9+

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


path:map_points(callback) ​

API level: 9+

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

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

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

18. UTF-8 Text Processing ​

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

API level: 1+

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

Arguments ​

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

Return Value ​

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

mt.text.classify(text) ​

API level: 4+

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

Arguments ​

ArgumentTypeUnitDefaultDescription
textstring-RequiredText cluster to classify

Return Value ​

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

Example ​

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

Notes ​

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

19. Character and Part Selection ​

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

API level: 13+

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

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

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

Arguments ​

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

Return Value ​

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

Example ​

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

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

API level: 13+

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

Arguments ​

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

Return Value ​

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

Example ​

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

20. Clip Time ​

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

API level: 1+

Gets the current progress from 0 to 1.

Arguments ​

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

Return Value ​

TypeUnitDescription
number0-1Current progress

Constraints and Errors ​

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


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

API level: 6+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
numbersecondsHost clip duration or fallback duration

Constraints and Errors ​

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

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

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

API level: 1+

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

Arguments ​

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

Return Value ​

TypeUnitDescription
numbersecondsTime remaining until the end of the clip

Example ​

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

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

API level: 1+

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

Arguments ​

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

Return Value (multiple) ​

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

Example ​

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

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

API level: 6+

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

Arguments ​

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

Return Value ​

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

Constraints and Errors ​

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

Example ​

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

character.opacity = appear * (1.0 - disappear)

Notes ​

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


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

API level: 4+

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

Arguments ​

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

Return Value ​

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

Constraints and Errors ​

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

Example ​

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

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

Notes ​

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


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

API level: 4+

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

Arguments ​

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

Return Value ​

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

Example ​

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

Interval Context ​

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

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

Interval Forms ​

Each interval uses one of the following forms.

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

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

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

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

Constraints and Errors ​

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

IMPORTANT

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

21. Deprecated Compatibility APIs ​

mt.storage Deprecated ​

API level: 1+

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

TypeDescription
tableRead-only compatibility storage

mt.polar_offset(angleDegrees, radius) Deprecated ​

API level: 3+

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

Arguments ​

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

Return Value (multiple) ​

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

WARNING

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

Example ​

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

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

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

API level: 1+

CAUTION

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

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

Arguments ​

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

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

Return Value ​

There is no return value.

Changes ​

Directly rewrites offset_x and offset_y for the target characters.

WARNING

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

Example ​

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

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

API level: 1+

CAUTION

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

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

Arguments ​

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

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementoffsetX
numbercanvas normalized displacementoffsetY

Constraints and Errors ​

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

Example ​

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

Notes ​

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


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

API level: 1+

CAUTION

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

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

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

Arguments ​

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

Return Value (multiple) ​

TypeUnitDescription
numbercanvas normalized displacementThe assigned offsetX
numbercanvas normalized displacementThe assigned offsetY

Changes ​

Directly updates the target's offset_x and offset_y.

Example ​

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

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

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

API level: 1+

CAUTION

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

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

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

Arguments ​

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

Return Value (one of) ​

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

The bounds table has the following fields.

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

Example ​

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

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

API level: 1+

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

WARNING

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

Arguments ​

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

Return Value (multiple) ​

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