Logo is a programming language designed in the 60s. Its most famous feature is turtle graphics: the programmer controls the "turtle" (cursor) with instructions like forward, left, right, repeat and the turtle leaves a 'trace' on the screen.
Today we'll build a compact, single-file logo interpreter in about 100 lines of pure JavaScript. To keep the code short, we'll only implement the four instructions above, plus color_cycle (not part of the standard Logo) that cycles through 36 HSV hues.
Click the 'Run' button below to see it in action!
Commands: forward N, left N, right N, repeat N [ ... ], color_cycle
The interpreter has three main parts: a tokenizer that splits the input into tokens, a parser that builds a simple AST, and an executor that walks the AST and draws on an HTML5 canvas using a turtle state. The color_cycle instruction advances the pen color through 36 evenly spaced hues on the HSV color wheel.
You only need one HTML file. The file contains UI controls, a canvas, and all JavaScript. Save the file and open it in any modern browser.
The tokenizer normalizes square brackets and splits the input on whitespace. It lowercases tokens so commands are case-insensitive. Brackets are kept as separate tokens so the parser can detect repeat blocks.
// Example tokenizer snippet
function tokenize(input) {
input = input.replace(/\[/g, ' [ ').replace(/\]/g, ' ] ');
const raw = input.trim().split(/\s+/).filter(Boolean);
return raw.map(t => t.toLowerCase());
}
The parser reads tokens and builds nodes: forward, left, right, repeat with a nested body, and color_cycle. It supports nested repeats by using a recursive parseBlock function.
// Parser returns an array of nodes
// Node examples:
// { type: 'forward', value: 100 }
// { type: 'repeat', count: 4, body: [...] }
// { type: 'color_cycle' }
The executor walks the AST and updates the turtle state: x, y, and angle. A forward draws a line from the current position to the new position computed with trigonometry. left and right change the angle. repeat runs its body multiple times.
An Abstract Syntax Tree or AST is a structured, in-memory representation of the program you wrote. Instead of working with raw text or tokens, the interpreter converts the program into a tree of nodes where each node represents a meaningful command or construct. The AST is what the executor walks to perform actions.
For the program:
repeat 4 [ color_cycle forward 150 right 90 ]
The parser produces an AST similar to this JSON:
[
{
"type": "repeat",
"count": 4,
"body": [
{ "type": "color_cycle" },
{ "type": "forward", "value": 150 },
{ "type": "right", "value": 90 }
]
}
]
The executor performs a simple traversal of the AST. For each node it:
forward or repeat).repeat), the executor recursively traverses that body the specified number of times.function execNodes(nodes) {
for (const node of nodes) {
if (node.type === 'forward') { ... }
else if (node.type === 'repeat') {
for (let k = 0; k < node.count; k++) execNodes(node.body);
}
// other node types...
}
}
// Forward example
const rad = degToRad(turtle.angle);
const nx = turtle.x + Math.cos(rad) * dist;
const ny = turtle.y + Math.sin(rad) * dist;
ctx.beginPath();
ctx.moveTo(turtle.x, turtle.y);
ctx.lineTo(nx, ny);
ctx.stroke();
turtle.x = nx; turtle.y = ny;
The color cycle precomputes 36 RGB strings by sampling hue values evenly from 0 to 360 degrees and converting HSV to RGB. Each color_cycle node increments an index and updates ctx.strokeStyle. Place color_cycle before a forward to draw that segment in the next hue.
// colorIndex advances and updates strokeStyle
colorIndex = (colorIndex + 1) % colors.length;
turtle.strokeStyle = colors[colorIndex];
ctx.strokeStyle = turtle.strokeStyle;
The following is a complete, runnable HTML file. It includes the UI, tokenizer, parser, executor, HSV conversion, and the color_cycle instruction.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Mini Logo Turtle with color_cycle Tutorial</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial; display:flex; gap:16px; padding:16px; }
#controls { width:360px; }
textarea { width:100%; height:240px; font-family: monospace; }
canvas { border:1px solid #ccc; background:#fff; }
button { margin-top:8px; padding:8px 12px; }
</style>
</head>
<body>
<div id="controls">
<h3>Mini Logo Turtle</h3>
<textarea id="program">
repeat 36 [ color_cycle forward 200 right 170 ]
</textarea>
<div>
<button id="run">Run</button>
<button id="clear">Clear</button>
<button id="reset">Reset Turtle</button>
</div>
<p><strong>Commands:</strong> <code>forward N</code>, <code>left N</code>, <code>right N</code>, <code>repeat N [ ... ]</code>, <code>color_cycle</code></p>
</div>
<canvas id="c" width="700" height="700"></canvas>
<script>
// Canvas setup
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Turtle state
let turtle = { x: W/2, y: H/2, angle: 0, pen: true, strokeStyle: '#000', lineWidth: 1.5 };
// Color cycle
const COLOR_STEPS = 36;
let colorIndex = 0;
const colors = generateHSVColors(COLOR_STEPS);
function generateHSVColors(steps) {
const arr = [];
for (let i = 0; i < steps; i++) {
const h = (i / steps) * 360;
const rgb = hsvToRgb(h, 1, 1);
arr.push(`rgb(${rgb.r},${rgb.g},${rgb.b})`);
}
return arr;
}
function hsvToRgb(h, s, v) {
const c = v * s;
const hp = h / 60;
const x = c * (1 - Math.abs((hp % 2) - 1));
let r1=0,g1=0,b1=0;
if (0 <= hp && hp < 1) { r1=c; g1=x; b1=0; }
else if (1 <= hp && hp < 2) { r1=x; g1=c; b1=0; }
else if (2 <= hp && hp < 3) { r1=0; g1=c; b1=x; }
else if (3 <= hp && hp < 4) { r1=0; g1=x; b1=c; }
else if (4 <= hp && hp < 5) { r1=x; g1=0; b1=c; }
else if (5 <= hp && hp < 6) { r1=c; g1=0; b1=x; }
const m = v - c;
return { r: Math.round((r1 + m) * 255), g: Math.round((g1 + m) * 255), b: Math.round((b1 + m) * 255) };
}
// Reset
function resetTurtle() {
turtle.x = W/2; turtle.y = H/2; turtle.angle = 0; turtle.pen = true;
turtle.strokeStyle = '#000'; turtle.lineWidth = 1.5;
colorIndex = 0;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,W,H);
ctx.lineWidth = turtle.lineWidth;
ctx.strokeStyle = turtle.strokeStyle;
ctx.fillStyle = '#000';
ctx.beginPath(); ctx.arc(turtle.x, turtle.y, 2, 0, Math.PI*2); ctx.fill();
}
resetTurtle();
// Tokenizer
function tokenize(input) {
input = input.replace(/\[/g, ' [ ').replace(/\]/g, ' ] ');
const raw = input.trim().split(/\s+/).filter(Boolean);
return raw.map(t => t.toLowerCase());
}
// Parser
function parseTokens(tokens) {
let i = 0;
function parseBlock() {
const nodes = [];
while (i < tokens.length) {
const tok = tokens[i++];
if (tok === ']') break;
if (tok === 'forward' || tok === 'fd') {
const val = tokens[i++]; nodes.push({type:'forward', value: Number(val)}); continue;
}
if (tok === 'left' || tok === 'lt') {
const val = tokens[i++]; nodes.push({type:'left', value: Number(val)}); continue;
}
if (tok === 'right' || tok === 'rt') {
const val = tokens[i++]; nodes.push({type:'right', value: Number(val)}); continue;
}
if (tok === 'repeat') {
const count = Number(tokens[i++]); const next = tokens[i++];
if (next !== '[') throw new Error("Expected '[' after repeat count");
const body = parseBlock();
nodes.push({type:'repeat', count: count, body: body});
continue;
}
if (tok === 'color_cycle') {
nodes.push({type:'color_cycle'}); continue;
}
if (tok.startsWith(';')) { continue; }
throw new Error('Unknown token: ' + tok);
}
return nodes;
}
return parseBlock();
}
// Executor
function degToRad(d) { return d * Math.PI / 180; }
function execNodes(nodes) {
for (const node of nodes) {
if (node.type === 'forward') {
const dist = node.value;
const rad = degToRad(turtle.angle);
const nx = turtle.x + Math.cos(rad) * dist;
const ny = turtle.y + Math.sin(rad) * dist;
if (turtle.pen) {
ctx.beginPath(); ctx.moveTo(turtle.x, turtle.y); ctx.lineTo(nx, ny); ctx.stroke();
}
turtle.x = nx; turtle.y = ny;
} else if (node.type === 'left') {
turtle.angle -= node.value;
} else if (node.type === 'right') {
turtle.angle += node.value;
} else if (node.type === 'repeat') {
for (let k = 0; k < node.count; k++) execNodes(node.body);
} else if (node.type === 'color_cycle') {
colorIndex = (colorIndex + 1) % colors.length;
turtle.strokeStyle = colors[colorIndex];
ctx.strokeStyle = turtle.strokeStyle;
} else {
throw new Error('Unknown node type: ' + node.type);
}
}
}
// UI wiring
document.getElementById('run').addEventListener('click', () => {
try {
ctx.lineWidth = turtle.lineWidth;
ctx.strokeStyle = turtle.strokeStyle;
const program = document.getElementById('program').value;
const tokens = tokenize(program);
const ast = parseTokens(tokens);
execNodes(ast);
} catch (err) {
alert('Error: ' + err.message);
}
});
document.getElementById('clear').addEventListener('click', () => ctx.clearRect(0,0,W,H));
document.getElementById('reset').addEventListener('click', resetTurtle);
</script>
</body>
</html>
repeat 18 [ repeat 40 [ color_cycle forward 100 right 170 ]
right 60 ]
penup and pendown to move without drawing.color_cycle N to advance by N steps or add setcolor H S V to set arbitrary HSV.requestAnimationFrame to animate the turtle movement.