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"