Elysium scripting Editor API index
Build

Drawing

The canvas - shapes, text, fonts, images, blur and glow, clipping and layers.

Drawing is immediate: every frame your frame handler describes what the screen should show, the canvas turns that into shapes, and next frame everything is gone and described again. There are no objects to create, move or delete - only calls. A script that draws a box at the same place every frame has a box at that place.

on( "frame", function( canvas )
	canvas:fill( 20, 20, 160, 28, color( 14, 12, 20, 180 ), 6 )          -- a rounded panel
	canvas:text( 32, 26, "hello", color( 255, 255, 255 ) )               -- text on it
end )

#The canvas

A canvas is handed to you by the frame event and by custom controls. Three rules cover most of what you need to know.

Coordinates are pixels, measured from the top left corner of the screen. On a custom control's canvas the origin is the control's own top left corner, and everything is cut off at its edges. render.screen() or c:screen() tell you how big the screen is.

Draw order is call order, within a layer. Later calls cover earlier ones. There are three layers, drawn one after the other, and c:layer() switches between them: back is the layer the cheat's own world overlays - player boxes, bars, impacts - are drawn in, and what you put there comes after them, so over them; mid, the default, is above all of that and below the menu; front is above the menu.

The canvas is only valid inside the callback that received it. Keeping it and drawing later raises an error, because by then the frame it belonged to is gone.

#Colours

Wherever a colour is wanted, a color goes: color( r, g, b [, a] ), each channel 0 to 255, alpha 255 (opaque) if left out. A plain table { r = 255, g = 0, b = 0 } or { 255, 0, 0, 128 } works too. A colour with alpha 0 draws nothing.

#Rounding

Calls that draw rectangles take an optional rounding: a number for all four corners, or a table { top_left, top_right, bottom_right, bottom_left } for each corner on its own. Radii of half a pixel or less are treated as square.

canvas:fill( 20, 20, 200, 40, color( 40, 30, 60 ), { 12, 12, 0, 0 } )   -- rounded on top only

#Shapes

#c:fill( x, y, w, h, color [, rounding] )

A filled rectangle.

#c:fill_gradient( x, y, w, h, top_left, top_right, bottom_right, bottom_left [, rounding] )

A rectangle whose four corners have four colours, blended across the surface.

canvas:fill_gradient( 20, 20, 200, 12,
	color( 168, 85, 247 ), color( 56, 189, 248 ),
	color( 56, 189, 248 ), color( 168, 85, 247 ) )

#c:frame( x, y, w, h, color [, thickness [, rounding]] )

The outline of a rectangle. thickness is in pixels, 1 by default.

#c:blur( x, y, w, h [, rounding [, tint]] )

Draws the picture behind the rectangle - the game and everything drawn earlier in the frame - blurred. tint multiplies the blurred picture (white, untouched, by default); lowering its alpha lets more of the sharp picture through. Put a translucent fill on top for the usual frosted-glass panel.

canvas:blur( 20, 20, 200, 60, 8 )
canvas:fill( 20, 20, 200, 60, color( 14, 12, 20, 140 ), 8 )

#c:glow( x, y, w, h, color [, rounding] )

A soft light in color around a rectangle. The rectangle is drawn into a separate pass, blurred, and added to the screen underneath everything else the cheat draws this frame - all three layers. Draw the shape itself with fill or frame on top, and the glow shows around its edges. Not available on a custom control's canvas, where it does nothing.

canvas:glow( 20, 20, 120, 30, color( 168, 85, 247, 200 ), 8 )
canvas:fill( 20, 20, 120, 30, color( 24, 20, 32 ), 8 )

#c:line( x1, y1, x2, y2, color [, thickness] )

A line between two points.

#c:circle( x, y, radius, color [, thickness [, segments]] )

The outline of a circle around x, y. segments is the number of straight pieces it is made of; 0, the default, picks a number that looks round at that radius.

#c:disc( x, y, radius, color [, segments] )

A filled circle.

#c:triangle( x1, y1, x2, y2, x3, y3, color [, filled [, thickness]] )

A triangle. filled is true by default; with false only the outline is drawn, thickness pixels wide.

#c:polygon( points, color [, filled [, thickness]] )

A convex polygon, filled by default, or outlined with filled = false. points is a flat list { x1, y1, x2, y2, ... } or a list of vec2, at least three points and at most 1024. A concave shape drawn with this call comes out wrong; build it from convex pieces.

#c:path( points, color [, closed [, thickness]] )

A connected line through the points - a graph, a trail, an outline. closed joins the last point back to the first. At least two points.

local points = { }

for i = 0, 40 do
	points[ #points + 1 ] = 20 + i * 5
	points[ #points + 1 ] = 100 + math.sin( i * 0.3 + render.time( ) * 3 ) * 20
end

canvas:path( points, color( 168, 85, 247 ), false, 2 )

#c:path_gradient( points, colors [, closed [, thickness]] )

A path whose colour changes along it: colors has one colour per point. If there are fewer colours than points, nothing is drawn.

#Text

#c:text( x, y, text, color [, options] )

Draws text with its top left corner at x, y. Newlines in text start new lines; the text is UTF-8. options is an optional table:

FieldMeaning
fontA font. Without it, the font set by c:font(), or the cheat's default.
align"left" (default), "center" or "right" - the text is placed around x.
valign"top" (default), "middle" or "bottom" - around y.
shadowtrue for a soft dark shadow, or a colour for a shadow of that colour.
outlinetrue or a colour: a one-pixel outline instead of a shadow.
canvas:text( 400, 300, "centred", color( 255, 255, 255 ), { align = "center", valign = "middle", shadow = true } )

A glyph the chosen font does not carry falls back to the cheat's own font, so a symbol is drawn instead of a gap.

#c:text_runs( x, y, runs [, options] )

Consecutive pieces of text, each in its own colour, laid out left to right as one line. runs is a list of { text, color } pairs; options is the same table as for c:text, and align and valign apply to the whole line.

canvas:text_runs( 20, 20, {
	{ "hp ", color( 160, 160, 170 ) },
	{ tostring( health ), color( 120, 220, 140 ) },
} )

#c:measure( text [, font] ) → width, height

How big text would be if drawn with c:text, in pixels. Use it to size a panel around its text or to right-align something.

#c:wrap( text, max_width [, font] ) → lines

Breaks text into a list of lines, none wider than max_width pixels. Lines are broken at spaces and at newlines; a single word longer than the width is cut inside the word. At most 8192 characters in, at most 256 lines out.

local lines = canvas:wrap( "a long sentence that has to fit into a narrow column", 140 )

for i, line in ipairs( lines ) do
	canvas:text( 20, 20 + ( i - 1 ) * 16, line, color( 255, 255, 255 ) )
end

#c:font( font )

Sets the font that later text calls on this canvas use when they name none, until the callback ends. c:font( nil ) goes back to the default.

#Fonts

A font is a typeface at one size. Building one is not free, so a script makes the fonts it needs once - at load, or the first time a size is asked for - and keeps them.

#render.font( source, size [, options] ) → font | nil, message

Loads a font.

  • source - either the name of an installed face: "Segoe UI", "Arial", "Tahoma", "Verdana", "Calibri", "Consolas", "Courier New", "Georgia", "Trebuchet MS", "Impact", "Times New Roman", "Lucida Console" (any other name without a dot or a slash is tried as <name>.ttf in the Windows Fonts folder) - or the path of a font file in the script's folder: "fonts/mono.ttf".
  • size - in pixels, between 4 and 200.
  • options.weight - for a variable font, a point on its weight axis (100 to 900). For the installed faces a weight of 600 or more picks the bold file. Static font files ignore it.

Answers nil and a message when the file cannot be read - check for it, because a missing font is an everyday event. The font itself is built on the first frame it is drawn with; fonts with the same file, size and weight share one build, so reloading a script a hundred times costs one.

local mono = render.font( "Consolas", 14 )

if not mono then
	warn( "no Consolas on this machine" )
end

#render.builtin_font( name [, size] ) → font

One of the cheat's own faces, ready to use. name is "ui", "ui_bold", "display", "lokeya", "icons" or "pixel"; size is "small", "normal" (the default) or "large". An unknown name raises an error.

#font:size() → number

The size in pixels the font was made at.

#font:ready() → boolean

Whether the font has been built yet. A font that is not ready draws nothing - and a font that failed to build never becomes ready.

#font:has_glyph( codepoint ) → boolean

Whether the font carries a glyph for a Unicode code point, such as 0x2764.

#font:measure( text ) → width, heightrender thread

The size of text in this font. The glyph atlas belongs to the render thread, so this answers nil anywhere else and before the font is built; inside a frame handler it always works. Inside a frame handler c:measure() does the same.

#Images

Textures are loaded from the script's own folder, or from memory. Like fonts they are decoded and uploaded on the first frame they are drawn, so a texture straight after loading is not ready yet.

#render.texture( path ) → texture | nil, message

Loads an image file - PNG, JPEG, BMP, TIFF and whatever else Windows' own image codecs can decode. nil and a message if the file cannot be read. Files larger than 32 MB are refused.

#render.texture_data( bytes ) → texture

An encoded image that is already in memory as a string - from fs.read or a download.

#render.texture_rgba( width, height, pixels ) → texture

A texture from raw pixels: 8 bits each of red, green, blue and alpha, row after row, so pixels is exactly width * height * 4 bytes long. At most 8192 by 8192. A way to draw heat maps and generated pictures.

-- a 2x2 texture: red, green / blue, white
local pixels = string.char( 255,0,0,255,  0,255,0,255,  0,0,255,255,  255,255,255,255 )
local tiny   = render.texture_rgba( 2, 2, pixels )

#render.svg( markup_or_path [, scale] ) → texture | nil, message

A vector image rasterised once at scale (1 by default; 0.05 to 16). A string that starts with < is the SVG markup itself; anything else is the path of a file in the script's folder.

#render.icon( name [, scale] ) → texture

One of the game's own item icons, by name: a weapon name as ent:weapon_name() returns it ("ak47", "awp", "knife"), "c4", "planted_c4", "defuser", "hegrenade", "molotov", "smokegrenade". The icons are large vector pictures: a scale around 0.35 gives about the size of the weapon icon in the player overlay. An icon that has not been loaded from the game's files yet is retried every frame until it is.

#texture:size() → width, height

The size in pixels. For an image that has not been built yet this is 0, 0 - ask again once the texture is ready.

#texture:ready() → boolean

Whether it has been uploaded and can be drawn.

#texture:failed() → boolean

Whether decoding failed. A failed texture never becomes ready.

#texture:frames() → integer

The number of frames of an animated GIF; 1 for everything else.

#texture:restart()

Starts an animated GIF from its first frame.

#c:image( texture, x, y, w, h [, tint [, rounding]] )

Draws a texture stretched into the rectangle. tint multiplies the picture (white, untouched, by default) - lower its alpha to fade the image. A texture that is not ready yet is skipped.

#c:image_uv( texture, x, y, w, h, u0, v0, u1, v1 [, tint] )

Draws part of a texture: u0, v0 to u1, v1 are the corners of the part, each from 0 to 1. For sprite sheets and for cropping.

#Clipping, layers and the screen

#c:clip( x, y, w, h [, absolute] )

Everything drawn from now until the matching c:unclip() is cut off outside the rectangle. Clips nest: a second clip inside the first is the overlap of the two. With absolute = true it replaces the clip below it instead of intersecting with it.

A clip a script forgets to close is closed when the callback ends, so a mistake here cannot cut off the menu.

#c:unclip()

Ends the innermost clip.

canvas:clip( 20, 20, 120, 40 )
canvas:text( 10, 30, "this text is cut off at the clip's edges", color( 255, 255, 255 ) )
canvas:unclip( )

#c:layer( name )

Chooses the layer later calls draw into:

  • "back" - the layer of the cheat's own world overlays. Your shapes come after theirs, so they cover them; everything in mid and front covers yours.
  • "mid" - the default. Above the overlays, below the menu.
  • "front" - above the menu (only dropdowns and toasts rise above it).

The layer lasts until the callback ends; every callback starts in mid. Not available on a custom control's canvas, which always draws inside its own rectangle.

#c:screen() → width, height

The size of the screen in pixels.

#render.screen() → width, height

The same, without a canvas.

#render.dt() → number

Seconds since the previous frame.

#render.fps() → number

Frames per second.

#render.time() → number

Seconds on a clock that only moves forward, whose zero is arbitrary - use it for differences and animations, not as a date. For a calendar time see time.unix().

#Habits that pay off

Elysium scripting guide · built 2026-10-03