GUI

2D overlay primitives, post-processing, and skybox shaders. Everything drawn over the 3D scene lives here.

local GUI = import("GUI")
local Dim = import("Primitives").Dim

local hp = GUI.Basic.Square()
hp.Size     = Dim.new(200, 12)
hp.Position = Dim.new(24, 24)
hp.Color    = Color3.FromHex("#9bd13b")

GUI.Basic, primitive factories

GUI.Basic.Square() -> Primitive

Solid-coloured rectangle. Tinted by .Color; sized by .Size.

GUI.Basic.Circle() -> Primitive

Solid-coloured circle inscribed in .Size. The major axis follows the longer of .Size.X and .Size.Y, pass equal width / height for a true circle.

GUI.Basic.Triangle() -> Primitive

Solid-coloured triangle inscribed in .Size: apex at top-center, base on the bottom edge. Useful for arrows / indicators, rotate via a custom shader if you need arbitrary orientations.

GUI.Basic.Image(asset) -> Primitive

Textured rectangle. Pass an ImageAsset; the asset's pixels become the surface.

GUI.Basic.Font(asset) -> Primitive

Text label. Pass a FontAsset; glyphs are rasterised at runtime sized by .TextSize and colored by the primitive's .Color. Supports synthetic styling (.FontStyle, .Underline, .Strikethrough) and inline color escapes in .Text (see below). Styling is applied at bake time — weight is a coverage dilation, italic is a horizontal shear, and underline / strikethrough are drawn rules. Pair with a real bold / italic TTF for pixel-perfect typography.

local label = GUI.Basic.Font(font)
label.Text          = "Headline {#ff5050}danger{} resumed"
label.TextSize      = 48
label.FontStyle     = "BoldItalic"
label.Underline     = true
label.Visible       = true
GUI.Basic.Clippable() -> Primitive new in 1.1

Invisible clip container with a pixel-space .Position, .Size, and .Rotation. Doesn't render anything itself — its job is to be a parent for children added via :AddClippable(child). Anything inside those children that falls outside the Clippable's shape is discarded in the children's fragment shader. Resizing, moving, or rotating the Clippable retroactively re-clips every child the next frame, so it works as a scrolling viewport, masked overlay, draggable panel, or rotating reticle without having to re-add children.

The clip area can be reshaped via :SetShape("Square" | "Circle" | "Triangle"). The default is "Square" (axis-aligned rect in the Clippable's local space). Each child's fragment shader transforms its pixel into the Clippable's local UV space (accounting for rotation), tests it against the chosen shape, and discards if outside — so a rotated Circle clip really is a rotated ellipse, not an AABB approximation.

local mask = GUI.Basic.Clippable()
mask.Position = Dim.new(100, 100)
mask.Size     = Dim.new(400, 300)
mask.Rotation = 15                       -- degrees; children clip to the rotated rect
mask:SetShape("Circle")               -- or "Square" (default) / "Triangle"

local img = GUI.Basic.Image(asset)
img.Position = Dim.new(80, 90)            -- partly outside the mask
img.Size     = Dim.new(500, 500)
mask:AddClippable(img)                     -- only pixels inside the rotated ellipse render
Custom shape via shader. The ruzit_clip_test(frag_pos) function in the fragment-shader prelude performs the inverse-rotation + shape test using the Clippable's rotation_pad.y (clip rotation), rotation_pad.z (clip shape ID), and clip_rect (clip xy position + zw size) uniforms. Custom child shaders that need to do their own clipping can call it directly; shaders that don't will skip clipping. To make a Clippable apply a custom shape via shader, attach a shader whose body discards pixels you want excluded — children whose fragment shader also calls ruzit_clip_test automatically pick up the shape because the test uses the Clippable's shape ID, not its sampled output.
GUI.Basic.Spline() -> Spline

Polyline primitive. Unlike the other GUI.Basic.* factories, a Spline has no global Position or Size — the nodes determine its shape. Each node contributes a position (Dim), a per-node thickness (pixels), and a per-node tangent angle that rotates how the curve enters and leaves the node. With angle = 0 you get a straight polyline through the nodes; non-zero angles bend the curve to leave / arrive at that tangent direction, so you can build arcs and S-curves with just a handful of nodes. Geometry is sampled along a cubic Bezier per segment (control points derived from the node tangents), then triangulated into a ribbon with caps at the start/end.

local line = GUI.Basic.Spline()
line.Color = Primitives.Color3.fromRGB(120, 200, 255)
line.Cap   = "Rounded"

-- All Angle = 0 gives a straight polyline through the points.
line:SetNodes{
    { Position = Dim.new(100, 200), Thickness = 6 },
    { Position = Dim.new(300, 200), Thickness = 8 },
    { Position = Dim.new(500, 120), Thickness = 4 },
}

-- Add tangent angles to arc the line through the middle node.
-- The first & last nodes' angles control how the line enters/leaves the endpoints;
-- the middle node's 45 degrees pulls the curve into a smooth arc through it.
line:SetNodes{
    { Position = Dim.new(100, 300), Thickness = 6, Angle = 0   },   -- leaves horizontally
    { Position = Dim.new(300, 200), Thickness = 8, Angle = 45  },   -- curve passes through at +45
    { Position = Dim.new(500, 300), Thickness = 4, Angle = 0   },   -- arrives horizontally
}

Spline

Polyline primitive (returned by GUI.Basic.Spline()). Renders in its own pass after the regular 2D items, sorted by .ZIndex among splines. Supports 2D fragment shaders via :AttachShader and the standard FRAGMENT_PRELUDE.

Properties

PropertyTypeDefaultNotes
ColorColor3(1, 1, 1)Whole-spline base color. Tweenable via :Tween's Color option.
Transparencynumber00..1, applied as alpha multiplier.
ZIndexnumber0Splines sort among themselves; all splines render above regular 2D items.
VisiblebooleantrueHides the spline without destroying it.
Stylestring"Solid""Solid" / "Dashed" / "Dotted". Pixel-accurate pattern is generated in the fragment shader using Length + Padding below.
Capstring"Rounded""Rounded" / "Miter" / "Square". Applied at the start and end nodes.
Lengthnumber12Length of each visible dash, in pixels along the line. Dashed only — ignored for Solid and Dotted.
Paddingnumber8Gap between dashes / dots, in pixels along the line. Used by both Dashed and Dotted styles. For Dotted, the visible dot extent is automatically 40% of Padding (no separate size knob — thickness controls dot diameter perpendicular to the line; padding controls the cycle).
NodeCountnumberRead-only.
AlivebooleanRead-only.
ChangedSignal<string>Fires with property name on any change. Connect for "anything changed" notifications.

Node table shape

{
    Position  = Dim.new(x, y),
    Thickness = 4,                    -- pixels, default 4
    Angle     = 0,                    -- degrees, tangent rotation. 0 = straight through; non-zero curves the line.
}

Methods

spline:SetNodes({ ... })method

Replace the entire node array. Cancels any active tween. Fires .Changed("Nodes").

spline:GetNodes()-> { SplineNode }method

Snapshot copy of the current node array.

spline:InsertNode(index, node)method

1-based insertion. index = 1 prepends, index > NodeCount appends. Doesn't touch the tween — useful for hand-editing during an animation.

spline:RemoveNode(index)method

1-based removal. Errors if out of range.

spline:SetNode(index, node)method

Replace a single node in place.

spline:AttachShader(asset, priority?)method

Attach a FragmentAsset (or ShaderAsset) as the fragment stage. The shader receives the same U: RuzitUni uniform and the standard prelude; in.uv.x is the along-line parameter (0 at start, 1 at end) and in.uv.y is the cross-line parameter (0 on one edge, 1 on the other).

spline:ClearShaders()method

Detach all shaders; reverts to the default solid-color fragment.

spline:Destroy()method

Mark dead; the spline stops rendering and is reaped on the next snapshot.

Animating splines

Splines tween through TweenService.new like every other tweenable primitive. The supported goal properties are Color, Transparency, ZIndex, and Nodes (an array of node tables with the same shape as :SetNodes):

local Tween = import("TweenService")
local tw = Tween.new(line, 0.6, "Quad", "InOut", {
    Color = Primitives.Color3.fromRGB(255, 80, 80),
    Nodes = {
        { Position = Dim.new(100, 200), Thickness = 10 },
        { Position = Dim.new(300, 120), Thickness = 10 },
        { Position = Dim.new(500, 240), Thickness = 10 },
        { Position = Dim.new(700, 120), Thickness = 10 },
    },
})
tw:Play()

Node-count changes are handled transparently:

v1 limitations. Splines render in a dedicated pass after the regular 2D items, so a spline cannot interleave at a particular z-index between other GUI primitives — all splines are above all primitives. Spatial queries (CheckArea / Raycast) currently use the spline's AABB rather than precise segment tests; expect false positives near the bounding rect.

Primitive

All five factories return a Primitive. Properties are read/write; assignment fires .Changed with the property name.

Properties

.Shape"Square" | "Circle" | "Triangle" | "Image" | "Text" | "Clippable"read-only

"Text" covers both Font and StyledFont primitives — styling is a property toggle, not a separate shape.

.SizeDim

Pixel-space size. For Text this is (0, 0), size is driven by .TextSize.

.PositionDim

Top-left position. Origin at the top-left of the window.

.AnchorPointDim

Fractional anchor in 0..1 used by layout systems (e.g. Declar) to position the primitive relative to its own pixel rectangle: (0, 0) anchors the top-left corner, (0.5, 0.5) centres the primitive on its .Position, (1, 1) anchors the bottom-right. The engine reads this for baked-text re-centring and for Billboard alignment; the renderer itself still uses .Position as a top-left anchor, so most game code only touches .AnchorPoint through a layout library. No .Changed fire — it's a layout input, not an observable visual property.

.Rotationnumber (degrees)

Rotation around the primitive's center. 0 is the natural orientation; positive values rotate clockwise in screen space (Y-down). Applies to every primitive shape and inherits through AnimatedImage / DrawableImg since both render via a Primitive. Note: spatial queries (GUI.Raycast, GUI.OverlapBox, etc.) still hit-test against the un-rotated AABB — visual rotation only.

.ColorColor3

Tint when texture-less; image tint when textured; box colour for Text.

.Transparencynumber

0 opaque, 1 fully transparent.

.ZIndexnumber

Higher = drawn on top.

.Visibleboolean

Hide without destroying. Defaults to false on every freshly-created primitive — flip it to true once you've finished configuring size / position / image / text, so half-built primitives don't flash on screen for a frame.

.Textstring

Text content. No-op on non-Text primitives. Glyphs are colored by the primitive's regular .Color — there's no separate TextColor. Inline color escapes are supported: {#RRGGBB} or {#RGB} switches the active color, {} resets to the base .Color, and {{ writes a literal {. Tags whose contents don't match are left as literal text.

label.Text = "Loaded {#3cb371}OK{}, {#ff5050}3 errors{} found"
.TextSizenumber

Glyph height in pixels.

.FontStylestring

Synthetic font variant applied at bake time. Defaults to "Regular". Setting an unknown value errors; call primitive:ListFontStyles() to enumerate. Italic variants shear the glyphs ~12°; weight variants dilate (or erode) glyph coverage. Both are inexpensive but visibly approximate — pair with a real bold / italic TTF if you need typographic precision.

.Underlineboolean

Draw a baseline rule. Off by default.

.Strikethroughboolean

Draw a rule through x-height. Combinable with .Underline.

.ChangedSignal<string>read-only

Methods

prim:GetPropertyChanged(propName) -> Signal<any> method new in 1.1

Per-property change signal. Fires with the new value as its only argument every time propName is set — "Position" fires with a Dim, "Color" with a Color3, "Transparency" with a number, etc. Lazily created: properties no one listens to cost zero. When the last connection disconnects the engine stops firing it entirely.

square:GetPropertyChanged("Position"):Connect(function(p)
    print(p.x, p.y)
end)
prim:ListFontStyles() -> { string } method

Only valid on Font primitives. Returns every value accepted by .FontStyle, in canonical casing: Regular, Italic, Light, LightItalic, Medium, MediumItalic, SemiBold, SemiBoldItalic, Bold, BoldItalic, ExtraBold, ExtraBoldItalic, Black, BlackItalic. Use to populate a dropdown or validate user input.

for _, style in label:ListFontStyles() do
    print(style)
end
prim:AddClippable(child) method new in 1.1

Only valid on Shape == "Clippable" primitives. Register child to be clipped to this primitive's current shape, position, size, and rotation every frame. Pixels of child outside the (possibly rotated) shape are discarded in the child's fragment shader. Reading the Clippable's Size / Position / Rotation / Shape retroactively re-clips every child on the next snapshot — no need to re-add. Last-add-wins for re-parenting.

prim:SetShape(name) method

Only valid on Shape == "Clippable" primitives. Choose the clip-area shape: "Square" (default — axis-aligned rectangle in the Clippable's local space), "Circle" (ellipse inscribed in the Size box), or "Triangle" (apex-up isosceles triangle). Combined with .Rotation you get rotated ellipses, rotated triangles, etc. Children re-clip on the next frame without needing to be re-added.

prim:RemoveClippable(child) method new in 1.1

Detach a child added via :AddClippable. The child resumes rendering unclipped. Idempotent if child wasn't added to this specific Clippable.

prim:Destroy()method
prim:AttachShader(asset, priority?)method

Attach a 2D fragment shader to this primitive's draw call. priority (integer, default 0) controls draw order when multiple shaders are attached: lower values are drawn first (become the base layer); higher values are drawn later (appear on top). Ties keep the attach order. For an effect that overlays an underlying one, give the underneath shader a lower priority — prim:AttachShader(base, 0); prim:AttachShader(overlay, 1) draws base first then overlay over it.

prim:DetachShader(asset)method
prim:ClearShaders()method
prim:SetData(asset, name, value)method
prim:GetData(asset, name)-> number?method

Skybox & post effects

Moved as of 1.2.1. SetSkybox, ClearSkybox, SetPostEffect and ClearPostEffect now live on LightingService. The signatures and semantics are unchanged — they still return a SceneShader handle — the only thing that moved is the namespace.

-- old (1.2.0 and earlier):  removed
-- GUI.SetSkybox(asset)
-- GUI.SetPostEffect(asset)

-- new (1.2.1+):
local LightingService = import("LightingService")
local sky    = LightingService.SetSkybox(Asset.GetAsset("Fragment", "sky.frag"))
local grade  = LightingService.SetPostEffect(Asset.GetAsset("Fragment", "damage.frag"))

AnimatedImage

Sprite-sheet animation primitive driven by Sparrow / Flash-style <TextureAtlas> XML. GUI.AnimatedImage(xml, source) parses the XML into a map of named frames; the XML itself does not define animations. Animations are built at runtime by calling :CreateTrack(frames, opts) on the returned AnimatedImage, passing the names of the SubTextures to play in order. Each track carries its own FPS, Looped, Priority, and DidLoop / Ended signals.

XML schema

Standard Sparrow output (e.g. from the FNF spritesheet & XML generator) works directly:

<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas imagePath="Test.png">
    <SubTexture name="Down0000" x="404" y="0" width="404" height="404"
                frameX="2" frameY="2" frameWidth="400" frameHeight="400"/>
    <SubTexture name="Up0000"   x="0"   y="0" width="404" height="404"
                frameX="2" frameY="2" frameWidth="400" frameHeight="400"/>
</TextureAtlas>

Per-SubTexture attributes:

Worked example

local GUI    = import("GUI")
local Asset  = import("Asset")
local Primitives = import("Primitives")
local Dim = Primitives.Dim

local sheet  = Asset.GetAsset("Image", "sprites.player")
local atlas  = Asset.GetAsset("File", "sprites.player_xml")
local player = GUI.AnimatedImage(atlas, sheet)
player.Position = Dim.new(100, 100)
player.Size     = Dim.new(200, 200)

-- Tracks are built at runtime from SubTexture names.
local down_idle = player:CreateTrack(
    {"Down0000", "Down0001", "Down0002", "Down0003"},
    { FPS = 12, Looped = true, Priority = 1 }
)
down_idle:Play()

-- Comma-separated string also works.
local up_hit = player:CreateTrack(
    "Up0000,Up0001,Up0002",
    { FPS = 18, Priority = 10 }
)
up_hit.Ended:Connect(function() up_hit:Stop() end)

function on_player_hit()
    up_hit:Play()       -- priority 10 wins over priority 1 idle
end                    -- when up_hit ends, down_idle becomes visible again

-- Icon-style usage: pin a single named frame.
player:JumpToFrame("Down0000")

Priority blending

Multiple tracks can be :Play()'d at the same time. Each tick the engine picks the highest-priority Playing track and renders its current frame. Lower-priority tracks keep ticking silently in the background. When a higher-priority track ends (non-looped) the next-highest still-Playing track immediately becomes visible. Looped tracks keep going forever until you :Stop() them.

Track options

Defaults: FPS = 12, Looped = false, Priority = 1. All three are live-editable on the returned track. Other fields:

JumpToFrame

image:JumpToFrame(name) accepts a SubTexture name OR a numeric index (0-based, bounded). The pinned frame stays visible until a track is :Play()'d on top — priority playback always wins over a manual pin. Call :ClearJump() to release.

DynImg sources

Pass a DynImg instead of a static Image asset and the AnimatedImage re-samples the source buffer each tick. The SubTexture rectangles still come from the XML; what changes is the underlying pixels. Useful for procedurally generated atlases, screen-captured spritesheets, or network-streamed textures.

Size semantics & trim

Setting animimg.Size = Dim.new(W, H) squashes and stretches the currently-displayed frame the same way a regular Image primitive scales. The logical frame size is the SubTexture's frameWidth × frameHeight (or its width × height if no frame* attributes are present), so two frames with different trimmed bounds still display at consistent dimensions.

AnimatedImage properties

.SizeDim

Mirror of the underlying Image primitive's Size. Writing squashes / stretches the currently-displayed frame.

.PositionDim

Top-left position of the rendered image.

.ZIndexnumber

Higher = drawn on top.

.Visibleboolean

Defaults to false like every other GUI primitive. Flip to true after creating tracks so you don't flash a half-set-up animation for a frame.

.ColorColor3

Tint multiplied into the current frame's pixels.

.Transparencynumber

0 opaque, 1 fully transparent.

.FrameCountnumberread-only

Total number of <SubTexture> entries parsed from the XML.

.Alivebooleanread-only

False after :Destroy().

AnimatedImage methods

image:CreateTrack(frames, opts?)-> AnimatedImageTrackmethod

Build an AnimatedImageTrack from a sequence of SubTexture names. frames is either an array ({"Down0000", "Down0001"}) or a comma-separated string ("Down0000,Down0001,Down0002"). Unknown frame names error. opts is an optional table with FPS (default 12), Looped (default false), and Priority (default 1). Returns a fresh track with its own state and signals — useful for layered animation. Tracks are owned by this AnimatedImage; :Destroy() stops them.

image:JumpToFrame(frame)method

Snap the displayed frame to a specific SubTexture by name ("Down0000") or by zero-based index. The pinned frame stays visible until a track is :Play()'d on top — priority playback wins. Useful for video-game icons that flip between named static frames.

image:ClearJump()method

Release the manual frame override.

image:HasFrame(name)-> booleanmethod

Whether the parsed atlas contains a SubTexture with this name.

image:GetFrameNames()-> { string }method

Every SubTexture name in source order. Handy for picking frame names dynamically (e.g. all frames starting with "down").

image:GetPrimitive()-> Primitivemethod

The underlying GUI Image primitive. Useful for clipping (clippable:AddClippable(image:GetPrimitive())) or attaching a shader (:AttachShader(asset)).

image:Destroy()method

Destroy the underlying primitive and stop every track. All :CreateTrack handles still resolve but their ticks stop firing.

AnimatedImageTrack properties

.FPSnumber

Playback rate. Live-editable; the next tick uses the new value. Clamped to a small positive minimum to avoid divide-by-zero.

.Loopedboolean

When true, the track wraps at the end and fires DidLoop. When false, it stops at the end and fires Ended.

.Prioritynumber

Higher priority wins when multiple tracks are Playing. When a higher-priority track ends (non-looped) and a lower-priority track is still Playing, the lower one takes over the display. Default 1.

.FrameCountnumberread-only

How many frames this track plays through.

.State"Idle" | "Playing" | "Stopped"read-only
.Elapsednumberread-only

Seconds elapsed in the current play cycle.

.DidLoopSignal<>read-only

Fires every time a looped track wraps. If the heart loop skipped multiple cycles in one tick (lag spike), the signal fires multiple times in a row.

.EndedSignal<>read-only

Fires once when a non-looped track reaches its end. Does NOT fire on :Stop().

AnimatedImageTrack methods

track:Play()method

Start the track. Resets Elapsed to 0 and transitions state to "Playing". Calling on an already-Playing track is a no-op.

track:Stop()method

Stop the track immediately. State goes to "Stopped". Does NOT fire Ended. The displayed frame switches to whichever lower-priority Playing track wins next tick.

track:Pause()method

Pause without resetting (state goes "Idle"). Elapsed is preserved so :Resume() continues from where you paused.

track:Resume()method

Resume from "Idle" back to "Playing".

track:Wait()method

Yield the calling coroutine until Ended fires. Must be called from inside a coroutine.

DrawableImg

GUI.DrawableImg(width, height) builds a CPU-side RGBA image you can draw shape primitives into from Lua — rectangles, lines, circles, pixels, fills. Every mutating call bumps an internal version counter, so the next frame the GPU re-uploads the bytes and any GUI primitive whose .Image points at this drawable picks up the change automatically. Assigning part.Texture = drawable on a 3D BasePart works the same way.

local mini = GUI.DrawableImg(128, 128)
mini:Fill(Color3.fromRGB(15, 20, 28))
mini:DrawCircle(64, 64, 50, Color3.fromRGB(255, 200, 0), 0, true)
mini:DrawRect(32, 32, 64, 8, Color3.fromRGB(0, 0, 0))

-- Assign as a primitive image so the renderer shows it.
local img = GUI.Basic.Image(mini)
img.Size     = Dim.new(256, 256)
img.Visible  = true

DrawableImg properties & methods

drawable:Width()-> numbermethod
drawable:Height()-> numbermethod
drawable:Source()-> stringmethod

Debug label for the drawable (e.g. "<drawable:128x128>"). Useful in logs.

drawable:Pixels()-> stringmethod

Return the current pixel buffer as a binary string (width * height * 4 bytes of RGBA). Snapshot value; further draws don't mutate the returned string.

drawable:IsAlive()-> booleanmethod
drawable:WritePixel(x, y, color, transparency?)method

Set one pixel. color is a Color3. transparency defaults to 0 (opaque). Out-of-bounds coordinates are clipped.

drawable:DrawRect(x, y, w, h, color, transparency?)method

Fill a rectangle. x, y is the top-left; w, h is the size. Clipped to the drawable's bounds.

drawable:DrawCube(x, y, w, h, color, transparency?)method

Alias for :DrawRect.

drawable:DrawLine(x0, y0, x1, y1, color, transparency?)method

Bresenham line from (x0,y0) to (x1,y1).

drawable:DrawCircle(cx, cy, radius, color, transparency?, filled?)method

Circle centered on (cx, cy) with radius radius. filled defaults to false (outline). Clipped to bounds.

drawable:Fill(color, transparency?)method

Fill the whole drawable with one color.

drawable:Clear()method

Zero out every byte (fully transparent black).

drawable:Apply(canvas, atX?, atY?)method

Blit a CanvasBuffer into this drawable at offset (atX, atY) (defaults to 0, 0). Source pixels with non-zero alpha are copied; clipped to bounds. Use this to batch many CPU draws into a CanvasBuffer cheaply, then commit to the GPU-visible DrawableImg in one shot.

drawable:Destroy()method

Free the drawable. After this, IsAlive() is false and primitives that referenced it stop refreshing.

CanvasBuffer

GUI.CanvasBuffer(width, height) is a scratch CPU buffer with the same drawing methods as DrawableImg, but it is NOT bound to the GPU. Use it when you want to batch many edits without paying for a GPU re-upload on each one — draw everything into the canvas, then canvas:Apply(drawable) blits the final result onto a real DrawableImg in a single texture write.

local board    = GUI.DrawableImg(256, 256)
local scratch  = GUI.CanvasBuffer(256, 256)

for i = 1, 200 do
    scratch:DrawCircle(math.random(256), math.random(256), 3, Color3.new(1, 1, 1), 0, true)
end

scratch:Apply(board)        -- single texture upload at this point

CanvasBuffer methods

canvas:Width()-> numbermethod
canvas:Height()-> numbermethod
canvas:Pixels()-> stringmethod

Snapshot of the current pixel buffer as RGBA bytes.

canvas:WritePixel(x, y, color, transparency?)method
canvas:DrawRect(x, y, w, h, color, transparency?)method
canvas:DrawCube(x, y, w, h, color, transparency?)method

Alias for :DrawRect.

canvas:DrawLine(x0, y0, x1, y1, color, transparency?)method
canvas:DrawCircle(cx, cy, radius, color, transparency?, filled?)method
canvas:Fill(color, transparency?)method
canvas:Clear()method

Zero out every byte.

canvas:Apply(drawable, atX?, atY?)method

Blit this canvas onto a DrawableImg at offset (atX, atY) (defaults to 0, 0). One texture upload happens at this point; the canvas itself never touches the GPU. Use it to commit batched CPU work.

DrawableImg vs CanvasBuffer at a glance

DrawableImgCanvasBuffer
Stored on GPU after draw?Yes (texture re-uploads each frame when dirty)No, CPU only
Show via a primitive directly?Yes, GUI.Basic.Image(drawable)No
Use as a 3D part texture?Yes, part.Texture = drawableNo
Cost per draw callSets a per-frame dirty flag, one upload per frameCheap, just memory write
Best whenYou want the result on screenYou want to batch many writes before committing

UIEffectVolume

2D analog of Renderable.EffectVolume. Spawns sprite particles in screen-space at the configured Position / Size (a 2D Dim region in pixels). Same property surface as the 3D version, minus 3D-only bits: positions and velocities live in 2D pixel space and EmissionDirection is a 2D vector. Particles render in the GUI pass with a ZIndex.

local GUI = import("GUI")
local Asset = import("Asset")
local Dim = import("Primitives").Dim

local star  = Asset.GetAsset("Image", "ui.star")
local burst = GUI.UIEffectVolume(star)
burst.Position = Dim.new(640, 360)         -- center, pixels
burst.Size     = Dim.new(200, 10)          -- spawn box
burst.Rate     = 40
burst.Speed    = { Min = 120, Max = 240 }
burst.Acceleration = { X = 0, Y = 300 }   -- gravity
burst.TimeScaleColor = { [0] = white, [1] = orange }
burst.TimeScaleTransparency = { [0] = 0, [1] = 1 }

Properties

PropertyTypeDefaultNotes
PositionDim(0.5, 0)Emitter center in pixels.
SizeDim(0, 100)Full extent of the spawn box.
ZIndexnumber0Order against other GUI primitives.
ImageImageAsset / DynImg / DrawableImg?Sprite texture.
EnabledbooleantrueDisable to stop spawning.
Ratenumber20Particles/sec.
MaxParticlesnumber1024Hard cap.
Lifetime{ Min, Max }1..2 sPer-particle lifetime range.
Speed{ Min, Max }80..160Pixels/sec.
ParticleSize{ Min, Max }16..16Base sprite size in pixels.
Rotation / RotSpeed{ Min, Max }0..0Per-particle.
AccelerationVec2 / Dim / { X, Y }(0, 200)2D pixel-space force applied to every particle equally.
RandomizeForce{ X?, Y? }all zeroPer-particle randomized force in pixels/sec². Each axis is a {min, max} range sampled at spawn. Example: { X = {min = -50, max = 50}, Y = {min = -120, max = 0} }.
Dragnumber0Velocity damping per second.
Spreadnumber0Cone half-angle in degrees.
EmissionDirectionVec2 / Dim / { X, Y }(0, -1)Initial velocity direction.
Color / TimeScaleColorColor3 or sequencewhiteSee EffectVolume for input formats.
Transparency / TimeScaleTransparencynumber or sequence0Aliases.
SizeOverLifenumber or sequence1Multiplier on ParticleSize.

Methods

emitter:Emit(n?)method

Force-spawn n particles now (default 1).

emitter:Clear()method
emitter:Destroy()method
emitter:AttachShader / DetachShader / ClearShaders / SetData / GetData method

Same particle WGSL contract as the 3D EffectVolume — see Particle shaders for the binding layout and the VsOut inputs. Both 2D and 3D particles use the same shader shape; the only difference is in.life_t is always 0 on 2D particles and the time field is F.resolution.z instead of F.viewport.z.

Spatial queries (GPU)

2D analogs of the GPU spatial queries, run as compute passes against every visible GUI primitive. One thread per primitive tests against the query shape; matches are written to a packed atomic-counter buffer, then the index list is read back and resolved to Primitive userdata. Filter callbacks (when supplied) run on the CPU after the GPU returns candidates, so they only fire on hits. Primitives with Visible = false are skipped.

origin, center, size arguments accept either a Dim or a plain { X, Y } / { x, y } table (also { number, number } array form), all in screen-space pixels.

GUI.Raycast(origin, direction, filter?, maxDistance?) → { Primitive, Distance, Position }? method

GPU-dispatched 2D ray test. Cubes / Images / Fonts / Clippables test as axis-aligned rectangles (Position .. Position + Size); circles as the inscribed ellipse; triangles as the actual triangle (apex at the top-middle of the AABB, base across the bottom). direction is normalized internally; zero-length errors. Hits are visited nearest-first; filter returns true to accept, false / nil to skip past. Returns nil when nothing was hit. Falls back to no-op if the GPU has not been initialized yet (no window opened).

local hit = GUI.Raycast(Dim.new(100, 100), Dim.new(1, 0))
if hit then
    print("hit at ", hit.Position.X, hit.Position.Y, "dist ", hit.Distance)
end
GUI.CheckArea(center, size, quality?, filter?) → { Primitive } method

Every visible primitive whose AABB overlaps the rectangle centered at center with full extents size. quality is accepted for parity with the 3D API but ignored — 2D primitives have no sub-quad geometry.

GUI.OverlapSphere(center, radius, filter?) → { Primitive } method

Every visible primitive whose AABB intersects the circle at center with the given radius. (Named "Sphere" to mirror the 3D API.)

GUI.OverlapBox(center, size, rotation?, filter?) → { Primitive } method

Every visible primitive whose AABB intersects the oriented box at center with full extents size, rotated by rotation radians (defaults to 0). The GPU test is exact OBB-vs-AABB via the separating-axis theorem.

GUI.OverlapFrustum(filter?) → { Primitive } method

Every visible primitive whose AABB intersects the current viewport rect (0, 0)..(window width, height). Useful as a fast pass for "what's on-screen right now."

GUI.GetItemsInZone(cframe, size, filter?) → { Primitive } method

Oriented zone query. cframe accepts a CFrame (uses position.xy and rotation.z), a Dim, or a plain { X, Y } table. Equivalent to OverlapBox on the 2D system; the 3D version's "mesh-precise" semantics don't apply here since GUI primitives have no sub-quad geometry.