Shapes

Learn how to draw common geometric shapes

A number of shapes can be drawn in the window: triangles, squares, rectangles, quadrilaterals, lines, circles, ellipses, polygons, and polylines. All shapes are automatically added to the window when created, and they share some common features:

shape.x           # x position
shape.y           # y position
shape.z           # depth (drawing order); higher z is drawn on top
shape.z = 10      # changing z re-inserts the object in the correct order
shape.width       # width
shape.height      # height
shape.color       # the color or color set
shape.color = 'blue'
shape.opacity     # alpha (0.0..1.0)
shape.opacity = 0.5
shape.add         # add to the window (done automatically on creation)
shape.remove      # remove from the window
shape.show        # make visible (without changing z-order)
shape.hide        # stop drawing this frame, but keep its place in the scene
shape.visible?    # true if currently drawn
shape.contains?(x, y)  # hit-testing

All instance attributes can be read and set individually on any shape:

t = Triangle.new
t.x1 = 50
t.y3 = 25
t.color = 'red'

Anchor points

What shape.x and shape.y mean depends on the kind of shape, but there’s a simple rule:

  • Vertex-defined shapes (Triangle, Quad, Polygon, Polyline) anchor at the centroid.
  • Bounding-box shapes (Rectangle, Square) anchor at the top-left corner.
  • Center-defined shapes (Circle, Ellipse) anchor at the center.

Setting shape.x = translates the whole shape so its anchor lands at the new position. The anchor is also the default pivot for rotation, except on Rectangle and Square, which rotate around the bounding-box center rather than the top-left.

Bounding-box shapes (Rectangle, Square) and center-anchored shapes (Circle, Ellipse) also accept symbols for x: and y: to align them to the window. Each aligns its bounding box, and the anchor follows. The vertex-anchored shapes (Triangle, Quad, Polygon, Polyline) don’t: with no edge to hug, passing a symbol raises a clear error.

Fill and stroke

Every closed shape (Triangle, Quad, Rectangle, Square, Circle, Ellipse, and Polygon) accepts a fill, a stroke (outline), or both:

Parameter Default Description
fill true Whether to draw the filled interior
stroke_width 0 Outline thickness; 0 means no outline
stroke_color Same as color Outline color

A few examples:

# Just an outline — no fill
Rectangle.new(x: 10, y: 10, width: 100, height: 60,
              fill: false, stroke_width: 3, stroke_color: 'red')

# Filled with a contrasting outline
Circle.new(x: 100, y: 100, radius: 40,
           color: 'navy', stroke_width: 2, stroke_color: 'white')

opacity: applies to both the fill and the stroke at the same time. Sharp corners use miter joins, clamped at four times the stroke width (the same limit SVG uses), so very acute angles cap cleanly instead of spiking off into space.

Line and Polyline are stroke-only: they don’t take fill: or stroke_color:. Use color: and stroke_width: for those.

Triangles

Create a triangle using:

Triangle.new

Here are all the parameters:

Parameter Default Description
x1 50 Vertex 1 x
y1 0 Vertex 1 y
x2 100 Vertex 2 x
y2 100 Vertex 2 y
x3 0 Vertex 3 x
y3 100 Vertex 3 y
z 0 Depth
rotate 0 Rotation in degrees
rx, ry Centroid Rotation center
color 'white' Color (single or 3-element array for per-vertex)
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color (single or 3-element array)
opacity nil Alpha override

Example:

tri = Triangle.new(x1: 50, y1: 0, x2: 100, y2: 100, x3: 0, y3: 100, color: 'green')
tri.x = 200    # translates all vertices (x and y refer to the centroid)
tri.color = 'red'

# Outline-only triangle with a per-vertex gradient stroke
Triangle.new(x1: 0, y1: 100, x2: 100, y2: 100, x3: 50, y3: 0,
             fill: false, stroke_width: 6,
             stroke_color: ['red', 'yellow', 'aqua'])

# Or pass the same vertices as point pairs
Triangle.new(points: [[50, 0], [100, 100], [0, 100]], color: 'green')

Squares

Create a square using:

Square.new
Parameter Default Description
x 0 Top-left x, or :left / :center / :right to align to the window
y 0 Top-left y, or :top / :center / :bottom to align to the window
padding 0 Gap from the window edge for aligned axes; override per-edge with padding_top, padding_right, padding_bottom, padding_left
size 100 Side length
z 0 Depth
rotate 0 Rotation in degrees
rx, ry Center Rotation center
color 'white' Color (single or 4-element array for per-vertex)
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color (single or 4-element array)
opacity nil Alpha override

Example:

sq = Square.new(x: 10, y: 10, size: 50)
sq.size = 75

Rectangles

Create a rectangle using:

Rectangle.new
Parameter Default Description
x 0 Top-left x, or :left / :center / :right to align to the window
y 0 Top-left y, or :top / :center / :bottom to align to the window
padding 0 Gap from the window edge for aligned axes; override per-edge with padding_top, padding_right, padding_bottom, padding_left
width 200 Width
height 100 Height
z 0 Depth
rotate 0 Rotation in degrees
rx, ry Center Rotation center
color 'white' Color (single or 4-element array for per-vertex)
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color (single or 4-element array)
opacity nil Alpha override

Example:

rect = Rectangle.new(x: 10, y: 10, width: 100, height: 50)
rect.width = 200
rect.height = 100

Quadrilaterals

A quadrilateral defined by four vertices in clockwise order:

Quad.new
Parameter Default Description
x1, y1 0, 0 Top-left vertex
x2, y2 100, 0 Top-right vertex
x3, y3 100, 100 Bottom-right vertex
x4, y4 0, 100 Bottom-left vertex
z 0 Depth
rotate 0 Rotation in degrees
rx, ry Centroid Rotation center
color 'white' Color (single or 4-element array for per-vertex)
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color (single or 4-element array)
opacity nil Alpha override

Example:

quad = Quad.new(x1: 0, y1: 0, x2: 80, y2: 0, x3: 100, y3: 100, x4: 20, y4: 100)
quad.x = 150   # translates all four vertices so the centroid lands at x = 150
quad.color = ['red', 'green', 'blue', 'yellow']

# Or pass the same vertices as point pairs
Quad.new(points: [[0, 0], [80, 0], [100, 100], [20, 100]])

quad.x and quad.y are the centroid of the four vertices. That’s the same convention Triangle, Polygon, and Polyline use. If you want top-left anchor semantics, reach for Rectangle or Square; if you specifically want vertex 1, read quad.x1 directly.

Lines

Create a line using:

Line.new
Parameter Default Description
x1 0 Start x
y1 0 Start y
x2 100 End x
y2 100 End y
z 0 Depth
stroke_width 1 Line thickness
dash 0 Dash length (0 means solid)
gap 5 Gap between dashes
rotate 0 Rotation in degrees
rx, ry Midpoint Rotation center
color 'white' Single color, or [start, end] for a gradient
opacity nil Alpha override

Example:

line = Line.new(x1: 0, y1: 0, x2: 200, y2: 200)
line.length  # => geometric length of the line
line.x1 = 50
line.stroke_width = 5

# A dashed line that fades from red to aqua along its length
Line.new(x1: 0, y1: 0, x2: 400, y2: 0, stroke_width: 6,
         dash: 12, gap: 6, color: ['red', 'aqua'])

# Or pass the endpoints as point pairs
Line.new(points: [[0, 0], [200, 200]])

Pass two colors to color: and Ruby 2D interpolates between them along the line. If the line is dashed, the gradient carries smoothly from one dash to the next so the whole stroke reads as a single fade.

Circles

Create a circle using:

Circle.new
Parameter Default Description
x 0 Center x, or :left / :center / :right to align to the window
y 0 Center y, or :top / :center / :bottom to align to the window
padding 0 Gap from the window edge for aligned axes; override per-edge with padding_top, padding_right, padding_bottom, padding_left
z 0 Depth
radius 50 Radius
sectors 30 Number of triangle sectors (smoothness)
rotate 0 Rotation in degrees
rx, ry Center Rotation center
color 'white' Single color only
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color
opacity nil Alpha override

Example:

circle = Circle.new(x: 200, y: 200, radius: 40, color: 'blue', sectors: 60)
circle.radius = 60
circle.color = 'purple'

Ellipses

An ellipse is like a circle that’s been stretched along one axis:

Ellipse.new(x: 100, y: 100, xradius: 60, yradius: 30)
Parameter Default Description
x 0 Center x, or :left / :center / :right to align to the window
y 0 Center y, or :top / :center / :bottom to align to the window
padding 0 Gap from the window edge for aligned axes; override per-edge with padding_top, padding_right, padding_bottom, padding_left
z 0 Depth
xradius 50 Horizontal radius
yradius 30 Vertical radius
sectors 30 Number of triangle sectors (smoothness)
rotate 0 Rotation in degrees
rx, ry Center Rotation center
color 'white' Single color only
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color
opacity nil Alpha override

Hit-testing uses the actual ellipse equation, so contains? is true for points genuinely inside the curve, not just inside its bounding box. It tilts with the shape when you rotate, too, so clicks and hovers land on the oval you actually see.

Polygons

A polygon is any closed shape with three or more vertices, defined as a list of [x, y] point pairs:

Polygon.new(points: [[50, 0], [100, 50], [75, 100], [25, 100], [0, 50]])
Parameter Default Description
points (required) Array of [x, y] pairs
z 0 Depth
rotate 0 Rotation in degrees
rx, ry Centroid Rotation center
color 'white' Color (single or one per vertex)
fill true Whether to fill the interior
stroke_width 0 Outline thickness
stroke_color Same as color Outline color (single or one per vertex)
opacity nil Alpha override

Example:

hex = Polygon.new(
  points: [[50, 0], [100, 25], [100, 75], [50, 100], [0, 75], [0, 25]],
  color: ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
)

Hit-testing uses an even-odd ray cast, so concave polygons (stars, arrows, gears) work correctly.

Polylines

A polyline is a connected sequence of line segments, handy for paths, charts, and outlines. It’s open by default, but can join back to its first point with closed: true:

Polyline.new(points: [[0, 0], [50, 100], [100, 0], [150, 100]])
Parameter Default Description
points (required) Array of [x, y] pairs
z 0 Depth
stroke_width 1 Line thickness
rotate 0 Rotation in degrees
rx, ry Centroid Rotation center
color 'white' Color (single, or one per vertex for a gradient along the path)
opacity nil Alpha override (single value, or one per vertex)
closed false Connect the last point back to the first

Example:

# A simple zigzag path
Polyline.new(points: [[0, 0], [50, 100], [100, 0], [150, 100]],
             stroke_width: 3, color: 'orange')

# Per-vertex opacity — the tail fades out
Polyline.new(
  points: [[50, 200], [150, 150], [250, 200], [350, 120]],
  stroke_width: 3, color: 'orange',
  opacity: [1.0, 1.0, 1.0, 0.35]
)

# A closed outline — the last point joins back to the first.
# It's just the outline, though; for a filled version, use Polygon.
Polyline.new(points: [[100, 100], [200, 100], [200, 200], [100, 200]],
             stroke_width: 2, color: 'white', closed: true)

Just like color:, opacity: accepts either a single value that applies everywhere, or an array with one entry per vertex. When you pass an array, SDL interpolates smoothly between the values along each segment (handy for chart indicator lines with a faded projection tail, or any path that should trail off at one end).

The getter mirrors whatever you set: an array when per-vertex is active, the scalar alpha otherwise. Assigning a single value reverts to uniform opacity.

contains? measures perpendicular distance to each segment and returns true when the point is within stroke_width / 2.

Per-vertex stroke gradients

Wherever a shape has natural vertices, the stroke can fade between colors around its perimeter, not just the fill. Pass an array of colors to stroke_color: (or color: on Polyline) with one entry per vertex, and Ruby 2D interpolates between them along the outline:

Quad.new(x1: 0, y1: 0, x2: 100, y2: 0, x3: 100, y3: 100, x4: 0, y4: 100,
         fill: false, stroke_width: 4,
         stroke_color: ['red', 'yellow', 'aqua', 'lime'])

This works on Triangle (3 colors), Quad / Rectangle / Square (4), and Polygon / Polyline (one per vertex). Circle and Ellipse stay single-color, since their sectors don’t have a meaningful vertex order.

If you give a shape a per-vertex fill and don’t set stroke_color:, the stroke automatically picks up the same gradient, handy when you want the outline to trace the edge of the fill exactly.

Rotation

All shapes support rotation via the rotate attribute, specified in degrees. Each shape has a sensible default pivot (the centroid for vertex-defined shapes, the bounding-box center for Rectangle and Square, and the anchor itself for circles and ellipses), and you can override it with rx and ry:

rect = Rectangle.new(x: 100, y: 100, width: 50, height: 50, rotate: 45)
rect.rotate = 90
rect.rx = 0    # rotate around the origin instead
rect.ry = 0

An Ellipse tilts right along with everything else, which is the only way to sit an oval on a diagonal. (A Circle accepts rotate too, but since it looks the same at every angle, nothing appears to change.)

ell = Ellipse.new(x: 200, y: 150, xradius: 80, yradius: 30,
                  color: 'teal', rotate: 30)
ell.rotate = 90    # update any time

Visibility vs. scene-graph membership

Every shape has two independent lifecycle controls, and it pays to know which one to reach for.

Scene-graph membership is whether the object exists in the window’s collection at all. Use add: false to construct a shape without registering it, and .add / .remove to flip that later. Removing a shape pulls it out of iteration, z-ordering, and per-object event dispatch entirely.

Visibility is whether the shape draws this frame. Use .show / .hide (or set visible: directly) to toggle it. Hidden shapes keep their slot, their z-order, and their per-object events — only the drawing is skipped.

Reach for .hide and .show when you want a cheap per-frame toggle (a blinking cursor, a UI overlay you flick on and off, a paused entity that should still respond to clicks). Reach for .remove and .add for genuine lifecycle changes: spawn-on-trigger entities, render-block-only shapes, anything you don’t want participating in the scene at all.

hud = Rectangle.new(x: 0, y: 0, width: 800, height: 40, color: 'navy', z: 10)
hud.hide              # stops drawing, keeps its z-order
hud.show              # draws again
hud.visible = false   # same thing as .hide

# add: false constructs without registering — useful for spawn-on-trigger
# entities or render-block overrides
powerup = Square.new(x: 200, y: 200, size: 32, color: 'yellow', add: false)
powerup.add  # later, when triggered

One-shot rendering

Every shape has a class-level .render method for one-off drawing inside a render block. These don’t create persistent objects — they’re drawn fresh each frame and accept the same kwargs as .new, including the full color vocabulary (names, hex strings, arrays, Color objects, per-vertex arrays) and opacity::

render do
  Rectangle.render(x: 0, y: 0, width: 50, height: 50, color: 'red')
  Circle.render(x: 100, y: 100, radius: 25, color: '#00ff00')
  Triangle.render(x1: 0, y1: 0, x2: 50, y2: 0, x3: 25, y3: 50,
                  color: ['red', 'green', 'blue'])
end

Continue to the next topic


Suggest an edit to this page