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?