[Arjen Markus] (19 december 2011) I am fascinated by mathematics in general and geometry in particular. So, this weekend I extended my old [mathematical notebook] with the specific purpose of visualising classical geometrical constructions. For this I needed an extended version - which you will find below (some convenience procedures and a different user-interface). The idea is to use the flexibility of the text widget to allow for an interactive display of the construction: via tags the example below highlights the steps in the construction of lines tangent to a circles. All in the classical tradition of the ancient Greek. I use the tag mechanism of the canvas to delete those items I do not need anymore - right now: just used by the "Reset" button. (Note: it is not perfect yet - opening a second file for instance does not refresh the text widget properly) **Example: Tangent lines to a circle** Here is a screenshot: [Matematical notebook - screenshot] ====== # Classical construction: # Tangent lines to a circle #
The classical tools for geometrical constructions, such as the hexagon or the bissectrix of an angle are the compasses and the straightedge.
Here we show how to use these tools to construct the lines through a given point that are tangent to a circle. The construction is shown in steps. (This illustrates that it is easy to make interactive displays).
First we draw the point P and the circle C (see the canvas on the right-hand side)
Then we take the following steps (press the Next button):
With compasses and straightedge you can construct all manner of curves, though sometimes the process is more mechanical than mathematical.
Here is an example: the witch of Agnesi is constructed by drawing a line through a fixed point on a circle and using the intersection through that circle and a tangent line to define a new point.
The process is illustrated in the figure on the right. Press the "Go" button to see how it works.
The curve has the parametric form:
x = 2 a cot t y = a (1-cos(2t))where "a" is the radius of the circle (cf. mathworld.wolfram.com/WitchofAgnesi.html). @init { proc intersectionCircleLine {circle line} { foreach {xc yc radius} $circle {break} foreach {x1 y1 x2 y2} $line {break} set dx [expr {$x2 - $x1}] set dy [expr {$y2 - $y1}] set length [expr {hypot($dx,$dy)}] set xn [expr {-$dy/$length}] set yn [expr {$dx/$length}] set mu [expr {($x1-$xc)*$xn + ($y1-$yc)*$yn}] set xmid [expr {$xc + $mu * $xn}] set ymid [expr {$yc + $mu * $yn}] set dist [expr {sqrt($radius**2 - ($xmid-$xc)**2 - ($ymid-$yc)**2)/$length}] set xi1 [expr {$xmid + $dx * $dist}] set yi1 [expr {$ymid + $dy * $dist}] set xi2 [expr {$xmid - $dx * $dist}] set yi2 [expr {$ymid - $dy * $dist}] return [list $xi1 $yi1 $xi2 $yi2] } proc drawWitch {x} { variable CNV variable coords $CNV delete line $CNV delete witch set line [list 0.0 -1.0 $x 1.0] set id1 [polyline $line black] set id2 [point $x 1.0 red] $CNV itemconfigure $id1 -tag line $CNV itemconfigure $id2 -tag line # # Determine the points of intersection and select the # right one - the one with y > -1.0 # set intersectionPoints [intersectionCircleLine {0.0 0.0 1.0} $line] foreach {xp yp} $intersectionPoints { if { $yp > -0.999 } { break } } # # Draw the auxiliary lines # set id1 [polyline [list $x 1.0 $x -1.0] red] set id2 [polyline [list $xp $yp $x $yp] red] set id3 [point $xp $yp red] set id4 [point $x $yp red] $CNV itemconfigure $id1 -tag line $CNV itemconfigure $id2 -tag line $CNV itemconfigure $id3 -tag line $CNV itemconfigure $id4 -tag line lappend coords $x $yp if { [llength $coords] > 2 } { set id [polyline $coords red] $CNV itemconfigure $id -tag witch } # # Note: the procedure is defined within the MathData namespace # if { $x < 4.0 } { after 100 [list ::MathData::drawWitch [expr {$x+0.1}]] } else { $CNV delete line $CNV itemconfigure $id -width 2 } } } @canvasright 400 400 { scale {-4.0 -4.0 4.0 4.0} infiniteLine -2.0 1.0 2.0 1.0 black circle 0.0 0.0 1.0 black point 0.0 -1.0 black console show puts [intersectionCircleLine {0.0 0.0 1.0} {0.0 0.0 0.0 1.0}] } @button Go { variable coords set coords {} drawWitch -4.0 } ====== **Code: Updated mathbook** ====== # mathbook.tcl -- # Script to show notes on mathematical subjects # # TODO: # - Implement a number of useful drawing commands # - Implement a formula renderer (a basic one _is_ available) # - Implement more convenient bindings # - Describe the application # # Missing commands: # @refresh - define your own refresh method # @label - allow a label (useful for variable text) # @button - allow a pushbutton # package require Tcl 8.5 package require Tk if { [tk windowingsystem] == "x11" } { . configure -background #dcdad5 option add *background #dcdad5 option add *foreground black option add *borderWidth 1 widgetDefault option add *activeBorderWidth 1 widgetDefault option add *selectBorderWidth 1 widgetDefault option add *font -adobe-helvetica-medium-r-normal-*-12-*-*-*-*-*-* option add *padX 2 option add *padY 4 option add *Listbox.background white option add *Listbox.selectBorderWidth 0 option add *Listbox.selectForeground white option add *Listbox.selectBackground #4a6984 option add *Entry.foreground black option add *Entry.background white option add *Entry.selectBorderWidth 0 option add *Entry.selectForeground white option add *Entry.selectBackground #4a6984 option add *Text.background white option add *Text.selectBorderWidth 0 option add *Text.selectForeground white option add *Text.selectBackground #4a6984 option add *Menu.activeBackground #4a6984 option add *Menu.activeForeground white option add *Menu.activeBorderWidth 0 option add *Menu.highlightThickness 0 option add *Menu.borderWidth 2 option add *MenuButton.activeBackground #4a6984 option add *MenuButton.activeForeground white option add *MenuButton.activeBorderWidth 0 option add *MenuButton.highlightThickness 0 option add *MenuButton.borderWidth 0 option add *highlightThickness 0 option add *troughColor #bdb6ad } # MathData -- # Namespace for the user-defined commands and data # namespace eval ::MathData:: { variable CNV "" variable TXT "" variable fontNormal "Courier 10" variable fontBold "Courier 10 bold" variable fontItalic "Courier 10 italic" } # scale -- # Set up the scaling for the given canvas # Arguments: # data List of data (x, y, x, y ...) # Result: # None # Side effects: # Scaling parameters set # Note: # TODO: Should make sure there is some scaling involved # if only using pixels # proc ::MathData::scale { data } { variable CNV variable SCALE set width [$CNV cget -width] set height [$CNV cget -height] set xmin 1.0e30 set xmax -1.0e30 set ymin 1.0e30 set ymax -1.0e30 foreach {x y} $data { if { $x < $xmin } { set xmin $x } if { $x > $xmax } { set xmax $x } if { $y < $ymin } { set ymin $y } if { $y > $ymax } { set ymax $y } } if { $xmin == $xmax } { set xmax [expr {$xmax+1.0}] } if { $ymin == $ymax } { set ymax [expr {$ymax+1.0}] } set SCALE(xscale) [expr {$width/double($xmax-$xmin)}] set SCALE(yscale) [expr {$height/double($ymax-$ymin)}] set SCALE(xmin) $xmin set SCALE(xmax) $xmax set SCALE(ymin) $ymin set SCALE(ymax) $ymax } # polyline -- # Draw a line consisting of multiple points # Arguments: # data List of data (x, y, x, y ...) # colour Colour to use (default: black) # Result: # Canvas ID of the polyline # Side effects: # Line drawn according to current scales # proc ::MathData::polyline { data {colour black} } { variable CNV variable SCALE set xscale $SCALE(xscale) set yscale $SCALE(yscale) set xmin $SCALE(xmin) set xmax $SCALE(xmax) set ymin $SCALE(ymin) set ymax $SCALE(ymax) set pixels {} foreach {x y} $data { set px [expr {$xscale*($x-$xmin)}] set py [expr {$yscale*($ymax-$y)}] lappend pixels $px $py } $CNV create line $pixels -fill $colour } # circle -- # Draw a circle with given centre and radius # Arguments: # xcentre X-coordinate of the centre # ycentre Y-coordinate of the centre # radius Radius of circle # colour Colour to use (default: black) # filled Filled or not (default: not) # Result: # Canvas ID of the circle # Side effects: # Line drawn according to current scales # proc ::MathData::circle { xcentre ycentre radius {colour black} {filled 0} } { variable CNV variable SCALE set xscale $SCALE(xscale) set yscale $SCALE(yscale) set xmin $SCALE(xmin) set xmax $SCALE(xmax) set ymin $SCALE(ymin) set ymax $SCALE(ymax) set pixels {} foreach {x y} [list [expr {$xcentre-$radius}] [expr {$ycentre-$radius}] \ [expr {$xcentre+$radius}] [expr {$ycentre+$radius}] ] { set px [expr {$xscale*($x-$xmin)}] set py [expr {$yscale*($ymax-$y)}] lappend pixels $px $py } $CNV create oval $pixels -fill [expr {$filled? $colour : {}}] -outline $colour } # point -- # Draw a point with given coordinates # Arguments: # xpoint X-coordinate # ypoint Y-coordinate # colour Colour to use (default: black) # Result: # Canvas ID of the point # Side effects: # Line drawn according to current scales # proc ::MathData::point { xpoint ypoint {colour black} } { variable CNV variable SCALE set xscale $SCALE(xscale) set yscale $SCALE(yscale) set xmin $SCALE(xmin) set xmax $SCALE(xmax) set ymin $SCALE(ymin) set ymax $SCALE(ymax) set pixels {} set px [expr {$xscale*($xpoint-$xmin)}] set py [expr {$yscale*($ymax-$ypoint)}] lappend pixels [expr {$px-1}] [expr {$py-1}] [expr {$px+1}] [expr {$py+1}] $CNV create rectangle $pixels -fill $colour -outline $colour } # text -- # Draw a text string at a given position # Arguments: # x X coordinate # y Y coordinate # string String to show # Result: # Canvas ID of the text object # Side effects: # String drawn # proc ::MathData::text { x y string } { variable CNV variable SCALE set xscale $SCALE(xscale) set yscale $SCALE(yscale) set xmin $SCALE(xmin) set xmax $SCALE(xmax) set ymin $SCALE(ymin) set ymax $SCALE(ymax) set px [expr {$xscale*($x-$xmin)}] set py [expr {$yscale*($ymax-$y)}] $CNV create text $px $py -text $string -anchor nw } # axes -- # Draw two lines representing the axes # Arguments: # None # Result: # None # Side effects: # Two lines drawn (no labels yet) # proc ::MathData::axes { } { variable CNV variable SCALE set width [$CNV cget -width] set height [$CNV cget -height] set xscale $SCALE(xscale) set yscale $SCALE(yscale) set xmin $SCALE(xmin) set xmax $SCALE(xmax) set ymin $SCALE(ymin) set ymax $SCALE(ymax) set px0 [expr {$xscale*(0.0-$xmin)}] set py0 [expr {$yscale*($ymax-0.0)}] $CNV create line $px0 0 $px0 $height -fill black $CNV create line 0 $py0 $width $py0 -fill black } # func -- # Repeatedly run a function and return xy-pairs # Arguments: # funcname Name of the function (procedure) # xmin Minimum x-value # xmax Maximum x-value # nosteps Number of steps (inbetween; default: 50) # Result: # List of x, y values # proc ::MathData::func { funcname xmin xmax { nosteps 50 } } { set coords {} set xstep [expr {($xmax-$xmin)/$nosteps}] for { set i 0 } { $i <= $nosteps } { incr i } { set x [expr {$xmin+$i*$xstep}] set y [$funcname $x] lappend coords $x $y } return $coords } # circleIntersection -- # Determine the points where two circles intersect # Arguments: # circle1 X-, Y-coordinate and radius of first circle # circle2 X-, Y-coordinate and radius of second circle # Result: # List of x/y coordinates or empty # proc ::MathData::circleIntersection { circle1 circle2 } { set coords {} foreach {x1 y1 r1} $circle1 {break} foreach {x2 y2 r2} $circle2 {break} # # Do we have an intersection? # set distc [expr {sqrt(($x1-$x2)**2 + ($y1-$y2)**2)}] if { $distc**2 <= $r1**2+$r2**2 } { set a [expr {0.5 * ($distc + ($r1**2-$r2**2)/$distc)}] set dist [expr {sqrt($r1**2 - $a**2)}] set dx [expr {$x2-$x1}] set dy [expr {$y2-$y1}] set dd [expr {hypot($dx,$dy)}] set xc [expr {$x1 + $a * $dx/$dd}] set yc [expr {$y1 + $a * $dy/$dd}] set xn [expr {$dist * $dy/$dd}] set yn [expr {-$dist * $dx/$dd}] set xp1 [expr {$xc + $xn}] set xp2 [expr {$xc - $xn}] set yp1 [expr {$yc + $yn}] set yp2 [expr {$yc - $yn}] set coords [list $xp1 $yp1 $xp2 $yp2] } return $coords } # infiniteLine -- # Draw a line through two points that extends indefinitely # Arguments: # xp1 X-coordinate first point # yp1 Y-coordinate first point # xp2 X-coordinate second point # yp2 Y-coordinate second point # colour Colour of the line (default: black) # Result: # Canvas ID of the line # proc ::MathData::infiniteLine { xp1 yp1 xp2 yp2 {colour black} } { variable CNV variable SCALE set dx [expr {$xp2-$xp1}] set dy [expr {$yp2-$yp1}] if { abs($dx) > abs($dy) } { set xn1 $SCALE(xmin) set lambda [expr {($xn1 - $xp1) / $dx}] set yn1 [expr {$yp1 + $dy * $lambda}] set xn2 $SCALE(xmax) set lambda [expr {($xn2 - $xp1) / $dx}] set yn2 [expr {$yp1 + $dy * $lambda}] } else { set yn1 $SCALE(ymin) set lambda [expr {($yn1 - $yp1) / $dy}] set xn1 [expr {$xp1 + $dx * $lambda}] set yn2 $SCALE(ymax) set lambda [expr {($yn2 - $yp1) / $dy}] set xn2 [expr {$xp1 + $dx * $lambda}] } polyline [list $xn1 $yn1 $xn2 $yn2] $colour } # MathBook -- # Namespace for the mathbook commands and data # namespace eval ::MathBook:: { variable count 0 variable CNV variable CNVRIGHT "" variable CNVCODE variable TXT variable REFRESH } # @init -- # Execute code once (when reading the notebook file) # Arguments: # code Code to run # Result: # Nothing # proc ::MathBook::@init { code } { namespace eval ::MathData $code } # @canvas -- # Create a canvas of given size # Arguments: # width Width in pixels # height Height in pixels # code Code to execute # Result: # Nothing # Side effect: # Canvas created # proc ::MathBook::@canvas { width height code } { variable CNV variable CNVCODE variable TXT variable count incr count set CNV $TXT.cnv$count set ::MathData::CNV $CNV set CNVCODE($CNV) $code canvas $CNV -width $width -height $height -bg white $TXT insert end "\n" $TXT window create end -window $CNV $TXT insert end "\n" namespace eval ::MathData $code } # @canvasright -- # Create a canvas of given size on the right of the text # Arguments: # width Width in pixels # height Height in pixels # code Code to execute # Result: # Nothing # Side effect: # Canvas created # proc ::MathBook::@canvasright { width height code } { variable CNV variable CNVCODE variable CNVRIGHT if { $CNVRIGHT eq "" } { set CNVRIGHT [canvas .cnv -width $width -height $height -bg white] grid configure $CNVRIGHT -row 0 -column 2 } else { $CNVRIGHT configure -width $width -height $height } set CNV $CNVRIGHT set ::MathData::CNV $CNV namespace eval ::MathData $code } # @entry -- # Create an entry widget of given width # Arguments: # name Name of the associated variable # width Width of the widget (in characters) # Result: # Nothing # Side effect: # Entry created # proc ::MathBook::@entry { name width } { variable TXT variable count incr count set entry $TXT.entry$count entry $entry -textvariable ::MathData::$name -width $width $TXT window create end -window $entry bind $entry
" $trimmed] == 0 } { $TXT insert end "\n" set tag "preform" set just "\n" continue } if { [string first "" $trimmed] == 0 } { $TXT insert end "\n" set tag [list normal $indent] set just "" continue } if { [string first "
" $trimmed] == 0 } {
$TXT insert end "\n\n"
continue
}
if { [string first "
" $trimmed] == 0 } {
$TXT insert end "\n"
continue
}
if { [string first "