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, .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
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.
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
| Property | Type | Default | Notes |
|---|---|---|---|
Color | Color3 | (1, 1, 1) | Whole-spline base color. Tweenable via :Tween's Color option. |
Transparency | number | 0 | 0..1, applied as alpha multiplier. |
ZIndex | number | 0 | Splines sort among themselves; all splines render above regular 2D items. |
Visible | boolean | true | Hides the spline without destroying it. |
Style | string | "Solid" | "Solid" / "Dashed" / "Dotted". Pixel-accurate pattern is generated in the fragment shader using Length + Padding below. |
Cap | string | "Rounded" | "Rounded" / "Miter" / "Square". Applied at the start and end nodes. |
Length | number | 12 | Length of each visible dash, in pixels along the line. Dashed only — ignored for Solid and Dotted. |
Padding | number | 8 | Gap 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). |
NodeCount | number | — | Read-only. |
Alive | boolean | — | Read-only. |
Changed | Signal<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
Replace the entire node array. Cancels any active
tween. Fires .Changed("Nodes").
Snapshot copy of the current node array.
1-based insertion. index = 1 prepends,
index > NodeCount appends. Doesn't
touch the tween — useful for hand-editing during an
animation.
1-based removal. Errors if out of range.
Replace a single node in place.
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).
Detach all shaders; reverts to the default solid-color fragment.
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:
- Target has more nodes than current: extra nodes are inserted into the current array at interpolated polyline positions before the tween starts (the line shape stays unchanged visually); each inserted node then animates to its target.
- Target has fewer nodes than current: excess current nodes animate toward the last target node; once the tween completes, the array is truncated.
- Same count: straight per-node lerp of Position / Thickness / Angle.
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.
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.
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
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.
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.
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.
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.
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.