How to Build a Mini Logo Turtle Interpreter in Pure JavaScript

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!

Mini Logo Turtle

Commands: forward N, left N, right N, repeat N [ ... ], color_cycle


Overview

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.


Files and Setup

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.


Key Components Explained

Tokenizer

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());
}

Parser

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' }

Executor and Turtle State

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.

Abstract Syntax Tree

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.

Why use an AST

AST example

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 }
    ]
  }
]

How the executor uses the AST

The executor performs a simple traversal of the AST. For each node it:

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;

Color Cycle

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;

Full Example

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>

Tips and Extensions

More JS tutorials:

Spinning squares - visual effect (25 lines)

Fireworks (60 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Sine scroller (30 lines)

Interactive animated sprites

Image transition effect (16 lines)