Logo to język programowania zaprojektowany w latach 60. Jego najsłynniejszą funkcją jest grafika żółwia: programista steruje "żółwiem" (kursorem) za pomocą instrukcji takich jak forward (naprzód), left (w lewo), right (w prawo), repeat (powtórz), a żółw zostawia 'ślad' na ekranie.
Dzisiaj zbudujemy kompaktowy, jedno-plikowy interpreter Logo w około 100 liniach czystego JavaScript. Aby kod był krótki, zaimplementujemy tylko cztery powyższe instrukcje plus color_cycle (niebędącą częścią standardowego Logo), która rotuje 36 odcieni HSV.
Kliknij przycisk 'Uruchom' poniżej, aby zobaczyć go w akcji!
Komendy: forward N, left N, right N, repeat N [ ... ], color_cycle
Interpreter składa się z trzech głównych części: tokenizera, który dzieli wejście na tokeny, parsera, który buduje proste drzewo AST, oraz egzekutora, który przechodzi przez AST i rysuje na elemencie canvas HTML5 przy użyciu stanu żółwia. Instrukcja color_cycle zmienia kolor pisaka poprzez 36 równomiernie rozmieszczonych odcieni na kole barw HSV.
Potrzebujesz tylko jednego pliku HTML. Plik zawiera kontrolki interfejsu użytkownika, element canvas oraz cały kod JavaScript. Zapisz plik i otwórz go w dowolnej nowoczesnej przeglądarce.
Tokenizer normalizuje nawiasy kwadratowe i dzieli dane wejściowe według białych znaków. Zmienia wielkość liter w tokenach na małe, więc komendy nie są wrażliwe na wielkość liter. Nawiasy są traktowane jako oddzielne tokeny, aby parser mógł wykryć bloki powtórzeń.
// Przykładowy fragment tokenizera
function tokenize(input) {
input = input.replace(/\[/g, ' [ ').replace(/\]/g, ' ] ');
const raw = input.trim().split(/\s+/).filter(Boolean);
return raw.map(t => t.toLowerCase());
}
Parser odczytuje tokeny i buduje węzły: forward, left, right, repeat z zagnieżdżonym ciałem oraz color_cycle. Obsługuje zagnieżdżone powtórzenia za pomocą rekurencyjnej funkcji parseBlock.
// Parser zwraca tablicę węzłów
// Przykłady węzłów:
// { type: 'forward', value: 100 }
// { type: 'repeat', count: 4, body: [...] }
// { type: 'color_cycle' }
Egzekutor przechodzi przez AST i aktualizuje stan żółwia: x, y oraz angle (kąt). Komenda forward rysuje linię z bieżącej pozycji do nowej pozycji obliczonej za pomocą trygonometrii. left i right zmieniają kąt. repeat uruchamia swoje ciało wielokrotnie.
Abstrakcyjne Drzewo Składniowe (lub AST) to ustrukturyzowana, wewnątrzpamięciowa reprezentacja napisanego programu. Zamiast pracować na surowym tekście lub tokenach, interpreter konwertuje program na drzewo węzłów, gdzie każdy węzeł reprezentuje znaczącą komendę lub konstrukcję. AST to element, po którym porusza się egzekutor, aby wykonać działania.
Dla programu:
repeat 4 [ color_cycle forward 150 right 90 ]
Parser tworzy AST podobne do tego JSON-a:
[
{
"type": "repeat",
"count": 4,
"body": [
{ "type": "color_cycle" },
{ "type": "forward", "value": 150 },
{ "type": "right", "value": 90 }
]
}
]
Egzekutor wykonuje proste przejście przez AST. Dla każdego węzła:
forward lub repeat).repeat), egzekutor rekurencyjnie przechodzi przez to ciało określoną liczbę razy.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);
}
// inne typy węzłów...
}
}
// Przykład Forward (Naprzód)
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;
Cykl kolorów wstępnie oblicza 36 ciągów RGB, próbkując wartości barwy (hue) równomiernie od 0 do 360 stopni i konwertując HSV na RGB. Każdy węzeł color_cycle inkrementuje indeks i aktualizuje ctx.strokeStyle. Umieść color_cycle przed forward, aby narysować ten segment w następnym odcieniu.
// colorIndex postępuje i aktualizuje strokeStyle
colorIndex = (colorIndex + 1) % colors.length;
turtle.strokeStyle = colors[colorIndex];
ctx.strokeStyle = turtle.strokeStyle;
Poniżej znajduje się kompletny, gotowy do uruchomienia plik HTML. Zawiera interfejs użytkownika, tokenizer, parser, egzekutor, konwersję HSV oraz instrukcję color_cycle.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Mini Żółw Logo z samouczkiem color_cycle</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 Żółw Logo</h3>
<textarea id="program">
repeat 36 [ color_cycle forward 200 right 170 ]
</textarea>
<div>
<button id="run">Uruchom</button>
<button id="clear">Wyczyść</button>
<button id="reset">Resetuj Żółwia</button>
</div>
<p><strong>Komendy:</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>
// Konfiguracja Canvas
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Stan żółwia
let turtle = { x: W/2, y: H/2, angle: 0, pen: true, strokeStyle: '#000', lineWidth: 1.5 };
// Cykl kolorów
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("Oczekiwano '[' po liczbie powtórzeń");
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('Nieznany token: ' + tok);
}
return nodes;
}
return parseBlock();
}
// Egzekutor
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('Nieznany typ węzła: ' + node.type);
}
}
}
// Podłączenie interfejsu
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('Błąd: ' + 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 i pendown, aby przemieszczać się bez rysowania.color_cycle N, aby przesunąć się o N kroków lub dodaj setcolor H S V, aby ustawić dowolne HSV.requestAnimationFrame, aby animować ruch żółwia.