genart is my Scala 3 library for making art with code. It provides a common API for sketches that run in a desktop window using Processing or in the browser using p5.js, compiled through Scala.js; this allows me to write Generative Artwork using a simple, high-level API in a functional language with an expressive typesystem, without having to worry too much about how the internals work.
Here’s a small diagram illustrating how this project’s architecture works (at a high level).
Drawing and animation
genart exposes APIs for setup, updates, drawing, and event handling for keyboard and pointer input; its canvas APIs support basic polygons, Bézier paths, gradients, text, images, and complicated clipping/blending.
Sketches
Here are a few sketches from the repository, compiled from Scala to run in your browser. Choose an example below and press Run sketch to try it.
Run the sketch to see it here.
Source
package examples
import genart.{Color, Frame, Sketch, SketchContext, Step}
import genart.lib.math.{Vec, floorMod}
import genart.lib.random.Rng
final class ColorWalk(context: SketchContext) extends Sketch:
private val canvas = context.canvas
private val background = Color.rgb(1.0, 1.0, 1.0)
private val typicalSpeed = math.toRadians(60.0)
private val persistenceSeconds = 2.0
private var rng = Rng(context.seed)
private var angle = 0.0
private var angularVelocity = 0.0
override def setup(): Unit =
val (initialAngle, next) = rng.uniform(0.0, 2.0 * math.Pi)
val (direction, remaining) = next.nextInt(2)
angle = initialAngle
angularVelocity = if direction == 0 then typicalSpeed else -typicalSpeed
rng = remaining
override def update(step: Step): Unit =
val (normal, next) = rng.nextGaussian
val decay = math.exp(-step.deltaSeconds / persistenceSeconds)
val previousVelocity = angularVelocity
angularVelocity = angularVelocity * decay +
typicalSpeed * math.sqrt(1.0 - decay * decay) * normal
angle += (previousVelocity + angularVelocity) / 2.0 * step.deltaSeconds
rng = next
def draw(frame: Frame): Unit =
val hue = math.toDegrees(angle)
val size = math.min(canvas.width / 2.0, canvas.height) * 0.7
val radius = size / 2.0
val squareX = canvas.width / 4.0
val wheelX = canvas.width * 0.75
val y = canvas.height / 2.0
canvas.background(background)
canvas.noStroke()
canvas.fill(Color.hsb(hue, 0.7, 0.85))
canvas.rect(Vec(squareX - radius, y - radius), Vec(size, size))
canvas.strokeWeight(size * 0.012)
for degrees <- 0 until 360 do
val angle = math.toRadians(degrees.toDouble)
canvas.stroke(Color.hsb(degrees.toDouble, 0.7, 0.85))
canvas.line(
Vec(wheelX, y),
Vec(wheelX + radius * math.cos(angle), y + radius * math.sin(angle))
)
canvas.noFill()
canvas.stroke(background)
canvas.strokeWeight(size * 0.04)
canvas.circle(Vec(wheelX, y), size * 1.04)
canvas.noStroke()
canvas.fill(background)
canvas.circle(Vec(wheelX, y), size * 0.92)
val markerAngle = floorMod(angle, 2.0 * math.Pi)
canvas.noFill()
canvas.stroke(Color.rgb(0.1, 0.1, 0.1))
canvas.strokeWeight(size * 0.008)
canvas.circle(
Vec(
wheelX + radius * 0.96 * math.cos(markerAngle),
y + radius * 0.96 * math.sin(markerAngle)
),
size * 0.1
)
object ColorWalk extends (SketchContext => ColorWalk):
def apply(context: SketchContext): ColorWalk = new ColorWalk(context)package examples
import genart.{
Color,
Frame,
Image,
Sketch,
SketchContext,
TextAlign,
TextBaseline,
TextStyle
}
import genart.lib.math.Vec
import scala.util.Using
final class Layers(context: SketchContext) extends Sketch:
private val canvas = context.canvas
private var stamps = Vector.empty[Image]
override def setup(): Unit =
stamps = Using.resource(canvas.surface(160, 160)) { layer =>
val drawing = layer.canvas
Vector.tabulate(2) { variant =>
drawing.clear()
drawing.noStroke()
for petal <- 0 until 3 do
val angle = petal * 2.0 * math.Pi / 3.0 + variant * math.Pi
drawing.fill(
Color.hsb(205.0 + variant * 120.0 + petal * 45.0, 0.65, 0.85, 0.65)
)
drawing.circle(
Vec(80.0 + 28.0 * math.cos(angle), 80.0 + 28.0 * math.sin(angle)),
84.0
)
drawing.noFill()
drawing.stroke(Color.rgb(0.15, 0.15, 0.15))
drawing.strokeWeight(2.0)
drawing.circle(Vec(80.0, 80.0), 144.0)
// Each snapshot survives both clearing and closing its source surface.
layer.snapshot()
}
}
def draw(frame: Frame): Unit =
canvas.background(Color.rgb(1.0, 1.0, 1.0))
canvas.noStroke()
canvas.fill(Color.rgb(0.93, 0.93, 0.93))
val tile = math.max(12.0, math.min(canvas.width, canvas.height) / 12.0)
for
row <- 0 until math.ceil(canvas.height / tile).toInt
column <- 0 until math.ceil(canvas.width / tile).toInt
if (row + column) % 2 == 0
do canvas.rect(Vec(column * tile, row * tile), Vec(tile, tile))
val side = math.min(canvas.width * 0.32, canvas.height * 0.28)
val size = Vec(side, side)
for (stamp, row) <- stamps.zipWithIndex do
val y = canvas.height * (0.28 + row * 0.42)
for copy <- 0 until 2 do
canvas.scoped {
canvas.translate(Vec(canvas.width * (0.3 + copy * 0.4), y))
canvas.rotate(if copy == 0 then -0.22 else 0.22)
canvas.image(stamp, size * -0.5, size)
}
canvas.fill(Color.rgb(0.15, 0.15, 0.15))
canvas.text(
if row == 0 then "First snapshot" else "After clear and redraw",
Vec(canvas.width / 2.0, y + side * 0.75),
TextStyle(
size = math.min(14.0, side * 0.12),
align = TextAlign.Center,
baseline = TextBaseline.Top
)
)
object Layers extends (SketchContext => Layers):
def apply(context: SketchContext): Layers = new Layers(context)package examples
import genart.{
Color,
Frame,
Gradient,
Sketch,
SketchContext,
TextAlign,
TextStyle
}
import genart.lib.math.Vec
final class Gradients(context: SketchContext) extends Sketch:
private val canvas = context.canvas
private val linear = Gradient.Linear(
Vec(0.0, 0.0),
Vec(220.0, 0.0),
Vector(
Gradient.Stop(0.0, Color.hsb(205.0, 0.8, 0.85)),
Gradient.Stop(0.5, Color.hsb(275.0, 0.6, 0.9)),
Gradient.Stop(1.0, Color.hsb(335.0, 0.7, 0.95))
)
)
private val radial = Gradient.Radial(
Vec(110.0, 70.0),
90.0,
Vector(
Gradient.Stop(0.0, Color.hsb(45.0, 0.3, 1.0)),
Gradient.Stop(1.0, Color.hsb(15.0, 0.85, 0.85))
)
)
def draw(frame: Frame): Unit =
canvas.background(Color.rgb(1.0, 1.0, 1.0))
val scale = math.min(canvas.width / 560.0, canvas.height / 320.0)
canvas.scoped {
canvas.translate(Vec(canvas.width / 2.0, canvas.height / 2.0))
canvas.scale(Vec(scale, scale))
canvas.translate(Vec(-280.0, -160.0))
canvas.noStroke()
for (label, gradient, x) <- Vector(
("Linear", linear, 30.0),
("Radial", radial, 310.0)
)
do
canvas.scoped {
// Setting paint before a transform keeps the gradient local to drawing.
canvas.fill(gradient)
canvas.translate(Vec(x, 35.0))
canvas.rect(Vec(0.0, 0.0), Vec(220.0, 140.0))
canvas.fill(Color.rgb(0.15, 0.15, 0.15))
canvas.text(
label,
Vec(110.0, 210.0),
TextStyle(size = 18.0, align = TextAlign.Center)
)
canvas.noFill()
canvas.stroke(gradient match
case radial: Gradient.Radial =>
radial.copy(center = Vec(110.0, 240.0), radius = 110.0)
case other => other)
canvas.strokeWeight(6.0)
canvas.line(Vec(0.0, 240.0), Vec(220.0, 240.0))
}
}
object Gradients extends (SketchContext => Gradients):
def apply(context: SketchContext): Gradients = new Gradients(context)package examples
import genart.{Color, Frame, PointerButton, PointerEvent, Sketch, SketchContext}
import genart.lib.math.{Vec, clamp}
final class DragShapes(context: SketchContext) extends Sketch:
private val canvas = context.canvas
private var centers = Vector.empty[Vec[2]]
private var dragging = Option.empty[(Int, Vec[2])]
private def side: Double = math.min(canvas.width, canvas.height) * 0.3
private def bounded(center: Vec[2]): Vec[2] =
val radius = side / 2.0
Vec(
clamp(center.x, radius, canvas.width - radius),
clamp(center.y, radius, canvas.height - radius)
)
override def setup(): Unit =
centers = Vector(
Vec(canvas.width * 0.4, canvas.height * 0.5),
Vec(canvas.width * 0.6, canvas.height * 0.5)
)
override def pointer(event: PointerEvent): Unit = event match
case PointerEvent.Pressed(at, PointerButton.Primary) =>
dragging = centers.indices.reverse.find { index =>
val delta = at - centers(index)
if index == 0 then delta.x.abs <= side / 2.0 && delta.y.abs <= side / 2.0
else delta.length <= side / 2.0
}.map(index => (index, at - centers(index)))
case PointerEvent.Moved(at) =>
dragging.foreach { case (index, offset) =>
centers = centers.updated(index, bounded(at - offset))
}
case PointerEvent.Released(_, PointerButton.Primary) | PointerEvent.Cancelled =>
dragging = None
case _ => ()
override def resized(): Unit =
centers = centers.map(bounded)
dragging = None
def draw(frame: Frame): Unit =
canvas.background(Color.rgb(1.0, 1.0, 1.0))
canvas.stroke(Color.rgb(0.15, 0.15, 0.15))
for index <- centers.indices do
canvas.strokeWeight(if dragging.exists(_._1 == index) then 4.0 else 2.0)
canvas.fill(Color.hsb(if index == 0 then 205.0 else 330.0, 0.6, 0.85, 0.75))
if index == 0 then
canvas.rect(centers(index) - Vec(side / 2.0, side / 2.0), Vec(side, side))
else canvas.circle(centers(index), side)
object DragShapes extends (SketchContext => DragShapes):
def apply(context: SketchContext): DragShapes = new DragShapes(context)There are more examples.