Turtle graphics the LOGO way

Another weekend fun project by Richard Suchenwirth 2000-12 -- I dug out my old LOGO book from 1986 (DR Logo on CP/M) and finally made an old wish come true: to reimplement their turtle graphics - in Tcl of course.

LOGO (Seymour Papert, MIT 1967 [L1 ]) is still somehow popular as an educational programming language. My impression was "a stripped-down Lisp with much less parens" - that's one reason why I liked it then. In contrast to most other languages (except Forth), statements are not strictly separated but can be strung together on one line, e.g. to draw a circle (the most intuitive way I've ever seen):

 repeat 360 [rt 1 fd 1]

where repeat, rt and fd are commands that take up as many arguments to their right as they need. This feature could easily be approximated in Tcl: each exported proc takes an additional args and evals that in the end. This functionality is wrapped into the to command which differs in syntax from Logo's but likewise gives an impression how simple The turtle commands are mostly two-letter abbreviations:

 bk   - move back (n pixels)
 cs   - clear screen
 fd   - move forward (n pixels, drawing a line if pen is down)
 home - move turtle to (0,0)
 ht   - hide turtle (a triangular cursor indicating drawing direction)
 lt   - left turn (in degrees)
 pd   - pen down
 pu   - pen up
 rt   - right turn (in degrees)
 st   - show turtle

I first tried to draw the turtle as a triangle myself, but soon found that Tcl's arrowhead on a line item does that faster and easier. The color and palette treatment was simplified - just use setpc/setbg with a color name for foreground/background. (In old Logo, you could use up to 4 palettes - one color each - with RGB values between 0 and 2...)


The story continues at Turtleshell...

 namespace eval Turtle {
        variable data
        proc Init canvas {
                variable data
                array set data {x 0.0 y 0.0 h 0.0 pen down fg blue show 1}
                set data(c) $canvas
                cs
                uplevel 1 namespace import -force ::Turtle::*
        }
        proc Show? {} {
                variable data
                update idletasks
                $data(c) delete withtag turtle
                if !$data(show) return
                set h1 [expr {atan(1.)*8*$data(h)/360.}]
                set x1 [expr {$data(x)+10*sin($h1)}]
                set y1 [expr {$data(y)-10*cos($h1)}]
                $data(c) create line $data(x) $data(y) $x1 $y1 -arrow last \
                        -arrowshape {10 10 3} -tag turtle -fill $data(fg)        
        }
        proc to {name argl body} {
                set body "variable data; $body; Show?; eval \$args"
                proc $name [lappend argl args] $body
        }
        namespace export -clear bk clean cs fd home ht lt pd pu rt \
                setbg seth setpc setpos setx sety st to
        to bk     n {fd -$n}
        to clean  {} {$data(c) delete all}
        to cs     {} {clean; home; pd}
        to fd     n {
                set h1 [expr {atan(1.)*8*$data(h)/360.}]
                set x1 [expr {$data(x)+$n*sin($h1)}]
                set y1 [expr {$data(y)-$n*cos($h1)}]
                if {$data(pen)=="down"} {
                   $data(c) create line $data(x) $data(y) $x1 $y1 -fill $data(fg)
                }
                set data(x) $x1
                set data(y) $y1
        }
        to home   {} {array set data {x 0.0 y 0.0 h 0.0}}
        to ht     {} {set data(show) 0}
        to lt     d  {rt -$d}
        to pd     {} {set data(pen) down}
        to pu     {} {set data(pen) up}
        to rt     d  {set data(h) [expr {$data(h)+$d}]}
        to setbg  col {$data(c) config -bg $col}
        to setpc  col {set data(fg) $col}
        to setpos {X Y} {set data(x) $X; set data(y) $Y}
        to seth   val {set data(h) $val}
        to setx   val {set data(x) $val}
        to sety   val {set data(y) $val}
        to st     {} {set data(show) 1}
        to rtree s {
            if $s<5 return
            fd $s 
            lt 30 rtree [expr $s*([random 5]+5)/10]
            rt 60 rtree [expr $s*([random 5]+5)/10]
            lt 30 bk $s
        }
     ##---------------- add working and nice-looking demo code here!
     variable demos {
         {
             setpc yellow; repeat 90 {fd 100 bk 100 rt 4} 
             setpc blue; repeat 90 {fd 30 bk 30 rt 4}
         }
         {setpc yellow web 30 setpc orange web 50 setpc red web 75}
         {repeat 360 {
             setpc [random:select [colors]] fd 100 bk 100 lt 1}
         }
         {
             seth [random 360]; set n [expr [random 100]+100]; 
             repeat $n {fd $n; rt 90; incr n -1}
         }
         {pu bk 100 pd rtree [expr [random 50]+25]}
         {set n 100; repeat 100 {fd [incr n -2] rt 89}}
     }
 } ;#-------------------------- end namespace Turtle
 proc colors {} {
     list red orange yellow green1 green3 blue purple black white
 }
 proc random n {expr {round($n*rand())}}
 proc random:select {list} {
     lindex $list [expr int(rand()*[llength $list])]
 }
 proc repeat {n body} {while {$n} {incr n -1; uplevel $body}}

Note how Tcl's and Logo's simplicities merge in the following demo code (not needed for Turtleshell), enhanced by a random tree ...

 proc turtletest {} {
     pack [canvas .c] -fill both -expand 1
     update
        Turtle::Init .c
        repeat 4 {rt 90 fd 50}
        seth 90 setx 100
        foreach i {red green black blue orange} {
                setpc $i pu fd 10 pd repeat 180 {fd 1 rt 2}
        }
        pu seth 180 fd 160 pd
        to square s {repeat 4 {fd $s rt 90}}
        to web s {repeat 36 {square $s rt 10}}
        to facet {x y z} {web $x web $y web $z}
        ht setbg black facet 30 40 70
        setpos 300 250 seth 0 setpc red rtree 60
 }
 turtletest

For a more elaborate demo (in fact, a usable interactive program) see Turtleshell.


RM: Here is a different implementation of repeat

 to repeat {n body} {while {$n} {uplevel 1 $body; incr n -1}}

It allows LOGO code to be placed behind a repeat statement.

 repeat 4 {fd 100 repeat 6 {fd 50 rt 60} bk 100 rt 90}

RS: Good suggestion - the freedom introduced by the to commands is of course not backpropagated to all Tcl commands. My idea was, since a numbered repeat is useful in other situations as well, to keep it independent from turtle updates etc. A compromise would be

 proc repeat {n body args} {
        while {$n} {incr n -1; uplevel 1 $body}
        uplevel 1 $args
 }

RM:

This version of to allows custom commands that can be called without qualifying them with Turtle::

 namespace eval Turtle {
    ...
    proc to {name argl body} {
        set body "variable data; $body; Show?; eval \$args"
        proc $name [lappend argl args] $body
        namespace export $name
        catch {uplevel 1 namespace import [namespace current]::$name}
    }
    namespace export -clear to
    ...
 }

RS: Great, thank you! I've also got something new, Logo's label command:

     to label  s {
         $data(c) create text $data(x) $data(y) \
                 -text $s -anchor nw -fill $data(fg)
     }

Only in Logo the writing direction is determined by turtle heading - I don't see a way to do that on a Tcl canvas at present...


See also Geometrical constructions