http://mathling.com/shape/paths  library module

http://mathling.com/shape/paths


Module with functions providing some interesting paths
No global parameters or randomizers

Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)

November 2021
Status: Active

Imports

http://mathling.com/type/modulated-lissajous
import module namespace lissa_t="http://mathling.com/type/modulated-lissajous"
       at "../types/modulated-lissajous.xqy"
http://mathling.com/type/pendulum
import module namespace pend_t="http://mathling.com/type/pendulum"
       at "../types/pendulum.xqy"
http://mathling.com/type/fern
import module namespace fern_t="http://mathling.com/type/fern"
       at "../types/fern.xqy"
http://mathling.com/geometric/curve
import module namespace curve="http://mathling.com/geometric/curve"
       at "../geo/curves.xqy"
http://mathling.com/type/modulated-knot
import module namespace knot_t="http://mathling.com/type/modulated-knot"
       at "../types/modulated-knot.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/geometric/path
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/type/polynomial
import module namespace polynomial="http://mathling.com/type/polynomial"
       at "../types/polynomial.xqy"
http://mathling.com/core/complex
import module namespace z="http://mathling.com/core/complex"
       at "../core/complex.xqy"
http://mathling.com/geometric/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy"
http://mathling.com/type/wiggle
import module namespace wiggle_t="http://mathling.com/type/wiggle"
       at "../types/wiggle.xqy"
http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"
http://mathling.com/core/sequences
import module namespace seq="http://mathling.com/core/sequences"
       at "../core/sequences.xqy"

Functions

Function: arc
declare function arc($center as map(xs:string,item()*), $radius as xs:double, $arc as xs:double, $start-angle as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


arc()
Make a circular path of $arc degrees from $start-angle

Params
  • center as map(xs:string,item()*): center of the circle
  • radius as xs:double: radius of the circle
  • arc as xs:double: degrees of arc
  • start-angle as xs:double: starting angle (degrees)
  • num-points as xs:integer: number of points to create along arc
Returns
  • map(xs:string,item()*)*
declare function this:arc(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double,
  $start-angle as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  (: for (α=β; α<θ+β α+=θ/n) :)
  for $point in 0 to $num-points - 1
  (: α = β + point*θ/n :)
  let $α := $start-angle + $point * ($arc div $num-points)
  return (
    geom:destination($center, $α, $radius)
  )
}

Function: reverse-arc
declare function reverse-arc($center as map(xs:string,item()*), $radius as xs:double, $arc as xs:double, $start-angle as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


reverse-arc()
Make a circular path from $start-angle down to $start-angle - $arc

Params
  • center as map(xs:string,item()*): center of the circle
  • radius as xs:double: radius of the circle
  • arc as xs:double: degrees of arc
  • start-angle as xs:double: starting angle (degrees)
  • num-points as xs:integer: number of points to create along arc
Returns
  • map(xs:string,item()*)*
declare function this:reverse-arc(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double,
  $start-angle as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  (: for (α=θ+β; α>=β α-=θ/n) :)
  for $point in 0 to $num-points
  (: α = β - point*θ/n :)
  let $α := $start-angle - $point * ($arc div $num-points)
  return (
    geom:destination($center, $α, $radius)
  )
}

Function: helix
declare function helix($center as map(xs:string,item()*), $radius as xs:double, $slope as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


helix()
Create a right-handed 3-D helix path

Params
  • center as map(xs:string,item()*): starting point, center of turn
  • radius as xs:double: radius of helix
  • slope as xs:double: slope of helix
  • num-points as xs:integer: how many points to draw helix(x) = {a cos(t), a sin(t), b t} a = radius; b/a = slope; so b = slope*a
Returns
  • map(xs:string,item()*)*
declare function this:helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
   for $t in 0 to $num-points - 1
   let $x := $radius * math:cos($t) + point:px($center)
   let $y := $radius * math:sin($t) + point:py($center)
   let $z := $slope * $radius * $t + point:pz($center)
   return point:point($x, $y, $z)
}

Function: helix
declare function helix($center as map(xs:string,item()*), $radius as xs:double, $slope as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


helix()
Create a right-handed 3-D helix path

Params
  • center as map(xs:string,item()*): starting point, center of turn
  • radius as xs:double: radius of helix
  • slope as xs:double: slope of helix
  • num-points as xs:integer: how many points to draw
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs 0 to 2π; gives double spirals helix(x) = {a cos(t), a sin(t), b t} a = radius; b/a = slope; so b = slope*a
Returns
  • map(xs:string,item()*)*
declare function this:helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($t) + $δy
      },
      function ($t as xs:double) as xs:double {
        $slope*$radius*$t + $δz
      },
      $min, $max
    )
  )
}

Function: left-helix
declare function left-helix($center as map(xs:string,item()*), $radius as xs:double, $slope as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


left-helix()
Create a right-handed 3-D helix path

Params
  • center as map(xs:string,item()*): starting point, center of turn
  • radius as xs:double: radius of helix
  • slope as xs:double: slope of helix
  • num-points as xs:integer: how many points to draw helix(x) = {-a cos(t), a sin(t), b t} a = radius; b/a = slope; so b = slope*a
Returns
  • map(xs:string,item()*)*
declare function this:left-helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
   for $t in 0 to $num-points - 1
   let $x := -$radius * math:cos($t) + point:px($center)
   let $y := $radius * math:sin($t) + point:py($center)
   let $z := $slope * $radius * $t + point:pz($center)
   return point:point($x, $y, $z)
}

Function: left-helix
declare function left-helix($center as map(xs:string,item()*), $radius as xs:double, $slope as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


left-helix()
Create a left-handed 3-D helix path

Params
  • center as map(xs:string,item()*): starting point, center of turn
  • radius as xs:double: radius of helix
  • slope as xs:double: slope of helix
  • num-points as xs:integer: how many points to draw
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs 0 to 2π; gives double spirals helix(x) = {-a cos(t), a sin(t), b t} a = radius; b/a = slope; so b = slope*a
Returns
  • map(xs:string,item()*)*
declare function this:left-helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        -$radius*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($t) + $δy
      },
      function ($t as xs:double) as xs:double {
        $slope*$radius*$t + $δz
      },
      $min, $max
    )
  )
}

Function: torus-knot
declare function torus-knot($center as map(xs:string,item()*), $radius as xs:double, $p as xs:integer, $q as xs:integer, $num-points as xs:integer) as map(xs:string,item()*)*


torus-knot()
Create a torus knot.

Params
  • center as map(xs:string,item()*): center point of knot
  • radius as xs:double: scaling of knot
  • p as xs:integer: knot parameter
  • q as xs:integer: know parameter For clean knots p and q should be mutually prime, if they aren't you miss some of the lobes bridges = q; crossings = p(q-1); generally p-lobed shape Normally p > q and you get clean lobes When p &lt; q, you get involuted circles
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $p as xs:integer, 
  $q as xs:integer, 
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($q*$t)*(3 + math:cos($p*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($q*$t)*(3 + math:cos($p*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($p*$t) + $δz
      },
      0, 2*math:pi()
    )
  )
}

Function: skewed-torus-knot
declare function skewed-torus-knot($center as map(xs:string,item()*), $radius as xs:double, $p as xs:integer, $q as xs:integer, $s as xs:integer, $r as xs:integer, $num-points as xs:integer) as map(xs:string,item()*)*


skewed-torus-knot()
Create a torus knot.

Params
  • center as map(xs:string,item()*): center point of knot
  • radius as xs:double: scaling of knot
  • p as xs:integer: knot parameter
  • q as xs:integer: knot parameter For clean knots p and q should be mutually prime, if they aren't you miss some of the lobes bridges = q; crossings = p(q-1); generally p-lobed shape Normally p > q and you get clean lobes When p &lt; q, you get involuted circles
  • s as xs:integer: skew parameter &lt; 4
  • r as xs:integeradius: scaling of knot: skew parameter &lt; 4 A normal torus knot has s=r=3; s!=r gives smooshed knots; s is very different from r gives overlaps perceived as complicated twists s=r>3 gives pudgier links s=r&lt;3 gives linked circles
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:skewed-torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $p as xs:integer, 
  $q as xs:integer,
  $s as xs:integer,
  $r as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($q*$t)*($s + math:cos($p*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($q*$t)*($r + math:cos($p*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($p*$t) + $δz
      },
      0, 2*math:pi()
    )
  )
}

Function: modulated-torus-knot
declare function modulated-torus-knot($center as map(xs:string,item()*), $radius as xs:double, $knot as map(xs:string,item()*), $num-points as xs:integer) as map(xs:string,item()*)*


modulated-torus-knot()
Create a modulated torus knot.

Params
  • center as map(xs:string,item()*): center point of knot
  • radius as xs:double: scaling of knot
  • knot as map(xs:string,item()*): knot parameters (see modulated torus knot type information)
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:modulated-torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $knot as map(xs:string,item()*),
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  trace((), "@"||$radius||" "||geom:quote($center)||" "||knot_t:describe($knot)),
  let $p := $knot=>knot_t:p()
  let $openness := $knot=>knot_t:openness()
  let $x-factors := $knot=>knot_t:x-factors()
  let $y-factors := $knot=>knot_t:y-factors()
  let $stretch := $knot=>knot_t:stretch()
  let $openness := $knot=>knot_t:openness()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        $radius*$stretch*(
          math:cos($p*$t)*
          ($openness +
            sum(
              let $n := count($x-factors)
              for $j in 1 to $n return (
                let $factor := $x-factors[$j]
                let $r := $factor=>knot_t:r()
                let $q := $factor=>knot_t:q()
                return (
                  if ($factor=>knot_t:skew()) then (
                    $r*math:sin($q*$t)
                  ) else (
                    $r*math:cos($q*$t)
                  )
                )
              )
            )
          )
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*(
          math:sin($p*$t)*
          ($openness +
            sum(
              let $n := count($y-factors)
              for $j in 1 to $n return (
                let $factor := $y-factors[$j]
                let $r := $factor=>knot_t:r()
                let $q := $factor=>knot_t:q()
                return (
                  if ($factor=>knot_t:skew()) then (
                    $r*math:sin($q*$t)
                  ) else (
                    $r*math:cos($q*$t)
                  )
                )
              )
            )
          )
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*0.2*(
          $openness + 
          math:sin(max(($p,$x-factors!knot_t:q(.)))*$t)
        ) + $δz
      },
      0, 2*math:pi()
    )
  )
}

Function: modulated-lissajous
declare function modulated-lissajous($center as map(xs:string,item()*), $radius as xs:double, $curve as map(xs:string,item()*), $num-points as xs:integer) as map(xs:string,item()*)*


modulated-lissajous()
Create a modulated Lissajous curve.

Params
  • center as map(xs:string,item()*): center point of curve
  • radius as xs:double: scaling of curve
  • curve as map(xs:string,item()*): curve parameters (see Lissajous type information)
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:modulated-lissajous(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $curve as map(xs:string,item()*),
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $x-factors := $curve=>lissa_t:x-factors()
  let $y-factors := $curve=>lissa_t:y-factors()
  let $stretch := $curve=>lissa_t:stretch()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*$stretch*sum(
          for $factor in $x-factors
          let $n := $factor=>lissa_t:n()
          let $φ := util:radians($factor=>lissa_t:phase())
          let $skew := $factor=>lissa_t:skew()
          return
            if ($skew) then math:sin($n*$t + $φ)
            else math:cos($n*$t + $φ)
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          for $factor in $y-factors
          let $n := $factor=>lissa_t:n()
          let $φ := util:radians($factor=>lissa_t:phase())
          let $skew := $factor=>lissa_t:skew()
          return
            if ($skew) then math:sin($n*$t + $φ)
            else math:cos($n*$t + $φ)
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin(max($x-factors=>lissa_t:n())*$t) + $δz
      },
      0, 2 * math:pi()
    )
  )
}

Function: rose-curve
declare function rose-curve($center as map(xs:string,item()*), $radius as xs:double, $n as xs:integer, (: k=n/d petals: 2k if even, k :) $d as xs:integer, $num-points as xs:integer) as map(xs:string,item()*)*


rose-curve()
Create a closed looping curve
(see https://en.wikipedia.org/wiki/Rose_(mathematics))

Params
  • center as map(xs:string,item()*): center point of rose
  • radius as xs:double: scaling of rose
  • n as xs:integer: rose parameterum-points: number of points to draw
  • d as xs:integer: rose parameter k = n/d $n > $d gives petals around a center $n &lt; $d gives involuted petals k = 1 is a circle Number of petals depends on k k is integer => k if odd; 2k if even k is n/3 w/ n mod 3 != 0; n petals if n is odd; 2k if even n and d mutually prime gives cleaner roses; not mutually prime drops some of the petals
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:rose-curve(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $n as xs:integer, (: k=n/d petals: 2k if even, k :)
  $d as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $k := $n div $d
  let $angle :=
    if ($n mod $d = 0) then (
      if ($k mod 2 = 0) then 2 * math:pi() else math:pi()
    ) else (
     2 * math:pi() * $d
    )
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($k*$t)*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:cos($k*$t)*math:sin($t) + $δy
      },
      0, $angle
    )
  )
}

Function: trefoil
declare function trefoil($center as map(xs:string,item()*), $radius as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


trefoil()
A trefoil; as a 3D curve (torus surface)
Compared to a torus(3,2) the lobes are more drop-like and smaller
Compared to a rose(3,1) the lobes are separated and larger

Params
  • center as map(xs:string,item()*): center point of curve
  • radius as xs:double: scaling of curve
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:trefoil(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*(math:sin($t) + 2*math:sin(2*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*(math:cos($t) - 2*math:cos(2*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*(-math:sin(3*$t)) + $δz
      },
      0, 2*math:pi()
    )
  )
}

Function: maurer-rose
declare function maurer-rose($center as map(xs:string,item()*), $radius as xs:double, $n as xs:integer, $d as xs:integer, $num-points as xs:integer) as map(xs:string,item()*)*


maurer-rose()
Construct a Maurer rose.
(see https://en.wikipedia.org/wiki/Maurer_rose)

Params
  • center as map(xs:string,item()*): center point of rose
  • radius as xs:double: scaling of rose
  • n as xs:integer: rose parameterum-points: number of points to draw for each line set Since the rose if formed by cross-cutting lines; a small n is more noticeable in a way the other curves are not; splining doesn't help num-points=360 (i.e. one per degree) gives a filled out appearance
  • d as xs:integer: rose parameter Generally n=small; d=order of magnitude larger; relatively prime
  • num-points as xs:integer: number of points to draw for each line set Since the rose if formed by cross-cutting lines; a small n is more noticeable in a way the other curves are not; splining doesn't help num-points=360 (i.e. one per degree) gives a filled out appearance
Returns
  • map(xs:string,item()*)*
declare function this:maurer-rose(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $n as xs:integer,
  $d as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        let $k := $t * $d
        return $radius * math:sin($n * $k) * math:cos($k) + $δx
      },
      function ($t as xs:double) as xs:double {
        let $k := $t * $d
        return $radius * math:sin($n * $k) * math:sin($k) + $δy
      },
      0, 2*math:pi()
    )
  )
}

Function: granny-knot
declare function granny-knot($center as map(xs:string,item()*), $radius as xs:double, $num-points as xs:integer) as map(xs:string,item()*)*


granny-knot()
Construct a granny knot.

Params
  • center as map(xs:string,item()*): center point of knot
  • radius as xs:double: scaling of knot
  • num-points as xs:integer: number of points to draw
Returns
  • map(xs:string,item()*)*
declare function this:granny-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $scaled-radius := $radius div 100
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $scaled-radius*(-22*math:cos($t) - 128*math:sin($t) - 44*math:cos(3*$t) - 78*math:sin(3*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $scaled-radius*(-10*math:cos(2*$t) - 27*math:sin(2*$t) + 38*math:cos(4*$t) + 46*math:sin(4*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $scaled-radius*(70*math:cos(3*$t) - 40*math:sin(3*$t)) + $δz
      },
      0, 2*math:pi()
    )
  )
}

Function: harmonograph
declare function harmonograph($center as map(xs:string,item()*), $radius as xs:double, $harmonograph as map(xs:string,item()*), $density as xs:integer, $extent as xs:double) as map(xs:string,item()*)*


harmonograph()
Draw the path of a harmonograph (multi-axis multi-pendulum system)

Params
  • center as map(xs:string,item()*): Center of graph
  • radius as xs:double: Basic scaling of graph
  • harmonograph as map(xs:string,item()*): Harmonograph descriptor (see pend_t:harmonograph())
  • density as xs:integer: Parameter to adjust fineness of the time ticks (t=i/density)
  • extent as xs:double: Basic extent of drawing Final number of ticks (and therefore points) is num-points*density
Returns
  • map(xs:string,item()*)*
declare function this:harmonograph(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $harmonograph as map(xs:string,item()*), 
  $density as xs:integer,
  $extent as xs:double
) as map(xs:string,item()*)*
{
  let $num-x-pendulums := pend_t:num-pendulums(pend_t:x($harmonograph))
  let $x-pendulums := pend_t:x($harmonograph)=>pend_t:pendulums()
  let $num-y-pendulums := pend_t:num-pendulums(pend_t:y($harmonograph))
  let $y-pendulums := pend_t:y($harmonograph)=>pend_t:pendulums()
  let $num-z-pendulums := pend_t:num-pendulums(pend_t:z($harmonograph))
  let $z-pendulums := pend_t:z($harmonograph)=>pend_t:pendulums()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      util:round($extent * $density),
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $x-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $y-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $z-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δz
      },
      1 div $density, $extent
    )
  )
  (:
  for $i in 1 to $extent*$density
  let $t := $i div $density
  let $x :=
    $radius*sum(
      $x-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:px($center)
  let $y := 
    $radius*sum(
      $y-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:py($center)
  let $z := 
    $radius*sum(
      $z-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:py($center)
  return point:point($x, $y, $z)
  :)
}

Function: wiggle
declare function wiggle($start as map(xs:string,item()*), $length as xs:double, $wiggle as map(xs:string,item()*)) as map(xs:string,item()*)*


wiggle()
Draw the path of a knot wiggle. Number of points will depend on wiggle
smoothness

Params
  • start as map(xs:string,item()*): Starting point
  • length as xs:double: Length
  • wiggle as map(xs:string,item()*): Wiggle descriptor
Returns
  • map(xs:string,item()*)*
declare function this:wiggle(
  $start as map(xs:string,item()*),
  $length as xs:double,
  $wiggle as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $smoothness := $length * ($wiggle=>wiggle_t:smoothness())
  return (
    curve:parametric(
      util:round($length),
      function ($t as xs:double) as xs:double {
        $t
      },
      function ($t as xs:double) as xs:double {
        let $θ := ($t * $length) div (2 * math:pi() * $smoothness)
        return point:y($start) + $wiggle=>wiggle_t:value($θ, $length)
      },
      point:x($start), point:x($start) + util:round($length)
    )
  )
}

Function: golden-spiral
declare function golden-spiral($num-points as xs:integer, $n as xs:integer, $scale as xs:double) as map(xs:string,item()*)*


golden-spiral()
Create a sequence of points following a (Fibonacci approximation of) a golden spiral

Params
  • num-points as xs:integer: Number of points in each (90 degree) arc of spiral
  • n as xs:integerum-points: Number of points in each (90 degree) arc of spiral: Number of Fibonacci generations
  • scale as xs:double: Scale of the spiral (need to do at this level to avoid jaggies)
Returns
  • map(xs:string,item()*)*
declare function this:golden-spiral(
  $num-points as xs:integer,
  $n as xs:integer,
  $scale as xs:double
) as map(xs:string,item()*)*
{
  let $fibs := seq:fibonacci-sequence($n)
  let $radii :=
    for $i in 1 to $n return if ($i=1) then $scale else $scale*$fibs[$i - 1]
  let $angles :=
    for $i in 1 to $n return
    switch ($i mod 4)
    case 0 return 90
    case 1 return 360
    case 2 return 270
    case 3 return 180
    default return ()
  let $centers :=
    fold-left(1 to $n, point:point(0,0),
      function ($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        $points,
        let $center := $points[last()]
        let $last :=
          if ($i = 1)
          then point:point(point:x($center), point:y($center) - $scale)
          else geom:destination($center, $angles[$i - 1] - 90, $radii[$i - 1])
        return
        switch ($i mod 4)
        case 0 return
          point:point(point:x($center), point:y($last) - $radii[$i])
        case 1 return
          point:point(point:x($last) - $radii[$i], point:y($center))
        case 2 return
          point:point(point:x($center), point:y($last) + $radii[$i])
        case 3 return
          point:point(point:x($last) + $radii[$i], point:y($center))
        default return ()
      }
    )[position() >= 2]
  for $i in 1 to $n
  return
    this:reverse-arc($centers[$i], $radii[$i], 90, $angles[$i], $num-points)
}

Function: angle-spiral
declare function angle-spiral($arc as xs:double, $center as map(xs:string,item()*), $radius as xs:double, $loops as xs:integer, $spread as xs:double) as map(xs:string,item()*)*


angle-spiral()
Create a path following a simple uniform spiral where the points are a
consistent number of degrees apart

Params
  • arc as xs:double: degrees of arc between each point
  • center as map(xs:string,item()*): center of spiral
  • radius as xs:double: distance from center to first point
  • loops as xs:integer: how many windings of spiral
  • spread as xs:double: how far about windings are
Returns
  • map(xs:string,item()*)*
declare function this:angle-spiral(
  $arc as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  let $num-points := 360 idiv $arc
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      return (
        (: new angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc 
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
}

Function: point-spiral
declare function point-spiral($num-points as xs:integer, $center as map(xs:string,item()*), $radius as xs:double, $loops as xs:integer, $spread as xs:double) as map(xs:string,item()*)*


point-spiral()
Create a path following a simple uniform spiral where there are the
same number of points in each loop

Params
  • num-points as xs:integer: number of points per loop
  • center as map(xs:string,item()*): center of spiral
  • radius as xs:double: distance from center to first point
  • loops as xs:integer: how many windings of spiral
  • spread as xs:double: how far about windings are
Returns
  • map(xs:string,item()*)*
declare function this:point-spiral(
  $num-points as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  let $arc := 360 div $num-points
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
}

Function: uniform-spiral
declare function uniform-spiral($distance as xs:double, $center as map(xs:string,item()*), $radius as xs:double, $loops as xs:integer, $spread as xs:double) as map(xs:string,item()*)*


uniform-spiral()
Create a path following a simple uniform spiral where there is about
the same distance between each point.

Params
  • distance as xs:double: distance between points
  • center as map(xs:string,item()*): center of spiral
  • radius as xs:double: distance from center to first point
  • loops as xs:integer: how many windings of spiral
  • spread as xs:double: how far apart windings are
Returns
  • map(xs:string,item()*)*
declare function this:uniform-spiral(
  $distance as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      let $avg-loop-radius := avg(($loop-radius, $radius + $loop*$spread))
      let $num-points := 2*math:pi()*$avg-loop-radius idiv $distance
      let $arc := 360 div $num-points
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
}

Function: spiral-function
declare function spiral-function($kind as xs:string, $spread as xs:double) as function(xs:integer, xs:integer, xs:integer) as xs:double


spiral-function()
Return a function that can be used with function-spiral() to create a
spiral of a particular kind.

Params
  • kind as xs:string: kind of spread function uniform: spiral expands uniformly (equivalent to arc-spiral()) accelerating: spiral expands in an accelerating way on each turn nautiloid: spirals pinch off at end to give nautilus like appearance wobbly: spiral veers back and forth in a wave (use higher $arc) very-wobbly: spiral veers even more
  • spread as xs:double: scaling factor on the expansion
Returns
  • function(xs:integer,xs:integer,xs:integer)asxs:double
declare function this:spiral-function($kind as xs:string, $spread as xs:double)
  as function(xs:integer, xs:integer, xs:integer) as xs:double
{
  switch($kind)
  case "uniform" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      xs:double(($loop - 1)*$spread) + ($point*$spread div $num-points)
    }
  case "accelerating" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop * $num-points + $point) * ($loop * $num-points + $point) * $spread div ($num-points * $num-points)
    }
  case "wobbly" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop - 1)*$spread + math:pow(-1,$point) * $point * $spread div $num-points
    }
  case "very-wobbly" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop - 1)*$spread + 2*math:pow(-1,$point) * $point * $spread div $num-points
    }
  case "nautiloid" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop * $num-points + $point) * (($loop * $num-points + $point) mod $num-points) * $spread div ($num-points * $num-points)
    }
  default return errors:error("ML-BADARGS", ("kind", $kind))
}

Function: function-spiral
declare function function-spiral($arc as xs:double, $center as map(xs:string,item()*), $radius as xs:double, $loops as xs:integer, $spread as function((:loop:)xs:integer,(:point:)xs:integer,(:num-points:)xs:integer) as xs:double) as map(xs:string,item()*)*


function-spiral()
Create an arc spiral using a function to compute the distance for
each point in the loop.

Params
  • arc as xs:double: degrees of arc between each point
  • center as map(xs:string,item()*): center of spiral
  • radius as xs:double: distance from center to first point
  • loops as xs:integer: how many windings of spiral
  • spread as function(xs:integer,xs:integer,xs:integer)asxs:double: function that is added to base radius to compute the distance
Returns
  • map(xs:string,item()*)*
declare function this:function-spiral(
  $arc as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as function((:loop:)xs:integer,(:point:)xs:integer,(:num-points:)xs:integer) as xs:double
) as map(xs:string,item()*)*
{
  let $num-points := 360 idiv $arc
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $radius + $spread($loop, xs:integer($t), $num-points)
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
}

Function: fern-spiral
declare function fern-spiral($start as map(xs:string,item()*), $fern as map(xs:string,item()*), $generations as xs:integer) as map(xs:string,item()*)*


fern-spiral()
Make a fern spiral
Fern spirals are created as a series of segments, where each segment
shrinks from the previous and decreases the lean by curl*curvature
curl and curvature are handled separately by branching and blade
creation

Overall path length is Σ[j=0,n-1]d*r^j = (d - rl)/(1 - r) where l=ar^(n-1)
in the limit (j=0,∞) = a/(1-r)
height will be less because of angle

Params
  • start as map(xs:string,item()*): starting point for spiral
  • fern as map(xs:string,item()*): parameter bundle defining curve, create with fern_t:fern() left: curve to the left; default=false scale: size of initial segment; default=100 lean: amount of initial lean (degrees); default=70 shrinkage: how much to shrink each segment; default=0.7 curvature: how much to change the angle (degrees); default=90 curl: tightness of curl; default=0.2
  • generations as xs:integer: number of segments in spiral
Returns
  • map(xs:string,item()*)*
declare function this:fern-spiral(
  $start as map(xs:string,item()*),
  $fern as map(xs:string,item()*),
  $generations as xs:integer
) as map(xs:string,item()*)*
{
  let $is-left := fern_t:is-left($fern)
  let $scale := fern_t:scale($fern)
  let $lean := fern_t:lean($fern)
  let $shrinkage := fern_t:shrinkage($fern)
  let $curvature := fern_t:curvature($fern)
  let $curl := fern_t:curl($fern)
  return (
    curve:dependent($generations, $start,
      function ($last as map(xs:string,item()*), $i as xs:integer) as map(xs:string,item()*) {
        let $d := $scale*math:pow($shrinkage,($i - 1))
        let $a :=
          if ($is-left)
          then util:remap-degrees($lean + ($i - 1)*($curvature*$curl))
          else util:remap-degrees($lean - ($i - 1)*($curvature*$curl))
        return geom:destination($last, -$a, $d)
      }
    )
  )
}

Function: eulers
declare function eulers($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


eulers()
Euler's spiral/clothoid

x = k * ∫[0:t]( sin(u²/2)du )
y = k * ∫[0:t]( cos(u²/2)du )

k is scaling; t as some number of multiples of π

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: k parameter above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
Returns
  • map(xs:string,item()*)*
declare function this:eulers(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*2*math:pi())
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {math:sin($u*$u div 2)}
    return
      fold-left(2 to count($ts), $scaling * util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + $scaling * util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {math:cos($u*$u div 2)}
    return
      fold-left(2 to count($ts), $scaling * util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + $scaling * util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  for $x at $i in $xs return point:point($x + $δx, $ys[$i] + $δy)
}

Function: polynomial
declare function polynomial($center as map(xs:string,item()*), $scaling as xs:double, $polynomial as map(xs:string,item()*), $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


polynomial()

Polynomial spiral: dφ/ds = Σ[n=1:n](ai*s^i)
s=arc length
dφ/ds=1 => circle
dφ/ds=s => Cornu spiral
dφ/ds=s² => double clothoid
dφ/ds=s² + 1 => like Corinthian capital
dφ/ds=s² - 4 => intersecting double spiral vase shape

s = Σ[j=0:J] (a[j]/(j+1))θ^(j+1)

https://www.atlantis-press.com/journals/gaf/125935071/view
κ(s) = Pκ'(s)
P is polynomial in s
x = ∫[t=0:s]sin(Pk(t))
y = ∫[t=0:s]cos(Pk(t))
k=0 => line, k=1 => circle, k=2 => Cornu spiral, k=3 => where the fun starts
k=3 cont. κ = s² - D => 2pts inflection for D>0, 1 for D=0, none for D&lt;0

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: overall scaling
  • polynomial as map(xs:string,item()*): the polynomial (see types/polynomial)
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range; >1 tends to get wonky
  • symmetric as xs:boolean: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
Returns
  • map(xs:string,item()*)*
declare function this:polynomial(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $polynomial as map(xs:string,item()*),
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $pk := polynomial:antiderivative($polynomial)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin($pk=>polynomial:value($u))
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos($pk=>polynomial:value($u))
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $spiral-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($spiral-center)
  return $pts!point:add(., $delta)
}

Function: meander
declare function meander($center as map(xs:string,item()*), $scaling as xs:double, $amplitudes as xs:double*, $wavelengths as xs:double*, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


meander()
θ(s) = Σ[i=1:N](A[i] sin( (2π/λ[i])s + φ[i] )
θ = tangential angle, s=arc length
For set of A,λ,φ; let's take φ[i]=0
(x,y) = (∫[0:s] cos(θ(s)), ∫[0:s] sin(θ(s))

Params
  • center as map(xs:string,item()*): center of meander
  • scaling as xs:double: overall scaling
  • amplitudes as xs:double*: vector of amplitude values (A[i]); will use last A[i] if wavelength vector is longer
  • wavelengths as xs:double*: vector of wavelength values (λ[i])
  • num-points as xs:integer: number of points to plot
  • extent as xs:double: extent of curve (multiple of π)
  • symmetric as xs:boolean: symmetric extent around 0
Returns
  • map(xs:string,item()*)*
declare function this:meander(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $amplitudes as xs:double*,
  $wavelengths as xs:double*,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $n := count($wavelengths)
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $invλs := $wavelengths!(2*math:pi() div .)
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * math:sin( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * math:sin( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $wave-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($wave-center)
  return $pts!point:add(., $delta)
}

Function: looping-meander
declare function looping-meander($center as map(xs:string,item()*), $scaling as xs:double, $amplitudes as xs:double*, $wavelengths as xs:double*, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


looping-meander()
dθ(s) = sΣ[i=1:N](A[i] sin( (2π/λ[i])s + φ[i] )
For set of A,λ,φ; let's take φ[i]=0
(x,y) = (∫[0:s] cos(dθ(s)), ∫[0:s] sin(dθ(s))
dθ(s) = dΣ[i=1:N](A[i] sin( (2π/λ[i])s ))
= Σ[i=1:N] d((A[i] sin( (2π/λ[i])s )))
= Σ[i=1:N] A[i] d((sin( (2π/λ[i])s )))
= Σ[i=1:N] A[i] (2π/λ[i]) cos( (2π/λ[i])s )

Params
  • center as map(xs:string,item()*): center of meander
  • scaling as xs:double: overall scaling
  • amplitudes as xs:double*: vector of amplitude values (A[i]); will use last A[i] if wavelength vector is longer
  • wavelengths as xs:double*: vector of wavelength values (λ[i])
  • num-points as xs:integer: number of points to plot
  • extent as xs:double: extent of curve (multiple of π)
  • symmetric as xs:boolean: symmetric extent around 0
Returns
  • map(xs:string,item()*)*
declare function this:looping-meander(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $amplitudes as xs:double*,
  $wavelengths as xs:double*,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $n := count($wavelengths)
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $invλs := $wavelengths!(2*math:pi() div .)
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * $invλs[$i] *
            math:cos( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * $invλs[$i] *
            math:cos( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $wave-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($wave-center)
  return $pts!point:add(., $delta)
}

Function: cotes
declare function cotes($kind as xs:string, $center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


cotes()
Cotes' spirals

A > 0, k > 0, ε real constants A=>size, k=>shape, ε=>angular position
1/r = A cosh(kθ + ε) case 1 Poinsot's
1/r = A exp(kθ + ε) case 2 Equiangular
1/r = A sinh(kθ + ε) case 3 Poinsot's
1/r = A (kθ + ε) case 4 Hyperbolic/reciprocal
1/r = A cos(kθ + ε) case 5 Epispiral

Params
  • kind as xs:string: what kind (one of poinsot-c, pointsot-s, equiangular, reciprocal, epispiral)
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:doubleind: what kind (one of poinsot-c, pointsot-s, equiangular, reciprocal, epispiral): k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs 0 to 2π; gives cardiod shapes
Returns
  • map(xs:string,item()*)*
declare function this:cotes(
  $kind as xs:string,
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($A <= 0) then errors:error("ML-BADARGS", ("A", $A)) else (),
  if ($k <= 0) then errors:error("ML-BADARGS", ("k", $k)) else (),
  let $r :=
    switch($kind)
    case "poinsot-c" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * util:cosh($k * $t + $ε))
      }
    case "poinsot-s" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * util:sinh($k * $t + $ε))
      }
    case "equiangular" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * math:exp($k * $t + $ε))
      }
    case "reciprocal" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * ($k * $t + $ε))
      }
    case "epispiral" return
      function ($t as xs:double) as xs:double {
        1 div ($A * math:cos($k * $t + $ε))
      }
    default return errors:error("ML-BADARGS", ("kind", $kind))
  let $θ := function ($t as xs:double) as xs:double {$t}
  return (
    (: point:valid() filters out NaNs and INFs :)
    if ($symmetric) then (
      curve:polar($num-points, $center, $r, $θ, -$extent*math:pi(), $extent*math:pi())[point:valid(.)]
    )
    else (
      curve:polar($num-points, $center, $r, $θ, 0, 2*$extent*math:pi())[point:valid(.)]
    )
  )
}

Function: poinsot-s
declare function poinsot-s($center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


poinsot-s()
Case 3 Cotes' spiral: 1/r = A sinh(kθ + ε)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:double: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:poinsot-s(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("poinsot-s", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
}

Function: poinsot-c
declare function poinsot-c($center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


poinsot-c()
Case 1 Cotes' spiral: 1/r = A cosh(kθ + ε)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:double: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:poinsot-c(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("poinsot-c", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
}

Function: equiangular
declare function equiangular($center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


equiangular()
Case 2 Cotes' spiral: 1/r = A exp(kθ + ε)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:double: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:equiangular(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("equiangular", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
}

Function: reciprocal
declare function reciprocal($center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


reciprocal()
Case 4 Cotes' spiral: 1/r = A (kθ + ε)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:double: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:reciprocal(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("reciprocal", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
}

Function: epispiral
declare function epispiral($center as map(xs:string,item()*), $scaling as xs:double, $A as xs:double, $k as xs:double, $ε as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


epispiral()
Case 5 Cotes' spiral: 1/r = A cos(kθ + ε)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • A as xs:double: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
  • k as xs:double: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
  • ε as xs:double: ε parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:epispiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("epispiral", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
}

Function: cochleoid
declare function cochleoid($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


cochleoid()
Cochleoid spiral: r = sinφ/φ

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:cochleoid(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then $scaling else $scaling * math:sin($t) div $t
      },
      $min, $max
    )
  )
}

Function: atom
declare function atom($center as map(xs:string,item()*), $scaling as xs:double, $a as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


atom()
Atom spiral: r=φ/(φ - a)
Spirals in then kicks out a>=1 for a &lt; 1 more of a circle in/out

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (basic size is generally in unit square)
  • a as xs:double: a parameter above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:atom(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t - $a = 0)
        then util:sign($scaling * $t) * xs:double("INF")
        else $scaling * $t div ($t - $a)
      },
      $min, $max
    )[point:valid(.)]
  )
}

Function: circle-involute
declare function circle-involute($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


circle-involute()
Involute of circle: r²=φ² + 1

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:circle-involute(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        -$scaling * math:sqrt($t*$t + 1)
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        $scaling * math:sqrt($t*$t + 1)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        $scaling * math:sqrt($t*$t + 1)
      },
      0, 2 * $extent * math:pi()
    )
  )
}

Function: lituus
declare function lituus($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


lituus()
Lituus: r²θ = k => r = √(k/θ)

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:lituus(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("-INF") else -math:sqrt($scaling div abs($t))
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("INF") else math:sqrt($scaling div $t)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("INF") else math:sqrt($scaling div $t)
      },
      0, 2 * $extent * math:pi()
    )
  )[point:valid(.)]
}

Function: gielis
declare function gielis($center as map(xs:string,item()*), $scaling as xs:double, $a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


gielis()
Gielis curve = super ellipse
r = (|cos(dφ)|^a | + |sin(dφ)|^b)^c

a=10, b=10, c=-2/9, d=3/4: has the shape of the petiole (leaf stem) of the nuphar luteum
a=15, b=15, c=-1/12, d=1: the shape of the cross section of the stem of the herb called scrophularia nodosa
a=1, b=1, c=-1/4, d=5/4: idem, of the equisetum
a=6, b=6, c=-1/10, d=7/4, idem, of the raspberry

Params
  • center as map(xs:string,item()*): center of ellipse
  • scaling as xs:double: scaling of ellipse
  • a as xs:double: a parameter above
  • b as xs:double: b parameter above; a vs b gives relative flatness
  • c as xs:doubleenter: center of ellipse: c parameter above
  • d as xs:double: d parameter above => number of symmetries is 4d
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:gielis(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = (|cos(dφ)|^a | + |sin(dφ)|^b)^c :)
        $scaling *
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
}

Function: super-rose
declare function super-rose($center as map(xs:string,item()*), $scaling as xs:double, $a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double, $f as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


super-rose()
Super rose = extension of rose, generalization of Gielis curve
r = sin(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c
a=1 b=1 c=-1 d=3 f=5/8 => spiky rose

Params
  • center as map(xs:string,item()*): center of rose
  • scaling as xs:double: scaling of rose
  • a as xs:double: a parameter above
  • b as xs:double: b parameter above; a vs b gives relative flatness
  • c as xs:doubleenter: center of rose: c parameter above
  • d as xs:double: d parameter above
  • f as xs:double: f parameter above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:super-rose(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $f as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = sin(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c :)
        $scaling *
        math:sin($f*$t) * 
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
}

Function: super-spiral
declare function super-spiral($center as map(xs:string,item()*), $scaling as xs:double, $a as xs:double, $b as xs:double, $c as xs:double, $d as xs:double, $f as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


super-spiral()
Super spiral = generalization of Gielis curve
r = e^(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c
Some cases:
a=b=5, c=-1, d=1, f=1/3; starish wide loops
a=b=5, c=-1/5, d=5/2, f=1/5; wobbly
a=b=100, c=-0.01, d=1, f=1/5; almost square

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral
  • a as xs:double: a parameter above
  • b as xs:double: b parameter above; a vs b gives relative flatness
  • c as xs:doubleenter: center of spiral: c parameter above
  • d as xs:double: d parameter above => number of symmetries is 4d
  • f as xs:double: f parameter above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:super-spiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $f as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = e^(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c :)
        $scaling *
        math:exp($f*$t) *
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
}

Function: logarithmic
declare function logarithmic($center as map(xs:string,item()*), $scaling as xs:double, $a as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


logarithmic()
Logarithmic spiral: r = e^(aφ)
Examples a[0.05:0.25] smaller a, closer windings, further from center start

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral
  • a as xs:double: a parameter above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:logarithmic(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = e^(aφ) :)
        $scaling * math:exp($a*$t) 
      },
      $min, $max
    )
  )
}

Function: fermat
declare function fermat($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


fermat()
Fermat's spiral: r^2 = φ => Archimedes but two joined outflows

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:fermat(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        -$scaling * math:sqrt(abs($t))
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        math:sqrt($scaling div $t)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    (: Positive branch only = Archimedes :)
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        math:sqrt($scaling div $t)
      },
      0, 2 * $extent * math:pi()
    )
  )
}

Function: tractrix
declare function tractrix($center as map(xs:string,item()*), $scaling as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


tractrix()
Tractrix: r = A cos(t), θ = tan(t) - t

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (A parameter)
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:tractrix(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = A cos(t) :)
        $scaling * math:cos($t)
      },
      function ($t as xs:double) as xs:double {
        (: θ = tan(t) - t :)
        math:tan($t) - $t
      },
      $min, $max
    )
  )
}

Function: doppler
declare function doppler($center as map(xs:string,item()*), $scaling as xs:double, $k as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


doppler()
Doppler: x = a(t cos(t) + kt), y = a t sin(t)
Like nested circles pinned to one side, but spirals
0 &lt; k &lt; 1 two spirals intersecting (for - and + angles)
k = 1 two spirals tangent (for - and + angles)
k > 1 two spirals increasing away from center

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (a parameter, above)
  • k as xs:double: k parameter, above
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:doppler(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $k as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        (: x = a(t cos(t) + kt) :)
        $scaling * ($t * math:cos($t) + $k*$t) + $δx
      },
      function ($t as xs:double) as xs:double {
        (: y = a(t sin(t)) :)
        $scaling * ($t * math:sin($t)) + $δy
      },
      $min, $max
    )
  )
}

Function: conchospiral
declare function conchospiral($center as map(xs:string,item()*), $scaling as xs:double, $μ as xs:double, $c as xs:double, $num-points as xs:integer, $extent as xs:double, $symmetric as xs:boolean) as map(xs:string,item()*)*


conchospiral()
Conchospiral (3d): r = μ^t a, θ = t, z = μ^t c
μ=>opening angle, c=>slope of cone; example μ=1.07,a=1,c=1.1

Params
  • center as map(xs:string,item()*): center of spiral
  • scaling as xs:double: scaling of spiral (a parameter, above)
  • μ as xs:double: opening angle (μ parameter, above)
  • c as xs:doubleenter: center of spiral: slope of cone (c parameter, above)
  • num-points as xs:integer: number of points to produce
  • extent as xs:double: number of multiples of π in angular range, extent => number of turns
  • symmetric as xs:boolean: use a symmetric extent around 0 vs extent from 0 to 2π
Returns
  • map(xs:string,item()*)*
declare function this:conchospiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $μ as xs:double,
  $c as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double { (: r = μ^t a :)
        $scaling * math:pow($μ, $t)
      }, 
      function ($t as xs:double) as xs:double {$t}, (: θ = t :)
      function ($t as xs:double) as xs:double { (: z = μ^t c :)
        $c * math:pow($μ, $t)
      },
      $min, $max
    )
  )
}

Function: lamé
declare function lamé($num-points as xs:integer, $a as xs:double, $b as xs:double, $α as xs:double) as map(xs:string,item()*)*


lamé()
Lamé curves: x = ±a cos(t)^(2/α); y = ±b cos(t)^(2/α);
α > 2 => super-ellipse

Params
  • num-points as xs:integer: number of points to draw
  • a as xs:double: scaling of x (a parameter above)
  • b as xs:double: scaling of y (b parameter above)
  • α as xs:double: α parameter above
Returns
  • map(xs:string,item()*)*
declare function this:lamé(
  $num-points as xs:integer,
  $a as xs:double,
  $b as xs:double,
  $α as xs:double
) as map(xs:string,item()*)*
{
  curve:parametric($num-points,
    function ($t as xs:double) as xs:double {
      if ($t < 0)
      then -$a * math:pow(math:cos($t), 2 div $α)
      else $a * math:pow(math:cos($t), 2 div $α)
    },
    function ($t as xs:double) as xs:double {
      if ($t < 0)
      then -$b * math:pow(math:sin($t), 2 div $α)
      else $b * math:pow(math:sin($t), 2 div $α)
    },
    -math:pi() div 2,
    math:pi() div 2
  )
}

Function: swoosh
declare function swoosh($center as map(xs:string,item()*), $r as xs:double) as map(xs:string,item()*)


swoosh()
Swoosh shape: two double curves

Params
  • center as map(xs:string,item()*)
  • r as xs:double
Returns
  • map(xs:string,item()*)
declare function this:swoosh(
  $center as map(xs:string,item()*),
  $r as xs:double
) as map(xs:string,item()*)
{
  let $c1 := point:point(-1.5 * $r, 0)
  let $c2 := point:point(-0.5 * $r, 0)
  let $c3 := point:point( 0.5 * $r, 0)
  let $c4 := point:point( 1.5 * $r, 0)
  let $edges := (
    edge:arc($c1, $r, point:point(-0.5*$r, 0), point:point(-1.5*$r, $r), false(), false()),
    edge:arc($c1, $r, point:point(-1.5*$r, $r), point:point(-2.5*$r, 0), false(), false()),
    edge:edge(point:point(-2.5*$r, 0), point:point(-1.5*$r, 0)),
    edge:arc($c2, $r, point:point(-1.5*$r, 0), point:point(-0.5*$r, $r), true(), false()),
    edge:arc($c2, $r, point:point(-0.5*$r, $r), point:point(0.5*$r, 0), true(), false()),
    edge:arc($c4, $r, point:point(0.5*$r, 0), point:point(1.5*$r, -$r), false(), false()),
    edge:arc($c4, $r, point:point(1.5*$r, -$r), point:point(2.5*$r, 0), false(), false()),
    edge:edge(point:point(2.5*$r, 0), point:point(1.5*$r, 0)),
    edge:arc($c3, $r, point:point(1.5*$r, 0), point:point(0.5*$r, -$r), true(), false()),
    edge:arc($c3, $r, point:point(0.5*$r, -$r), point:point(-0.5*$r, 0), true(), false())
  )
  return path:polygon($edges)=>geom:translate(point:px($center), point:py($center))
}

Function: waveline
declare function waveline($from as map(xs:string,item()*), $to as map(xs:string,item()*), $waveheight as xs:double) as map(xs:string,item()*)*

Params
  • from as map(xs:string,item()*)
  • to as map(xs:string,item()*)
  • waveheight as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:waveline(
  $from as map(xs:string,item()*),
  $to as map(xs:string,item()*),
  $waveheight as xs:double
) as map(xs:string,item()*)*
{
  this:waveline($from, $to, $waveheight, 4 * $waveheight)
}

Function: waveline
declare function waveline($from as map(xs:string,item()*), $to as map(xs:string,item()*), $waveheight as xs:double, $wavelength as xs:double) as map(xs:string,item()*)*

Params
  • from as map(xs:string,item()*)
  • to as map(xs:string,item()*)
  • waveheight as xs:double
  • wavelength as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:waveline(
  $from as map(xs:string,item()*),
  $to as map(xs:string,item()*),
  $waveheight as xs:double,
  $wavelength as xs:double
) as map(xs:string,item()*)*
{
  let $distance := geom:distance($from, $to)
  let $waves := ($distance idiv $wavelength)
  let $slant := edge:angle($from, $to)
  return (
    curve:parametric(
      max((4, $waves * 6)) cast as xs:integer,
      function ($t as xs:double) as xs:double {$t * $distance},
      function ($t as xs:double) as xs:double {$waveheight * math:sin($t * math:pi() * 2 * $waves)},
      0.0,
      1.0
    )=>geom:rotate(-$slant)=>geom:translate(point:px($from), point:py($from))
  )
}

Function: complex-circular-arc
declare function complex-circular-arc($r as xs:double, $z0 as map(xs:string,item()*), $θ as xs:double*, $n-points as xs:integer, $gain-f as function(xs:double) as xs:double) as map(xs:string,item()*)*


complex-circular-arc()
Calculate circular arc at radius r from z0 between θ[1] and θ[2] using
n-points in the complex plane. Produces sequence of points in the
complex plane.

Params
  • r as xs:double: base radius
  • z0 as map(xs:string,item()*): center point
  • θ as xs:double*: starting and ending angles
  • n-points as xs:integer: number of points to generate
  • gain-f as function(xs:double)asxs:double: multiplier of radii of points along the arc; a function of fraction of arc running from 1/n-points to first point in arc to 1 for last; use constant 1 for actual circular arc
Returns
  • map(xs:string,item()*)*
declare function this:complex-circular-arc(
  $r as xs:double,
  $z0 as map(xs:string,item()*),
  $θ as xs:double*,
  $n-points as xs:integer,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  for $α at $i in util:linspace($n-points, $θ[1], $θ[2]) return (
    z:complex-rφ($r * $gain-f($i div $n-points), $α)=>z:add($z0)
  )
}

Function: complex-rounded-rec
declare function complex-rounded-rec($r as xs:double, $z0 as map(xs:string,item()*), $θ as xs:double*, $p as xs:double, (: power: 0 to infinity; p < 2 is convex :) $n-points as xs:integer, $gain-f as function(xs:double) as xs:double) as map(xs:string,item()*)*


complex-rounded-rec()
Calculate a quasi-rectangular arc of power p, radius r from z0 between
θ(1) to θ(2) using n points in the complex plane;
The original used matlab cosd and sind, but I don't have those, so I'm
just going to round things close to zero.
"This program has been rewritten with degrees to avoid problems with
cos(pi/2) and sin(pi) raised to small powers" Cye H. Waldman, 2013
When p = 2 you get the square corners; p > 2 bulges in converging to plus
sign; p &lt; 2, bulges out converging to square.

Params
  • r as xs:double: base radius
  • z0 as map(xs:string,item()*): center point
  • θ as xs:double*: starting and ending angles
  • p as xs:double: power
  • n-points as xs:integer: number of points to generate
  • gain-f as function(xs:double)asxs:double: multiplier of radii of points along the arc; a function of fraction of arc running from 1/n-points to first point in arc to 1 for last; use constant 1 for actual circular arc
Returns
  • map(xs:string,item()*)*
declare function this:complex-rounded-rec(
  $r as xs:double,
  $z0 as map(xs:string,item()*),
  $θ as xs:double*,
  $p as xs:double, (: power: 0 to infinity; p &lt; 2 is convex :)
  $n-points as xs:integer,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  for $α at $i in util:linspace($n-points, $θ[1], $θ[2])
  let $cos := math:cos($α)
  let $cos := if (abs($cos) < $config:ε) then 0 else $cos
  let $sin := math:sin($α)
  let $sin := if (abs($sin) < $config:ε) then 0 else $sin
  let $adjust := $gain-f($i div $n-points)
  return (
    z:complex(
      $adjust * $r * util:sign($cos) * math:pow(abs($cos), $p),
      $adjust * $r * util:sign($sin) * math:pow(abs($sin), $p)
    )=>z:add($z0)
  )
}

Function: pseudospiral
declare function pseudospiral($n-points as xs:integer, $sequence as xs:integer*, $gain-f as function(xs:double) as xs:double) as map(xs:string,item()*)*


pseudospiral()
Construct a pseudospiral consisting of joined circular arcs.
Result is a set of points.
If sequence is monotonically increasing and positive, we get an actual
spiral. Negative or decreasing values create loops and overlaps of various
sorts.

Params
  • n-points as xs:integer: number of points per arc
  • sequence as xs:integer*: sequence of radii
  • gain-f as function(xs:double)asxs:double: multiplier of radii of points along the arc; a function of fraction of arc running from 1/n-points to first point in arc to 1 for last; use constant 1 for actual circular arc
Returns
  • map(xs:string,item()*)*
declare function this:pseudospiral(
  $n-points as xs:integer, 
  $sequence as xs:integer*,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  let $rotation := math:pi() div 2   (: angular rotation for each arc :)
  let $z0 := z:complex(0,0)          (: initial starting point :)
  let $θ := (0, $rotation)           (: initial θ range :)
  let $n := count($sequence) - 1
  return
    fold-left(1 to $n, ($θ, $z0, ()), function ($data as item()*, $k as xs:integer) as item()* {
      let $θ := (head($data), head(tail($data)))
      let $z0 := head(tail(tail($data)))
      let $zstep := tail(tail(tail($data)))
      let $r := $sequence[$k]
      let $c4th := this:complex-circular-arc($r, $z0, $θ, $n-points, $gain-f)
      return (
        if ($k < $n) then (
          let $rnext := $sequence[$k + 1]
          let $z0 := $c4th[last()]=>z:sub(z:complex-rφ($rnext, $θ[2]))
          let $θ := $θ!(. + $rotation)
          return ($θ, $z0, ($zstep, tail($c4th)))
        ) else (
          ($θ, $z0, ($zstep, tail($c4th), $c4th[last()]))
        )
      )
    })=>tail()=>tail()=>tail()
}

Function: pseudospiral-p
declare function pseudospiral-p($n-points as xs:integer, $sequence as xs:integer*, $p as xs:double, $gain-f as function(xs:double) as xs:double) as map(xs:string,item()*)*


pseudospiral-p()
Construct a pseudospiral consisting of joined circular arcs
Result is a set of points. If sequence is monotonically increasing and
positive, we get an actual spiral. Negative or decreasing values create
loops and overlaps of various sorts. The idea here is to call this with
various values of p to create a sheaf of curves following the path.

Params
  • n-points as xs:integer: number of points per arc
  • sequence as xs:integer*: sequence of radii
  • p as xs:double: the power to use (collapse of circular arc)
  • gain-f as function(xs:double)asxs:double: multiplier of radii of points along the arc; a function of fraction of arc running from 1/n-points to first point in arc to 1 for last; use constant 1 for actual circular arc
Returns
  • map(xs:string,item()*)*
declare function this:pseudospiral-p(
  $n-points as xs:integer,
  $sequence as xs:integer*,
  $p as xs:double,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  let $rotation := math:pi() div 2  (: angular rotation for each arc :)
  let $z0 := z:complex(0,0)         (: initial starting point :)
  let $θ := (0, $rotation)          (: initial θ range :)
  let $n := count($sequence) - 1
  return
    fold-left(1 to $n, ($θ, $z0, ()), function ($data as item()*, $k as xs:integer) as item()* {
      let $θ := (head($data), head(tail($data)))
      let $z0 := head(tail(tail($data)))
      let $zstep := tail(tail(tail($data)))
      let $r := $sequence[$k]
      let $c4th := this:complex-rounded-rec($r, $z0, $θ, $p, $n-points, $gain-f)
      return (
        if ($k < $n) then (
          let $rnext := $sequence[$k + 1]
          let $z0 := $c4th[last()]=>z:sub(z:complex-rφ($rnext, $θ[2]))
          let $θ := $θ!(. + $rotation)
          return ($θ, $z0, ($zstep, tail($c4th)))
        ) else (
          ($θ, $z0, ($zstep, tail($c4th), $c4th[last()]))
        )
      )
    })=>tail()=>tail()=>tail()
}

Function: pseudospiral-triangles
declare function pseudospiral-triangles($sequence as xs:integer*, (: driver sequence :) $debug as xs:boolean) as map(xs:string,item()*)*


pseudospiral-triangles()
Construct gnomic tiling triangles following the pseudospiral path
defined by the radius sequence. Result is a set of polygons, preceded,
if debug is true(), by the path of the outer controlling spiral.

Params
  • sequence as xs:integer*: sequence of radii
  • debug as xs:boolean
Returns
  • map(xs:string,item()*)*
declare function this:pseudospiral-triangles(
  $sequence as xs:integer*, (: driver sequence :)
  $debug as xs:boolean
) as map(xs:string,item()*)*
{
  let $kmax := count($sequence) - 1
  let $θ1 := math:sqrt(3) div 2
  let $θ := math:pi() div 3
  (: Trapezoidal gnomons :)
  let $gnomons := (
    let $ztri := array {
      z:as-complex(0),
      z:complex(-1, $θ1)=>z:times(0.5),
      z:as-complex(-1),
      z:as-complex(0),
      z:as-complex(0)
    }
    let $Z := 
      array:for-each($ztri, function ($v as map(*)) as map(*) {$v=>z:times($sequence[1])})
    return (
      fold-left(1 to $kmax, ($ztri, $Z),
        function ($data as array(*)*, $k as xs:integer) as array(*)* {
          let $zprev := head($data)
          let $Z := tail($data)
          let $zgno := (
            z:as-complex(0),
            z:as-complex(-$sequence[$k]),
            z:as-complex(-$sequence[$k])=>z:add(
              z:complex-rφ($sequence[$k + 1] - $sequence[$k], 4 * $θ)
            ),
            z:complex-rφ($sequence[$k + 1] - $sequence[$k], 5 * $θ),
            z:as-complex(0)
          )
          let $znext := (
            for $val in $zgno return (
              $val=>z:multiply(z:complex-rφ(1, -2*($k - 1)*$θ))
            )
          )
          let $znext := (
            if ($k = 1) then (
              array {
                for $v in $znext return $v=>z:sub($znext[1])=>z:add($zprev(1))
              }
            ) else (
              array {
                for $v in $znext return $v=>z:sub($znext[1])=>z:add($zprev(3))
              }
            )
          )
          return (
            $znext, ($Z, $znext)
          )
        }
      )=>tail()
    )
  )
  (: 'super' triangles: previous triangle plus trapezoid :)
  let $triangles := (
    path:polygon(geom:to-edges((
      for $i in 1 to 4 return $gnomons[1]($i)
    )))=>map:put("pid", 1),
    path:triangle(
      $gnomons[2](4), $gnomons[1](2), $gnomons[2](3)
    )=>map:put("pid", 2),
    for $k in 3 to $kmax + 1 return (
      path:triangle(
        $gnomons[$k](4), $gnomons[$k - 1](4), $gnomons[$k](3)
      )=>map:put("pid", $k)
    )
  )
  let $outer-spiral := (
    let $n-points := 50
    let $no-gain := function ($t as xs:double) as xs:double {1}
    let $z0 := point:centroid($gnomons[1]?*)
    let $θ := (7 * math:pi() div 6, math:pi() div 2)
    let $r := point:distance($z0, geom:vertices($triangles[1])[1])
    return (
      this:complex-circular-arc($r, $z0, $θ, $n-points, $no-gain),
      fold-left(2 to $kmax + 1, ($θ, ()), function ($data as item()*, $k as xs:integer) as item()* {
        let $θ := (head($data), head(tail($data)))
        let $zstep := tail(tail($data))
        let $w := $triangles[$k]
        let $θ := $θ!(. + -2 * math:pi() div 3)
        let $z0 := point:centroid(geom:vertices($w)[position() < last()])
        let $r := point:distance($z0, geom:vertices($triangles[$k])[1])
        let $c4th := this:complex-circular-arc($r, $z0, $θ, $n-points, $no-gain)
        return (
          ($θ, ($zstep, $c4th))
        )
      })=>tail()=>tail()
    )
  )
  return (
    if ($debug) then path:path(geom:to-edges($outer-spiral)) else (),
    $triangles
  )
}

Function: circle-spiral
declare function circle-spiral($circle as map(xs:string,item()*), $turns as xs:double, $fineness as xs:integer) as map(xs:string,item()*)


circle-spiral()
Fill a circle with a spiral with the given number of turns.

Params
  • circle as map(xs:string,item()*): the circle to fill
  • turns as xs:double: how many turns of the spiral
  • fineness as xs:integer: how many points per turn
Returns
  • map(xs:string,item()*): simple path of the points
declare function this:circle-spiral(
  $circle as map(xs:string,item()*),
  $turns as xs:double,
  $fineness as xs:integer
) as map(xs:string,item()*)
{
  let $center := ellipse:center($circle)
  let $radius := ellipse:radius($circle)
  let $n-points := util:round($turns * $fineness)
  let $f :=
    function ($t as xs:double) as map(xs:string,item()*) {
      point:destination(
        $center,
        360 * ($t mod 1),
        $radius * ($t div $turns)
      )
    }
  return (
    curve:plot($n-points, $f, $turns, false(), false())
  )
}

Function: circle-spiral
declare function circle-spiral($circle as map(xs:string,item()*), $turns as xs:double) as map(xs:string,item()*)


circle-spiral()
Fill a circle with a spiral with the given number of turns using default
fineness (50).

Params
  • circle as map(xs:string,item()*): the circle to fill
  • turns as xs:double: how many turns of the spiral
Returns
  • map(xs:string,item()*): simple path of the points
declare function this:circle-spiral(
  $circle as map(xs:string,item()*),
  $turns as xs:double
) as map(xs:string,item()*)
{
  this:circle-spiral($circle, $turns, 50)
}

Function: bend
declare function bend($path as map(xs:string,item()*), $center as map(xs:string,item()*), $radius as xs:double, $arc as xs:double, (: degrees :) $scaling as xs:double) as map(xs:string,item()*)*


bend()
Map a path onto a circular arc. Each point in the path is mapped to a point
an equivalent fraction along the arc. The y-axis of each input point is mapped
to a point perpendicular to the target arc at the corresponding point with the
distance from the arc being the distance of the input point from the midline
of the bounding box of the input path, scaled. This method is therefore most
appropriate for paths that are broadly horizontal.

Params
  • path as map(xs:string,item()*): input path
  • center as map(xs:string,item()*): center of circular arc
  • radius as xs:double: radius of circular arc
  • arc as xs:double: degrees of arc (starting at 0°)
  • scaling as xs:double: how much to scale the y distance
Returns
  • map(xs:string,item()*)*: mapped points of the input path
declare function this:bend(
  $path as map(xs:string,item()*),
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double, (: degrees :)
  $scaling as xs:double
) as map(xs:string,item()*)*
{
  let $path := $path=>path:with-edge-ts()
  let $edge-ts := (0.0, $path("edge-ts"))
  let $points := path:vertices($path)
  let $n := count($points)
  let $bb := geom:bounding-box($points)
  let $mid-y := box:min-py($bb) + box:height($bb) div 2
  for $p at $i in $points
  let $t := $edge-ts[$i]
  let $angle := $t * $arc
  let $d := $radius + (point:py($p) - $mid-y)*$scaling
  return $center=>point:destination($angle, $d)
}

Function: bend-path
declare function bend-path($path as map(xs:string,item()*), $target-path as map(xs:string,item()*), $scaling as xs:double) as map(xs:string,item()*)*


bend-path()
Map one path onto another. Each point in the input path is mapped to a point
an equivalent fraction along the target path. The y-axis of each input point
is mapped to a point perpendicular to the target path at the corresponding point
with the distance from the path being the distance of the input point from the
midline of the bounding box of the input path, scaled. This method is therefore
most appropriate for paths that are broadly horizontal.

Params
  • path as map(xs:string,item()*): input path
  • target-path as map(xs:string,item()*)
  • scaling as xs:double: how much to scale the y distance
Returns
  • map(xs:string,item()*)*: mapped points of the input path
declare function this:bend-path(
  $path as map(xs:string,item()*),
  $target-path as map(xs:string,item()*),
  $scaling as xs:double
) as map(xs:string,item()*)*
{
  let $path := $path=>path:with-edge-ts()
  let $edge-ts := (0.0, $path("edge-ts"))
  let $points := path:vertices($path)
  let $target-path := $target-path=>path:with-edge-ts()
  let $n := count($points)
  let $bb := geom:bounding-box($points)
  let $mid-y := box:min-py($bb) + box:height($bb) div 2
  for $p at $i in $points
  let $t := $edge-ts[$i]
  let $angle := $target-path=>geom:normal-angle($t)
  let $d := (point:py($p) - $mid-y)*$scaling
  return $target-path=>path:path-point($t)=>point:destination($angle, $d)
}

Function: stroke-path
declare function stroke-path($path as map(xs:string,item()*), $fatness as xs:double) as map(xs:string,item()*)


stroke-path()
Convert the path into a stroke that smoothly widens towards the middle
and tapers to the ends.

Params
  • path as map(xs:string,item()*): source path (Bézier ok, arcs not)
  • fatness as xs:double: how wide to get at the middle
Returns
  • map(xs:string,item()*): polygon for the stroke
declare function this:stroke-path(
  $path as map(xs:string,item()*),
  $fatness as xs:double
) as map(xs:string,item()*)
{
  let $reverse := (
    let $rev-path := path:reverse($path)=>path:with-edge-ts()
    let $edge-ts := (0.0, $rev-path("edge-ts"))
    for $edge at $i in path:edges($rev-path)
    let $nstart := geom:normal-angle($edge, 0.0)
    let $nend := geom:normal-angle($edge, 1.0)
    let $dstart := util:mix(0, $fatness, 0.5 - abs(0.5 - $edge-ts[$i]))
    let $dend := util:mix(0, $fatness, 0.5 - abs(0.5 - $edge-ts[$i + 1]))
    let $nmid := geom:normal-angle($edge, 0.5)
    let $dmid := avg(($dstart, $dend))
    return (
      switch(edge:kind($edge))
      case "edge" return (
        edge:edge(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend)
        )
      )
      case "quad" return (
        edge:quad(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend),
          edge:controls($edge)[1]=>point:destination($nmid, $dmid)
        )
      )
      case "cubic" return (
        edge:cubic(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend),
          edge:controls($edge)[1]=>point:destination($nmid, $dmid),
          edge:controls($edge)[2]=>point:destination($nmid, $dmid)
        )
      )
      default return $edge
    )
  )
  return (
    util:merge-into($path=>path:property-map(),
      path:polygon( (path:edges($path), $reverse) )
    )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : Module with functions providing some interesting paths
 : No global parameters or randomizers
 :
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since November 2021
 : @custom:Status Active
 :)
module namespace this="http://mathling.com/shape/paths"; 

import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace seq="http://mathling.com/core/sequences"
       at "../core/sequences.xqy";
import module namespace z="http://mathling.com/core/complex"
       at "../core/complex.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy";
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace curve="http://mathling.com/geometric/curve"
       at "../geo/curves.xqy";
import module namespace fern_t="http://mathling.com/type/fern"
       at "../types/fern.xqy";
import module namespace knot_t="http://mathling.com/type/modulated-knot"
       at "../types/modulated-knot.xqy";
import module namespace lissa_t="http://mathling.com/type/modulated-lissajous"
       at "../types/modulated-lissajous.xqy";
import module namespace pend_t="http://mathling.com/type/pendulum"
       at "../types/pendulum.xqy";
import module namespace wiggle_t="http://mathling.com/type/wiggle"
       at "../types/wiggle.xqy";
import module namespace polynomial="http://mathling.com/type/polynomial"
       at "../types/polynomial.xqy";

declare namespace svg="http://www.w3.org/2000/svg";

declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";

(:~
 : arc()
 : Make a circular path of $arc degrees from $start-angle
 : 
 : @param $center: center of the circle
 : @param $radius: radius of the circle
 : @param $arc: degrees of arc
 : @param $start-angle: starting angle (degrees)
 : @param $num-points: number of points to create along arc
 :)
declare function this:arc(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double,
  $start-angle as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  (: for (α=β; α<θ+β α+=θ/n) :)
  for $point in 0 to $num-points - 1
  (: α = β + point*θ/n :)
  let $α := $start-angle + $point * ($arc div $num-points)
  return (
    geom:destination($center, $α, $radius)
  )
};

(:~
 : reverse-arc()
 : Make a circular path from $start-angle down to $start-angle - $arc
 : 
 : @param $center: center of the circle
 : @param $radius: radius of the circle
 : @param $arc: degrees of arc
 : @param $start-angle: starting angle (degrees)
 : @param $num-points: number of points to create along arc
 :)
declare function this:reverse-arc(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double,
  $start-angle as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  (: for (α=θ+β; α>=β α-=θ/n) :)
  for $point in 0 to $num-points
  (: α = β - point*θ/n :)
  let $α := $start-angle - $point * ($arc div $num-points)
  return (
    geom:destination($center, $α, $radius)
  )
};

(:~
 : helix()
 : Create a right-handed 3-D helix path
 :
 : @param $center: starting point, center of turn
 : @param $radius: radius of helix
 : @param $slope: slope of helix
 : @param $num-points: how many points to draw
 :
 : helix(x) = {a cos(t), a sin(t), b t}
 : a = radius; b/a = slope; so b = slope*a
 :)
declare function this:helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
   for $t in 0 to $num-points - 1
   let $x := $radius * math:cos($t) + point:px($center)
   let $y := $radius * math:sin($t) + point:py($center)
   let $z := $slope * $radius * $t + point:pz($center)
   return point:point($x, $y, $z)
};

(:~
 : helix()
 : Create a right-handed 3-D helix path
 :
 : @param $center: starting point, center of turn
 : @param $radius: radius of helix
 : @param $slope: slope of helix
 : @param $num-points: how many points to draw
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
 :
 : helix(x) = {a cos(t), a sin(t), b t}
 : a = radius; b/a = slope; so b = slope*a
 :)
declare function this:helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($t) + $δy
      },
      function ($t as xs:double) as xs:double {
        $slope*$radius*$t + $δz
      },
      $min, $max
    )
  )
};

(:~
 : left-helix()
 : Create a right-handed 3-D helix path
 :
 : @param $center: starting point, center of turn
 : @param $radius: radius of helix
 : @param $slope: slope of helix
 : @param $num-points: how many points to draw
 :
 : helix(x) = {-a cos(t), a sin(t), b t}
 : a = radius; b/a = slope; so b = slope*a
 :)
declare function this:left-helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
   for $t in 0 to $num-points - 1
   let $x := -$radius * math:cos($t) + point:px($center)
   let $y := $radius * math:sin($t) + point:py($center)
   let $z := $slope * $radius * $t + point:pz($center)
   return point:point($x, $y, $z)
};

(:~
 : left-helix()
 : Create a left-handed 3-D helix path
 :
 : @param $center: starting point, center of turn
 : @param $radius: radius of helix
 : @param $slope: slope of helix
 : @param $num-points: how many points to draw
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
 :
 : helix(x) = {-a cos(t), a sin(t), b t}
 : a = radius; b/a = slope; so b = slope*a
 :)
declare function this:left-helix(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $slope as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        -$radius*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($t) + $δy
      },
      function ($t as xs:double) as xs:double {
        $slope*$radius*$t + $δz
      },
      $min, $max
    )
  )
};

(:~
 : torus-knot()
 : Create a torus knot. 
 :
 : @param $center: center point of knot
 : @param $radius: scaling of knot
 : @param $p: knot parameter
 : @param $q: know parameter
 :   For clean knots p and q should be mutually prime, if they aren't you
 :   miss some of the lobes
 :   bridges = q; crossings = p(q-1); generally p-lobed shape 
 :   Normally p > q and you get clean lobes
 :   When p &lt; q, you get involuted circles
 : @param $num-points: number of points to draw
 :)
declare function this:torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $p as xs:integer, 
  $q as xs:integer, 
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($q*$t)*(3 + math:cos($p*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($q*$t)*(3 + math:cos($p*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($p*$t) + $δz
      },
      0, 2*math:pi()
    )
  )
}; 

(:~
 : skewed-torus-knot()
 : Create a torus knot. 
 :
 : @param $center: center point of knot
 : @param $radius: scaling of knot
 : @param $p: knot parameter
 : @param $q: knot parameter
 :   For clean knots p and q should be mutually prime, if they aren't you
 :   miss some of the lobes
 :   bridges = q; crossings = p(q-1); generally p-lobed shape 
 :   Normally p > q and you get clean lobes
 :   When p &lt; q, you get involuted circles
 : @param $s: skew parameter &lt; 4
 : @param $r: skew parameter &lt; 4
 :   A normal torus knot has s=r=3; 
 :   s!=r gives smooshed knots;
 :   s is very different from r gives overlaps perceived as complicated twists
 :   s=r>3 gives pudgier links
 :   s=r&lt;3 gives linked circles
 : @param $num-points: number of points to draw
 :)
declare function this:skewed-torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $p as xs:integer, 
  $q as xs:integer,
  $s as xs:integer,
  $r as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($q*$t)*($s + math:cos($p*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($q*$t)*($r + math:cos($p*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin($p*$t) + $δz
      },
      0, 2*math:pi()
    )
  )
};

(:~
 : modulated-torus-knot()
 : Create a modulated torus knot. 
 :
 : @param $center: center point of knot
 : @param $radius: scaling of knot
 : @param $knot: knot parameters (see modulated torus knot type information)
 : @param $num-points: number of points to draw
 :)
declare function this:modulated-torus-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $knot as map(xs:string,item()*),
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  trace((), "@"||$radius||" "||geom:quote($center)||" "||knot_t:describe($knot)),
  let $p := $knot=>knot_t:p()
  let $openness := $knot=>knot_t:openness()
  let $x-factors := $knot=>knot_t:x-factors()
  let $y-factors := $knot=>knot_t:y-factors()
  let $stretch := $knot=>knot_t:stretch()
  let $openness := $knot=>knot_t:openness()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        $radius*$stretch*(
          math:cos($p*$t)*
          ($openness +
            sum(
              let $n := count($x-factors)
              for $j in 1 to $n return (
                let $factor := $x-factors[$j]
                let $r := $factor=>knot_t:r()
                let $q := $factor=>knot_t:q()
                return (
                  if ($factor=>knot_t:skew()) then (
                    $r*math:sin($q*$t)
                  ) else (
                    $r*math:cos($q*$t)
                  )
                )
              )
            )
          )
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*(
          math:sin($p*$t)*
          ($openness +
            sum(
              let $n := count($y-factors)
              for $j in 1 to $n return (
                let $factor := $y-factors[$j]
                let $r := $factor=>knot_t:r()
                let $q := $factor=>knot_t:q()
                return (
                  if ($factor=>knot_t:skew()) then (
                    $r*math:sin($q*$t)
                  ) else (
                    $r*math:cos($q*$t)
                  )
                )
              )
            )
          )
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*0.2*(
          $openness + 
          math:sin(max(($p,$x-factors!knot_t:q(.)))*$t)
        ) + $δz
      },
      0, 2*math:pi()
    )
  )
};

(:~ 
 : modulated-lissajous()
 : Create a modulated Lissajous curve.
 :
 : @param $center: center point of curve
 : @param $radius: scaling of curve
 : @param $curve: curve parameters (see Lissajous type information)
 : @param $num-points: number of points to draw
 :)
declare function this:modulated-lissajous(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $curve as map(xs:string,item()*),
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $x-factors := $curve=>lissa_t:x-factors()
  let $y-factors := $curve=>lissa_t:y-factors()
  let $stretch := $curve=>lissa_t:stretch()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*$stretch*sum(
          for $factor in $x-factors
          let $n := $factor=>lissa_t:n()
          let $φ := util:radians($factor=>lissa_t:phase())
          let $skew := $factor=>lissa_t:skew()
          return
            if ($skew) then math:sin($n*$t + $φ)
            else math:cos($n*$t + $φ)
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          for $factor in $y-factors
          let $n := $factor=>lissa_t:n()
          let $φ := util:radians($factor=>lissa_t:phase())
          let $skew := $factor=>lissa_t:skew()
          return
            if ($skew) then math:sin($n*$t + $φ)
            else math:cos($n*$t + $φ)
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*math:sin(max($x-factors=>lissa_t:n())*$t) + $δz
      },
      0, 2 * math:pi()
    )
  )
};

(:~
 : rose-curve()
 : Create a closed looping curve
 : (see https://en.wikipedia.org/wiki/Rose_(mathematics))
 : 
 : @param $center: center point of rose
 : @param $n: rose parameter
 : @param $d: rose parameter
 :   k = n/d
 :   $n > $d gives petals around a center
 :   $n &lt; $d gives involuted petals
 :   k = 1 is a circle
 :   Number of petals depends on k
 :     k is integer => k if odd; 2k if even
 :     k is n/3 w/ n mod 3 != 0; n petals if n is odd; 2k if even
 :   n and d mutually prime gives cleaner roses; not mutually prime drops 
 :   some of the petals
 : @param $radius: scaling of rose
 : @param $num-points: number of points to draw
 :)
declare function this:rose-curve(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $n as xs:integer, (: k=n/d petals: 2k if even, k :)
  $d as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $k := $n div $d
  let $angle :=
    if ($n mod $d = 0) then (
      if ($k mod 2 = 0) then 2 * math:pi() else math:pi()
    ) else (
     2 * math:pi() * $d
    )
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*math:cos($k*$t)*math:cos($t) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*math:cos($k*$t)*math:sin($t) + $δy
      },
      0, $angle
    )
  )
};

(:~
 : trefoil()
 : A trefoil; as a 3D curve (torus surface)
 : Compared to a torus(3,2) the lobes are more drop-like and smaller
 : Compared to a rose(3,1) the lobes are separated and larger
 :
 : @param $center: center point of curve
 : @param $radius: scaling of curve
 : @param $num-points: number of points to draw
 :)
declare function this:trefoil(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $radius*(math:sin($t) + 2*math:sin(2*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*(math:cos($t) - 2*math:cos(2*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*(-math:sin(3*$t)) + $δz
      },
      0, 2*math:pi()
    )
  )
};

(:~ 
 : maurer-rose()
 : Construct a Maurer rose.
 : (see https://en.wikipedia.org/wiki/Maurer_rose)
 :
 : @param $center: center point of rose
 : @param $n: rose parameter
 : @param $d: rose parameter
 :   Generally n=small; d=order of magnitude larger; relatively prime
 : @param $radius: scaling of rose
 : @param $num-points: number of points to draw for each line set
 :   Since the rose if formed by cross-cutting lines; a small n is more
 :   noticeable in a way the other curves are not; splining doesn't help
 :   num-points=360 (i.e. one per degree) gives a filled out appearance
 :)
declare function this:maurer-rose(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $n as xs:integer,
  $d as xs:integer,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        let $k := $t * $d
        return $radius * math:sin($n * $k) * math:cos($k) + $δx
      },
      function ($t as xs:double) as xs:double {
        let $k := $t * $d
        return $radius * math:sin($n * $k) * math:sin($k) + $δy
      },
      0, 2*math:pi()
    )
  )
};

(:~ 
 : granny-knot()
 : Construct a granny knot.
 :
 : @param $center: center point of knot
 : @param $radius: scaling of knot
 : @param $num-points: number of points to draw
 :)
declare function this:granny-knot(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $num-points as xs:integer
) as map(xs:string,item()*)*
{
  let $scaled-radius := $radius div 100
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      $num-points,
      function ($t as xs:double) as xs:double {
        $scaled-radius*(-22*math:cos($t) - 128*math:sin($t) - 44*math:cos(3*$t) - 78*math:sin(3*$t)) + $δx
      },
      function ($t as xs:double) as xs:double {
        $scaled-radius*(-10*math:cos(2*$t) - 27*math:sin(2*$t) + 38*math:cos(4*$t) + 46*math:sin(4*$t)) + $δy
      },
      function ($t as xs:double) as xs:double {
        $scaled-radius*(70*math:cos(3*$t) - 40*math:sin(3*$t)) + $δz
      },
      0, 2*math:pi()
    )
  )
};


(:~
 : harmonograph()
 : Draw the path of a harmonograph (multi-axis multi-pendulum system)
 :
 : @param $center: Center of graph
 : @param $radius: Basic scaling of graph
 : @param $harmonograph: Harmonograph descriptor (see pend_t:harmonograph())
 : @param $density: Parameter to adjust fineness of the time ticks (t=i/density)
 : @param $extent: Basic extent of drawing
 :   Final number of ticks (and therefore points) is num-points*density
 :)
declare function this:harmonograph(
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $harmonograph as map(xs:string,item()*), 
  $density as xs:integer,
  $extent as xs:double
) as map(xs:string,item()*)*
{
  let $num-x-pendulums := pend_t:num-pendulums(pend_t:x($harmonograph))
  let $x-pendulums := pend_t:x($harmonograph)=>pend_t:pendulums()
  let $num-y-pendulums := pend_t:num-pendulums(pend_t:y($harmonograph))
  let $y-pendulums := pend_t:y($harmonograph)=>pend_t:pendulums()
  let $num-z-pendulums := pend_t:num-pendulums(pend_t:z($harmonograph))
  let $z-pendulums := pend_t:z($harmonograph)=>pend_t:pendulums()
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $δz := point:pz($center)
  return (
    curve:parametric(
      util:round($extent * $density),
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $x-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δx
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $y-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δy
      },
      function ($t as xs:double) as xs:double {
        $radius*sum(
          $z-pendulums!pend_t:damped-pendulum($t, .)
        ) + $δz
      },
      1 div $density, $extent
    )
  )
  (:
  for $i in 1 to $extent*$density
  let $t := $i div $density
  let $x :=
    $radius*sum(
      $x-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:px($center)
  let $y := 
    $radius*sum(
      $y-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:py($center)
  let $z := 
    $radius*sum(
      $z-pendulums!pend_t:damped-pendulum($t, .)
    ) + point:py($center)
  return point:point($x, $y, $z)
  :)
};

(:~
 : wiggle()
 : Draw the path of a knot wiggle. Number of points will depend on wiggle
 : smoothness
 :
 : @param $start: Starting point
 : @param $length: Length 
 : @param $wiggle: Wiggle descriptor
 :)
declare function this:wiggle(
  $start as map(xs:string,item()*),
  $length as xs:double,
  $wiggle as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $smoothness := $length * ($wiggle=>wiggle_t:smoothness())
  return (
    curve:parametric(
      util:round($length),
      function ($t as xs:double) as xs:double {
        $t
      },
      function ($t as xs:double) as xs:double {
        let $θ := ($t * $length) div (2 * math:pi() * $smoothness)
        return point:y($start) + $wiggle=>wiggle_t:value($θ, $length)
      },
      point:x($start), point:x($start) + util:round($length)
    )
  )
};

(:~
 : golden-spiral()
 : Create a sequence of points following a (Fibonacci approximation of) a golden spiral
 :
 : @param $num-points: Number of points in each (90 degree) arc of spiral
 : @param $n: Number of Fibonacci generations
 : @param $scale: Scale of the spiral (need to do at this level to avoid jaggies)
 :)
declare function this:golden-spiral(
  $num-points as xs:integer,
  $n as xs:integer,
  $scale as xs:double
) as map(xs:string,item()*)*
{
  let $fibs := seq:fibonacci-sequence($n)
  let $radii :=
    for $i in 1 to $n return if ($i=1) then $scale else $scale*$fibs[$i - 1]
  let $angles :=
    for $i in 1 to $n return
    switch ($i mod 4)
    case 0 return 90
    case 1 return 360
    case 2 return 270
    case 3 return 180
    default return ()
  let $centers :=
    fold-left(1 to $n, point:point(0,0),
      function ($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        $points,
        let $center := $points[last()]
        let $last :=
          if ($i = 1)
          then point:point(point:x($center), point:y($center) - $scale)
          else geom:destination($center, $angles[$i - 1] - 90, $radii[$i - 1])
        return
        switch ($i mod 4)
        case 0 return
          point:point(point:x($center), point:y($last) - $radii[$i])
        case 1 return
          point:point(point:x($last) - $radii[$i], point:y($center))
        case 2 return
          point:point(point:x($center), point:y($last) + $radii[$i])
        case 3 return
          point:point(point:x($last) + $radii[$i], point:y($center))
        default return ()
      }
    )[position() >= 2]
  for $i in 1 to $n
  return
    this:reverse-arc($centers[$i], $radii[$i], 90, $angles[$i], $num-points)
};

(:~
 : angle-spiral()
 : Create a path following a simple uniform spiral where the points are a
 : consistent number of degrees apart
 :
 : @param $arc: degrees of arc between each point
 : @param $center: center of spiral
 : @param $radius: distance from center to first point
 : @param $loops: how many windings of spiral
 : @param $spread: how far about windings are
 :)
declare function this:angle-spiral(
  $arc as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  let $num-points := 360 idiv $arc
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      return (
        (: new angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc 
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
};

(:~
 : point-spiral()
 : Create a path following a simple uniform spiral where there are the
 : same number of points in each loop
 :
 : @param $num-points: number of points per loop
 : @param $center: center of spiral
 : @param $radius: distance from center to first point
 : @param $loops: how many windings of spiral
 : @param $spread: how far about windings are
 :)
declare function this:point-spiral(
  $num-points as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  let $arc := 360 div $num-points
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
};

(:~
 : uniform-spiral()
 : Create a path following a simple uniform spiral where there is about
 : the same distance between each point.
 :
 : @param $distance: distance between points
 : @param $center: center of spiral
 : @param $radius: distance from center to first point
 : @param $loops: how many windings of spiral
 : @param $spread: how far apart windings are
 :)
declare function this:uniform-spiral(
  $distance as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as xs:double
) as map(xs:string,item()*)*
{
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      let $loop-radius := $radius + ($loop - 1)*$spread
      let $avg-loop-radius := avg(($loop-radius, $radius + $loop*$spread))
      let $num-points := 2*math:pi()*$avg-loop-radius idiv $distance
      let $arc := 360 div $num-points
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $loop-radius + $t * $spread div $num-points
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
};

(:~
 : spiral-function()
 : Return a function that can be used with function-spiral() to create a
 : spiral of a particular kind.
 : 
 : @param $kind: kind of spread function
 :   uniform: spiral expands uniformly (equivalent to arc-spiral())
 :   accelerating: spiral expands in an accelerating way on each turn
 :   nautiloid: spirals pinch off at end to give nautilus like appearance
 :   wobbly: spiral veers back and forth in a wave (use higher $arc)
 :   very-wobbly: spiral veers even more
 : @param $spread: scaling factor on the expansion
 :)
declare function this:spiral-function($kind as xs:string, $spread as xs:double)
  as function(xs:integer, xs:integer, xs:integer) as xs:double
{
  switch($kind)
  case "uniform" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      xs:double(($loop - 1)*$spread) + ($point*$spread div $num-points)
    }
  case "accelerating" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop * $num-points + $point) * ($loop * $num-points + $point) * $spread div ($num-points * $num-points)
    }
  case "wobbly" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop - 1)*$spread + math:pow(-1,$point) * $point * $spread div $num-points
    }
  case "very-wobbly" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop - 1)*$spread + 2*math:pow(-1,$point) * $point * $spread div $num-points
    }
  case "nautiloid" return
    function ($loop as xs:integer, $point as xs:integer, $num-points as xs:integer) as xs:double
    {
      ($loop * $num-points + $point) * (($loop * $num-points + $point) mod $num-points) * $spread div ($num-points * $num-points)
    }
  default return errors:error("ML-BADARGS", ("kind", $kind))
};

(:~
 : function-spiral()
 : Create an arc spiral using a function to compute the distance for
 : each point in the loop.
 :
 : @param $arc: degrees of arc between each point
 : @param $center: center of spiral
 : @param $radius: distance from center to first point
 : @param $loops: how many windings of spiral
 : @param $spread: function that is added to base radius to compute the distance
 :)
declare function this:function-spiral(
  $arc as xs:double,
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $loops as xs:integer,
  $spread as function((:loop:)xs:integer,(:point:)xs:integer,(:num-points:)xs:integer) as xs:double
) as map(xs:string,item()*)*
{
  let $num-points := 360 idiv $arc
  return
  fold-left(1 to $loops, (0),
    function ($angle-and-points as item()*, $loop as xs:integer) as item()* {
      let $last-angle := head($angle-and-points)
      return (
        (: Next angle :)
        util:remap-degrees($last-angle + $num-points * $arc),
        (: Points :)
        tail($angle-and-points),
        curve:polar($num-points, $center,
          (:r:) function ($t as xs:double) as xs:double {
            $radius + $spread($loop, xs:integer($t), $num-points)
          },
          (:θ:) function ($t as xs:double) as xs:double {
            $last-angle + $t * $arc
          },
          0, $num-points - 1
        )
      )
    }
  )=>tail()
};

(:~ 
 : fern-spiral()
 : Make a fern spiral
 : Fern spirals are created as a series of segments, where each segment
 : shrinks from the previous and decreases the lean by curl*curvature
 : curl and curvature are handled separately by branching and blade
 : creation
 : 
 : Overall path length is Σ[j=0,n-1]d*r^j = (d - rl)/(1 - r) where l=ar^(n-1) 
 : in the limit (j=0,∞) = a/(1-r) 
 : height will be less because of angle
 :
 : @param $start: starting point for spiral
 : @param $fern: parameter bundle defining curve, create with fern_t:fern()
 :   left: curve to the left; default=false
 :   scale: size of initial segment; default=100
 :   lean: amount of initial lean (degrees); default=70
 :   shrinkage: how much to shrink each segment; default=0.7
 :   curvature: how much to change the angle (degrees); default=90
 :   curl: tightness of curl; default=0.2
 : @param $generations: number of segments in spiral
 :)
declare function this:fern-spiral(
  $start as map(xs:string,item()*),
  $fern as map(xs:string,item()*),
  $generations as xs:integer
) as map(xs:string,item()*)*
{
  let $is-left := fern_t:is-left($fern)
  let $scale := fern_t:scale($fern)
  let $lean := fern_t:lean($fern)
  let $shrinkage := fern_t:shrinkage($fern)
  let $curvature := fern_t:curvature($fern)
  let $curl := fern_t:curl($fern)
  return (
    curve:dependent($generations, $start,
      function ($last as map(xs:string,item()*), $i as xs:integer) as map(xs:string,item()*) {
        let $d := $scale*math:pow($shrinkage,($i - 1))
        let $a :=
          if ($is-left)
          then util:remap-degrees($lean + ($i - 1)*($curvature*$curl))
          else util:remap-degrees($lean - ($i - 1)*($curvature*$curl))
        return geom:destination($last, -$a, $d)
      }
    )
  )
};

(:~
 : eulers()
 : Euler's spiral/clothoid
 :
 : x = k * ∫[0:t]( sin(u²/2)du )
 : y = k * ∫[0:t]( cos(u²/2)du )
 : 
 : k is scaling; t as some number of multiples of π
 : 
 : @param $center: center of spiral
 : @param $scaling: k parameter above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
 :)
declare function this:eulers(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*2*math:pi())
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {math:sin($u*$u div 2)}
    return
      fold-left(2 to count($ts), $scaling * util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + $scaling * util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {math:cos($u*$u div 2)}
    return
      fold-left(2 to count($ts), $scaling * util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + $scaling * util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  for $x at $i in $xs return point:point($x + $δx, $ys[$i] + $δy)
};

(:~
 : polynomial()
 : 
 : Polynomial spiral: dφ/ds = Σ[n=1:n](ai*s^i)
 : s=arc length
 : dφ/ds=1 => circle
 : dφ/ds=s => Cornu spiral
 : dφ/ds=s² => double clothoid
 : dφ/ds=s² + 1 => like Corinthian capital
 : dφ/ds=s² - 4 => intersecting double spiral vase shape
 :
 : s = Σ[j=0:J] (a[j]/(j+1))θ^(j+1)
 : 
 : https://www.atlantis-press.com/journals/gaf/125935071/view
 : κ(s) = Pκ'(s)
 : P is polynomial in s
 : x = ∫[t=0:s]sin(Pk(t))
 : y = ∫[t=0:s]cos(Pk(t))
 : k=0 => line, k=1 => circle, k=2 => Cornu spiral, k=3 => where the fun starts
 : k=3 cont. κ = s² - D => 2pts inflection for D>0, 1 for D=0, none for D&lt;0
 :
 : @param $center: center of spiral
 : @param $scaling: overall scaling
 : @param $polynomial: the polynomial (see types/polynomial)
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range; >1 tends to get wonky
 : @param $symmetric: use a symmetric extent around 0 vs 0 to 2π; gives double spirals
 :)
declare function this:polynomial(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $polynomial as map(xs:string,item()*),
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $pk := polynomial:antiderivative($polynomial)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin($pk=>polynomial:value($u))
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos($pk=>polynomial:value($u))
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $spiral-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($spiral-center)
  return $pts!point:add(., $delta)
};

(:~
 : meander()
 : θ(s) = Σ[i=1:N](A[i] sin( (2π/λ[i])s + φ[i] )
 : θ = tangential angle, s=arc length
 : For set of A,λ,φ; let's take φ[i]=0
 : (x,y) = (∫[0:s] cos(θ(s)), ∫[0:s] sin(θ(s))
 :
 : @param $center: center of meander
 : @param $scaling: overall scaling
 : @param $amplitudes: vector of amplitude values (A[i]); will use last A[i]
 :   if wavelength vector is longer
 : @param $wavelengths: vector of wavelength values (λ[i])
 : @param $num-points: number of points to plot
 : @param $extent: extent of curve (multiple of π)
 : @param $symmetric: symmetric extent around 0
 :)
declare function this:meander(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $amplitudes as xs:double*,
  $wavelengths as xs:double*,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $n := count($wavelengths)
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $invλs := $wavelengths!(2*math:pi() div .)
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * math:sin( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * math:sin( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $wave-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($wave-center)
  return $pts!point:add(., $delta)
};

(:~
 : looping-meander()
 : dθ(s) = sΣ[i=1:N](A[i] sin( (2π/λ[i])s  + φ[i] )
 : For set of A,λ,φ; let's take φ[i]=0
 : (x,y) = (∫[0:s] cos(dθ(s)), ∫[0:s] sin(dθ(s))
 : dθ(s) = dΣ[i=1:N](A[i] sin( (2π/λ[i])s ))
 :       = Σ[i=1:N] d((A[i] sin( (2π/λ[i])s )))
 :       = Σ[i=1:N] A[i] d((sin( (2π/λ[i])s )))
 :       = Σ[i=1:N] A[i] (2π/λ[i]) cos( (2π/λ[i])s )
 :
 : @param $center: center of meander
 : @param $scaling: overall scaling
 : @param $amplitudes: vector of amplitude values (A[i]); will use last A[i]
 :   if wavelength vector is longer
 : @param $wavelengths: vector of wavelength values (λ[i])
 : @param $num-points: number of points to plot
 : @param $extent: extent of curve (multiple of π)
 : @param $symmetric: symmetric extent around 0
 :)
declare function this:looping-meander(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $amplitudes as xs:double*,
  $wavelengths as xs:double*,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($num-points < 1) then () else
  let $n := count($wavelengths)
  let $δx := point:px($center)
  let $δy := point:py($center)
  let $ts :=
    if ($symmetric)
    then util:linspace($num-points, -$extent*math:pi(), $extent*math:pi())
    else util:linspace($num-points, 0, $extent*math:pi())
  let $invλs := $wavelengths!(2*math:pi() div .)
  let $xs :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:cos(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * $invλs[$i] *
            math:cos( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts), util:integral(0, $ts[1], $f, 4),
        function ($xs as xs:double*, $i as xs:integer) as xs:double* {
          $xs,
          $xs[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $ys :=
    let $f := function ($u as xs:double) as xs:double {
      $scaling * math:sin(
        sum(
          for $i in 1 to $n return (
            ($amplitudes[$i], $amplitudes[last()])[1] * $invλs[$i] *
            math:cos( $invλs[$i] * $u )
          )
        )
      )
    }
    return
      fold-left(2 to count($ts),util:integral(0, $ts[1], $f, 4),
        function ($ys as xs:double*, $i as xs:integer) as xs:double* {
          $ys,
          $ys[last()] + util:integral($ts[$i - 1], $ts[$i], $f, 4)
        }
      )
  let $pts :=
    for $x at $i in $xs return point:point($x, $ys[$i])
  let $wave-center :=
    box:box(
      min($pts!point:px(.)), min($pts!point:py(.)),
      max($pts!point:px(.)), max($pts!point:py(.))
    )=>box:center()
  let $delta := $center=>point:sub($wave-center)
  return $pts!point:add(., $delta)
};

(:~
 : cotes()
 : Cotes' spirals
 :
 : A > 0, k > 0, ε real constants A=>size, k=>shape, ε=>angular position
 : 1/r = A cosh(kθ + ε) case 1 Poinsot's
 : 1/r = A exp(kθ + ε)  case 2 Equiangular
 : 1/r = A sinh(kθ + ε) case 3 Poinsot's
 : 1/r = A (kθ + ε)     case 4 Hyperbolic/reciprocal
 : 1/r = A cos(kθ + ε)  case 5 Epispiral
 :
 : @param $kind: what kind (one of poinsot-c, pointsot-s, equiangular, reciprocal, epispiral)
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs 0 to 2π; gives cardiod shapes
 :)
declare function this:cotes(
  $kind as xs:string,
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($A <= 0) then errors:error("ML-BADARGS", ("A", $A)) else (),
  if ($k <= 0) then errors:error("ML-BADARGS", ("k", $k)) else (),
  let $r :=
    switch($kind)
    case "poinsot-c" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * util:cosh($k * $t + $ε))
      }
    case "poinsot-s" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * util:sinh($k * $t + $ε))
      }
    case "equiangular" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * math:exp($k * $t + $ε))
      }
    case "reciprocal" return
      function ($t as xs:double) as xs:double {
        $scaling div ($A * ($k * $t + $ε))
      }
    case "epispiral" return
      function ($t as xs:double) as xs:double {
        1 div ($A * math:cos($k * $t + $ε))
      }
    default return errors:error("ML-BADARGS", ("kind", $kind))
  let $θ := function ($t as xs:double) as xs:double {$t}
  return (
    (: point:valid() filters out NaNs and INFs :)
    if ($symmetric) then (
      curve:polar($num-points, $center, $r, $θ, -$extent*math:pi(), $extent*math:pi())[point:valid(.)]
    )
    else (
      curve:polar($num-points, $center, $r, $θ, 0, 2*$extent*math:pi())[point:valid(.)]
    )
  )
};

(:~
 : poinsot-s()
 : Case 3 Cotes' spiral: 1/r = A sinh(kθ + ε)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:poinsot-s(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("poinsot-s", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
};

(:~
 : poinsot-c()
 : Case 1 Cotes' spiral: 1/r = A cosh(kθ + ε)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:poinsot-c(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("poinsot-c", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
};

(:~
 : equiangular()
 : Case 2 Cotes' spiral: 1/r = A exp(kθ + ε)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:equiangular(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("equiangular", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
};

(:~
 : reciprocal()
 : Case 4 Cotes' spiral: 1/r = A (kθ + ε)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:reciprocal(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("reciprocal", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
};

(:~
 : epispiral()
 : Case 5 Cotes' spiral: 1/r = A cos(kθ + ε)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $A: A parameter, above; for most 1.0 is fine; epispirals need small fraction A
 : @param $k: k parameter, above; for most 0.5 is fine; epispirals better with smaller k
 : @param $ε: ε parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:epispiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $A as xs:double,
  $k as xs:double,
  $ε as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  this:cotes("epispiral", $center, $scaling, $A, $k, $ε, $num-points, $extent, $symmetric)
};

(:~
 : cochleoid()
 : Cochleoid spiral: r = sinφ/φ
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:cochleoid(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then $scaling else $scaling * math:sin($t) div $t
      },
      $min, $max
    )
  )
};

(:~
 : atom()
 : Atom spiral: r=φ/(φ - a)
 : Spirals in then kicks out a>=1 for a &lt; 1 more of a circle in/out
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (basic size is generally in unit square)
 : @param $a: a parameter above 
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:atom(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t - $a = 0)
        then util:sign($scaling * $t) * xs:double("INF")
        else $scaling * $t div ($t - $a)
      },
      $min, $max
    )[point:valid(.)]
  )
};

(:~
 : circle-involute()
 : Involute of circle: r²=φ² + 1
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:circle-involute(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        -$scaling * math:sqrt($t*$t + 1)
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        $scaling * math:sqrt($t*$t + 1)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        $scaling * math:sqrt($t*$t + 1)
      },
      0, 2 * $extent * math:pi()
    )
  )
};

(:~
 : lituus()
 : Lituus: r²θ = k => r = √(k/θ)
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:lituus(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("-INF") else -math:sqrt($scaling div abs($t))
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("INF") else math:sqrt($scaling div $t)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        if ($t = 0) then xs:double("INF") else math:sqrt($scaling div $t)
      },
      0, 2 * $extent * math:pi()
    )
  )[point:valid(.)]
};

(:~
 : gielis()
 : Gielis curve = super ellipse
 : r = (|cos(dφ)|^a | + |sin(dφ)|^b)^c
 : 
 : a=10, b=10, c=-2/9, d=3/4: has the shape of the petiole (leaf stem) of the nuphar luteum
 : a=15, b=15, c=-1/12, d=1: the shape of the cross section of the stem of the herb called scrophularia nodosa
 : a=1, b=1, c=-1/4, d=5/4: idem, of the equisetum
 : a=6, b=6, c=-1/10, d=7/4, idem, of the raspberry
 :
 : @param $center: center of ellipse
 : @param $scaling: scaling of ellipse
 : @param $a: a parameter above
 : @param $b: b parameter above; a vs b gives relative flatness
 : @param $c: c parameter above
 : @param $d: d parameter above => number of symmetries is 4d 
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:gielis(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = (|cos(dφ)|^a | + |sin(dφ)|^b)^c :)
        $scaling *
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
};

(:~
 : super-rose()
 : Super rose = extension of rose, generalization of Gielis curve
 : r = sin(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c
 : a=1 b=1 c=-1 d=3 f=5/8 => spiky rose
 :
 : @param $center: center of rose
 : @param $scaling: scaling of rose
 : @param $a: a parameter above
 : @param $b: b parameter above; a vs b gives relative flatness
 : @param $c: c parameter above
 : @param $d: d parameter above
 : @param $f: f parameter above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:super-rose(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $f as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = sin(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c :)
        $scaling *
        math:sin($f*$t) * 
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
};

(:~
 : super-spiral()
 : Super spiral = generalization of Gielis curve
 : r = e^(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c 
 : Some cases: 
 : a=b=5, c=-1, d=1, f=1/3; starish wide loops
 : a=b=5, c=-1/5, d=5/2, f=1/5; wobbly
 : a=b=100, c=-0.01, d=1, f=1/5; almost square
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral
 : @param $a: a parameter above
 : @param $b: b parameter above; a vs b gives relative flatness
 : @param $c: c parameter above
 : @param $d: d parameter above => number of symmetries is 4d 
 : @param $f: f parameter above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:super-spiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $b as xs:double,
  $c as xs:double,
  $d as xs:double,
  $f as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = e^(fφ)(|cos(dφ)|^a + |sin(dφ)|^b)^c :)
        $scaling *
        math:exp($f*$t) *
        math:pow(
          math:pow(abs(math:cos($d*$t)), $a) + math:pow(abs(math:sin($d*$t)), $b),
          $c
        )
      },
      $min, $max
    )
  )
};

(:~
 : logarithmic()
 : Logarithmic spiral: r = e^(aφ)
 : Examples a[0.05:0.25] smaller a, closer windings, further from center start
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral
 : @param $a: a parameter above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:logarithmic(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $a as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = e^(aφ) :)
        $scaling * math:exp($a*$t) 
      },
      $min, $max
    )
  )
};

(:~
 : fermat()
 : Fermat's spiral: r^2 = φ => Archimedes but two joined outflows
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:fermat(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  if ($symmetric) then (
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        -$scaling * math:sqrt(abs($t))
      },
      -$extent * math:pi(), 0
    ),
    curve:polar($num-points idiv 2, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        math:sqrt($scaling div $t)
      },
      $extent * math:pi() div ($num-points idiv 2), $extent * math:pi()
    )
  ) else (
    (: Positive branch only = Archimedes :)
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = √φ :)
        math:sqrt($scaling div $t)
      },
      0, 2 * $extent * math:pi()
    )
  )
};

(:~
 : tractrix()
 : Tractrix: r = A cos(t), θ = tan(t) - t
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (A parameter)
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:tractrix(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double {
        (: r = A cos(t) :)
        $scaling * math:cos($t)
      },
      function ($t as xs:double) as xs:double {
        (: θ = tan(t) - t :)
        math:tan($t) - $t
      },
      $min, $max
    )
  )
};

(:~
 : doppler()
 : Doppler: x = a(t cos(t) + kt), y = a t sin(t) 
 : Like nested circles pinned to one side, but spirals
 : 0 &lt; k &lt; 1 two spirals intersecting (for - and + angles)
 : k = 1 two spirals tangent (for - and + angles)
 : k > 1 two spirals increasing away from center
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (a parameter, above)
 : @param $k: k parameter, above
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:doppler(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $k as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  let $δx := point:px($center)
  let $δy := point:py($center)
  return (
    curve:parametric($num-points,
      function ($t as xs:double) as xs:double {
        (: x = a(t cos(t) + kt) :)
        $scaling * ($t * math:cos($t) + $k*$t) + $δx
      },
      function ($t as xs:double) as xs:double {
        (: y = a(t sin(t)) :)
        $scaling * ($t * math:sin($t)) + $δy
      },
      $min, $max
    )
  )
};

(:~
 : conchospiral()
 : Conchospiral (3d): r = μ^t a, θ = t, z = μ^t c
 :  μ=>opening angle, c=>slope of cone; example μ=1.07,a=1,c=1.1
 :
 : @param $center: center of spiral
 : @param $scaling: scaling of spiral (a parameter, above)
 : @param $μ: opening angle (μ parameter, above)
 : @param $c: slope of cone (c parameter, above)
 : @param $num-points: number of points to produce
 : @param $extent: number of multiples of π in angular range, extent => number of turns
 : @param $symmetric: use a symmetric extent around 0 vs extent from 0 to 2π
 :)
declare function this:conchospiral(
  $center as map(xs:string,item()*),
  $scaling as xs:double,
  $μ as xs:double,
  $c as xs:double,
  $num-points as xs:integer,
  $extent as xs:double,
  $symmetric as xs:boolean
) as map(xs:string,item()*)*
{
  let $min := if ($symmetric) then -$extent * math:pi() else 0
  let $max := if ($symmetric) then $extent * math:pi() else $extent * 2 * math:pi()
  return (
    curve:polar($num-points, $center,
      function ($t as xs:double) as xs:double { (: r = μ^t a :)
        $scaling * math:pow($μ, $t)
      }, 
      function ($t as xs:double) as xs:double {$t}, (: θ = t :)
      function ($t as xs:double) as xs:double { (: z = μ^t c :)
        $c * math:pow($μ, $t)
      },
      $min, $max
    )
  )
};

(:~
 : lamé()
 : Lamé curves: x = ±a cos(t)^(2/α); y = ±b cos(t)^(2/α); 
 : α > 2 => super-ellipse
 : 
 : @param $num-points: number of points to draw
 : @param $a: scaling of x (a parameter above)
 : @param $b: scaling of y (b parameter above)
 : @param $α: α parameter above
 :)
declare function this:lamé(
  $num-points as xs:integer,
  $a as xs:double,
  $b as xs:double,
  $α as xs:double
) as map(xs:string,item()*)*
{
  curve:parametric($num-points,
    function ($t as xs:double) as xs:double {
      if ($t < 0)
      then -$a * math:pow(math:cos($t), 2 div $α)
      else $a * math:pow(math:cos($t), 2 div $α)
    },
    function ($t as xs:double) as xs:double {
      if ($t < 0)
      then -$b * math:pow(math:sin($t), 2 div $α)
      else $b * math:pow(math:sin($t), 2 div $α)
    },
    -math:pi() div 2,
    math:pi() div 2
  )
};

(:~
 : swoosh()
 : Swoosh shape: two double curves
 :)
declare function this:swoosh(
  $center as map(xs:string,item()*),
  $r as xs:double
) as map(xs:string,item()*)
{
  let $c1 := point:point(-1.5 * $r, 0)
  let $c2 := point:point(-0.5 * $r, 0)
  let $c3 := point:point( 0.5 * $r, 0)
  let $c4 := point:point( 1.5 * $r, 0)
  let $edges := (
    edge:arc($c1, $r, point:point(-0.5*$r, 0), point:point(-1.5*$r, $r), false(), false()),
    edge:arc($c1, $r, point:point(-1.5*$r, $r), point:point(-2.5*$r, 0), false(), false()),
    edge:edge(point:point(-2.5*$r, 0), point:point(-1.5*$r, 0)),
    edge:arc($c2, $r, point:point(-1.5*$r, 0), point:point(-0.5*$r, $r), true(), false()),
    edge:arc($c2, $r, point:point(-0.5*$r, $r), point:point(0.5*$r, 0), true(), false()),
    edge:arc($c4, $r, point:point(0.5*$r, 0), point:point(1.5*$r, -$r), false(), false()),
    edge:arc($c4, $r, point:point(1.5*$r, -$r), point:point(2.5*$r, 0), false(), false()),
    edge:edge(point:point(2.5*$r, 0), point:point(1.5*$r, 0)),
    edge:arc($c3, $r, point:point(1.5*$r, 0), point:point(0.5*$r, -$r), true(), false()),
    edge:arc($c3, $r, point:point(0.5*$r, -$r), point:point(-0.5*$r, 0), true(), false())
  )
  return path:polygon($edges)=>geom:translate(point:px($center), point:py($center))
};

declare function this:waveline(
  $from as map(xs:string,item()*),
  $to as map(xs:string,item()*),
  $waveheight as xs:double
) as map(xs:string,item()*)*
{
  this:waveline($from, $to, $waveheight, 4 * $waveheight)
};

declare function this:waveline(
  $from as map(xs:string,item()*),
  $to as map(xs:string,item()*),
  $waveheight as xs:double,
  $wavelength as xs:double
) as map(xs:string,item()*)*
{
  let $distance := geom:distance($from, $to)
  let $waves := ($distance idiv $wavelength)
  let $slant := edge:angle($from, $to)
  return (
    curve:parametric(
      max((4, $waves * 6)) cast as xs:integer,
      function ($t as xs:double) as xs:double {$t * $distance},
      function ($t as xs:double) as xs:double {$waveheight * math:sin($t * math:pi() * 2 * $waves)},
      0.0,
      1.0
    )=>geom:rotate(-$slant)=>geom:translate(point:px($from), point:py($from))
  )
};

(:~ 
 : complex-circular-arc()
 : Calculate circular arc at radius r from z0 between θ[1] and θ[2] using
 : n-points in the complex plane. Produces sequence of points in the 
 : complex plane.
 :
 : @param $r: base radius
 : @param $z0: center point
 : @param $θ: starting and ending angles
 : @param $n-points: number of points to generate
 : @param $gain-f: multiplier of radii of points along the arc; a function of
 :   fraction of arc running from 1/n-points to first point in arc to 1 
 :   for last; use constant 1 for actual circular arc
 :)
declare function this:complex-circular-arc(
  $r as xs:double,
  $z0 as map(xs:string,item()*),
  $θ as xs:double*,
  $n-points as xs:integer,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  for $α at $i in util:linspace($n-points, $θ[1], $θ[2]) return (
    z:complex-rφ($r * $gain-f($i div $n-points), $α)=>z:add($z0)
  )
};

(:~ 
 : complex-rounded-rec()
 : Calculate a quasi-rectangular arc of power p, radius r from z0 between
 : θ(1) to θ(2) using n points in the complex plane;
 : The original used matlab cosd and sind, but I don't have those, so I'm
 : just going to round things close to zero.
 : "This program has been rewritten with degrees to avoid problems with
 : cos(pi/2) and sin(pi) raised to small powers" Cye H. Waldman, 2013
 : When p = 2 you get the square corners; p > 2 bulges in converging to plus
 : sign; p &lt; 2, bulges out converging to square.
 :
 : @param $r: base radius
 : @param $z0: center point
 : @param $θ: starting and ending angles
 : @param $p: power
 : @param $n-points: number of points to generate
 : @param $gain-f: multiplier of radii of points along the arc; a function of
 :   fraction of arc running from 1/n-points to first point in arc to 1 
 :   for last; use constant 1 for actual circular arc
 :)
declare function this:complex-rounded-rec(
  $r as xs:double,
  $z0 as map(xs:string,item()*),
  $θ as xs:double*,
  $p as xs:double, (: power: 0 to infinity; p &lt; 2 is convex :)
  $n-points as xs:integer,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  for $α at $i in util:linspace($n-points, $θ[1], $θ[2])
  let $cos := math:cos($α)
  let $cos := if (abs($cos) < $config:ε) then 0 else $cos
  let $sin := math:sin($α)
  let $sin := if (abs($sin) < $config:ε) then 0 else $sin
  let $adjust := $gain-f($i div $n-points)
  return (
    z:complex(
      $adjust * $r * util:sign($cos) * math:pow(abs($cos), $p),
      $adjust * $r * util:sign($sin) * math:pow(abs($sin), $p)
    )=>z:add($z0)
  )
};

(:~
 : pseudospiral()
 : Construct a pseudospiral consisting of joined circular arcs.
 : Result is a set of points. 
 : If sequence is monotonically increasing and positive, we get an actual
 : spiral. Negative or decreasing values create loops and overlaps of various
 : sorts.
 :
 : @param $n-points: number of points per arc
 : @param $sequence: sequence of radii
 : @param $gain-f: multiplier of radii of points along the arc; a function of
 :   fraction of arc running from 1/n-points to first point in arc to 1 
 :   for last; use constant 1 for actual circular arc
 :)
declare function this:pseudospiral(
  $n-points as xs:integer, 
  $sequence as xs:integer*,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  let $rotation := math:pi() div 2   (: angular rotation for each arc :)
  let $z0 := z:complex(0,0)          (: initial starting point :)
  let $θ := (0, $rotation)           (: initial θ range :)
  let $n := count($sequence) - 1
  return
    fold-left(1 to $n, ($θ, $z0, ()), function ($data as item()*, $k as xs:integer) as item()* {
      let $θ := (head($data), head(tail($data)))
      let $z0 := head(tail(tail($data)))
      let $zstep := tail(tail(tail($data)))
      let $r := $sequence[$k]
      let $c4th := this:complex-circular-arc($r, $z0, $θ, $n-points, $gain-f)
      return (
        if ($k < $n) then (
          let $rnext := $sequence[$k + 1]
          let $z0 := $c4th[last()]=>z:sub(z:complex-rφ($rnext, $θ[2]))
          let $θ := $θ!(. + $rotation)
          return ($θ, $z0, ($zstep, tail($c4th)))
        ) else (
          ($θ, $z0, ($zstep, tail($c4th), $c4th[last()]))
        )
      )
    })=>tail()=>tail()=>tail()
};

(:~
 : pseudospiral-p()
 : Construct a pseudospiral consisting of joined circular arcs
 : Result is a set of points. If sequence is monotonically increasing and
 : positive, we get an actual spiral. Negative or decreasing values create
 : loops and overlaps of various sorts. The idea here is to call this with 
 : various values of p to create a sheaf of curves following the path.
 :
 : @param $n-points: number of points per arc
 : @param $sequence: sequence of radii
 : @param $p: the power to use (collapse of circular arc)
 : @param $gain-f: multiplier of radii of points along the arc; a function of
 :   fraction of arc running from 1/n-points to first point in arc to 1 
 :   for last; use constant 1 for actual circular arc
 :)
declare function this:pseudospiral-p(
  $n-points as xs:integer,
  $sequence as xs:integer*,
  $p as xs:double,
  $gain-f as function(xs:double) as xs:double
) as map(xs:string,item()*)*
{
  let $rotation := math:pi() div 2  (: angular rotation for each arc :)
  let $z0 := z:complex(0,0)         (: initial starting point :)
  let $θ := (0, $rotation)          (: initial θ range :)
  let $n := count($sequence) - 1
  return
    fold-left(1 to $n, ($θ, $z0, ()), function ($data as item()*, $k as xs:integer) as item()* {
      let $θ := (head($data), head(tail($data)))
      let $z0 := head(tail(tail($data)))
      let $zstep := tail(tail(tail($data)))
      let $r := $sequence[$k]
      let $c4th := this:complex-rounded-rec($r, $z0, $θ, $p, $n-points, $gain-f)
      return (
        if ($k < $n) then (
          let $rnext := $sequence[$k + 1]
          let $z0 := $c4th[last()]=>z:sub(z:complex-rφ($rnext, $θ[2]))
          let $θ := $θ!(. + $rotation)
          return ($θ, $z0, ($zstep, tail($c4th)))
        ) else (
          ($θ, $z0, ($zstep, tail($c4th), $c4th[last()]))
        )
      )
    })=>tail()=>tail()=>tail()
};

(:~
 : pseudospiral-triangles()
 : Construct gnomic tiling triangles following the pseudospiral path
 : defined by the radius sequence. Result is a set of polygons, preceded,
 : if debug is true(), by the path of the outer controlling spiral.
 :
 : @param $sequence: sequence of radii
 :)
declare function this:pseudospiral-triangles(
  $sequence as xs:integer*, (: driver sequence :)
  $debug as xs:boolean
) as map(xs:string,item()*)*
{
  let $kmax := count($sequence) - 1
  let $θ1 := math:sqrt(3) div 2
  let $θ := math:pi() div 3
  (: Trapezoidal gnomons :)
  let $gnomons := (
    let $ztri := array {
      z:as-complex(0),
      z:complex(-1, $θ1)=>z:times(0.5),
      z:as-complex(-1),
      z:as-complex(0),
      z:as-complex(0)
    }
    let $Z := 
      array:for-each($ztri, function ($v as map(*)) as map(*) {$v=>z:times($sequence[1])})
    return (
      fold-left(1 to $kmax, ($ztri, $Z),
        function ($data as array(*)*, $k as xs:integer) as array(*)* {
          let $zprev := head($data)
          let $Z := tail($data)
          let $zgno := (
            z:as-complex(0),
            z:as-complex(-$sequence[$k]),
            z:as-complex(-$sequence[$k])=>z:add(
              z:complex-rφ($sequence[$k + 1] - $sequence[$k], 4 * $θ)
            ),
            z:complex-rφ($sequence[$k + 1] - $sequence[$k], 5 * $θ),
            z:as-complex(0)
          )
          let $znext := (
            for $val in $zgno return (
              $val=>z:multiply(z:complex-rφ(1, -2*($k - 1)*$θ))
            )
          )
          let $znext := (
            if ($k = 1) then (
              array {
                for $v in $znext return $v=>z:sub($znext[1])=>z:add($zprev(1))
              }
            ) else (
              array {
                for $v in $znext return $v=>z:sub($znext[1])=>z:add($zprev(3))
              }
            )
          )
          return (
            $znext, ($Z, $znext)
          )
        }
      )=>tail()
    )
  )
  (: 'super' triangles: previous triangle plus trapezoid :)
  let $triangles := (
    path:polygon(geom:to-edges((
      for $i in 1 to 4 return $gnomons[1]($i)
    )))=>map:put("pid", 1),
    path:triangle(
      $gnomons[2](4), $gnomons[1](2), $gnomons[2](3)
    )=>map:put("pid", 2),
    for $k in 3 to $kmax + 1 return (
      path:triangle(
        $gnomons[$k](4), $gnomons[$k - 1](4), $gnomons[$k](3)
      )=>map:put("pid", $k)
    )
  )
  let $outer-spiral := (
    let $n-points := 50
    let $no-gain := function ($t as xs:double) as xs:double {1}
    let $z0 := point:centroid($gnomons[1]?*)
    let $θ := (7 * math:pi() div 6, math:pi() div 2)
    let $r := point:distance($z0, geom:vertices($triangles[1])[1])
    return (
      this:complex-circular-arc($r, $z0, $θ, $n-points, $no-gain),
      fold-left(2 to $kmax + 1, ($θ, ()), function ($data as item()*, $k as xs:integer) as item()* {
        let $θ := (head($data), head(tail($data)))
        let $zstep := tail(tail($data))
        let $w := $triangles[$k]
        let $θ := $θ!(. + -2 * math:pi() div 3)
        let $z0 := point:centroid(geom:vertices($w)[position() < last()])
        let $r := point:distance($z0, geom:vertices($triangles[$k])[1])
        let $c4th := this:complex-circular-arc($r, $z0, $θ, $n-points, $no-gain)
        return (
          ($θ, ($zstep, $c4th))
        )
      })=>tail()=>tail()
    )
  )
  return (
    if ($debug) then path:path(geom:to-edges($outer-spiral)) else (),
    $triangles
  )
};

(:~
 : circle-spiral()
 : Fill a circle with a spiral with the given number of turns.
 :
 : @param $circle: the circle to fill
 : @param $turns: how many turns of the spiral
 : @param $fineness: how many points per turn
 : @return simple path of the points
 :)
declare function this:circle-spiral(
  $circle as map(xs:string,item()*),
  $turns as xs:double,
  $fineness as xs:integer
) as map(xs:string,item()*)
{
  let $center := ellipse:center($circle)
  let $radius := ellipse:radius($circle)
  let $n-points := util:round($turns * $fineness)
  let $f :=
    function ($t as xs:double) as map(xs:string,item()*) {
      point:destination(
        $center,
        360 * ($t mod 1),
        $radius * ($t div $turns)
      )
    }
  return (
    curve:plot($n-points, $f, $turns, false(), false())
  )
};

(:~
 : circle-spiral()
 : Fill a circle with a spiral with the given number of turns using default
 : fineness (50).
 :
 : @param $circle: the circle to fill
 : @param $turns: how many turns of the spiral
 : @return simple path of the points
 :)
declare function this:circle-spiral(
  $circle as map(xs:string,item()*),
  $turns as xs:double
) as map(xs:string,item()*)
{
  this:circle-spiral($circle, $turns, 50)
};

(:~
 : bend()
 : Map a path onto a circular arc. Each point in the path is mapped to a point
 : an equivalent fraction along the arc. The y-axis of each input point is mapped
 : to a point perpendicular to the target arc at the corresponding point with the
 : distance from the arc being the distance of the input point from the midline
 : of the bounding box of the input path, scaled. This method is therefore most
 : appropriate for paths that are broadly horizontal.
 :
 : @param $path: input path
 : @param $center: center of circular arc
 : @param $radius: radius of circular arc
 : @param $arc: degrees of arc (starting at 0°)
 : @param $scaling: how much to scale the y distance
 : @return mapped points of the input path
 :)
declare function this:bend(
  $path as map(xs:string,item()*),
  $center as map(xs:string,item()*),
  $radius as xs:double,
  $arc as xs:double, (: degrees :)
  $scaling as xs:double
) as map(xs:string,item()*)*
{
  let $path := $path=>path:with-edge-ts()
  let $edge-ts := (0.0, $path("edge-ts"))
  let $points := path:vertices($path)
  let $n := count($points)
  let $bb := geom:bounding-box($points)
  let $mid-y := box:min-py($bb) + box:height($bb) div 2
  for $p at $i in $points
  let $t := $edge-ts[$i]
  let $angle := $t * $arc
  let $d := $radius + (point:py($p) - $mid-y)*$scaling
  return $center=>point:destination($angle, $d)
};

(:~
 : bend-path()
 : Map one path onto another. Each point in the input path is mapped to a point
 : an equivalent fraction along the target path. The y-axis of each input point
 : is mapped to a point perpendicular to the target path at the corresponding point
 : with the distance from the path being the distance of the input point from the
 : midline of the bounding box of the input path, scaled. This method is therefore
 : most appropriate for paths that are broadly horizontal.
 :
 : @param $path: input path
 : @param $center: center of circular arc
 : @param $radius: radius of circular arc
 : @param $arc: degrees of arc (starting at 0°)
 : @param $scaling: how much to scale the y distance
 : @return mapped points of the input path
 :)
declare function this:bend-path(
  $path as map(xs:string,item()*),
  $target-path as map(xs:string,item()*),
  $scaling as xs:double
) as map(xs:string,item()*)*
{
  let $path := $path=>path:with-edge-ts()
  let $edge-ts := (0.0, $path("edge-ts"))
  let $points := path:vertices($path)
  let $target-path := $target-path=>path:with-edge-ts()
  let $n := count($points)
  let $bb := geom:bounding-box($points)
  let $mid-y := box:min-py($bb) + box:height($bb) div 2
  for $p at $i in $points
  let $t := $edge-ts[$i]
  let $angle := $target-path=>geom:normal-angle($t)
  let $d := (point:py($p) - $mid-y)*$scaling
  return $target-path=>path:path-point($t)=>point:destination($angle, $d)
};

(:~
 : stroke-path()
 : Convert the path into a stroke that smoothly widens towards the middle
 : and tapers to the ends.
 :
 : @param $path: source path (Bézier ok, arcs not)
 : @param $fatness: how wide to get at the middle
 : @return polygon for the stroke
 :)
declare function this:stroke-path(
  $path as map(xs:string,item()*),
  $fatness as xs:double
) as map(xs:string,item()*)
{
  let $reverse := (
    let $rev-path := path:reverse($path)=>path:with-edge-ts()
    let $edge-ts := (0.0, $rev-path("edge-ts"))
    for $edge at $i in path:edges($rev-path)
    let $nstart := geom:normal-angle($edge, 0.0)
    let $nend := geom:normal-angle($edge, 1.0)
    let $dstart := util:mix(0, $fatness, 0.5 - abs(0.5 - $edge-ts[$i]))
    let $dend := util:mix(0, $fatness, 0.5 - abs(0.5 - $edge-ts[$i + 1]))
    let $nmid := geom:normal-angle($edge, 0.5)
    let $dmid := avg(($dstart, $dend))
    return (
      switch(edge:kind($edge))
      case "edge" return (
        edge:edge(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend)
        )
      )
      case "quad" return (
        edge:quad(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend),
          edge:controls($edge)[1]=>point:destination($nmid, $dmid)
        )
      )
      case "cubic" return (
        edge:cubic(
          edge:start($edge)=>point:destination($nstart, $dstart),
          edge:end($edge)=>point:destination($nend, $dend),
          edge:controls($edge)[1]=>point:destination($nmid, $dmid),
          edge:controls($edge)[2]=>point:destination($nmid, $dmid)
        )
      )
      default return $edge
    )
  )
  return (
    util:merge-into($path=>path:property-map(),
      path:polygon( (path:edges($path), $reverse) )
    )
  )
};