Overview
• source code • report an issue
Runal is a simple creative coding environment for text and ascii art, that runs in the terminal.
It works similarly as processing or p5js and can either be programmed with JavaScript, or used as a Go package.
Features
- Text only: explore text and ascii art directly in the terminal
- Familiar: if you know processing or p5js, you already know Runal
- Simple primitives: provides a set of simple primitives for 2D shapes, trigonometry, randomization, colors...
- Multi-language: it can be programmed with Javascript, or used as a Go library
- Fast feedback loop: reloads your sketch each time you modify it
- Export: save your canvas to png images of gif animations
- Cross-platform: runs on Linux, macOS, and Windows
Installation
If you want to use Runal as a Go package, see Go package.
Runal runtime is available for Linux, macOS and Windows.
Quick-install
On linux or macOS, you can run this quick-install bash script:
curl -sSL empr.cl/get/runal | bash
Manual installation
Download the last release for your platform.
Linux & macOS
In your terminal:
# Extract files
mkdir -p runal && tar -zxvf signls_runal.tar.gz -C runal
cd runal
# Run runal
./runal
# Run runal demo
./runal -demo
Windows
We recommend using Windows Terminal with a good monospace font like Iosevka to display Runal correctly on Windows.
Unzip the archive and, in the same directory, run:
; Run runal
.\runal.exe
; Run runal demo
.\runal.exe -demo
Replace ./runal by .\runal.exe for every following commands.
Go install
If you're a Go developer, you can install it via go install:
go install github.com/emprcl/runal@latest
Build it yourself
You can also build it yourself if your want to.
Usage
JavaScript runtime
You will use JavaScript for scripting your sketch. Your js file should contain a setup and a draw method. Both methods take a single argument (here c) representing a canvas object that holds all the available primitives:
// sketch.js
function setup(c) {}
function draw(c) {}
And you can then execute the file with:
./runal -f sketch.js
The js file will be automatically reloaded when modified, no need to restart the command.
To exit, just type ctrl+c
.
Go package
Because Runal is written in Go, you can also use it as a Go package.
// sketch.go
package main
import (
"context"
"os"
"os/signal"
"github.com/emprcl/runal"
)
func main() {
runal.Run(context.Background(), setup, draw, onKey)
}
func setup(c *runal.Canvas) {}
func draw(c *runal.Canvas) {}
func onKey(c *runal.Canvas, key string) {}
Examples
Perlin noise map
// perlin_noise_map.js
function setup(c) {
// Here we're just saying to print each cells 2 times
// just to get a more balenced output.
// A terminal cell is not a square, so the result can feel
// squished a little bit.
c.cellPaddingDouble();
}
function draw(c) {
// For each cell of the canvas, we map a perlin noise value (between 0 and 1)
// to an ansi color (between 150 and 231).
// Adding c.framecount in the noise parameter makes the canvas move over the map.
for (let i = 0; i < c.width; i++) {
for (let j = 0; j < c.height; j++) {
let color = c.map(
c.noise2D(
i * 0.009 + c.framecount / 1000,
j * 0.009 + c.framecount / 1000,
),
0,
1,
150,
231,
);
// We will use "§" character for drawing,
// width the computed foreground color, and a black background.
c.stroke("§", color, "#000000");
c.point(i, j);
}
}
}
function onKey(c, key) {
// When hitting the "c" key, we save the current
// canvas in a png file.
if (key == "c") {
c.saveCanvas(`canvas_${Date.now()}.png`);
}
// When hitting the space keep, we update the noise seed
// to get a new map.
if (key == " ") {
c.noiseSeed(Date.now());
c.redraw();
}
}
More
You can find more small examples in the Github repository.
Reference
This documentation covers the full JavaScript API.
You're missing a specific method or feature? Feel free to open an issue.
Go properties and methods are the same as JavaScript, with an uppercase first letter (js: c.width, go: c.Width).
Properties
c.width
Returns the width of the canvas.
c.height
Returns the height of the canvas.
c.framecount
Number of frames rendered since the beginning.
c.isLooping
Boolean indicating whether the render loop is active.
Canvas
Drawing in the terminal is a little bit weird, because each cell is not a square. Therefore, results can look squished depending on what you're trying to do.
One simple way to fix this problem is to use 2 cells instead of one for drawing one canvas pixel, but the question is : which character to draw in the second cell?
You can control this behavior with 2 methods:
- c.cellPadding(char): it allows you to define which character to use in the second cell. One obvious option is to use a black space, but other choice may give you fun results.
- c.cellPaddingDouble(): it just duplicates the first character.
c.cellPadding(char)
Sets a character used for cell spacing between elements.
c.cellPaddingDouble()
Makes every cell duplicated.
c.clear()
Clears the canvas.
c.size(w, h)
Resizes the canvas.
Control
c.loop()
Starts the draw loop if stopped.
c.noLoop()
Stops the draw loop if started.
c.redraw()
Forces one frame render in noLoop mode.
c.fps(fps)
Sets the desired frames per second.
c.push()
Saves the current drawing state (stroke, fill, background, rotate, translate, scale).
c.pop()
Restores the last saved drawing state (stroke, fill, background, rotate, translate, scale).
Colors
For every color arguments (see Draw, you can set 2 types of color values:
- hexadecimal (ex:
#000000
): the hexadecimal color code - ANSI 256 (between 0 and 255): one of the 256 ANSI colors
Draw
There are 3 types of zones when you draw:
- stroke: a point, a line or the outline of a shape
- fill: the inside of a closed shape
- background: the background of the canvas
For each zones, you have 3 settings:
- text: the unicode character set to use when drawing
- foreground: the color of the text
- background: the color of the background of the cell
It's a little bit confusing but it's very powerful.
If you have a better API suggestion, please create an issue.
c.fill(text, foregroundColor, backgroundColor)
Sets the fill style used to render shapes.
c.fillText(text)
Sets only the fill character.
c.fillFg(color)
Sets only the foreground color for fill.
c.fillBg(color)
Sets only the background color for fill.
c.noFill()
Disables fill rendering.
c.stroke(text, foregroundColor, backgroundColor)
Sets the stroke style used to outline shapes.
c.strokeText(text)
Sets only the stroke character.
c.strokeFg(color)
Sets only the foreground color for stroke.
c.strokeBg(color)
Sets only the background color for stroke.
c.background(text, foregroundColor, backgroundColor)
Sets the background fill style with custom character and colors.
c.backgroundText(text)
Sets the character used for the background.
c.backgroundFg(color)
Sets the foreground color for the background style.
c.backgroundBg(color)
Sets the background color for the background style.
Shapes
c.text(str, x, y)
Draws a string at position (x, y).
c.point(x, y)
Draws a point at the given position.
c.line(x1, y1, x2, y2)
Draws a straight line between two points.
c.circle(x, y, r)
Draws a circle centered at (x, y) with the given radius r.
c.ellipse(x, y, rx, ry)
Draws an ellipse centered at (x, y) with radiuses rx and ry.
c.rect(x, y, w, h)
Draws a rectangle starting at (x, y) with width w and height h.
c.square(x, y, size)
Draws a square with the given top-left corner and side length.
c.triangle(x1, y1, x2, y2, x3, y3)
Draws a triangle using three vertex points.
c.quad(x1, y1, x2, y2, x3, y3, x4, y4)
Draws a quadrilateral defined by four points.
Curve
c.bezier(x1, y1, x2, y2, x3, y3, x4, y4)
Draws a Bezier curve using four control points.
Math
All JavaScript Math namespace objects are usable.
Runal doesn't define trigonometry functions, please use Math.sin(), Math.cos() etc...
c.map(value, inputStart, inputEnd, outputStart, outputEnd)
Maps a value from one range to another.
c.dist(x1, y1, x2, y2)
Returns the euclidean distance between two points (x1, y1) and (x2, y2).
Random
c.random(min, max)
Returns a random float between min and max.
c.randomSeed(seed)
Sets the seed for random number generation.
Noise
c.noise1D(x)
Generates 1D Perlin noise (float number, range [0, 1]) for a given input.
c.noise2D(x, y)
Generates 2D Perlin noise (float number, range [0, 1]) for a given (x, y) coordinate.
c.noiseSeed(seed)
Sets the seed for noise generation.
c.loopAngle(duration)
Returns the angular progress (in radians, range [0, 2π]) through a looping cycle of given duration in seconds.
c.noiseLoop(angle, radius)
Returns a noise value (range [0, 1]) by sampling the noise on a circular path of the given radius. Angle is the loop angle in radians, from 0 to 2π. You can use loopAngle for the angle value to control the loop duration. This is useful for creating cyclic animations or evolving patterns that repeat perfectly after one full loop.
c.noiseLoop1D(angle, radius, x)
Returns a 1D noise value (range [0, 1]) that loops as the angle progresses. It samples a 2D noise space using the given radius and combines it with a horizontal offset.
c.noiseLoop2D(angle, radius, x, y)
Returns a 2D noise value (range [0, 1]) that loops as the angle progresses. It samples a circular path in 2D noise space, offset by the (x, y) coordinates.
Transform
c.translate(x, y)
Moves the origin of the canvas.
c.rotate(angle)
Rotates the drawing context by the given angle in radians.
c.scale(factor)
Scales the drawing context by the given factor.
Image
c.saveCanvasToPNG(filename)
Exports the current canvas to an image file (png).
c.saveCanvasToGIF(filename, duration)
Exports the current canvas to an animated gif file for a given duration (in seconds).
c.savedCanvasFont(path)
Sets a custom font (tff) file used for rendering text characters in exported images generated via SaveCanvasTo...() methods.
Log
You can use JavaScript console.log() to log things.
let x = Math.osc(c.framecount);
console.log(x);
Entries will be written in a console.log file in your current directory.
You can display the logs live in another terminal pane with:
tail -f console.log
The console.log file is deleted upon exit.
Events
onKey(c, key)
Listen to keyboard events. Key contains the key string. **It's a root function, which means it should be placed at the same level as setup() and draw().
// mySketch.js
setup(c) { ... }
draw(c) { ... }
onKey(c, key) {
// save the current canvas in a png file
// when the "c" key is pressed.
if (key == "c") {
saveCanvasToPNG("canvas.png");
}
}