Skip to main content

Spray

Plays a ParticleEmitter on the screen. You put a regular ParticleEmitter inside a GUI object, set it up in the Properties window the same way you would for a 3D effect, and Spray draws its particles with ImageLabels.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Spray = require(ReplicatedStorage.Packages.Spray)

local button = script.Parent -- a TextButton with a ParticleEmitter called Confetti inside
local confetti = Spray.New(button.Confetti)

button.Activated:Connect(function()
	confetti:Emit(40)
end)

Spray draws on the client, so require it from a LocalScript, or from a ModuleScript that a LocalScript requires.

Where the emitter goes

Roblox only draws a ParticleEmitter that's inside a Part or an Attachment, so one inside a GUI does nothing on its own. Spray is what draws it. The emitter needs a GuiObject above it in the Explorer, like a Frame, an ImageLabel or a TextButton. It's usually a direct child, but a Folder in between works too, which is handy for keeping several effects under one button.

Particles come out of the center of that GuiObject and are drawn inside it, so they move, scale and hide along with it. If you tween the frame, the effect follows. When the GuiObject, or anything above it, has ClipsDescendants on, particles get cut off at its edges unless you set the SprayIgnoreClips attribute.

What the effect looks like comes from the emitter's own properties: Texture, Color, Size, Transparency, Squash, Lifetime, Speed, Rotation, RotSpeed, Acceleration, Drag, SpreadAngle, EmissionDirection, Shape, the flipbook settings, Rate, TimeScale and the rest. The few settings a 3D emitter has no property for are attributes, listed further down.

Playing, pausing and the clock

Every Spray has a clock that counts seconds from the start of the effect. What you see on screen is always calculated from that number and nothing else, so an effect can be paused, jumped to any moment, or played backwards, and you still get exactly the frame you'd have seen by playing up to that point.

A Spray is either playing, which means the clock moves forward every frame, or paused. A new Spray starts paused at 0 seconds.

  • Spray:Emit releases particles. On a paused Spray it first rewinds to 0 and starts playing. On a Spray that's already playing, the new particles join the ones already on screen, the same as ParticleEmitter:Emit().
  • Spray:Pause stops the clock and Spray:Resume starts it again.
  • Spray:Forward and Spray:SetTime move the clock. They don't pause or resume anything.

If the emitter has Enabled on and a Rate above 0, it also releases particles on its own for as long as the Spray is playing, like a 3D emitter does, so calling Spray:Resume is enough to start it.

Attributes

These are the settings a 3D emitter has no property for. They all go on the ParticleEmitter itself, and they're all optional. To add one, select the emitter, scroll to the bottom of the Properties window, press the + button next to Attributes, and type the name and type exactly as they appear here.

Attribute Type Default What it does
SprayScale number 1 Multiplies size and speed together, so the whole effect gets bigger or smaller without changing its shape. When an effect looks right but is the wrong size, change this one first.
SpraySizeScale number 1 Multiplies size only, on top of SprayScale.
SpraySpeedScale number 1 Multiplies speed only, on top of SprayScale.
SprayUnit string RelativeYY Which side of the GuiObject counts as one stud. RelativeYY is its height, RelativeXX its width, RelativeMin the shorter side and RelativeMax the longer one. Offset makes one stud equal one pixel.
SprayEmissionSize Vector2 0, 0 Size of the area particles come out of, as a fraction of the GuiObject. 0, 0 is a single point in the middle and 1, 1 covers the whole GuiObject. The emitter's Shape, ShapeStyle and ShapeInOut are only used when this isn't 0, 0.
SprayMaxParticles number 400 The most particles that can be on screen at once. Spray:Emit never releases more than this, and a Rate too high to fit gets lowered.
SprayGlowLayers number 1 How many copies of each particle get drawn, from 1 to 8. The extra copies are a fading halo that imitates LightEmission. Each copy is one more ImageLabel, so it costs performance.
SprayZIndex number the GuiObject's ZIndex The ZIndex that ZOffset gets added to.
SprayFlipbookGrid number 4 Frames per row of the flipbook when FlipbookLayout is Custom. The other layouts already say how many frames they have.
SprayFlipbookResolution number 1024 Size of the flipbook texture in pixels. Spray needs it to cut each frame out of the sheet.
SprayIgnoreClips boolean false Lets particles leave the GuiObject even when it, or something above it, has ClipsDescendants on.
SprayPrewarm number 0 How many particles to create ahead of time when the Spray is built. It does the same as calling Spray:Prewarm with that number.

An attribute with the wrong type, like a string where a number should go, is ignored and the default is used instead.

From 3D to the screen

One stud is the height of the GuiObject in pixels, or whichever side SprayUnit picks. In a frame 200 pixels tall, a Size of 0.1 gives particles 20 pixels across, and a Speed of 2 moves them 400 pixels per second. Because it's based on the GuiObject and not on the screen, the effect keeps its look on a phone or after the window is resized.

Acceleration gets its Y flipped because on the screen +Y points down, so gravity written as 0, -10, 0 still pulls the particles down.

For EmissionDirection, Top, Bottom, Left and Right point where you'd expect on the screen. Front and Back point into and out of the screen, which a flat GUI can't show, so both work like Top. Only the X of SpreadAngle is used, and 180 spreads particles all the way around.

ZOffset is added to the base ZIndex (see SprayZIndex), so emitters in the same GuiObject stack the way you'd expect. It can't lift particles above the GuiObject's siblings when the ScreenGui uses ZIndexBehavior.Sibling, though. If an effect has to show in front of some other part of your UI, put it inside a GuiObject that's already in front.

GUIs only have normal alpha blending, so LightEmission is imitated. Brightness above 1 pushes colors toward white, which gives you the bright core glowing particles have, and SprayGlowLayers adds a halo scaled by LightEmission. Overlapping particles still don't add their light together the way they do in 3D. LightInfluence tints the particles toward Lighting.Ambient.

Particles always move with their GuiObject, as if LockedToPart were on.

The Spray type

The module exports the type of the object Spray.New returns as Spray.Spray. You only need it to annotate variables or function parameters in your own code:

local Spray = require(ReplicatedStorage.Packages.Spray)

local effects: { Spray.Spray } = {}

local function PlayAll(list: { Spray.Spray })
	for _, effect in list do
		effect:Emit(20)
	end
end

Properties​

Emitter​

This item is read only and cannot be modified. Read Only
Spray.Emitter: ParticleEmitter

The ParticleEmitter this Spray plays, the same one you passed to Spray.New. Spray reads it but never changes or destroys it.

Functions​

New​

Spray.New(
Target: ParticleEmitter--

The emitter to play. It needs a GuiObject somewhere above it.

) → Spray

Creates a Spray that plays Target.

This reads the emitter's properties and attributes, and adds an empty Frame inside the GuiObject where the particles will be drawn. The Frame is named Spray_ plus the emitter's name. Nothing shows up yet, because the Spray starts paused at 0 seconds. Particles appear after Spray:Emit, or after Spray:Resume on an emitter that uses Rate.

local sparkles = Spray.New(frame.Sparkles)
sparkles:Emit(30)

Create each Spray once and keep it around. Calling Spray.New every time you want a burst builds a new Frame and new ImageLabels on every call, and the old ones stay in your UI until you call Spray:Destroy on the Sprays that made them.

Errors

TypeDescription
"Spray.New expects a ParticleEmitter"What you passed isn't a ParticleEmitter.
"Spray: ... has no GuiObject ancestor to draw into"Nothing above the emitter is a GuiObject, for example because it's inside a Part.

Emit​

Spray:Emit(
Count: number--

How many particles to release.

) → ()

Releases Count particles at once.

What happens depends on whether the Spray is playing:

  • If it's paused, it rewinds to 0 seconds, clears whatever was on screen and starts playing, so the effect runs from the beginning. A new Spray is paused, and so is one you stopped with Spray:Pause. Moving the clock with Spray:SetTime or Spray:Forward first doesn't change this, since those don't start playback.
  • If it's already playing, the new particles join the ones already on screen, the same as ParticleEmitter:Emit() does in 3D.
button.Activated:Connect(function()
	sparkles:Emit(25)
end)

Count is rounded down and capped at SprayMaxParticles. Passing 0 releases nothing, but a paused Spray still rewinds and starts playing, which is a way to restart only the Rate stream.

CAUTION

A Spray remembers its last 64 bursts. If you call this more than 64 times within one particle lifetime, the oldest bursts disappear early. For a steady flow of particles, use the emitter's Rate instead.

Pause​

Spray:Pause() → ()

Stops the clock. The particles stay on screen, frozen where they are, until the clock moves again.

fx:Pause()
task.wait(1)
fx:Resume() -- carries on from the same moment

Pausing doesn't hide anything. If you want the particles gone right away, Spray:Cleanup removes them. Does nothing if the Spray is already paused.

Resume​

Spray:Resume() → ()

Starts the clock again from wherever it is. That's where Spray:Pause stopped it, or wherever Spray:SetTime or Spray:Forward moved it.

Unlike Spray:Emit, this doesn't rewind, so you can use it to start an effect partway through. On an emitter that uses Rate, that makes it start already full of particles instead of empty:

local smoke = Spray.New(frame.Smoke)
smoke:SetTime(2)
smoke:Resume() -- plays as if it had been running for 2 seconds

It's also all you need to start a Rate emitter, since the stream runs whenever the Spray is playing. Does nothing if the Spray is already playing.

Forward​

Spray:Forward(
DeltaTime: number--

Seconds to move the clock. Negative values go backwards.

) → ()

Moves the clock by DeltaTime seconds and redraws right away. Negative values go backwards, and the clock never goes below 0.

It doesn't pause or resume the Spray, so a playing Spray keeps playing from the new moment. On a paused one it works like a scrubber:

fx:Emit(30)
fx:Pause()
fx:Forward(0.1) -- 0.1 seconds into the effect
fx:Forward(-0.05) -- back to 0.05

TimeScale changes how fast the clock runs while the Spray is playing, but it doesn't affect this method, so Forward(1) always moves the clock exactly 1 second.

SetTime​

Spray:SetTime(
Time: number--

Seconds from the start of the effect.

) → ()

Jumps the clock to Time seconds after the start of the effect and redraws right away. You get exactly the frame you'd have seen by playing up to that moment.

Like Spray:Forward, it doesn't pause or resume anything, and like Forward it ignores TimeScale. Negative values count as 0.

fx:Emit(60)
fx:Pause()
fx:SetTime(0.35) -- the effect as it looks 0.35 seconds in

Prewarm​

Spray:Prewarm(
Count: number?--

How many particles to prepare. Leave it out to use SprayMaxParticles.

) → ()

Creates the ImageLabels for Count particles now, so later bursts reuse them instead of creating new ones in the middle of gameplay.

Creating ImageLabels is most of what the first burst of an effect costs, and a big burst can cause a small hitch. Calling this during a loading screen, or anywhere a hitch won't be noticed, moves that cost there.

local explosion = Spray.New(frame.Explosion)
explosion:Prewarm() -- room for SprayMaxParticles particles

Count is the total the pool should hold, not how many to add, so calling Prewarm(100) twice still leaves room for 100. It never goes past SprayMaxParticles. With SprayGlowLayers above 1, each particle takes that many ImageLabels.

Spray:Cleanup throws the pool away. The SprayPrewarm attribute does the same as this method every time the Spray is built, including rebuilds after a cleanup.

UseRandom​

Spray:UseRandom(
Use: boolean?--

true or nil for a new seed on every run, false to always use the fixed seed.

) → ()

Chooses whether each run of the effect looks different or exactly the same.

With true, which is the default, the Spray picks a new random seed every time the effect restarts, so no two runs look alike. That's what a 3D emitter does. With false, it always uses the seed set with Spray:RandomSeed, so every run is identical, down to the flipbook frame each particle shows.

fx:UseRandom(false)
fx:RandomSeed(7)
fx:Emit(40) -- looks the same every time

The change applies the next time the effect restarts, which is the next Spray:Emit on a paused Spray. Particles already on screen keep the seed they have.

RandomSeed​

Spray:RandomSeed(
Seed: number--

Any whole number.

) → ()

Sets the seed used while Spray:UseRandom is false. The same seed always gives the same particles, down to which flipbook frame each one shows. A different seed gives different particles, and those repeat exactly too.

Call it before Spray:Emit. If UseRandom(false) is already on, the new seed applies immediately, even to an effect that's in the middle of playing, so for a clean result set the seed first and emit after.

Decimals are rounded down. Until you call this, the seed is 0.

Cleanup​

Spray:Cleanup() → ()

Throws away everything the Spray built: the particles on screen, the Frame and ImageLabels that draw them, and the copy of the emitter's settings. The Spray itself keeps working, but it has to build all of that again the next time you use it.

There are two reasons to call it:

  • You changed the emitter's properties or attributes while the game is running. Spray only reads them when it builds, so call this and the next call reads them again.
  • You're done with a heavy effect for a while and want its ImageLabels gone.
fx.Emitter.Color = ColorSequence.new(Color3.new(1, 0, 0))
fx:Cleanup()
fx:Emit(20) -- red particles now

After this the Spray is paused at 0 seconds. The next Spray:Emit, Spray:Resume, Spray:Forward, Spray:SetTime or Spray:Prewarm builds everything again, which costs about as much as creating the Spray did, so don't call this between bursts of an effect you'll keep using. Your Spray:UseRandom and Spray:RandomSeed settings are kept.

Destroy​

Spray:Destroy() → ()

Cleans up the Spray for good. It does what Spray:Cleanup does and then makes the object unusable, so anything you do with it afterwards, including calling :Destroy() again, throws "Spray: this object has been destroyed". That way a leftover reference fails loudly where you use it instead of misbehaving somewhere else.

The ParticleEmitter isn't touched. Destroy it yourself if you don't need it anymore.

Call this when the UI the effect lives in goes away. If you destroy the UI but not the Spray, a Spray that was playing stays in Spray's update loop and in memory.

Show raw api
{
    "functions": [
        {
            "name": "Advance",
            "desc": "Called by the shared scheduler, and only while Playing.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "Spray"
                },
                {
                    "name": "DeltaTime",
                    "desc": "",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "static",
            "ignore": true,
            "source": {
                "line": 442,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "New",
            "desc": "Creates a Spray that plays `Target`.\n\nThis reads the emitter's properties and attributes, and adds an empty Frame inside the\nGuiObject where the particles will be drawn. The Frame is named `Spray_` plus the\nemitter's name. Nothing shows up yet, because the Spray starts paused at 0 seconds.\nParticles appear after [Spray:Emit], or after [Spray:Resume] on an emitter that uses\n`Rate`.\n\n```lua\nlocal sparkles = Spray.New(frame.Sparkles)\nsparkles:Emit(30)\n```\n\nCreate each Spray once and keep it around. Calling `Spray.New` every time you want a\nburst builds a new Frame and new ImageLabels on every call, and the old ones stay in your\nUI until you call [Spray:Destroy] on the Sprays that made them.",
            "params": [
                {
                    "name": "Target",
                    "desc": "The emitter to play. It needs a GuiObject somewhere above it.",
                    "lua_type": "ParticleEmitter"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Spray"
                }
            ],
            "function_type": "static",
            "errors": [
                {
                    "lua_type": "\"Spray.New expects a ParticleEmitter\"",
                    "desc": "What you passed isn't a ParticleEmitter."
                },
                {
                    "lua_type": "\"Spray: ... has no GuiObject ancestor to draw into\"",
                    "desc": "Nothing above the emitter is a GuiObject, for example because it's inside a Part."
                }
            ],
            "source": {
                "line": 476,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Emit",
            "desc": "Releases `Count` particles at once.\n\nWhat happens depends on whether the Spray is playing:\n\n- If it's paused, it rewinds to 0 seconds, clears whatever was on screen and starts\n  playing, so the effect runs from the beginning. A new Spray is paused, and so is one\n  you stopped with [Spray:Pause]. Moving the clock with [Spray:SetTime] or\n  [Spray:Forward] first doesn't change this, since those don't start playback.\n- If it's already playing, the new particles join the ones already on screen, the same as\n  `ParticleEmitter:Emit()` does in 3D.\n\n```lua\nbutton.Activated:Connect(function()\n\tsparkles:Emit(25)\nend)\n```\n\n`Count` is rounded down and capped at `SprayMaxParticles`. Passing 0 releases nothing, but\na paused Spray still rewinds and starts playing, which is a way to restart only the\n`Rate` stream.\n\n:::caution\nA Spray remembers its last 64 bursts. If you call this more than 64 times within one\nparticle lifetime, the oldest bursts disappear early. For a steady flow of particles, use\nthe emitter's `Rate` instead.\n:::",
            "params": [
                {
                    "name": "Count",
                    "desc": "How many particles to release.",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 546,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Pause",
            "desc": "Stops the clock. The particles stay on screen, frozen where they are, until the clock\nmoves again.\n\n```lua\nfx:Pause()\ntask.wait(1)\nfx:Resume() -- carries on from the same moment\n```\n\nPausing doesn't hide anything. If you want the particles gone right away, [Spray:Cleanup]\nremoves them. Does nothing if the Spray is already paused.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 594,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Resume",
            "desc": "Starts the clock again from wherever it is. That's where [Spray:Pause] stopped it, or\nwherever [Spray:SetTime] or [Spray:Forward] moved it.\n\nUnlike [Spray:Emit], this doesn't rewind, so you can use it to start an effect partway\nthrough. On an emitter that uses `Rate`, that makes it start already full of particles\ninstead of empty:\n\n```lua\nlocal smoke = Spray.New(frame.Smoke)\nsmoke:SetTime(2)\nsmoke:Resume() -- plays as if it had been running for 2 seconds\n```\n\nIt's also all you need to start a `Rate` emitter, since the stream runs whenever the\nSpray is playing. Does nothing if the Spray is already playing.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 622,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Forward",
            "desc": "Moves the clock by `DeltaTime` seconds and redraws right away. Negative values go\nbackwards, and the clock never goes below 0.\n\nIt doesn't pause or resume the Spray, so a playing Spray keeps playing from the new\nmoment. On a paused one it works like a scrubber:\n\n```lua\nfx:Emit(30)\nfx:Pause()\nfx:Forward(0.1) -- 0.1 seconds into the effect\nfx:Forward(-0.05) -- back to 0.05\n```\n\n`TimeScale` changes how fast the clock runs while the Spray is playing, but it doesn't\naffect this method, so `Forward(1)` always moves the clock exactly 1 second.",
            "params": [
                {
                    "name": "DeltaTime",
                    "desc": "Seconds to move the clock. Negative values go backwards.",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 653,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "SetTime",
            "desc": "Jumps the clock to `Time` seconds after the start of the effect and redraws right away.\nYou get exactly the frame you'd have seen by playing up to that moment.\n\nLike [Spray:Forward], it doesn't pause or resume anything, and like `Forward` it ignores\n`TimeScale`. Negative values count as 0.\n\n```lua\nfx:Emit(60)\nfx:Pause()\nfx:SetTime(0.35) -- the effect as it looks 0.35 seconds in\n```",
            "params": [
                {
                    "name": "Time",
                    "desc": "Seconds from the start of the effect.",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 677,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Prewarm",
            "desc": "Creates the ImageLabels for `Count` particles now, so later bursts reuse them instead of\ncreating new ones in the middle of gameplay.\n\nCreating ImageLabels is most of what the first burst of an effect costs, and a big burst\ncan cause a small hitch. Calling this during a loading screen, or anywhere a hitch won't\nbe noticed, moves that cost there.\n\n```lua\nlocal explosion = Spray.New(frame.Explosion)\nexplosion:Prewarm() -- room for SprayMaxParticles particles\n```\n\n`Count` is the total the pool should hold, not how many to add, so calling `Prewarm(100)`\ntwice still leaves room for 100. It never goes past `SprayMaxParticles`. With\n`SprayGlowLayers` above 1, each particle takes that many ImageLabels.\n\n[Spray:Cleanup] throws the pool away. The `SprayPrewarm` attribute does the same as this\nmethod every time the Spray is built, including rebuilds after a cleanup.",
            "params": [
                {
                    "name": "Count",
                    "desc": "How many particles to prepare. Leave it out to use `SprayMaxParticles`.",
                    "lua_type": "number?"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 710,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "UseRandom",
            "desc": "Chooses whether each run of the effect looks different or exactly the same.\n\nWith `true`, which is the default, the Spray picks a new random seed every time the effect\nrestarts, so no two runs look alike. That's what a 3D emitter does. With `false`, it\nalways uses the seed set with [Spray:RandomSeed], so every run is identical, down to the\nflipbook frame each particle shows.\n\n```lua\nfx:UseRandom(false)\nfx:RandomSeed(7)\nfx:Emit(40) -- looks the same every time\n```\n\nThe change applies the next time the effect restarts, which is the next [Spray:Emit] on a\npaused Spray. Particles already on screen keep the seed they have.",
            "params": [
                {
                    "name": "Use",
                    "desc": "`true` or `nil` for a new seed on every run, `false` to always use the fixed seed.",
                    "lua_type": "boolean?"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 740,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "RandomSeed",
            "desc": "Sets the seed used while [Spray:UseRandom] is `false`. The same seed always gives the same\nparticles, down to which flipbook frame each one shows. A different seed gives different\nparticles, and those repeat exactly too.\n\nCall it before [Spray:Emit]. If `UseRandom(false)` is already on, the new seed applies\nimmediately, even to an effect that's in the middle of playing, so for a clean result set\nthe seed first and emit after.\n\nDecimals are rounded down. Until you call this, the seed is 0.",
            "params": [
                {
                    "name": "Seed",
                    "desc": "Any whole number.",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 760,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Cleanup",
            "desc": "Throws away everything the Spray built: the particles on screen, the Frame and ImageLabels\nthat draw them, and the copy of the emitter's settings. The Spray itself keeps working,\nbut it has to build all of that again the next time you use it.\n\nThere are two reasons to call it:\n\n- You changed the emitter's properties or attributes while the game is running. Spray\n  only reads them when it builds, so call this and the next call reads them again.\n- You're done with a heavy effect for a while and want its ImageLabels gone.\n\n```lua\nfx.Emitter.Color = ColorSequence.new(Color3.new(1, 0, 0))\nfx:Cleanup()\nfx:Emit(20) -- red particles now\n```\n\nAfter this the Spray is paused at 0 seconds. The next [Spray:Emit], [Spray:Resume],\n[Spray:Forward], [Spray:SetTime] or [Spray:Prewarm] builds everything again, which costs\nabout as much as creating the Spray did, so don't call this between bursts of an effect\nyou'll keep using. Your [Spray:UseRandom] and [Spray:RandomSeed] settings are kept.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 794,
                "path": "Spray/init.luau"
            }
        },
        {
            "name": "Destroy",
            "desc": "Cleans up the Spray for good. It does what [Spray:Cleanup] does and then makes the object\nunusable, so anything you do with it afterwards, including calling `:Destroy()` again,\nthrows \"Spray: this object has been destroyed\". That way a leftover reference fails\nloudly where you use it instead of misbehaving somewhere else.\n\nThe ParticleEmitter isn't touched. Destroy it yourself if you don't need it anymore.\n\nCall this when the UI the effect lives in goes away. If you destroy the UI but not the\nSpray, a Spray that was playing stays in Spray's update loop and in memory.",
            "params": [],
            "returns": [],
            "function_type": "method",
            "source": {
                "line": 825,
                "path": "Spray/init.luau"
            }
        }
    ],
    "properties": [
        {
            "name": "Emitter",
            "desc": "The ParticleEmitter this Spray plays, the same one you passed to [Spray.New]. Spray reads\nit but never changes or destroys it.",
            "lua_type": "ParticleEmitter",
            "readonly": true,
            "source": {
                "line": 236,
                "path": "Spray/init.luau"
            }
        }
    ],
    "types": [],
    "name": "Spray",
    "desc": "Plays a ParticleEmitter on the screen. You put a regular ParticleEmitter inside a GUI\nobject, set it up in the Properties window the same way you would for a 3D effect, and\nSpray draws its particles with ImageLabels.\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Spray = require(ReplicatedStorage.Packages.Spray)\n\nlocal button = script.Parent -- a TextButton with a ParticleEmitter called Confetti inside\nlocal confetti = Spray.New(button.Confetti)\n\nbutton.Activated:Connect(function()\n\tconfetti:Emit(40)\nend)\n```\n\nSpray draws on the client, so require it from a LocalScript, or from a ModuleScript that a\nLocalScript requires.\n\n## Where the emitter goes\n\nRoblox only draws a ParticleEmitter that's inside a Part or an Attachment, so one inside a\nGUI does nothing on its own. Spray is what draws it. The emitter needs a GuiObject above it\nin the Explorer, like a Frame, an ImageLabel or a TextButton. It's usually a direct child,\nbut a Folder in between works too, which is handy for keeping several effects under one\nbutton.\n\nParticles come out of the center of that GuiObject and are drawn inside it, so they move,\nscale and hide along with it. If you tween the frame, the effect follows. When the\nGuiObject, or anything above it, has `ClipsDescendants` on, particles get cut off at its\nedges unless you set the `SprayIgnoreClips` attribute.\n\nWhat the effect looks like comes from the emitter's own properties: Texture, Color, Size,\nTransparency, Squash, Lifetime, Speed, Rotation, RotSpeed, Acceleration, Drag,\nSpreadAngle, EmissionDirection, Shape, the flipbook settings, Rate, TimeScale and the\nrest. The few settings a 3D emitter has no property for are attributes, listed further\ndown.\n\n## Playing, pausing and the clock\n\nEvery Spray has a clock that counts seconds from the start of the effect. What you see on\nscreen is always calculated from that number and nothing else, so an effect can be\npaused, jumped to any moment, or played backwards, and you still get exactly the frame\nyou'd have seen by playing up to that point.\n\nA Spray is either playing, which means the clock moves forward every frame, or paused. A\nnew Spray starts paused at 0 seconds.\n\n- [Spray:Emit] releases particles. On a paused Spray it first rewinds to 0 and starts\n  playing. On a Spray that's already playing, the new particles join the ones already on\n  screen, the same as `ParticleEmitter:Emit()`.\n- [Spray:Pause] stops the clock and [Spray:Resume] starts it again.\n- [Spray:Forward] and [Spray:SetTime] move the clock. They don't pause or resume anything.\n\nIf the emitter has `Enabled` on and a `Rate` above 0, it also releases particles on its\nown for as long as the Spray is playing, like a 3D emitter does, so calling\n[Spray:Resume] is enough to start it.\n\n## Attributes\n\nThese are the settings a 3D emitter has no property for. They all go on the\nParticleEmitter itself, and they're all optional. To add one, select the emitter, scroll to\nthe bottom of the Properties window, press the + button next to Attributes, and type the\nname and type exactly as they appear here.\n\n| Attribute | Type | Default | What it does |\n| --- | --- | --- | --- |\n| `SprayScale` | number | `1` | Multiplies size and speed together, so the whole effect gets bigger or smaller without changing its shape. When an effect looks right but is the wrong size, change this one first. |\n| `SpraySizeScale` | number | `1` | Multiplies size only, on top of `SprayScale`. |\n| `SpraySpeedScale` | number | `1` | Multiplies speed only, on top of `SprayScale`. |\n| `SprayUnit` | string | `RelativeYY` | Which side of the GuiObject counts as one stud. `RelativeYY` is its height, `RelativeXX` its width, `RelativeMin` the shorter side and `RelativeMax` the longer one. `Offset` makes one stud equal one pixel. |\n| `SprayEmissionSize` | Vector2 | `0, 0` | Size of the area particles come out of, as a fraction of the GuiObject. `0, 0` is a single point in the middle and `1, 1` covers the whole GuiObject. The emitter's Shape, ShapeStyle and ShapeInOut are only used when this isn't `0, 0`. |\n| `SprayMaxParticles` | number | `400` | The most particles that can be on screen at once. [Spray:Emit] never releases more than this, and a `Rate` too high to fit gets lowered. |\n| `SprayGlowLayers` | number | `1` | How many copies of each particle get drawn, from 1 to 8. The extra copies are a fading halo that imitates `LightEmission`. Each copy is one more ImageLabel, so it costs performance. |\n| `SprayZIndex` | number | the GuiObject's ZIndex | The ZIndex that `ZOffset` gets added to. |\n| `SprayFlipbookGrid` | number | `4` | Frames per row of the flipbook when `FlipbookLayout` is `Custom`. The other layouts already say how many frames they have. |\n| `SprayFlipbookResolution` | number | `1024` | Size of the flipbook texture in pixels. Spray needs it to cut each frame out of the sheet. |\n| `SprayIgnoreClips` | boolean | `false` | Lets particles leave the GuiObject even when it, or something above it, has `ClipsDescendants` on. |\n| `SprayPrewarm` | number | `0` | How many particles to create ahead of time when the Spray is built. It does the same as calling [Spray:Prewarm] with that number. |\n\nAn attribute with the wrong type, like a string where a number should go, is ignored and\nthe default is used instead.\n\n## From 3D to the screen\n\nOne stud is the height of the GuiObject in pixels, or whichever side `SprayUnit` picks. In\na frame 200 pixels tall, a Size of 0.1 gives particles 20 pixels across, and a Speed of 2\nmoves them 400 pixels per second. Because it's based on the GuiObject and not on the\nscreen, the effect keeps its look on a phone or after the window is resized.\n\n`Acceleration` gets its Y flipped because on the screen +Y points down, so gravity written\nas `0, -10, 0` still pulls the particles down.\n\nFor `EmissionDirection`, Top, Bottom, Left and Right point where you'd expect on the\nscreen. Front and Back point into and out of the screen, which a flat GUI can't show, so\nboth work like Top. Only the X of `SpreadAngle` is used, and 180 spreads particles all the\nway around.\n\n`ZOffset` is added to the base ZIndex (see `SprayZIndex`), so emitters in the same\nGuiObject stack the way you'd expect. It can't lift particles above the GuiObject's\nsiblings when the ScreenGui uses `ZIndexBehavior.Sibling`, though. If an effect has to\nshow in front of some other part of your UI, put it inside a GuiObject that's already in\nfront.\n\nGUIs only have normal alpha blending, so `LightEmission` is imitated. `Brightness` above 1\npushes colors toward white, which gives you the bright core glowing particles have, and\n`SprayGlowLayers` adds a halo scaled by `LightEmission`. Overlapping particles still don't\nadd their light together the way they do in 3D. `LightInfluence` tints the particles\ntoward `Lighting.Ambient`.\n\nParticles always move with their GuiObject, as if `LockedToPart` were on.\n\n## The Spray type\n\nThe module exports the type of the object [Spray.New] returns as `Spray.Spray`. You only\nneed it to annotate variables or function parameters in your own code:\n\n```lua\nlocal Spray = require(ReplicatedStorage.Packages.Spray)\n\nlocal effects: { Spray.Spray } = {}\n\nlocal function PlayAll(list: { Spray.Spray })\n\tfor _, effect in list do\n\t\teffect:Emit(20)\n\tend\nend\n```",
    "source": {
        "line": 208,
        "path": "Spray/init.luau"
    }
}