## Composing curves

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"```

 Category Graphics Category Mathematics