Mar 052010
 

Two programs for you for Thursday night’s entry. A crude Logo interpreter written in Javascript using the HTML5 canvas tag and a quick logo program to go with it. Now when I say crude, I mean crude.

  • You have to separate everything with whitespace because the parser is finicky.
  • All the loops are unrolled so careful with your nesting depth.
  • The only commands supported are the basics: FD, LT, RT, PU, PD and REPEAT.

That said you can still make some fun stuff with it. Source is in the “Read more”.

JPLT.Class.create("JPLT.Turtle", JPLT.Object,
	function() {
		this.width = 500;
		this.height = 500;
		this.queue = [];
		this.x = this.width/2;
		this.y = this.height/2;
		this.r = 0;
		this.isPenDown = true;
		this.debug = false;
		this.save = null;
 
		this.createElement();
		this.appendElement();
 
	},
 
	{
		context: function() {
			return this.element.getContext("2d");
		},
 
		createElement: function() {
			this.element = document.createElement("canvas");
 
			this.element.width = this.width;
			this.element.height = this.height;
			this.element.style.border = "1px solid #ccc";
 
			return this.element;
		},
 
		appendElement: function() {
			var body = document.documentElement || document.body;
			body.appendChild(this.element);
		},
 
		reset: function() {
			this.clear();
			this.x = this.width/2;
			this.y = this.height/2;
			this.r = 0;
			this.isPenDown = true;
			this.save = null;
		},
 
		run: function() {
			if (!this.timer) {
				this.reset();
				this.timer = window.setInterval(this.delegate(this.paint), 10);	
			}
		},
 
		stop: function() {
			window.clearInterval(this.timer);
			this.timer = null;
		},
 
		clear: function() {
			var ctx = this.context();
			ctx.clearRect(0,0,this.width,this.height);
		},
 
		normalizeAngle: function() {
			if (this.r > Math.PI * 2)
				this.r -= Math.PI * 2;
			else if (this.r < 0) 
				this.r += Math.PI * 2;
		},
 
		normalizePosition: function() {
			if (this.x > this.width) {
				this.x -= this.width;
			}
			else if (this.x < 0) {
				this.x += this.width;
			}
 
			if (this.y > this.height) {
				this.y -= this.height;
			}
			else if (this.x < 0) {
				this.y += this.height;
			}
		},
 
		leftTurn: function(repeat) {
			var n = Math.min(repeat,10);
 
			this.r -= Math.PI/180 * n;
 
			this.normalizeAngle();
 
			return n;
		},
 
		rightTurn: function(repeat) {
			var n = Math.min(repeat,10);
 
			this.r += Math.PI/180 * n;
 
			this.normalizeAngle();
 
			return n;
		},
 
		forward: function(repeat) {
			var n = Math.min(repeat,3);
 
			this.x += Math.cos(this.r - Math.PI/2) * n;
			this.y += Math.sin(this.r - Math.PI/2) * n;
 
			this.normalizePosition();
 
			return n;
		},
 
		penUp: function() {
			this.isPenDown = false;
		},
 
		penDown: function() {
			this.isPenDown = true;
		},
 
		repeat: function() {
			return 1;
		},
 
		addToQueue: function(f, r) {
			if (!f)
				throw new Error("Invalid instruction");
 
			this.queue.push({ func:f, repeat:r });
		},
 
		setQueue: function(q) {
			if (this.timer)
				this.stop();
 
			this.queue = q;
			this.log("Program is " + this.queue.length + " commands.");
			this.run();
		},
 
		getCurrentInstruction: function() {
			return this.queue[0];
		},
 
		processQueue: function() {
			var instruction = this.getCurrentInstruction();
 
			instruction.repeat -= instruction.func.call(this, instruction.repeat, instruction.queue);
 
			if (!instruction.repeat) {
				this.queue.shift();				
			}			
		},
 
		paint: function() {
			var oldX = this.x;
			var oldY = this.y;
			var ctx = this.context();
 
			try {
				if (this.queue.length) {
					this.processQueue();
 
					if (this.debug)
						this.log("x="+this.x+" y="+this.y+" r="+this.r);
 
					ctx.fillStyle = "black";
					ctx.strokeStyle = "red";
 
					if (this.save)
						ctx.putImageData(this.save,oldX-10,oldY-10);
 
					if (this.isPenDown) {
						ctx.beginPath();
						ctx.moveTo(oldX,oldY);
						ctx.lineTo(this.x, this.y);
						ctx.stroke();						
					}
 
					this.save = ctx.getImageData(this.x-10, this.y-10, 20, 20);
 
					ctx.save();
					ctx.translate(this.x,this.y);
					ctx.rotate(this.r);
					ctx.beginPath();
					ctx.moveTo(0,-5);
					ctx.lineTo(5,5);
					ctx.lineTo(0,3);
					ctx.lineTo(-5,5);
					ctx.lineTo(0,-5);
					ctx.fill();
					ctx.restore();
				}
				else {
					this.log("Program complete");
					this.stop();
				}
			}
			catch (e) {
				this.log("Stopping execution due to error");
				this.stop();
				throw(e);
			}
		}
	}
);
JPLT.Class.create("JPLT.Logo", JPLT.Object,
	function() {
	},
	{
		instructionMap: {
			'forward': 	{ func:JPLT.Turtle.prototype.forward, hasParam:true },
			'fd': 		{ func:JPLT.Turtle.prototype.forward, hasParam:true },
			'left': 	{ func:JPLT.Turtle.prototype.leftTurn, hasParam:true },
			'lt': 		{ func:JPLT.Turtle.prototype.leftTurn, hasParam:true },
			'right': 	{ func:JPLT.Turtle.prototype.rightTurn, hasParam:true },
			'rt': 		{ func:JPLT.Turtle.prototype.rightTurn, hasParam:true }, 
			'penup': 	{ func:JPLT.Turtle.prototype.penUp, hasParam:false },
			'pu': 		{ func:JPLT.Turtle.prototype.penUp, hasParam:false },
			'pendown': 	{ func:JPLT.Turtle.prototype.penDown, hasParam:false },
			'pd': 		{ func:JPLT.Turtle.prototype.penDown, hasParam:false },
			'repeat': 	{ func:JPLT.Turtle.prototype.repeat, hasParam:true }
		},
 
		parse:function(text, instructions) {
			// Strip comments
			text = text.replace(/;.+?[\n\r]+/,"");
 
			// Split into tokens
			var tokens = text.split(/\s+/);
			var instruction;
			var token;
			var param;
			if (!instructions)
			 	instructions = [];
 
			while (tokens.length) {
				token = tokens.shift().toLowerCase();
 
				if (token) {
					instruction = this.instructionMap[token];
 
					if (!instruction) {
						throw new Error("Invalid command:" + token);
					}
 
					param = instruction.hasParam ? tokens.shift() : null;
 
					if (isNaN(param)) {
						throw new Error("Invalid parameter: " + param)
					}
 
					if (token == "repeat") {						
						if (tokens[0] != '[') {
							throw new Error("Unexpected repeat block: " + tokens[0]);
						}
 
						tokens.shift();
						var subtokens = [];
						var nest = 0;
						while(tokens && (tokens[0] != ']' || nest)) {
							if (tokens[0] == '[')
								nest++;
							else if (tokens[0] == ']')
								nest--;
 
							subtokens.push(tokens.shift());
						}
						tokens.shift();
						for (var i=0; i<param; i++) {
							this.parse(subtokens.join(' '), instructions);
						}
					}
					else {
						instructions.push({
							func: instruction.func,
							repeat: param
						});
					}
				}
			}
 
			return instructions;
		}
	}
);
  • Toby Ho

    Cool stuff. Broken link?