# This is a package that is used to generate vectors suitable # to draw relatively accurate arcs using TCL's canvas item # -smooth raw option (which uses cubic Beziers). # # The approach taken here is to generate the Bezier vectors for # arcs by (linearly) scaling the so-called "magic number" used # to draw 90 degree arcs. I am not sure if this is a standard # approach, but it seems to (visually) work reasonably well. package require Tcl 8.5 package require Trans2D set NS BezierArc package provide $NS 0.0 namespace eval $NS { namespace export Arc # The derivation of this number (and slight variations to it) # can be found from various online sources such as # http://spencermortensen.com/articles/bezier-circle/ # This is the number for a unit circle, 90 degree arcs. variable MAGICNUMBER 0.551915024494 variable PI set PI [expr {acos(-1)}] } proc ${NS}::Arc {arccenter arcradius arcstartangle arcstopangle direction} { # Returns a vector intended for use in canvas line and polygon item # creation with the "-smooth raw" option. Angles are in radians. variable MAGICNUMBER variable PI # Ensure the arc angles are positive. set arcstartangle [MakePosAngle $arcstartangle] set arcstopangle [MakePosAngle $arcstopangle] # Determine arc magnitude. switch -exact -- [string tolower $direction] { cw { set arcmagnitude [MakePosAngle [expr {$arcstartangle - $arcstopangle}]] } ccw { set arcmagnitude [MakePosAngle [expr {$arcstopangle - $arcstartangle}]] } default { error "Invalid arc direction, $direction. Limited to cw|ccw." } } # First generate an arc from 0 degrees. set nfullquadrants [expr int(floor($arcmagnitude * 2.0 / $PI))] set partialquadrant [expr {$arcmagnitude - ($nfullquadrants * $PI)/2}] # ...starting with the partial quadrant. This should work even if # there is no partial. A close-to-zero angle here could likely be # filtered out, but that is left to the calling scope. set m_adj [expr {$MAGICNUMBER * $partialquadrant * 2.0 / $PI}] set fixedcontrolpoints [list 1.0 0.0 1.0 $m_adj] set adjcontrolpoints [list 1.0 -$m_adj 1.0 0.0] # Rotate adjcontrolpoints to match the desired angle. set R [Trans2D::Rotation $partialquadrant] set unitarc [list {*}$fixedcontrolpoints {*}[Trans2D::ApplyTransform $R $adjcontrolpoints]] # ...and then rotating this by 90 degrees and prepending with # a full-quadrant arc, deleting the intermediary "knot" point. set R [Trans2D::Rotation [expr {$PI / 2.0}]] for {set i 0} {$i < $nfullquadrants} {incr i} { set unitarc [Trans2D::ApplyTransform $R $unitarc] set unitarc [list 1.0 0.0 1.0 $MAGICNUMBER $MAGICNUMBER 1.0 {*}$unitarc] } # unitarc now contains a ccw circular vector of the appropriate angle # Rotate to make this match the arcstartangle, scale to the appropriate # radius and translate to the arc center, in that order. set R [Trans2D::Rotation $arcstartangle] set S [Trans2D::Scale $arcradius] set T [Trans2D::Position $arccenter] # A clockwise direction requires flipping the data y coordinate since the arc # was generated in a counter-clockwise fashion. if {[string match cw [string tolower $direction]]} { set F [Trans2D::Reflection y] set Tnet [Trans2D::CompoundTransforms $T $S $R $F] } else { # CCW arc. set Tnet [Trans2D::CompoundTransforms $T $S $R] } set unitarc [Trans2D::ApplyTransform $Tnet $unitarc] return $unitarc } proc ${NS}::MakePosAngle { angle_rad } { # Ensures/converts a negative angle to positive by adding # 360 degrees. variable PI while {$angle_rad < 0.0} { set angle_rad [expr {$angle_rad + 2.0 * $PI}] } return $angle_rad }The following demonstration code creates a canvas that is replicated in the animated gif at the top of this page.

#!/bin/sh # the next line restarts using tclsh \ exec wish "$0" ${1+"$@"} package require Trans2D package require BezierArc set PI [expr {acos(-1)}] set c [canvas .c -width 150 -height 150 -background black] pack .c # Right-handed coordinate system with origin at the canvas centre. set cTw [Trans2D::CompoundTransforms [Trans2D::Position 75 75] [Trans2D::Reflection y]] # Create a static circle with a slightly smaller radius than the arc # we are about to draw - for comparison purposes. $c create oval {*}[Trans2D::ApplyTransform $cTw {-45 -45 45 45}] -fill purple # Create a dummy line item type that will be used to draw the arc. # Note the required smooth "raw" option. set arcID [$c create line 0 0 0 0 0 0 0 0 -smooth raw -fill orange -arrow last] # ... and another one that will be used to show the control points. set lineID [$c create line 0 0 0 0 0 0 0 0 -fill grey -dash .] set ang_step [expr {(2.0 * $PI/75)}] for {set ang_rad 0.0} {$ang_rad < (2.0 * $PI)} {set ang_rad [expr {$ang_rad + $ang_step}]} { set coords [Trans2D::ApplyTransform $cTw [BezierArc::Arc {0.0 0.0} 50.0 0.0 $ang_rad ccw]] $c coords $arcID {*}$coords $c coords $lineID {*}$coords update after 100 }