A single drawable on a DrawCanvas, returned by the primitive constructors and canvas:body. Methods update geometry, color, rotation/scale, visibility, or attach physics.
Position relative to the parent (assign a vec2/vec3); a physics body follows at the next physics step. nil once removed.
rotation
number?
read/write
Rotation in radians about Z, relative to the parent, or nil once the primitive is removed. Assign a number; a physics body follows at the next physics step.
restitution
number?
read/write
Bounciness, 0 or more (0 absorbs an impact, 1 keeps all its energy); nil if the handle has no physics body. Assign a number to change it at runtime.
friction
number?
read/write
Surface friction coefficient; nil if the handle has no physics body. Assign a number to change it at runtime.
ccd
boolean
read/write
Whether this body, when moving fast, also sweeps moving (kinematic and dynamic) bodies so it never tunnels through them; fast bodies always sweep static geometry. Assign a boolean to toggle.
The group this primitive belongs to (its transform composes with the group's), or nil if it is ungrouped. Assign a primitive to reparent (the local transform is kept, and a physics body moves with the primitive), or nil to detach; reparenting across canvases, or under the primitive itself or one of its descendants, raises.
cell
integer?
read/write
The atlas cell index (0-based) for a sprite drawn from an Atlas; assign to animate. Nil for primitives that are not atlas sprites; assigning to those is ignored.
Whether this primitive's drawn shape covers point (canvas-local world space). Geometry only — a named primitive is tested regardless of its visibility. Always false for text and groups, which have no single shape.
Example
-- A new shape can be tested from the next frame on.datumhue.time.wait_frames(1)
print(prim:contains(vec2(10, 10))) --> trueprint(prim:contains(vec2(30, 0))) --> false
The primitive's axis-aligned bounding box in canvas-local world space, or nil for primitives without a single drawn shape (text, groups, canvas:body bodies).
Attach a physics body. A DrawPrimitive's body simulates in the app's 2D world, a SceneNode's in its 3D world. See physics.BodyOptions for the shape + material fields. With no shape, the collider fits what the handle draws, measured when add_body runs: a draw primitive's drawing (a circle for a round one, a text's laid-out block, a group's children combined), or a scene node's mesh (a plane is a thin slab under it, a torus the disc it spans, a billboard sprite an upright cylinder standing on its pivot, anything else a unit box). A draw primitive's named shape also takes omitted sizes from its drawing; a scene node's takes width, height, and depth 1 and radius 0.5. The collider, unless built from points, is centered on the drawing; refit_body measures again. Sizes and points are in the handle's own units: the collider scales and mirrors with the handle and every group or node above it, except that a sprite sized in pixels keeps its unscaled collider. A size or point, or a collider reaching, past 1000000 units raises. The body follows whatever moves, turns, or scales the handle or anything above it at the next physics step: a static or kinematic body goes where its handle goes; a dynamic body simulates on its own and jumps, keeping its velocity, when your code moves it or something above it. Replacing a body keeps its joints. Raises, keeping any previous body, for a text span or a voxel volume, a negative or non-finite size, a non-finite point, a triangle naming a point that does not exist, or a world other than the jointed body's own.
Example
local floor = canvas:rect({
pos = vec2(-160, -120),
width = 320,
height = 20,
color = color("#7fbbb3"),
})
-- Without a shape, the collider fits what is drawn.
floor:add_body({ type = "static" })
prim:add_body({ restitution = 0.8 })
Box, cylinder, or cone height, or a capsule's straight length between its caps, in the handle's own units.
depth
number
taken from what the handle draws, or 1
Box depth in the handle's own units, 3D only.
radius
number
taken from what the handle draws, or 0.5
Circle, sphere, capsule, cylinder, or cone radius, in the handle's own units.
restitution
number
0.3
Bounciness, 0 or more: 0 absorbs an impact, 1 keeps all its energy.
friction
number
0.5
Surface friction, 0 or more.
mass
number
derived from the collider's size
Mass override, more than 0.
sensor
boolean
false
Make the body a non-colliding trigger volume that reports the bodies overlapping it: dynamic and kinematic ones, and static ones unless the sensor is static itself.
Axes to lock rotation on, on top of a 2D body's in-plane constraints.
ccd
boolean
false
Sweep this body against moving (kinematic and dynamic) bodies too when it moves fast; fast bodies always sweep static geometry so they never tunnel through it.
Attach a kinematic character controller to this body and return its CharacterController handle. The body must be kinematic. The controller solves walking/sliding/stair-stepping geometry only — gravity, jump, input, and camera stay in your code. Lengths, absolute or relative, are 0 to 1000000. Raises if the body is missing, not kinematic, or already has a controller (remove() to rebind).
Step-climbing config, or false to disable; stepping is expensive. Needs a flat-bottomed body (cube or cylinder); a capsule's rounded bottom slides back off the ledge. The body also needs head room of its own height plus max_height above where it stands, so a low ceiling stops it stepping.
normal_nudge_factor
number
0.0001
Tiny anti-stick push along contact normals while sliding, 0 to 1.
Apply an instantaneous, mass-correct velocity change of impulse / mass. Accepts vec2 or vec3.
Example
prim:add_body({ mass = 2 })
prim:apply_impulse(vec2(100, 0))
-- The velocity changes by impulse / mass, in canvas units a second.print(prim.velocity.x) --> 50.0
Set, replace, or clear (pass nil) the handler fired when this body starts colliding. contact is nil when either body is a sensor. Not fired once this body's own handle has been removed, even by an earlier collision handler in the same step; other may have been removed that way, and its handle still compares equal to one you hold.
Set, replace, or clear (pass nil) the handler fired when this body stops colliding. Removing either body of a touching pair (its handle, or only its body, as remove_body does) ends the contact on the next physics step; other may be the removed body, and its handle still compares equal to one you hold. Not fired once this body's own handle has been removed, even by an earlier collision handler in the same step.
Measure what the handle draws again and resize its collider to fit, in place: velocity, joints, and contacts carry on. Sizes given to add_body stay; the ones it took from the drawing, and the collider's center, follow the drawing as it is now (a text laid out again, a group whose children moved). Raises, keeping the collider, for a handle with no body, a voxel volume (its body follows its cells), or a collider that would reach more than 1000000 units from the body.