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
Solid-coloured rectangle. Tinted by .Color;
sized by .Size.
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.
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.
Textured rectangle. Pass an ImageAsset; the
asset's pixels become the surface.
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
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
"Text" covers both Font and StyledFont primitives — styling is a property toggle, not a separate shape.
Pixel-space size. For Text this is (0, 0), size is driven by .TextSize.
Top-left position. Origin at the top-left of the window.
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.
Tint when texture-less; image tint when textured; box colour for Text.
0 opaque, 1 fully transparent.
Higher = drawn on top.
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.
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"
Glyph height in pixels.
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.
Draw a baseline rule. Off by default.
Draw a rule through x-height. Combinable with .Underline.
Methods
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)
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
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.
Detach a child added via :AddClippable. The
child resumes rendering unclipped. Idempotent if
child wasn't added to this specific
Clippable.
Attach a 2D fragment shader to this primitive's draw call.
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:
name— lookup key used at runtime. Required.x,y,width,height— sub-rectangle within the source image.frameX,frameY,frameWidth,frameHeight— optional. Restores trimmed sprites to their original cell size (the sub-image is placed at offset(-frameX, -frameY)within a transparentframeWidth × frameHeightcanvas), so frames of different trimmed bounds don't jitter in playback.
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:
track.FrameCount,track.State("Idle"/"Playing"/"Stopped"),track.Elapsed.track.DidLoopfires every wrap of a looped track.track.Endedfires once when a non-looped track reaches its end (does not fire on:Stop()).:Play,:Stop,:Pause,:Resume,:Wait(yields until Ended).
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
Mirror of the underlying Image primitive's Size. Writing squashes / stretches the currently-displayed frame.
Top-left position of the rendered image.
Higher = drawn on top.
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.
Tint multiplied into the current frame's pixels.
0 opaque, 1 fully transparent.
Total number of <SubTexture> entries parsed from the XML.
False after :Destroy().
AnimatedImage methods
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.
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.
Release the manual frame override.
Whether the parsed atlas contains a SubTexture with this name.
Every SubTexture name in source order. Handy for picking
frame names dynamically (e.g. all frames starting with
"down").
The underlying GUI Image primitive. Useful for clipping
(clippable:AddClippable(image:GetPrimitive()))
or attaching a shader (:AttachShader(asset)).
Destroy the underlying primitive and stop every track. All
:CreateTrack handles still resolve but their
ticks stop firing.
AnimatedImageTrack properties
Playback rate. Live-editable; the next tick uses the new value. Clamped to a small positive minimum to avoid divide-by-zero.
When true, the track wraps at the end and fires
DidLoop. When false, it stops at the end and
fires Ended.
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.
How many frames this track plays through.
Seconds elapsed in the current play cycle.
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.
Fires once when a non-looped track reaches its end. Does
NOT fire on :Stop().
AnimatedImageTrack methods
Start the track. Resets Elapsed to 0 and
transitions state to "Playing". Calling on an
already-Playing track is a no-op.
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.
Pause without resetting (state goes "Idle").
Elapsed is preserved so :Resume() continues
from where you paused.
Resume from "Idle" back to "Playing".
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
Debug label for the drawable (e.g.
"<drawable:128x128>"). Useful in logs.
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.
Set one pixel. color is a
Color3. transparency defaults to
0 (opaque). Out-of-bounds coordinates are clipped.
Fill a rectangle. x, y is the top-left;
w, h is the size. Clipped to the drawable's
bounds.
Alias for :DrawRect.
Bresenham line from (x0,y0) to (x1,y1).
Circle centered on (cx, cy) with radius
radius. filled defaults to
false (outline). Clipped to bounds.
Fill the whole drawable with one color.
Zero out every byte (fully transparent black).
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.
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
Snapshot of the current pixel buffer as RGBA bytes.
Alias for :DrawRect.
Zero out every byte.
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
| DrawableImg | CanvasBuffer | |
|---|---|---|
| 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 = drawable | No |
| Cost per draw call | Sets a per-frame dirty flag, one upload per frame | Cheap, just memory write |
| Best when | You want the result on screen | You 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
| Property | Type | Default | Notes |
|---|---|---|---|
Position | Dim | (0.5, 0) | Emitter center in pixels. |
Size | Dim | (0, 100) | Full extent of the spawn box. |
ZIndex | number | 0 | Order against other GUI primitives. |
Image | ImageAsset / DynImg / DrawableImg? | — | Sprite texture. |
Enabled | boolean | true | Disable to stop spawning. |
Rate | number | 20 | Particles/sec. |
MaxParticles | number | 1024 | Hard cap. |
Lifetime | { Min, Max } | 1..2 s | Per-particle lifetime range. |
Speed | { Min, Max } | 80..160 | Pixels/sec. |
ParticleSize | { Min, Max } | 16..16 | Base sprite size in pixels. |
Rotation / RotSpeed | { Min, Max } | 0..0 | Per-particle. |
Acceleration | Vec2 / Dim / { X, Y } | (0, 200) | 2D pixel-space force applied to every particle equally. |
RandomizeForce | { X?, Y? } | all zero | Per-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} }. |
Drag | number | 0 | Velocity damping per second. |
Spread | number | 0 | Cone half-angle in degrees. |
EmissionDirection | Vec2 / Dim / { X, Y } | (0, -1) | Initial velocity direction. |
Color / TimeScaleColor | Color3 or sequence | white | See EffectVolume for input formats. |
Transparency / TimeScaleTransparency | number or sequence | 0 | Aliases. |
SizeOverLife | number or sequence | 1 | Multiplier on ParticleSize. |
Methods
Force-spawn n particles now (default 1).
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.
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
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.
Every visible primitive whose AABB intersects the circle at
center with the given radius. (Named
"Sphere" to mirror the 3D API.)
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.
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."
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.