Turtle graphics (JavaScript)

From LiteratePrograms
Jump to: navigation, search

This program is a code dump.
Code dumps are articles with little or no documentation or rearrangement of code. Please help to turn it into a literate program. Also make sure that the source of this code does consent to release it under the CC0 license.

Contents

[edit] theory

Turtle graphics are one of the best known features of Logo. Although originally, turtle graphics were produced mechanically by robotic turtles, the best known variety is the software simulation. As an educational tool, the turtle is often fondly remembered, perhaps because it took the rather abstract notion of programming as implementing arbitrary algorithms with a sequence of instructions manipulating the limited state of a computer, and modelled it with the concrete notion of drawing arbitrary pictures with a sequence of instructions manipulating the limited state of a turtle.

The approach is direct: we parse the source expression to form a program tree, which we then interpret to produce graphical output.

Both the parser and interpreter are very simplistic; they mainly take care of the ancillary details needed to map a string of source text to a sequence of function applications from the system dictionary. The most complicated aspect is due to the fact that we have taken the unusual approach of implementing a reversible turtle, whose plans can be run either forwards or backwards, and hence we need to provide for interpretation in both directions. (See inverse and under).

[edit] practice

Caveat: the author is not in the habit of writing JavaScript — as one can write FORTRAN in any language, this may be very non-idiomatic.

<<script code>>=
function codevec(l)
{
  var v = l.slice(0)
  v.sarg = function() { return checkarg(this.shift()) }
  v.varg = function() { return [].concat(this.sarg()) }
  v.call = function(l) {
		for(var w = codevec(l); w.length; this.unshift(w.pop())); }
  return v
}

var plan = {}	// source tokens for user-defined words
var user = {}	// functions for user-defined words
var dict = {	// functions for system-defined words
system dictionary
}
inverter
interpreter
parser
driver

[edit] system dictionary

<<system dictionary>>=
'pen':		function(ctx,v) { ctx.penState = !ctx.penState },
'mirror':	function(ctx,v) { ctx.scale(1,-1) },
'reverse':	function(ctx,v) { ctx.scale(-1,1) },
'turn':		function(ctx,v) { ctx.rotate(v.sarg()*3.1415926/180) },
'forward':	function(ctx,v) { ctx.translate(v.sarg(),0); ctx.displace() },
'scale':	function(ctx,v) { 
			var sc = v.sarg()
			ctx.lineWidth /= sc
			ctx.scale(sc,sc)
				},
'repeat':	function(ctx,v) { 
			var n  = v.sarg()
			var rv = v.varg()
			if(n < 0)	{ n = -n; rv = invert(rv) }
			while(n--)	{ interp(ctx, rv) }
			/////////////////////
			// if(n == 0)	return
			// v.call(rv.concat(['repeat',n-1,rv]))
			/////////////////////
				},
'poly':		function(ctx, v) {
			var n  = v.sarg()
			var a  = 360/(n<0?-n:n)
			v.call(['repeat',n,v.varg().concat(['turn',a])]) },
'def':		function(ctx,v) {
			var name = v.shift()
			plan[name] = v.varg()
			user[name] = function(ctx,v) { v.call(plan[name]) }
				},
'inverse':	function(ctx,v)	{ v.call(invert(v.varg())) },
'identity':	function(ctx,v) { v.call(       v.varg() ) },
'under':	function(ctx,v) {
			var av = v.varg()
			var uv = v.varg()
			v.call(av.concat(uv.concat(invert(av))))
				},
'within':	function(ctx,v)	{
			var av = v.varg()
			var wv = v.varg()
			v.call(av.concat(wv.concat(av)))
				},
'flip':		function(ctx,v) {
			var fn = v.sarg()
			var bv = v.varg()
			var av = v.varg()
			v.call([fn,av,bv])
				},
'':		ignore
<<inverter>>=
function invert(ov)
{
  var ov = codevec(ov)
  var nv = []
  while(ov.length)  {
    var e = ov.sarg()
    switch(e)  {
    case 'pen':
    case 'mirror':
    case 'reverse':
      nv.unshift(e)
      break
    case 'turn':
    case 'forward':
      nv = [e,-ov.sarg()].concat(nv)
      break
    case 'scale':
      nv = [e,1/ov.sarg()].concat(nv)
      break
    case 'repeat':
    case 'poly':
      var c  = ov.sarg()
      var ev = ov.varg()
      nv = [e,-c,ev].concat(nv)
      break
    case 'inverse':
      nv = ['identity',ov.varg()].concat(nv)
      break
    case 'identity':
      nv = ['inverse',ov.varg()].concat(nv)
      break
    case 'under':
      var av = ov.varg()
      var uv = ov.varg()
      nv = [e,av,invert(uv)].concat(nv)
      break
    case 'within':
      var av = ov.varg()
      var wv = ov.varg()
      nv = [e,invert(av),invert(wv)].concat(nv)
      break
    case 'flip':
      var fn = ov.sarg()
      var bv = ov.varg()
      var av = ov.varg()
      e = [fn,av,bv]
      // fall through
    default:
      return invert(e.concat(ov)).concat(nv)
    }
  }
  return nv
}

[edit] interpreter

<<interpreter>>=

function checkarg(e) { return (plan[e] || e) }
function checkop(e)  { return (dict[e] || user[e] || ignore) }

function ignore(ctx, v) { }
function donext(ctx, v) { checkop(v.shift())(ctx,v) }
function interp(ctx, v) { for(v = codevec(v); v.length; donext(ctx,v)); }

[edit] parser

<<parser>>=
function tokens(s) { return s.replace(/([\[\]])/g," $1 ").split(/\s+/) }
function tree(v)
{
  var t = []
  v.nxt = function() { return this.length ? this.shift() : ']' }
  for(var e = v.nxt(); e != ']'; e = v.nxt()) { t.push(e == '[' ? tree(v) : e) }
  return(t)
}

[edit] wrapping up

Now we provide a driver that will manipulate elements in a given web page (as well as provide platform workarounds).

<<driver>>=
function turtle(t,c)
{
  /////////////////////////
  if(document.getElementById("kludge").checked)	{
    dict['forward'] = function(ctx,v)	{
	var x = v.sarg()
	if(ctx.penState)	{
		ctx.beginPath()
		ctx.moveTo(0,0)
		ctx.lineTo(x,0)
		ctx.stroke()
	}
	ctx.translate(x,0)
    }
  } else {
    dict['forward'] = function(ctx,v) {
	ctx.translate(v.sarg(),0); ctx.displace() }
  }
  /////////////////////////

  var txt = document.getElementById(t)
  var cvs = document.getElementById(c)
  var src = txt.value
  var ctx = cvs.getContext("2d")

  ctx.save()
  ctx.clearRect(0,0,cvs.width,cvs.height)
  ctx.translate(cvs.width/2,cvs.height/2)
  ctx.beginPath()	// start track display
  ctx.moveTo(0,0)

  ctx.penState = 1
  ctx.displace = function() { this.penState ? ctx.lineTo(0,0)
					    : ctx.moveTo(0,0) }
  plan = {}
  user = {}
  interp(ctx, tree(tokens(src)))

  ctx.stroke()		// show the track
  ctx.beginPath()	// start turtle display
  ctx.moveTo(0,-5)
  ctx.lineTo(20,0)
  ctx.lineTo(0, 5)
  ctx.closePath()
  ctx.fill()		// show the turtle
  ctx.restore()
}

function init() { document.getElementById("kludge").checked = !!window.opera }

Finally, we create a web page embedding the source and output windows, as well as some sample turtle programs and documentation.

<<terrapin.htm>>=
<html><head>
<title>a buggy concatenative turtle</title>
<!-- 20070131 [DL] swap renamed flip.  improve tokenization -->
<!-- 20060822 [DL] lame kludge for Opera 9.01 -->
<!-- 20060820 [DL] written -->
<script type="text/javascript"><!--
<<script code>>
//--></script>
<style type="text/css">
body	 { font-family: arial; font-size: 10pt;
	   background-color: #CCC; color: #000; }
h1	 { font-size: 14pt; }
canvas   { background-color: #EEE; }
textarea { background-color: #EEE; }
.doc	 { float: right; border: 1px solid #000; background-color: #FEC; }
</style></head><body onLoad="init();turtle('txt','cvs')">
<h1>a buggy concatenative turtle <i>(mobilis terrapin)</i></h1>
<canvas height=256 width=256 id="cvs" onClick="turtle('txt','cvs')"></canvas>
<button onClick="turtle('txt','cvs')">&lt;&lt; draw </button>
<textarea cols=60 rows=20 id="txt" onPaste="turtle('txt','cvs')">
def jog [ forward 30 under [ turn 45 ] [ forward 10 ] ]
def joggle [ under jog reverse reverse ]
poly 16 [ poly 4 joggle ]
</textarea>
<p>Requires javascript and &lt;canvas&gt; support <small> (Safari,Opera,Firefox?)</small>.  Userproofing not included.</p>
<p>My Opera can't hack matrix changes while drawing &mdash; if you don't see any output, enable
<label for="kludge"> the
<input id="kludge" type="checkbox" onClick="turtle('txt','cvs')"/>
bletcherous kludge</label></p> 
<hr>
<div class="doc">
<table>
<tr><td>pen             </td><td> toggle pen up/down</td></tr>
<tr><td>mirror		</td><td> toggle left/right</td></tr>
<tr><td>reverse		</td><td> toggle forward/back</td></tr>
<tr><td>turn X		</td><td> rotate X degrees</td></tr>
<tr><td>forward X	</td><td> translate X units</td></tr>
<tr><td>scale X		</td><td> multiply unit size</td></tr>
<tr><td></td></tr>
<tr><td>repeat N [ ]	</td><td> repeat a plan N times</td></tr>
<tr><td>poly N [ ]	</td><td> repeat with implicit turning</td></tr>
<tr><td>within [ ] [ ]	</td><td> sandwich a plan<br>(within a b == a b a)</td></tr>
<tr><td></td></tr>
<tr><td>inverse [ ]	</td><td> produce the opposite of a plan <br>(inverse a == a<sup>-1</sup>)</td></tr>
<tr><td>under [ ] [ ]	</td><td> make a do/undo sandwich<br>(under a b == a b a<sup>-1</sup>)</td></tr>
<tr><td></td></tr>
<tr><td>flip NAME	</td><td> switch argument order<br>(flip f b a == f a b)</td></tr>
<tr><td>def NAME [ ]	</td><td> define a subplan<br>(sorry, no arguments)</td></tr>
</table>
</div>
repeat 4 [ forward 100 turn 90 ]<br>
<hr>
poly 6 [ poly 36 [ reverse pen forward 20 ] ]<br>
<hr>
def spot [ poly 6 [ forward 10 ] ]<br>
def arm [ under [ forward 70 turn -60 ] ]<br>
poly 12 [ arm spot ]<br> 
<hr>
def coil [ repeat 300 [ turn 5 forward 5 scale .99 ] ]<br>
def branch [ flip under [ ] ]<br>
poly 2 [ branch coil ]<br>
<hr>
def jog [ forward 30 under [ turn 45 ] [ forward 10 ] ]<br>
def joggle [ under jog reverse reverse ]<br>
poly 6 [ poly 5 joggle ]<br>
<hr>
def t [ turn 90 ]<br>
def f [ forward 60 ]<br>
def h [ forward 30 ]<br>
def x [ under [ under t f ] h ]<br>
<br>
def u [ under t f h ]<br>
def q [ under [ repeat 2 u ] ]<br>
def w [ under [ repeat 1 u ] ]<br>
def e [ under [ repeat 0 u ] ]<br>
<br>
poly 4 [ q [<br>
poly 4 [ w [<br>
poly 4 [ e [ <br>
x inverse x<br>
] ] ] ] ] ]<br>
</body> </html>
Download code
hijacker
hijacker
hijacker
hijacker