Recursive curves

if 0 {Richard Suchenwirth 2004-01-18 - The C curve is a quite beautiful image created by a simple recursive function, originally described by William Gosper. I met it in the Winston/Horn book on LISP and of course wanted to have it in Tcl/Tk too, and on my PocketPC. Translation wasn't difficult - somehow Tcl is "the LISP of the 21st century".

WikiDbImage ccurve.gif WikiDbImage dragon.gif

As the code is a bit slow on PocketPC (171 sec for the C curve, 615 sec for the dragon curve!), I chose to update the canvas after every line segment is drawn, for animation. Also, the related but very different dragon curve was added - I think fractals developed from these studies. So here is yet another weekend fun project, which again demonstrates interesting things in few lines of code:

Question: does the PocketPc have a float coprocessor? If not, the difference with Recursive curves (table based) could be huge.

KPV See also Dragon Curve.

AK And IFS.

KBK The dragon curve is closely related to the Penney numerals, an interesting binary system for encoding complex numbers.

}

 proc main {} {
    pack [canvas .c -width 240 -height 280]
    set ::x0 150; set ::y0 190
    set ::pi_4 [expr atan(1)]
 # ETFS configurability: uncomment what you want
    set t [time {ccurve .c 120 0 2}]
    #set t [time {dragon .c 160 0 1 2.4}]
    wm title . "[expr {[lindex $t 0]/1000000.}] sec"
    bind . <Return> {exec wish $argv0 &; exit}
 }

# C curve drawer, translated from Lisp

 proc ccurve {w len angle minlen} {
    if {$len<$minlen} {
       plotline $w $len $angle
    } else {
       set len [expr {$len/sqrt(2)}]
       ccurve $w $len [expr {$angle+$::pi_4}] $minlen
       ccurve $w $len [expr {$angle-$::pi_4}] $minlen
    }
 }

# Dragon curve drawer

 proc dragon {w len angle s minlen} {
    global pi_4
    if {$len<$minlen} {
       plotline $w $len $angle
    } else {
       set len [expr {$len/sqrt(2)}]
       dragon $w $len [expr {$angle+$s*$pi_4}] 1 $minlen
       dragon $w $len [expr {$angle-$s*$pi_4}] -1 $minlen
    }
 }

# Plot a line from last end point in specified direction and length

 proc plotline {w len ang} {
    global x0 y0
    update
    set x [expr {$x0-$len*sin($ang)}]
    set y [expr {$y0-$len*cos($ang)}]
    $w create line $x0 $y0 $x $y
    set x0 $x; set y0 $y
 }

# Let's go!

 main

NEM Very nice. Of course, the [update] call in the code slows it down quite a lot. ccurve takes just over 2 seconds with the update on my box (2.4GHz pentium4), while without the update takes just 0.12 seconds. A marginal speed increase can be had by precomputing the sqrt(2). To get a trade-off between speed and updating the GUI, perhaps it would be better to update the screen once a second or so:

 proc plotline {w len ang} {
     global x0 y0 time
     if {[clock seconds] > $time} { update; set time [clock seconds] }
     set x [expr {$x0-$len*sin($ang)}]
     set y [expr {$y0-$len*cos($ang)}]
     $w create line $x0 $y0 $x $y
     set x0 $x; set y0 $y
 }
 set ::time [clock seconds]
 main

I haven't tested this as it takes less than a second to run on my machine at uni.


MS proposes the following: first replace in main

    set ::x0 150; set ::y0 190

by

    set ::points {150 190}

and then redefine plotline to draw only every say 100 points, using a single call to the canvas.

 set count0 100
 set count  100
 proc plotline {w len ang} {
    global points
    set x0 [lindex $points end-1]
    set y0 [lindex $points end]
    set x [expr {$x0-$len*sin($ang)}]
    set y [expr {$y0-$len*cos($ang)}]
    lappend points $x $y
    if {![incr ::count -1]} {
       $w create line $points
       set points [list $x $y]
       update
       set ::count $::count0
    } 
 }

This is noticeably faster already: on my machine the c-curve takes 0.2 sec (down from 4 sec), the dragon curve takes 0.6 sec (down from 16 sec).

Further optimisations are possible in the algorithm itself: inlining the values of 'sqrt(2)' and 'pi_4', replacing the second recursive call by a while loop, maybe specialised trig computations. But the effect will not be as large as this.

RS: Hm... is this worse in performance? At least it's simpler (and brings runtime on the iPaq down to 12 resp. 20 sec):

 proc plotline {w len ang} {
    global points
    set x [expr {[lindex $points end-1]-$len*sin($ang)}]
    set y [expr {[lindex $points end]-$len*cos($ang)}]
    if {[llength [lappend points $x $y]]>200} {
       $w create line $points
       set points [list $x $y]
       update
    } 
 }

MS notes that it is indeed simpler ... and probably faster too. Even faster is

 proc plotline {w len ang} {
    global points
    lappend points [expr {[lindex $points end-1]-$len*sin($ang)}]
    lappend points [expr {[lindex $points end-1]-$len*cos($ang)}]
    if {[llength points]>200} {
       $w create line $points
       set points [lrange $point end-1 end]
       update
    } 
 }

Just for the sake of it: most floating point operations can be eliminated, as shown in Recursive curves (table based). But it is not that much faster.