Keith Vetter 2002-11-19 - This draws the Mandelbrot Fractal. You can highlight a portion and zoom in to it, or zoom back out.
I'm not totally happy with the coloring scheme. I originally tried varying the brightness of the base color (using code from Making color gradients) and you can choose that option. But I found I preferred just choosing colors at random.
One curiousity: drawing pixel by pixel into a canvas was too slow. Instead, I placed a blank image on the canvas and am drawing the fractal into it (and following the advice of Tk image Dos and Don'ts I'm painting by rows).
I could probably get better numeric precision if I used the mpexpr package but I'll leave that as a future improvement.
For a short version, see also Mandelbrot and Julia sets
##+################################################################
#
# TkMandelbrot -- draws the mandelbrot fractal
# based on http://www.students.tut.fi/~warp/Mandelbrot/
# by Keith Vetter
#
# Revisions:
# KPV Nov 13, 2002 - initial revision
#
##+################################################################
###################################################################
set tcl_precision 17
set Cwidth 500 ;# Canvas size
set Cheight 500
set Rmin -2.0 ;# Left side
set Rmax 1.0 ;# Right side
set Imin -1.5 ;# Bottom
set Imax [expr {$Imin + ($Rmax - $Rmin) * $Cheight /$Cwidth}]
set Rscale [expr {($Rmax - $Rmin) / $Cwidth}]
set Iscale [expr {($Imax - $Imin) / $Cheight}]
set maxIters 50
set S(draw) 0
set S(color) red
set S(title) "Tk Mandelbrot"
set S(version) 1.0
lappend S(stack) [list $Rmin $Imax $Rmax $Imin]
expr srand([clock clicks])
##+################################################################
#
# DoDisplay -- sets up our gui
#
proc DoDisplay {} {
global Cwidth Cheight
wm title . $::S(title)
frame .bottom -bd 2 -relief ridge
button .redraw -text "Redraw" -command Redraw
set font "[font actual [.redraw cget -font]] -weight bold"
.redraw configure -font $font
catch {image create photo ::img::blank -width 1 -height 1}
button .clear -text Clear -font $font -command Clear
button .zoomin -text "Zoom In" -font $font -command ZoomIn
button .zoomout -text "Zoom Out" -font $font -command ZoomOut
button .color -text "Select Color" -font $font -command {ChangeColor 0}
button .random -text "Random Colors" -font $font -command {ChangeColor 1}
button .about -image ::img::blank -command About -highlightthickness 0
frame .flbl
label .lbl -bd 2 -relief ridge -textvariable S(msg)
canvas .c -width $Cwidth -height $Cheight -bd 2 -relief ridge -bg gray50 \
-highlightthickness 0
.c xview moveto 0 ; .c yview moveto 0
image create photo ::img::myImage -width $Cwidth -height $Cheight
.c create image 0 0 -image ::img::myImage -anchor nw -tag image
ToggleButtons 0
pack .bottom -side right -fill y -ipadx 10 -ipady 5
set row -1
grid rowconfigure .bottom [incr row] -minsize 5
grid .zoomin -in .bottom -sticky ew -pady 2 -row [incr row]
grid .zoomout -in .bottom -sticky ew -pady 2 -row [incr row]
grid rowconfigure .bottom [incr row] -minsize 20
grid .redraw -in .bottom -sticky ew -pady 2 -row [incr row]
grid .clear -in .bottom -sticky ew -pady 2 -row [incr row]
grid rowconfigure .bottom [incr row] -minsize 20
grid rowconfigure .bottom [incr row] -weight 1
grid .color -in .bottom -sticky ew -pady 2 -row [incr row]
grid .random -in .bottom -sticky ew -pady 2 -row [incr row]
grid rowconfigure .bottom [incr row] -minsize 5
pack .flbl -side bottom -fill x
pack .lbl -in .flbl -side bottom -fill x
pack .c -fill both -expand 1
bind .c <Button-1> [list DoBox 0 %x %y]
bind .c <B1-Motion> [list DoBox 1 %x %y]
bind all <Alt-c> {console show}
update
pack propagate .flbl 0 ;# Don't let it grow
place .about -in .bottom -relx 1 -rely 1 -anchor se
}
##+################################################################
#
# ToggleButtons -- changes button state if we're drawing
#
proc ToggleButtons {drawing} {
global S
array set state {0 disabled 1 normal}
if {$drawing} {
foreach w {.zoomin .zoomout .clear .color .random} {
$w config -state disabled
}
.redraw config -text "Stop Drawing"
return
}
foreach w {.clear .color .random} {
$w config -state normal
}
.zoomout config -state $state([expr {[llength $S(stack)] > 1}])
.zoomin config -state $state([expr {[llength [.c find withtag box]] > 1}])
.redraw config -text "Redraw"
}
##+################################################################
#
# Render -- Renders the mandelbrot set
#
proc Render {} {
global Cwidth Cheight Rmin Rmax Imin Imax maxIters Rscale Iscale
global S
set sTime [clock click -milliseconds]
ToggleButtons 1
set S(draw) 1
if {[winfo ismapped .c]} { ;# Recompute scaling factors
set Cheight [winfo height .c]
set Cwidth [winfo width .c]
set Rscale [expr {($Rmax - $Rmin) / $Cwidth}]
set Iscale [expr {($Imax - $Imin) / $Cheight}]
}
Clear
::img::myImage config -width $Cwidth -height $Cheight
set step 4 ;# Do interlaced drawing
for {set start 0} {$start < $step} {incr start} {
for {set x $start} {$x < $Cwidth} {incr x $step} {
set c_re [expr {$Rmin + $x * $Rscale}]
set data ""
for {set y 0} {$y < $Cheight} {incr y} {
set c_im [expr {$Imax - $y * $Iscale}]
set z_re $c_re
set z_im $c_im
for {set n 0} {$n < $maxIters} {incr n} {
set z_re2 [expr {$z_re * $z_re}] ;# Have we escaped yet???
set z_im2 [expr {$z_im * $z_im}]
if {($z_re2 + $z_im2) > 4} {
break
}
set z_im [expr {2 * $z_re * $z_im + $c_im}]
set z_re [expr {$z_re2 - $z_im2 + $c_re}]
}
lappend data $::colors($n)
}
::img::myImage put $data -to $x 0
update
if {$S(draw) == 0} break
}
if {$S(draw) == 0} break
}
set S(draw) 0
ToggleButtons 0
set sTime [expr {([clock click -milliseconds] - $sTime) / 1000}]
INFO "Time: [Duration $sTime]"
}
##+################################################################
#
# gradient -- adjusts a color to be "closer" to either white or black
# see https://wiki.tcl-lang.org/2847
#
proc gradient {rgb factor} {
foreach {r g b} [winfo rgb . $rgb] {break}
# Figure out color depth and number of bytes to use in the final result.
set max 255; set len 2
if {($r > 255) || ($g > 255) || ($b > 255)} {set max 65535; set len 4}
# Compute new red value by incrementing the existing value by a
# value that gets it closer to either 0 (black) or $max (white)
set range [expr {$factor >= 0.0 ? $max - $r : $r}]
set increment [expr {int($range * $factor)}]
incr r $increment
# Compute a new green value in a similar fashion
set range [expr {$factor >= 0.0 ? $max - $g : $g}]
set increment [expr {int($range * $factor)}]
incr g $increment
# Compute a new blue value in a similar fashion
set range [expr {$factor >= 0.0 ? $max - $b : $b}]
set increment [expr {int($range * $factor)}]
incr b $increment
### Format the new rgb string
set rgb [format "#%.${len}X%.${len}X%.${len}X" \
[expr {($r>$max)?$max:(($r<0)?0:$r)}] \
[expr {($g>$max)?$max:(($g<0)?0:$g)}] \
[expr {($b>$max)?$max:(($b<0)?0:$b)}]]
return $rgb
}
##+################################################################
#
# GradientColors
#
# Get maxIters number of colors in a gradient from black to white of
# color RGB.
#
proc GradientColors {{rgb red} {min -.5} {max .75}} {
global S colors maxIters
set S(color) $rgb
for {set i 0} {$i <= $maxIters} {incr i} {
set grad [expr {$min + 1.0* $i * ($max - $min) / $maxIters}]
set colors($i) [gradient $rgb $grad]
}
set colors($maxIters) black
}
##+################################################################
#
# RandomColors -- picks colors randomly
#
proc RandomColors {} {
global colors maxIters
for {set i 0} {$i <= $maxIters} {incr i} {
set colors($i) [format "\#%04X%04X%04X" [expr {int(rand() * 0xFFFF)}] \
[expr {int(rand() * 0xFFFF)}] [expr {int(rand() * 0xFFFF)}]]
}
set colors($maxIters) black
}
##+################################################################
#
# ChangeColor -- puts in a new color scheme
#
proc ChangeColor {random} {
global S maxIters
if {$random} {
RandomColors
INFO "Selecting new colors randomly -- press Redraw to see"
} else {
set color [tk_chooseColor -initialcolor $S(color) -parent . \
-title "Tk Mandelbrot Color"]
if {$color == ""} return
INFO "Setting new color $color -- press Redraw to see"
GradientColors $color
}
}
##+################################################################
#
# Canvas2Z -- converts from canvas to mandelbrot coordinates
#
proc Canvas2Z {x y} {
global Rmin Imax Rscale Iscale
set re [expr {$Rmin + $Rscale * $x}]
#set im [expr {$Imin + $Iscale * $y}]
set im [expr {$Imax - $Iscale * $y}]
return [list $re $im]
}
##+################################################################
#
# DoBox -- handles mousing to create the zoom box
#
proc DoBox {what x y} {
global B
.c delete box
if {$what == 0} { ;# Button down
.zoomin config -state disabled ;# No box, no button
set B(x0) [.c canvasx $x]
set B(y0) [.c canvasx $y]
} else { ;# Button motion
set B(x1) [.c canvasx $x]
set B(y1) [.c canvasx $y]
.c create rect $B(x0) $B(y0) $B(x1) $B(y1) -outline white -tag box \
-dash 1
.zoomin config -state normal ;# Have box, have button
}
}
##+################################################################
#
# Redraw -- starts or stops drawing of the fractal
#
proc Redraw {} {
global S
if {$S(draw)} {
INFO "stopping"
set S(draw) 0
return
}
INFO "redrawing..."
Render
}
##+################################################################
#
# ZoomIn -- zooms in the display to the box on the screen
#
proc ZoomIn {} {
global S Rmin Rmax Imin Imax
INFO "zooming in..."
if {[.c find withtag box] != ""} {
foreach {x0 y0 x1 y1} [.c bbox box] break
.c delete box
foreach {Rmin2 Imax2} [Canvas2Z $x0 $y0] break
foreach {Rmax2 Imin2} [Canvas2Z $x1 $y1] break
foreach {Rmin Rmax Imin Imax} \
[list $Rmin2 $Rmax2 $Imin2 $Imax2] break
}
lappend S(stack) [list $Rmin $Imax $Rmax $Imin]
after 1 Render
}
##+################################################################
#
# ZoomOut -- pops coordinates off stack and renders them
#
proc ZoomOut {} {
global S Rmin Rmax Imin Imax
if {[llength $S(stack)] < 2} return
INFO "zooming out..."
set a [lindex $S(stack) end-1]
set S(stack) [lrange $S(stack) 0 end-1] ;# Leave current at the end
foreach {Rmin Imax Rmax Imin} $a break
after 1 Render
}
proc INFO {msg} {
set ::S(msg) $msg
}
proc About {} {
tk_messageBox -icon info -parent . -title "About $::S(title)" \
-message "$::S(title)\n\nby Keith Vetter\nNovember, 2002"
}
##+################################################################
#
# Duration - Prints out seconds in a nice format
# https://wiki.tcl-lang.org/789
#
proc Duration { int_time } {
if {$int_time == 0} {return "0 secs"}
set timeList [list]
foreach div {86400 3600 60 1} mod {0 24 60 60} name {day hr min sec} {
set n [expr {$int_time / $div}]
if {$mod > 0} {set n [expr {$n % $mod}]}
if {$n > 1} {
lappend timeList "$n ${name}s"
} elseif {$n == 1} {
lappend timeList "$n $name"
}
}
return [join $timeList]
}
proc Clear {} {
.c delete box
::img::myImage blank
}
################################################################
################################################################
################################################################
DoDisplay
RandomColors
INFO "Welcome to Tk Mandelbrot"
RenderKris 2007-08-05 - I tweaked the interlaced rendering a little:
in proc Render
replace
set step 4 ;# Do interlaced drawing
for {set start 0} {$start < $step} {incr start} {with
foreach {start wid step} {0 8 8 4 4 8 2 2 4 1 1 1} {and
::img::myImage put $data -to $x 0
with
for {set xx $x} {$xx < $x+$wid} {incr xx} {
::img::myImage put $data -to $xx 0
}This may slow down the rendering but I think the "first results" are better than with the thin stripes.
RVB This is a great script! I added another color scheme (HSV variation)
##+################################################################
#
# RvbColors -- picks colors
#
proc RvbColors {} {
global colors maxIters
set s 0.8
set v 0.9
set scale_colors {}
set nc $maxIters
for {set i 0} {$i <= $maxIters} {incr i} {
set h [expr (360.0*$i)/$nc]
set k [expr int($h/60.0) % 6]
set f [expr $h/60.0 - $k]
set p [expr $v*(1-$s)]
set q [expr $v*(1-$f*$s)]
set t [expr $v*(1-(1-$f)*$s)]
switch -- $k {
0 {set r $v; set g $t; set b $p}
1 {set r $q; set g $v; set b $p}
2 {set r $p; set g $v; set b $t}
3 {set r $p; set g $q; set b $v}
4 {set r $t; set g $p; set b $v}
5 {set r $v; set g $p; set b $q}
}
set r [expr {int($r*0xFFFF)}]
set g [expr {int($g*0XFFFF)}]
set b [expr {int($b*0XFFFF)}]
set colors($i) [format "\#%04X%04X%04X" $r $g $b]
}
set colors($maxIters) black
}