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 and .Size. 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 rect is cut at the GPU scissor stage. Resizing or moving the Clippable retroactively re-clips every child the next frame, so it works as a scrolling viewport, masked overlay, or draggable panel without having to re-add children. Works on every other primitive type — including DynImg-backed Image primitives, since by the time clipping runs they're a single textured rectangle.

local mask = GUI.Basic.Clippable()
mask.Position = Dim.new(100, 100)
mask.Size     = Dim.new(400, 300)

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)              -- pixels outside (100,100,400,300) are cut

-- Move the mask -- the image gets re-clipped automatically.
mask.Position = Dim.new(200, 100)

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.

.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 scissored to this primitive's current Position+Size every frame. Pixels of child outside this rect are cut. Reading the Clippable's Size or Position retroactively re-clips every child on the next snapshot — no need to re-add. Last-add-wins for re-parenting.

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)method

Attach a 2D fragment shader to this primitive's draw call.

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.