[Arjen Markus] 2 october 2002. The wonderful page by [Keith Vetter] on the graphics (and mechanics) of the [Spirograph] made me think of my own childhood. And from there on to a generalisation of the concept, and from there on to the script below. Well, it is not actually a proper generalisation, but it will do as the results are satisfactory in terms of graphics and maths. The best metaphore, however, is a physical one (back to my student days now :-). Suppose you are watching someone drawing a curve. You concentrate on the motion of his or her stylus, and copy the result on a sheet of paper yourself. Actually, you have to do that to see the final curve, because this person is sitting in some kind of vehicle that is following a path on its own right. For example: the person is drawing a straight line while sitting on the horse in a merry-go-round. The result? Well, it depends on the speed of the merry-go-round, the size of the line that is being drawn and so on. In short, you need to ''compose'' the motion of the person and of the stylus. In mathematical terms: * There are two parametrised curves (time is the obvious parameter here!). * The locus and orientation of a point on the one curve determines the relative coordinate system of the second curve. In the script below this idea is elaborated: define parameterised curves, and construct new curves out of them. You can even use composed curves as the basic ingredients. ''Explanation of the design:'' * With UniqueID I construct a unique name, no more (just a counter) * I have an implementation routine like ParamCurveImpl that takes all the specific arguments to do its true job. * I do not want to show that in the "production code", so I use '''interp alias''' to store them safely away. * '''interp alias''' also makes a proc with this unique name. * When I call this proc, the call is translated into a call to ParamCurveImp with the constant, hidden arguments. * This is the equivalent of creating a Java object (or other OO-type languages) with a bunch of arguments that are stored in the object's fields ---- ====== # compose_curves.tcl -- # # Package for composing parametrised curves # (sample Workbench module) # # Notes: # This package is a quick hack to get started only # # Version information: # version 0.1: initial implementation, october 2002 package require Tk package provide ComposeCurves 0.1 namespace eval ::composecurves { variable unique_id 0 namespace export paramCurve compositeCurve display # paramCurve -- # Construct a procedure that implements a parametrised curve # and return its name # # Arguments: # xexpr Expression for calculating x-coordinate from parameter p # yexpr Ditto for calculating y-coordinate from parameter p # # Result: # Name of procedure that will calculate the locus at parameter p, # this procedure returns the coordinate pair (x,y) as a list. # # Note: # The expressions must use the variable p as the parameter, # e.g. "{$p} {$p*$p}" for the parabola with equation y = x^2 # proc paramCurve {xexpr yexpr} { set name [UniqueID "CURVE"] interp alias {} $name {} [namespace current]::ParamCurveImpl $xexpr $yexpr return $name } # ParamCurveImpl -- # Calculate the x and y coordinates as function of parameter p # # Arguments: # xexpr Expression for calculating x-coordinate from parameter p # yexpr Ditto for calculating y-coordinate from parameter p # p Value of parameter # # Result: # Name of procedure that will calculate the locus at parameter p, # this procedure returns the coordinate pair (x,y) as a list. # proc ParamCurveImpl {xexpr yexpr p} { # NOFRINK return [list [expr $xexpr] [expr $yexpr]] } # UniqueID -- # Construct a unique ID for a new procedure # # Arguments: # prefix Prefix to be used # # Result: # String of the form "prefix##0" # proc UniqueID {prefix} { variable unique_id set name "$prefix##$unique_id" incr unique_id return $name } # compositeCurve -- # Construct a procedure that implements the composition of the given # curves and return its name # # Arguments: # curve1 Curve to be imposed upon the loci of the second # curve2 Curve providing loci and orientation # # Result: # Name of procedure that will calculate the locus at parameter p, # this procedure returns the coordinate pair (x,y) as a list. # proc compositeCurve {curve1 curve2} { set name [UniqueID "COMPOSITE"] interp alias {} $name {} [namespace current]::CompositeCurveImpl $curve1 $curve2 return $name } # CompositeCurveImpl -- # Calculate the x and y coordinates as function of parameter p, # based on the composition of the two curves # # Arguments: # curve1 Curve to be imposed upon the loci of the second # curve2 Curve providing loci and orientation # p Value of parameter # # Result: # (x,y) coordinates # # Note: # The construction uses a second parameter value (p+0.001) to # determine the tangent. This assumes the parameter value is # in the order of 1 to 100, say. # proc CompositeCurveImpl {curve1 curve2 p} { set pd [expr {$p+0.001}] foreach {x1 y1} [$curve1 $p] break foreach {x2 y2} [$curve2 $p] break foreach {x2d y2d} [$curve2 $pd] break set xt [expr {$x2d-$x2}] set yt [expr {$y2d-$y2}] set tt [expr {hypot($xt,$yt)}] set xt [expr {$xt/$tt}] set yt [expr {$yt/$tt}] set xn [expr {-$yt}] set yn $xt set xp [expr {$x2+$x1*$xt+$y1*$xn}] set yp [expr {$y2+$y1*$yt+$y1*$yn}] return [list $xp $yp] } # display -- # Quick and dirty implementation to calculate and display a polyline # # Arguments: # curve Name of the curve to be calculated # colour Colour to use # # Result: # None # # Side effect: # Display of polyline, scaled within -20 to 20 for x and y # proc display {curve colour} { set xycoords {} for {set i 0} {$i < 2000} {incr i} { set p [expr {$i*0.01}] foreach {x y} [$curve $p] break set x [expr {int(10*($x+20.0))}] set y [expr {int(10*(20.0-$y))}] lappend xycoords $x $y } .cnv create line $xycoords -fill $colour } } ;# End of namespace # # Run the program # namespace import ::composecurves::* canvas .cnv -width 400 -height 400 -background white pack .cnv -fill both set line [paramCurve {0.7*$p} {0.7*$p}] set circle [paramCurve {cos($p)} {sin($p)}] set circle2 [paramCurve {cos(2.6*$p)} {sin(2.6*$p)}] set parabola [paramCurve {0.4*($p-10.0)} {0.16*($p-10.0)*($p-10.0)}] set lc [compositeCurve $line $circle] set cl [compositeCurve $circle2 $line] set pp [compositeCurve $circle2 $parabola] set clc [compositeCurve $circle2 $lc] display $lc "black" display $cl "red" display $pp "green" display $clc "magenta" ====== <> Graphics | Mathematics