When working with geometric shapes in creative coding, I prefer to keep things as simple as possible. Here's how I represent and work with basic geometric primitives, focusing on clarity and simplicity over complex object-oriented approaches.
Points
The simplest representation - just x and y coordinates:
const point = [100, 200]
Some point utility functions:
function drawPoint(ctx, point, size = 2) {
ctx.beginPath()
ctx.arc(point[0], point[1], size, 0, Math.PI * 2)
ctx.fill()
}
function distance(p1, p2) {
const dx = p2[0] - p1[0]
const dy = p2[1] - p1[1]
return Math.sqrt(dx * dx + dy * dy)
}
Lines
A line is just two points, simple!
const line = [
[100, 100],
[200, 200],
]
Some line utility functions:
function drawLine(ctx, line) {
ctx.beginPath()
ctx.moveTo(line[0][0], line[0][1])
ctx.lineTo(line[1][0], line[1][1])
ctx.stroke()
}
function pointOnLine(line, t) {
return [
line[0][0] + (line[1][0] - line[0][0]) * t, //
line[0][1] + (line[1][1] - line[0][1]) * t,
]
}
Example:
const line = [
[20, 30],
[180, 160],
]
drawLine(ctx, line)
const point = pointOnLine(line, 0.4)
drawPoint(ctx, point)
Polylines
A polyline is just an array of points.
const polyline = [
[100, 100],
[200, 200],
[300, 300],
Some utility functions:
function drawPolyline(ctx, polyline) {
ctx.beginPath()
ctx.moveTo(polyline[0][0], polyline[0][1])
for (let i = 1; i < polyline.length; i++) {
ctx.lineTo(polyline[i][0], polyline[i][1])
}
ctx.stroke()
}
Example:
Circles
A circle is always a point and radius, no reason to complicate it!
const circle = [150, 150, 50] // [center x, center y, radius]
Some circle utility functions:
function drawCircle(ctx, circle) {
ctx.beginPath()
ctx.arc(circle[0], circle[1], circle[2], 0, Math.PI * 2)
ctx.stroke()
}
/**
* Checks if a point lies inside a circle using the distance formula
* @param {number[]} point - Point coordinates [x, y]
* @param {number[]} circle - Circle definition [centerX, centerY, radius]
* @returns {boolean} True if point is inside or on the circle, false otherwise
*/
function pointInCircle(point, circle) {
const dx = point[0] - circle[0]
const dy = point[1] - circle[1]
return dx * dx + dy * dy <= circle[2] * circle[2]
}
/**
* Calculates a point on a circle's circumference at a given angle
* @param {number[]} circle - Circle definition [centerX, centerY, radius]
* @param {number} angle - Angle in radians
* @returns {number[]} Point coordinates [x, y] on the circle's circumference
*/
function pointOnCircle(circle, angle) {
return [
circle[0] + Math.cos(angle) * circle[2], //
circle[1] + Math.sin(angle) * circle[2],
]
}
Rectangles
Rectangles ruin the simplicity streak we were on, there are so many ways to represent them! Let's just reuse the way we represent lines.
const rect = [
[100, 100],
[200, 150],
] // [[minX, minY], [maxX, maxY]]
And some very important utility functions:
function drawRect(ctx, rect) {
const [[minX, minY], [maxX, maxY]] = rect
const width = maxX - minX
const height = maxY - minY
ctx.strokeRect(minX, minY, width, height)
}
function pointInRect(point, rect) {
const [[minX, minY], [maxX, maxY]] = rect
return point[0] >= minX && point[0] <= maxX && point[1] >= minY && point[1] <= maxY
}
function rectCenter(rect) {
const [[minX, minY], [maxX, maxY]] = rect
return [(minX + maxX) / 2, (minY + maxY) / 2]
}
Polygons
A polygon is just an array of points that form a closed shape. We'll represent it as an array of [x,y] coordinates:
const polygon = [
[100, 100],
[200, 150],
[150, 200],
]
Some polygon utility functions:
function drawPolygon(ctx, polygon) {
ctx.beginPath()
ctx.moveTo(polygon[0][0], polygon[0][1])
for (let i = 1; i < polygon.length; i++) {
ctx.lineTo(polygon[i][0], polygon[i][1])
}
ctx.closePath()
ctx.stroke()
}
Full Example
Practical Usage Examples
Here's how these primitives work together in practice:
// Creating a grid of points
function createGrid(cols, rows, spacing) {
const points = []
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
points.push({
x: x * spacing,
y: y * spacing,
})
}
}
return points
}
// Creating a polygon (points in a circle)
function createPolygon(sides, radius, center = { x: 0, y: 0 }) {
const points = []
for (let i = 0; i < sides; i++) {
const angle = (i / sides) * Math.PI * 2
points.push({
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
})
}
return points
}
// Connect points with lines
function connectPoints(points) {
const lines = []
for (let i = 0; i < points.length - 1; i++) {
lines.push({
start: points[i],
end: points[i + 1],
})
}
return lines
}
Un Deux Trois
Description
Un Deux Trois is a generative art piece that balances repetition, variation, and spatial rhythm using a minimal geometric vocabulary. The canvas is divided into a uniform 24x24 grid, where each cell becomes a stage for simple yet expressive visual gestures: one, two, or three parallel lines.
The composition evolves from left to right. The first third of the canvas features a single line per cell, creating a calm, sparse texture. In the middle third, the density increases with two lines per cell, introducing subtle rhythm and movement. The final third culminates in three parallel lines per cell, evoking intensity and complexity.
Despite the fixed structure of the grid, random rotation of the line groups introduces a layer of unpredictability. Each cluster of lines is rotated to a random angle, lending the piece an organic flow that contrasts its strict geometry. This interplay between order and randomness invites the viewer to consider the visual and emotional weight of density, repetition, and direction in space.
The name Un Deux Trois (French for One Two Three) playfully references the numerical progression at the core of the concept. The final result is a field of quiet motion and structured chaos—lines dancing in silent synchrony across a gridded stage.
Code Sample
for (let j = 0; j < cellCountOneSide; j++) {
const pct = j / cellCountOneSide
for (let i = 0; i < cellCountOneSide; i++) {
ctx.save()
// translate to cell position
const x = i * cellSize + cellSize / 2
const y = j * cellSize + cellSize / 2
ctx.translate(x, y)
// rotate to random angle
const theta = Math.random() * 2.0 * Math.PI
ctx.rotate(theta)
ctx.beginPath()
if (pct < 0.333) {
// draw one line
} else if (pct < 0.666) {
// draw two lines
} else {
// draw three lines
}
ctx.strokeStyle = 'white'
ctx.lineWidth = 2
ctx.stroke()
ctx.closePath()
ctx.restore()
}
}