This article was originally posted on my website
I'm officially releasing, and open-sourcing, Draw: a programming language and environment for graphics.
Table of contents
- Motivation
- How it started
- Overview
- Design Decisions
- Examples
- A peek under the hood
- Current issues
- Missing features
- Potential applications
- What about alternatives?
Motivation
I like developing games. That means I have to work with graphics. Not just for creating the players, backgrounds, and buttons, etc, but also for putting graphics in a consistent format that is convenient to load programmatically.
For example, converting from this:
to something like this:
That way I don't have to hardcode the position of each frame within the game and can easily switch between them or replace the animation.
Whilst cheap assets are available online, they aren't always available or easy to find. Or cheap. And whilst there are people who can make good graphics, that can take time and you need something whilst you develop the game - programmer art.
There are two main ways I have approached creating and modifying graphics.
The first is by opening up Microsoft Paint and a lot of trial and error. This often involves a lot of repetition. To make something that can be loaded programmatically requires a lot of precision.
The second way is with code.
Java Swing, JavaFX, and HTML5 JavaScript all have a similar canvas API.
With Javascript I enter this code into the console on any browser tab.
document.body.innerHTML=''; // clear page
let canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
document.body.appendChild(canvas);
let ctx = canvas.getContext("2d");
// use context here to render to canvas
And then, after programmatically rendering whatever it is I want on the canvas, I right-click on it and save the result.
This worked fine if I wanted to create something like a chessboard, but it became messy when I tried to work with images and some features required running the code on a server.
Because many of the games I made used Java (most of my earlier games used Java Swing), I would sometimes use that to get around some of these problems. I would write some code on startup to load an image, do some processing, and save it in another file. Then I would compile and execute it, and look at the final image to see if it was the result that I wanted.
Not only did this require a lot of boilerplate, but it had a longer feedback loop, and requiring me writing, saving, and executing code that I only used once.
If only there was a way for me to use code to create graphics, with minimal boilerplate, without even having to save the code, and to see the output immediately.
Enter Draw.
How it started
For a school computing project, I wanted to make an evolutionary tower defence game, when I realised that I would really like to have the aforementioned graphics language and environment to design the graphics in the game. So I decided to go with that instead.
Fortunately Robert Nystrom (also known as munificent) had posted some chapters for his online book Crafting Interpreters. (Check his stuff out, it's really interesting. He also wrote the book Game Programming patters).
In this online book, he teaches how to make a programming language. He designed one called Lox, and made two implementations of it. The first implementation, fortunately for me, was in Java. And the second one is in C. At the time of writing, he is about 75% of the way through the second book. When I started this project, he was around 40% of the way into the first book.
The first implementation is what is known as a Tree Walk Interpreter. This is one of the easiest ways to implement a programming language, but it is less efficient. The implementation is of a virtual machine bytecode interpreter.
The first implementation was good enough for my purposes since I only intented to write small scripts, performance was not a very high priority. Also, since it was written in Java, it is easier to integrate with JavaFX, a popular Java GUI library that includes canvas.
So I followed his tutorial, did his exercises, added some more features, made some other changes. And then I implemented the GUI.
Whilst it is not very advanced yet, I have already found it very useful.
Overview
The Lox language is syntactically similar to Javascript since it is dynamically typed and uses curly brackets, but it is more semantically similar.
For simplicity, Draw mostly follows Lox.
// variable declaration
var message = "hello"; // only double quotes currently supported
var example; // can also be declared without value
// if statements
var age = 17;
if (age >= 18) {
// perform action
} else if (age == 16) {
// perform alternate action
} else {
// perform some other action
}
// while statements
var index = 0;
while (index < 10) {
index++;
// perform action
}
// for statements
for (var i = 0; i < 10; i++) {
// perform action
}
// functions
function factorial(num) {
return num <= 1 ? 1 : num * factorial(num-1);
}
// anonymous functions
var printHello = function(name) {
println("hello " + name);
// no string templates yet
// println prints to a new line, like Java's System.out.println
}
printHello("world");
// closures
// object oriented programming
class Square {
// constructor
init(length) {
// fields
this.length = length;
}
// methods
area() {
return this.length ** 2;
}
perimeter() {
return 4 * this.length;
}
static numSides() {
return 4;
}
}
var smallSquare = Square(3);
println(smallSquare.area());
println(Square.numSides);
// inheritance
class Parent {
speak() {
println("Hello World!");
}
}
class Child extends Parent {
}
Child().speak() // Hello World!
In the program, I have a help/welcome page that goes through the standard library and syntax, it also provides some tutorials.
Design decisions
- Context methods moved into canvas
In JavaFX, Java Swing, and HTML5 canvas, you don't render directly to the canvas. You have to obtain a graphics context from a canvas and render to that.
I've always thought that using a graphics context ruined a good metaphor. In real life, we draw on canvases, not contexts. Whilst graphics context is useful for 3D rendering, that is not something I expect to support in Draw.
So I "got rid of the middle man". Under the hood, I still use context, but the user will just render to the canvas.
- Syntax similar to Lox with some additional features
Some things have been changed to be more like javascript, e.g function
instead of fun
. This is intended to reduce the learning curve, make it easy to port to and from javascript, and seem more familiar.
Print is a function rather than a statement
I have also added a ternary operator:
println(2*3 < 5 ? "foo" : "bar"); // expect bar
array literals:
var lst = [3, 4, 5, 2, 1];
println(lst[0]); // expect 3
println(
lst.map(
function(item) {
return item * 2;
}
)
); // expect [6, 8, 10, 4, 2]
break and continue statements, multi-line comments (also supported nested comments), and do while statements, prefix and postfix unary increment and decrement:
var a = 3;
a++; // a = 4
--a; // a = 3
multiple short hand assignment operators:
var number = 10;
number += 3; //add 3 to number
number %= 2; //same as number = number % 2
number **=3; //cube number
// also -=, /=, *=
and a few data structures such as Array lists, Hashmaps, Strings, and Characters.
In addition to this, I have implemented a canvas object, a colour object, an image object, and a Math API.
Examples
Modern Art
var canvas = getCanvas();
for (var i = 0; i < 1000; i++) {
var r = Math.random() * 255;
var g = Math.random() * 255;
var b = Math.random() * 255;
canvas.setColor(Color(r, g, b, 255));
var x = Math.random() * 500;
var y= Math.random() * 400;
var w= Math.random() * 100;
var h = Math.random() * 100;
canvas.fillRect(x, y, w, h);
}
Chessboard
var canvas = getCanvas();
var black = Color(0, 0, 0, 255);
var white = Color(255, 255, 255, 255);
var size = 45;
for (var row = 0; row < 8; row++) {
for (var col = 0; col < 8; col++) {
var tile = (row+col) % 2 == 0 ? black : white;
canvas.setColor(tile);
canvas.fillRect(col*size, row*size, size, size);
}
}
Asteroids
For my game Deluge, I wanted to add asteroids as obstacles. I also wanted to release my game soon, so I didn't want to wait for someone else to make it for me before releasing.
So I whipped up Draw and decided to try myself.
My initial thought is that an asteroid is sort of like a bumpier circle. A circle could be thought of as a bunch of points equally far from one point.
var canvas = getCanvas(); // get the main canvas in current tab
var centreX = 200;
var centreY = 200;
var distance = 50;
for (var angle = 0; angle < 360; angle++) {
var x = centreX + Math.sin(Math.toRadians(angle)) * distance;
var y = centreY + Math.cos(Math.toRadians(angle)) * distance;
canvas.fillRect(x, y, 1, 1);
}
This resulted in a blurry circle, and wasn't quite what I had in mind when I said that an asteroid was like a bumpy circle. Instead of filling one pixel 360 times, I draw lines between each point. This makes it easier to modify into an asteroid - by changing the distance of the points.
To do this, instead of using filling 1-pixel rectangles, I will draw a path, using the canvas API.
I position the first point, and add lines between the points.
var canvas = getCanvas();
var centreX = 200;
var centreY = 200;
var distance = 50;
canvas.beginPath();
canvas.moveTo(centreX, centreY + distance);
for (var angle = 0; angle < 360; angle++) {
var x = centreX + Math.sin(Math.toRadians(angle)) * distance;
var y = centreY + Math.cos(Math.toRadians(angle)) * distance;
canvas.lineTo(x, y);
}
canvas.drawPath();
To make it less round, and more asteroid like, I will vary the distance of the points from the centre, but also keep them within a certain range.
var canvas = getCanvas();
function randomInRange(min, max) {
return min + Math.random() * (max-min);
}
var centreX = 200;
var centreY = 200;
var minDistance = 40;
var maxDistance = 60;
var distance = randomInRange(40, 60);
canvas.beginPath();
canvas.moveTo(centreX, centreY + 50);
for (var angle = 0; angle < 360; angle++) {
distance = randomInRange(40, 60);
var x = centreX + Math.sin(Math.toRadians(angle)) * distance;
var y = centreY + Math.cos(Math.toRadians(angle)) * distance;
canvas.lineTo(x, y);
}
canvas.drawPath();
This produces a very jagged asteroid. To make it smoother, I will vary the angles from the centre, so that the lines are further apart. Because the distances from the centre have changed, I will need to close the path at the end so that the canvas automatically joins the last point to the first one.
var canvas = getCanvas();
function randomInRange(min, max) {
return min + Math.random() * (max-min);
}
var centreX = 200;
var centreY = 200;
var minDistance = 40;
var maxDistance = 60;
var distance = 50;
canvas.beginPath();
canvas.moveTo(centreX, centreY + 50);
for(var angle = 0; angle < 360; angle += randomInRange(15, 25)) {
var x = centreX + Math.sin(Math.toRadians(angle)) * distance;
var y = centreY + Math.cos(Math.toRadians(angle)) * distance;
canvas.lineTo(x, y);
distance = randomInRange(40, 60);
}
canvas.closePath();
canvas.drawPath();
To add a finishing touch, I will make the outline thicker, and fill the asteroid with a dark brown colour.
var canvas = getCanvas();
function randomInRange(min, max) {
return min + Math.random() * (max-min);
}
var centreX = 200;
var centreY = 200;
var minDistance = 40;
var maxDistance = 60;
var distance = 50;
canvas.setColor(Color(90, 10, 40, 255));
canvas.setLineWidth(5);
canvas.beginPath();
canvas.moveTo(centreX, centreY + 50);
for(var angle = 0; angle < 360; angle += randomInRange(15, 25)) {
var x = centreX + Math.sin(Math.toRadians(angle)) * distance;
var y = centreY + Math.cos(Math.toRadians(angle)) * distance;
canvas.lineTo(x, y);
distance = randomInRange(40, 60);
}
canvas.closePath();
canvas.fillPath();
canvas.setColor(Color(0, 0, 0, 255));
canvas.drawPath();
For some time, I have used this in my game:
until the graphic designer made this:
Magic Real Estate
For Ludum Dare, I made Magic Real Estate.
I initially started with rectangles. Then I spent some time making graphics for the players using Draw. The code is not great to look at, but it beat trying to draw by hand.
By changing a few lines, I was able to draw both wizards.
This was the final result:
The difference was two lines of code.
var canvas = getCanvas();
/*
I started with the hat, this is just a triangle, uncommenting the next line
makes it red rather than blue, for the other wizard
*/
canvas.setColor(Color(70, 50, 180, 255));
//canvas.setColor(Color(255, 0, 0, 255));
canvas.beginPath();
canvas.moveTo(100, 100);
canvas.lineTo(150, 0);
canvas.lineTo(200, 100);
canvas.fillPath();
/*
Then I moved on to the clothes, also a triangle
*/
canvas.beginPath();
canvas.moveTo(120, 150);
canvas.lineTo(180, 150);
canvas.lineTo(230, 270);
canvas.lineTo(70, 270);
canvas.fillPath();
canvas.setLineWidth(30); // arms are thick lines
canvas.drawLine(120, 150, 60, 200);
canvas.drawLine(180, 150, 250, 200);
/*
next was the face
This was a quadratic curve between two points
*/
canvas.setColor(Color(240, 220, 160, 255));
canvas.beginPath();
canvas.moveTo(100, 100);
canvas.quadraticCurveTo(150, 250, 200, 100);
canvas.fillPath();
/*
The outline of the beard is also a quadratic curve but not a filled one
*/
canvas.setLineWidth(5);
canvas.setColor(Color(200, 210, 200, 255));
canvas.beginPath();
canvas.moveTo(105, 105);
canvas.quadraticCurveTo(150, 250, 195, 105);
canvas.drawPath();
/*
The goatee included two curves, one filled and the other outlined,
with different thicknesses
*/
canvas.beginPath();
canvas.moveTo(122, 152);
canvas.quadraticCurveTo(150, 250, 180, 152);
canvas.fillPath();
canvas.setLineWidth(10);
canvas.beginPath();
canvas.moveTo(130, 152);
canvas.quadraticCurveTo(150, 120, 170, 152);
canvas.drawPath();
/*
Next was the hair and eye lashes, just lines
*/
canvas.drawLine(105, 105, 100, 200);
canvas.drawLine(195, 105, 200, 200);
canvas.setLineWidth(7);
canvas.drawLine(120, 110, 130, 106);
canvas.drawLine(170, 106, 180, 110);
// eyes - just black circles
canvas.setColor(Color(0, 0, 0, 255));
canvas.fillCircle(130, 120, 13);
canvas.fillCircle(170, 120, 13);
/*
and finally the staff, uncommenting the next line makes it black
*/
canvas.setColor(Color(255, 215, 0, 255));
canvas.setLineWidth(20);
canvas.drawLine(50, 20, 50, 250);
I recommend writing this code block by block. Whilst this is not the best way to make such graphics (too many magic numbers and constants), using Draw made me finish this much faster than trying to do it without code, because I was able to trial and error many changes and get feedback quickly, and able to achieve a level of precision that I rarely get elsewhere.
Spritesheets
Starting with this:
I was able to produce this:
with this code:
var canvas = getCanvas();
var dir = "insert_directory_here";
var spritesheet = loadImage(dir);
for (var row = 0; row < 4; row++) {
for (var col = 0; col < 4; col++) {
var frame = spritesheet.getSubimage(col*50, 20+row*100, 40, 80);
// set pink colour to transparent
frame = frame.setTransparentColor(Color(255, 0, 255, 255));
canvas.drawImage(frame, col*40, row*80);
}
}
// saveImage(canvas.toImage(), "outputdirectory") would have saved it, I right clicked
// on the canvas to save
A peek under the hood
There are two make packages in this project, for the interpreter and for the GUI.
The GUI contains basic editor code: keyboard shortcuts, file and line search, file opening and saving.
Because JavaFx can use CSS, it also has themes - but they don't look very good right now, so I stick with the default theme most of the time.
The GUI also contains support for two kinds of tabs: "draw tabs" and "help tabs". Each draw tab is bound to the interpreter. A draw tab contains a text section for the code, a console, a canvas, and a run button. When the run button is pressed, the code in the text area is interpreted, and when getCanvas()
is called, the canvas in the current tab is returned. println
and print
also logs to the current console.
The interpeter package is responsible for executing the code. First lexical analysis takes place in "Scanner.java". The output is a list of tokens. Then an abstract syntax tree is created, this happens in "Parser.java". Then semantic analysis takes place in "Resolver.java". And finally the abstract syntax tree is traversed and executed in "Interpreter.java".
It is in "Interpreter.java" where all the global functions are defined.
They all implement DrawCallable
.
This is the code for the println function:
globals.define("println", new DrawCallable() {
@Override
public int arity() {
return 1;
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
Main.getConsole().println(stringify(arguments.get(0)));
return null;
}
});
This is the code for the clock function:
globals.define("clock", new DrawCallable() {
@Override
public int arity() {
return 0;
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
return (double)System.currentTimeMillis();
}
});
Library classes extend LoxInstance. And the DrawMath class extends DrawClass because it is static.
Here is one of the simplest library classes, Color:
package com.drawlang.drawinterpreter;
import javafx.scene.paint.*;
public class DrawColor extends DrawInstance {
public Color color;
DrawColor(Color color) {
super(null);
this.color = color;
}
@Override
Object get(Token name) {
switch (name.lexeme) {
case "r": return color.getRed()*255;
case "g": return color.getGreen()*255;
case "b": return color.getBlue()*255;
case "a": return color.getOpacity()*255;
default:
throw new RuntimeError(name, "Undefined property '" + name.lexeme + "'.");
}
}
}
To obtain an instance of a color within the Drawlang code, I defined a global function that looks like a constructor in "Interpreter.java"
globals.define("Color", new DrawCallable() {
@Override
public int arity() {
return 4;
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
return new DrawColor( new Color(
(double)arguments.get(0)/255f, (double) arguments.get(1)/255f,
(double) arguments.get(2)/255f, (double) arguments.get(3)/255f
));
}
});
In addition to getting fields, children of DrawInstance can also support setting fields.
Current issues
- Continue statement in for loop causes infinte loop. For statements are "desugared into while statements" in the parsing stage.
This
for (var i = 0; i < 5; i++) {
// do stuff
}
becomes:
{
var i = 0;
while (i < 5) {
// do stuff
i++
}
This is the parser code for handling for statements:
private Stmt forStatement() {
consume(LEFT_PAREN, "Expect '(' after 'for'.");
// checks the initializer, e.g for('var i = 0') or for('i = 0')
// this also allows for no initializer - e.g for(;)
Stmt initializer;
if (match(SEMICOLON)) {
initializer = null;
} else if (match(VAR)) {
initializer = varDeclaration();
} else {
initializer = expressionStatement();
}
// checks for a condition - for(var i = 0; 'i < 5')
Expr condition = null;
if (!check(SEMICOLON)) {
condition = expression();
}
consume(SEMICOLON, "Expect ';' after loop condition.");
// check for incrementer expression - for(var i = 0; i < 5; 'i = i + 1')
Expr increment = null;
if (!check(RIGHT_PAREN)) {
increment = expression();
}
consume(RIGHT_PAREN, "Expect ')' after for clauses.");
loops++;
Stmt body = statement();
// if there is an incrementer add it to the end of the body
if (increment != null)
body = new Stmt.Block(Arrays.asList(body, new Stmt.Expression(increment)));
// if there is no condition it will loop infinitely
if (condition == null) condition = new Expr.Literal(true);
body = new Stmt.While(condition, body);
// if there is an initializer then add it to the start of the body
if (initializer != null)
body = new Stmt.Block(Arrays.asList(initializer, body));
loops--;
return body;
}
Then the interpreter executes it as though it was a while loop.
@Override
public Void visitWhileStmt(Stmt.While stmt) {
// keep executing statement body while statement
// condition evaluates to a non falsey value
while (isTruthy(evaluate(stmt.condition))) {
try {
execute(stmt.body);
} catch (BreakException breakException) {
break;
} catch (ContinueException continueException) {
}
}
return null;
}
For most cases, this works. But it means that continue statements never call the increment statement so it loops indefinitely.
Two ways I have thought of handling it are:
- Don't desugar for loops, handle them differently in the interpreter.
- In the AST, have an incrementer expression field that is set to null for while loops and the increment expression when it comes to for loops. Then, in the interpreter, call the incrementer if it exists when handling continue statements.
I am leaning towards the second solution.
- State persisting across execution.
Setting the current colour or line width of the canvas sets the default the next time the script is run in a tab. I would like it to start from a clean slate.
- Colour fields are non-modifiable because set has not been implemented.
- No animation The interpreter is executed in one thread. There is no support for keyboard input or for doing something for X amount of time. I am not sure if I would like to support this, but it could allow people to quickly prototype games.
Missing features
- Iterators. Right now I just have basic, C-style loops.
- Modules and imports
- Switch statements
- Headless mode. Right now the concept of a current tab is built in. This makes implementing imports harder.
- Foreign function interface. Allowing people to define library modules in other languages.
- Multiple assignment E.g
var a, b, c = 3, 4, 5;
- Line numbers
- Syntax highlighting
- Swizzling This is a handy feature in glsl.
gl_FragColor.rgb = glFragColor.bgr;
It is possible to create cool looking effects with this. Here is an example in a game engine tutorial I followed:
It might be possible to implement this in the get and set method of the DrawColor wrapper. By checking if the letters in the method are r, g, b, or a. And checking the length. Then returning an array or colour with the respective fields. And also setting the respective fields.
// rough java pseudocode
@Override
Object get(Token name) {
boolean validFields = true;
ArrayList<Char> fields = new ArrayList<Char>();
for (Char field : name) {
if (!(field.equals('r') || field.equals('g') || field.equals('b') || field.equals('a'))) {
validFields = false;
// throw error
} else {
fields.add(field);
}
}
if (fields.size() === 4) return new DrawColor(new Color(fields))
}
@Override
void set(Token name) {
// similar to get
}
Potential applications
- Compression. The file size of the code is often many times smaller than the file size of the output image. Not only that, but it is often possible to create images that look very different with code that is very similar by changing one parameter. One example of this is in an earlier post about biomorphs.
This image takes 2.29kb:
And this code takes 570 bytes:
var canvas = Canvas(160, 130);
function drawBranch(x, y, size, angle, angleDiff, iterations) {
if (iterations == 0) return;
var endX = x + Math.sin(Math.toRadians(angle)) * size;
var endY = y - Math.cos(Math.toRadians(angle)) * size;
canvas.drawLine(x, y, endX, endY);
drawBranch(endX, endY, size, angle+angleDiff, angleDiff, iterations-1);
drawBranch(endX, endY, size, angle-angleDiff, angleDiff, iterations-1);
}
drawBranch(80, 80, 10, 0, 35, 8);
saveImage(canvas.toImage(), "output_dir");
Changing the parameters to this:
drawBranch(80, 80, 10, 0.8, 0, 65, 10);
Produces this:
What about alternatives?
The first alternative that comes to mind is Processing and Processing JS.
For me, Draw has a few advantages over Processing.
- I don't like it. This is the main reason I prefer Draw. I personally do not like their API. Maybe I am biased because I am used to using some form of Canvas API and Processing just looks wrong to me, but, for whatever reason, their API does not correspond to how I think about images so I made my own.
- It is statically typed. I like static types, but I don't think it is a good idea for the kind of things I had in my when creating draw. I mostly intended to write small scripts and the advantages of static types are not visible in small scripts, only the disadvantages.
-
It was designed for non-programmers. From the Wikipedia page:
Processing is an open-source graphical library and integrated development environment (IDE) built for the electronic arts, new media art, and visual design communities with the purpose of teaching non-programmers the fundamentals of computer programming in a visual context
And from the ProcessingJS website:
Processing started as an open source programming language based on Java to help the electronic arts and visual design communities learn the basics of computer programming in a visual context
I made Draw for the opposite reason: I want to help people who know how to program, and think in terms of algorithms, create graphics.
Having said this, at the time of writing this, the maturity of Processing means that it is probably better for what I wrote Draw to achieve.
How to get started
The code is available here. You can download the JAR file from the classes folder.
Top comments (0)