Sprite and text effects
Shadow, outline, glow and blur on 2D sprites and on text, with what each one costs, what it does to a moving sprite, and the hand-rolled recipe it replaces.
What this is for#
Until now a 2D sprite could carry a tint and an image, and nothing else. To lift a character off its background you had to build the effect by hand: create a second sprite, tint it black, offset it, put it behind, and keep it in step with the first one on every single frame. It works. It is also four extra lines of bookkeeping per character, and the day you forget one of them the shadow trails behind its owner.
Four commands do that for you, on sprites and on text:
hero = LoadSprite("hero.png")
SpritePosition(hero, 160, 120)
SpriteShadow(hero, 4, 4, 6, 128) ' offset x, offset y, softness, opacity
SpriteOutline(hero, 2, 255, 255, 255) ' thickness, colour
SpriteGlow(hero, 0, 240, 255, 8) ' colour, size
DO
Sync()
LOOP
Nothing else to maintain. Move the sprite, scale it, flip it, play its animation: the effects follow.
The four effects#
Shadow#
SpriteShadow(spriteID, offsetX, offsetY, softness, opacity)
SpriteShadowColor(spriteID, red, green, blue)
offsetX and offsetY are in your own screen units, counted the same way as
SpritePosition: positive Y is down. softness spreads the shadow;
opacity runs 0 to 255. The colour is black unless you say otherwise.
SpriteShadow(hero, 3, 5, 4, 110)
SpriteShadowColor(hero, 20, 0, 40) ' a cold shadow, not a grey one
A shadow of opacity 0 is no shadow. That is how you turn it off.
Outline#
SpriteOutline(spriteID, thickness, red, green, blue)
The outline follows the silhouette of the sprite, not the rectangle it lives in: a transparent PNG gets an outline around the drawing, not around the file. Thickness is in screen units, and 1 to 3 is the useful range. Above that the eight directions the outline is built from start to show as eight lobes.
SpriteOutline(hero, 2, 255, 255, 255) ' the readable-on-anything outline
SpriteOutline(hero, 0, 0, 0, 0) ' off
Glow#
SpriteGlow(spriteID, red, green, blue, size)
A coloured halo, spreading size units past the silhouette. It sits behind the
sprite and in front of the shadow, which is what lets you use both.
SpriteGlow(pickup, 255, 210, 0, 10) ' this one is worth walking to
Blur#
SpriteBlur(spriteID, radius)
Blur is the odd one out. The other three are drawn around the sprite; blur replaces the sprite's own image with a blurred copy of it. That means two things worth knowing:
- it costs nothing per frame (the blurred image is computed once), and
- the blur is clipped at the edge of the image. A drawing that touches the edge of its PNG loses the part of its halo that would fall outside. Leave a few transparent pixels of margin in the file if you plan to blur it.
SpriteColor keeps working on a blurred sprite.
Turning everything off, and back on#
SpriteEffects(spriteID, 0) ' all four off
SpriteEffects(spriteID, 1) ' all four back, exactly as they were
SpriteEffects does not erase your settings. It is a switch, not a reset, so a
character can flash without you having to re-declare its shadow afterwards.
The same four on text#
score = CreateText("SCORE 0")
TextPositionSet(score, 10, 10)
TextSizeSet(score, 6)
TextOutline(score, 3, 0, 0, 0) ' readable on any background
TextShadow(score, 2, 2, 4, 160)
TextGlow(score, 255, 80, 0, 6)
TextBlur(score, 0)
TextEffects(score, 1)
The signatures match the sprite ones exactly, so there is nothing new to learn. Two differences in behaviour:
- A glow wins over a shadow's softness. Both are drawn by the same mechanism, and a glow with no softness is invisible while a shadow with no softness is still a shadow. When you ask for both, the glow gets the softness.
- These apply to vector text, not to bitmap-font text. A text drawn with
TextFontImageis a composition of sprites; the commands are silently ignored on it. Use the sprite effects on the sprites in that case.
A worked example: readable on anything#
The problem these commands actually solve is contrast you do not control. A white score is invisible on snow; a black one is invisible at night. An outline solves it once, for every background you will ever draw.
CreateText(1, "SCORE 0")
TextPositionSet(1, 10, 10)
TextSizeSet(1, 8)
TextColorSet(1, 255, 255, 255, 255)
TextOutline(1, 3, 0, 0, 0)
hero = LoadSprite("hero.png")
SpriteName(hero, "hero")
SpriteShadow(hero, 4, 6, 5, 120)
SpriteOutline(hero, 2, 255, 255, 255)
points = 0
DO
points = points + 1
TextString(1, "SCORE " + Str(points))
Sync()
LOOP
Reading an effect back#
Every parameter you write has a reader, the way SpriteColor has
SpriteColorRed. That is the dialect's rule, and these commands follow it.
IF SpriteEffectsEnabled(hero) = 1
SpriteEffects(hero, 0)
ENDIF
alpha = SpriteShadowAlpha(hero) ' 0 to 255
size = SpriteGlowSize(hero) ' 0 means no glow
The full set, per family: SpriteShadowOffsetX, SpriteShadowOffsetY,
SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed,
SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed,
SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize,
SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen,
SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, plus the same
seventeen with a Text prefix.
Two rules worth knowing:
- A sprite that has never been given an effect reports itself as ON. That is the default state; answering 0 would tell a program its effects are cut before it has set any.
- Asking about a sprite that does not exist answers 0, it does not stop the
program. That is how the dialect treats questions everywhere:
SpriteHitanswers 0 when nothing is touched, and programs read that 0 without testing it.
What it costs#
Shadow, outline and glow are drawn, so they cost draw calls: one for the shadow (three when it is soft), four for a glow, eight for an outline. A screen with two hundred outlined sprites is a screen drawing eighteen hundred quads, and it will show. They are meant for the handful of things the player is meant to look at: the character, the pickup, the selected unit.
Blur costs nothing per frame.
A practical rule: effects on what moves and matters, none on the background.
What crosses into an exported game#
| Target | Shadow, outline, glow | Blur | Text effects |
|---|---|---|---|
| The editor | yes | yes | yes |
| HTML5 export | yes | yes | yes |
| Native and wasm (Bevy) | yes | no | no |
Shadow, outline and glow are built out of ordinary sprites, so they travel everywhere sprites do. Blur and the text effects need a bridge the Bevy targets do not have yet: the export conversion report names them rather than dropping them silently, so a build tells you what it could not carry.
Where the effects are not#
There is no SpriteEffect(id, "glow", ...) taking the effect name as a string.
Each effect is its own command so that its parameters can be named, documented
and completed as you type. For anything beyond these four, two other commands take over:
SpriteBlendModes(id, src, dst) for additive or multiplied blending (a fire, a
laser), and SpriteShader(id, shaderID) to put a shader compiled by
LoadSpriteShader onto a sprite. Both were declared in the catalogue for a long
time without an implementation, and calling them raised an error; they work now.