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
PU
FD 120
RT 30
REPEAT 12 [ 
PD
REPEAT 3 [ FD 100 RT 120 ]
PU
FD 50
RT 30
]
  • Toby Ho

    Cool stuff. Broken link?