http://mathling.com/art/curl  library module

http://mathling.com/art/curl


Curls: circle arc curls

Parameters:
curl.mode: what kinds of curls are we making?
Acts as a master switch for default parameter settings
smooth: fat-sinuous-curl
tangent-connected arcs, sliced by rotation from origin with translation
kinked: fat-kinked-curl
rotated from origin, but buggy in an interesting way
interpolated: fat-interpolated-curl
sliced by splines created via tropism function
smoke: smoky-curl
sliced by messing with radius and angle of minicurls
ribbon: ribbon
tangent-connected arc, sliced by rotation from random connection point
curl.scaling: basic sizing of curls
curl.generations: mean number of generations
curl.fickleness: standard deviation of number of generations (as fraction)
curl.fade: how size of curls fades with generation
curl.angle.min: minumum arc of curls
curl.angle.max: maximum arc of curls
curl.radius.min: minimum radius of curls

curl.n-slices: how many rendering slices to do
curl.slice.extent: extent of slices
curl.angle.extent: extent of angles of slices
curl.interpolation: how much to interpolate points in paths
curl.jitter.percent: how much to jitter interpolation

curl.gradients: gradient palettes to use in slicing
curl.opacity: base opacity of curl slices
curl.opacity.fade: fade of opacity across slices
curl.opacity.mode: mode to apply opacity fade
none: all slices have same opacity
symmetric: slices fade to edges
top: slices fade from first to last
bottom: slices fade from last to first
curl.colour.mode: colour selection mode
singular: every slice the same colour from gradient
multiple: every slice a random colour from gradient
centered: every slice a random colour centered on single random colour
sliced: colours sliced across gradient

curl.tropism.function: tropism function to use for slicing (interpolated)

Randomizers:
curl.angle: angle of extent for minicurls
curl.radius: radius of minicurls
curl.generations: how many minicurls
curl.colour: colouring for curl slices

Rendering parameters:
colours.{1-5}.gradient, populated from curl.gradients
stroke-opacity, defaults to empty to support slicing

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

Status: Stable

Function Index

Imports

http://mathling.com/type/curl
import module namespace curl="http://mathling.com/type/curl"
       at "../types/curl.xqy"
http://mathling.com/math
import module namespace mmath="http://mathling.com/math"
       at "../math/math.xqy"
http://mathling.com/svg/gradients
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy"
http://mathling.com/type/distribution
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric/spline
import module namespace spline="http://mathling.com/geometric/spline"
       at "../geo/spline.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/type/defref
import module namespace defref="http://mathling.com/type/defref"
       at "../types/defref.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.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"

Functions

Function: component-map
declare function component-map($render as xs:boolean, $mode as xs:string) as map(xs:string,item()*)


Map to use in components:expand in caller.

Params
  • render as xs:boolean
  • mode as xs:string
Returns
  • map(xs:string,item()*)
declare function this:component-map(
  $render as xs:boolean,
  $mode as xs:string
) as map(xs:string,item()*)
{
  map {
    "namespace": "http://mathling.com/art/curl",
    "render": true(),
    "mode": "default"
  }
}

Function: component-map
declare function component-map($render as xs:boolean) as map(xs:string,item()*)

Params
  • render as xs:boolean
Returns
  • map(xs:string,item()*)
declare function this:component-map(
  $render as xs:boolean
) as map(xs:string,item()*)
{
  this:component-map($render, "default")
}

Function: component-map
declare function component-map() as map(xs:string,item()*)

Returns
  • map(xs:string,item()*)
declare function this:component-map(
) as map(xs:string,item()*)
{
  this:component-map(true())
}

Function: rendering-parameters
declare function rendering-parameters($canvas as map(xs:string,item()*), $algorithm-parameters as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • canvas as map(xs:string,item()*)
  • algorithm-parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:rendering-parameters(
  $canvas as map(xs:string,item()*),
  $algorithm-parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $all-gradients := core:parameter("curl.gradients", $algorithm-parameters)
  let $mode := core:parameter("curl.mode", $algorithm-parameters)
  return
    map {
      "meta-description": "Arc curls",
      "colours.1.gradient": $all-gradients[1],
      "colours.2.gradient": $all-gradients[2],
      "colours.3.gradient": $all-gradients[3],
      "colours.4.gradient": $all-gradients[4],
      "colours.5.gradient": $all-gradients[5],
      "stroke-opacity": ""
    }
}

Function: algorithm-mode-parameters
declare function algorithm-mode-parameters($resolution as xs:string, $canvas as map(xs:string,item()*), $mode as xs:string, $reverse as xs:boolean) as map(xs:string,item()*)

Params
  • resolution as xs:string
  • canvas as map(xs:string,item()*)
  • mode as xs:string
  • reverse as xs:boolean
Returns
  • map(xs:string,item()*)
declare function this:algorithm-mode-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string,item()*),
  $mode as xs:string,
  $reverse as xs:boolean
) as map(xs:string,item()*)
{
  let $basic-scaling := box:width(core:canvas($resolution)) idiv 7
  return
  switch ($mode)
  case "smooth" return
    map {
      "description": "Arc curls: smooth",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 10,
      "curl.angle.extent": 10,
      "curl.opacity": 1,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "interpolated" return
    map {
      "description": "Arc curls: interpolated",
      "curl.mode": $mode,
      "curl.scaling": 1.5 * $basic-scaling,
      "curl.generations": 10,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("curl"),
      "curl.n-slices": 50,
      "curl.slice.extent": 75,
      "curl.angle.extent": 20,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 10,
      "curl.jitter.percent": 0
    }
  case "kinked" return
    map {
      "description": "Arc curls: kinked",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5, (: 0.2 :)
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 20,
      "curl.angle.extent": 20,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "smoke" return
    map {
      "description": "Arc curls: smoke",
      "curl.mode": $mode,
      "curl.scaling": 0.1 * $basic-scaling,
      "curl.generations": 12,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 30,
      "curl.angle.max": 330,
      "curl.gradients": ("lajolla"),
      "curl.n-slices": 150,
      "curl.slice.extent": 15,
      "curl.angle.extent": 5,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "top",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "ribbon" return
    map {
      "description": "Arc curls: ribbon",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 10,
      "curl.angle.extent": 10,
      "curl.opacity": 1,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "symmetric",
      "curl.colour.mode": "centered",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  default return map {}
}

Function: algorithm-mode-parameters
declare function algorithm-mode-parameters($mode-and-reverse as xs:string, $resolution as xs:string, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • mode-and-reverse as xs:string
  • resolution as xs:string
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:algorithm-mode-parameters(
  $mode-and-reverse as xs:string,
  $resolution as xs:string,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $mode := substring-before($mode-and-reverse,'-')
  let $reverse :=
    if (substring-after($mode-and-reverse,'-')="reverse") then true() else false()
  return this:algorithm-mode-parameters($resolution, $canvas, $mode, $reverse)
}

Function: algorithm-parameters
declare function algorithm-parameters($resolution as xs:string, $canvas as map(xs:string, item()*)) as map(xs:string,item()*)

Params
  • resolution as xs:string
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:algorithm-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string, item()*)
) as map(xs:string,item()*)
{
  this:algorithm-mode-parameters($resolution, $canvas, "smooth", false())
}

Function: randomizers
declare function randomizers($canvas as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • canvas as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:randomizers(
  $canvas as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "curl.angle":
      dist:uniform(
        core:parameter("curl.angle.min", $parameters),
        core:parameter("curl.angle.max", $parameters)
      )=>dist:cast("integer")
    ,
    "curl.radius":
      dist:normal(
        core:parameter("curl.scaling", $parameters),
        core:parameter("curl.scaling", $parameters) idiv 10
      )=>dist:min(core:parameter("curl.radius.min", $parameters))
       =>dist:truncation("ceiling")
       =>dist:cast("integer")
    ,
    "curl.generations":
      let $generations := core:parameter("curl.generations", $parameters)
      return
      dist:normal(
        $generations,
        min((
          1,
          mmath:round(
            $generations *
            core:parameter("curl.fickleness", $parameters)
          )
        ))
      )=>dist:min(1)=>dist:cast("integer")
    ,
    (: Note: this gets std/max adjusted for additional gradients if necessary :)
    "curl.colour":
      let $n := gradient:n-gradient-colours((core:parameter("curl.gradients", $parameters))[1])
      return
        dist:normal(rand:get-last#2, $n idiv 20)=>
          dist:cast("integer")=>
          dist:min(1)=>
          dist:max($n)=>
          dist:truncation("ceiling")
  }
}

Function: colophon
declare function colophon($parameters as map(xs:string,item()*)) as xs:string?

Params
  • parameters as map(xs:string,item()*)
Returns
  • xs:string?
declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string?
{
  "Arc curls created by stitching together circle arcs at their tangents"
}

Function: metadata
declare function metadata($canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*))

Params
  • canvas as map(xs:string,item()*)
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
declare function this:metadata(
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
)
{
   ()
}

Function: minicurl
declare function minicurl($radius as xs:double, $angle as xs:double) as map(xs:string,item()*)

Params
  • radius as xs:double
  • angle as xs:double
Returns
  • map(xs:string,item()*)
declare function this:minicurl($radius as xs:double, $angle as xs:double) as map(xs:string,item()*)
{
  map {
    "radius": $radius,
    "angle": $angle
  }
}

Function: minicurl
declare function minicurl($radius as xs:double, $angle as xs:double, $initial-angle as xs:double) as map(xs:string,item()*)

Params
  • radius as xs:double
  • angle as xs:double
  • initial-angle as xs:double
Returns
  • map(xs:string,item()*)
declare function this:minicurl($radius as xs:double, $angle as xs:double, $initial-angle as xs:double) as map(xs:string,item()*)
{
  this:minicurl($radius, $angle)=>
    map:put("initial-angle", $initial-angle)
}

Function: radius
declare function radius($minicurl as map(xs:string,item()*)) as xs:double

Params
  • minicurl as map(xs:string,item()*)
Returns
  • xs:double
declare function this:radius($minicurl as map(xs:string,item()*)) as xs:double
{
  $minicurl("radius")
}

Function: angle
declare function angle($minicurl as map(xs:string,item()*)) as xs:double

Params
  • minicurl as map(xs:string,item()*)
Returns
  • xs:double
declare function this:angle($minicurl as map(xs:string,item()*)) as xs:double
{
  $minicurl("angle")
}

Function: initial-angle
declare function initial-angle($minicurl as map(xs:string,item()*)) as xs:double

Params
  • minicurl as map(xs:string,item()*)
Returns
  • xs:double
declare function this:initial-angle($minicurl as map(xs:string,item()*)) as xs:double
{
  ($minicurl("initial-angle"),0)[1]
}

Function: describe
declare function describe($minicurls as map(xs:string,item()*)*) as xs:string

Params
  • minicurls as map(xs:string,item()*)*
Returns
  • xs:string
declare function this:describe($minicurls as map(xs:string,item()*)*) as xs:string
{
  string-join(
    for $mini in $minicurls return (
      this:radius($mini)||"∠"||this:angle($mini)||
      (if (this:initial-angle($mini)!=0)
       then "+"||this:initial-angle($mini)
       else "")
    )," "
  )
}

Function: minicurls
declare function minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • curl as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $generations := $curl=>curl:generations()
  let $initial-angle := $curl=>curl:initial-angle()
  let $fade := $curl=>curl:fade()
  let $radii := curl:radii($generations, $curl)
  let $angles := curl:angles($generations, $curl)
  for $i in 1 to $generations
  let $radius :=
    mmath:round(math:pow($fade,($i - 1)) * $radii[$i])
  let $angle := mmath:round(math:pow($fade,($i - 1)) * $angles[$i])
  return
    if ($i=1)
    then this:minicurl($radius, $angle, curl:initial-angle($curl))
    else this:minicurl($radius, $angle)
}

Function: symmetric-minicurls
declare function symmetric-minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • curl as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:symmetric-minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $generations := $curl=>curl:generations()
  let $initial-angle := $curl=>curl:initial-angle()
  let $fade := $curl=>curl:fade()
  let $radii := curl:radii($generations, $curl)
  let $angles := curl:angles($generations, $curl)
  for $i in 1 to $generations
  let $pow-fade := math:pow($fade,abs(($generations idiv 2) - ($i - 1)))
  let $radius := mmath:round($pow-fade * $radii[$i])
  let $angle := mmath:round($pow-fade * $angles[$i])
  return
    if ($i=1)
    then this:minicurl($radius, $angle, curl:initial-angle($curl))
    else this:minicurl($radius, $angle)
}

Function: minicurls
declare function minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      core:randomize("curl.angle", $randomizers),
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
}

Function: minicurls
declare function minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • initial-angle as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      $initial-angle,
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
}

Function: symmetric-minicurls
declare function symmetric-minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:symmetric-minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:symmetric-minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      core:randomize("curl.angle", $randomizers),
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
}

Function: symmetric-minicurls
declare function symmetric-minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • initial-angle as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:symmetric-minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:symmetric-minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      $initial-angle,
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
}

Function: curl
declare function curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*) as map(xs:string,item()*)?

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)?
declare function this:curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*) as map(xs:string,item()*)?
{
  let $edges :=
    fold-left(1 to count($minicurls), (),
      function($edges as map(xs:string,item()*)*, $i as xs:integer) {
        $edges,
        let $last := $edges[last()]
        let $start := if (empty($edges)) then $origin else $last=>edge:end()
        let $lω :=
          if (empty($edges)) then $minicurls[$i]=>this:initial-angle()
          else mmath:round(point:angle($last=>edge:arc-center(), $last=>edge:end()))
        let $last-flipped :=
          if (empty($edges)) then true() else $last=>edge:arc-flipped()
        let $last-radius :=
          if (empty($edges))
          then 0
          else $last=>edge:arc-radius()
        let $last-center :=
          if (empty($edges))
          then $origin
          else $last=>edge:arc-center()
        let $α := $minicurls[$i]=>this:angle()
        let $radius := $minicurls[$i]=>this:radius()
        let $center :=
          point:destination($last-center, $lω, $last-radius + $radius)
        let $θ :=
          mmath:remap-degrees(point:angle($center, $start))
        let $end :=
          if ($last-flipped)
          then point:snap(point:destination($center, $θ + $α, $radius))
          else point:snap(point:destination($center, $θ - $α, $radius))
        return (
          edge:arc($center, $radius, $start, $end, not($last-flipped), $α > 180)
        )
      }
    )
  where exists($edges)
  return path:path($edges)
}

Function: fat-sinuous-curl
declare function fat-sinuous-curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:fat-sinuous-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $rotation := mmath:decimal($i * $angle-sep, 2)
    let $translation := mmath:decimal($i * $slice-sep, 2)
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "translate("||$translation||","||$translation||") scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($origin)||","||point:y($origin)||")"
        }
      )
  )
}

Function: fat-kinked-curl
declare function fat-kinked-curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:fat-kinked-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  for $i in 1 to $n-slices
  let $edges :=
    for $edge in $curl=>geom:edges()
    let $ends := $edge=>edge:arc-ends()
    let $start := geom:translate($ends[1], $i * $slice-sep, $i * $slice-sep)
    let $end := geom:translate($ends[2], $i * $slice-sep, $i * $slice-sep)
    return
       edge:arc(
         $edge=>edge:arc-center(),
         $edge=>edge:arc-radius() + $i * $slice-sep,
         $start,
         $end,
         $edge=>edge:arc-flipped(),
         $edge=>edge:arc-large()
       )
  let $rotated := geom:rotate(path:path($edges), $i * $angle-sep, $origin)
  let $scaled := geom:scale($rotated, $scale, $scale, $origin)
  let $opacity :=
    switch ($opacity-mode)
    case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
    case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
    case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
    default return $curl-opacity * $fade-scale
  let $stop :=
    switch($colour-mode)
    case "singular" return $single-stop
    case "multiple" return rand:select-random(1 to $n-colours)
    case "centered" return rand:randomize(1, $single-stop, $centered-colour)
    case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
    default return 1 + ceiling($gradient-sep*($i - 1))
  let $properties :=
    map {
      "gradient": $gradient-ix,
      "stop": $stop,
      "stroke-opacity": mmath:decimal($opacity,2)
    }
  return $scaled=>geom:with-properties($properties)
}

Function: smoky-curl
declare function smoky-curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:smoky-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $angle-min := core:parameter("curl.angle.min", $parameters)
  let $radius-min := core:parameter("curl.radius.min", $parameters)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  for $i in 1 to $n-slices
  let $newcurls :=
    for $minicurl in $minicurls
    let $radius := rand:randomize(dist:normal($minicurl=>this:radius(), $i * $slice-sep)=>dist:min($radius-min))
    let $angle := rand:randomize(dist:normal($minicurl=>this:angle(), $i * $slice-sep)=>dist:min($angle-min))
    return this:minicurl($radius, $angle)
  let $curl := this:curl($origin, $newcurls)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $opacity :=
    switch ($opacity-mode)
    case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
    case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
    case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
    default return $curl-opacity * $fade-scale
  let $stop :=
    switch($colour-mode)
    case "singular" return $single-stop
    case "multiple" return rand:select-random(1 to $n-colours)
    case "centered" return rand:randomize(1, $single-stop, $centered-colour)
    case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
    default return 1 + ceiling($gradient-sep*($i - 1))
  let $properties :=
    map {
      "gradient": $gradient-ix,
      "stop": $stop,
      "stroke-opacity": mmath:decimal($opacity,2)
    }
  return $curl=>geom:with-properties($properties)
}

Function: tropism
declare function tropism($pt as map(xs:string,item()*), $origin as map(xs:string,item()*), $i as xs:integer, $d as xs:double)

Params
  • pt as map(xs:string,item()*)
  • origin as map(xs:string,item()*)
  • i as xs:integer
  • d as xs:double
declare function this:tropism($pt as map(xs:string,item()*), $origin as map(xs:string,item()*), $i as xs:integer, $d as xs:double)
{
  let $θ := point:angle($origin, $pt)
  return
    point:snap(
      geom:translate($pt,
        $i * $d * math:cos(5*$θ) * math:cos(3*$θ),
        $i * $d * math:sin(5*$θ) * math:cos(3*$θ)
      )
    )
}

Function: fat-interpolated-curl
declare function fat-interpolated-curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:fat-interpolated-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $path :=
    if ($curl-interpolation > 0) then (
      if ($curl-jitter-percent > 0) then (
        geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
      ) else (
        geom:interpolate($curl, $curl-interpolation)
      )
    ) else (
      $curl=>geom:points()
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $tropism := core:parameter("curl.tropism.function", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  return
    fold-left(1 to $n-slices, spline:open-spline($path),
      function($paths as map(xs:string,item()*)*, $i as xs:integer) {
        $paths,
        let $points :=
          for $point in $path
          return $tropism($point, $origin, $i, $slice-sep)
        let $opacity :=
          switch ($opacity-mode)
          case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
          case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
          case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
          default return $curl-opacity * $fade-scale
        let $stop :=
          switch($colour-mode)
          case "singular" return $single-stop
          case "multiple" return rand:select-random(1 to $n-colours)
          case "centered" return rand:randomize(1, $single-stop, $centered-colour)
          case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
          default return 1 + ceiling($gradient-sep*($i - 1))
        let $properties :=
          map {
            "gradient": $gradient-ix,
            "stop": $stop,
            "stroke-opacity": mmath:decimal($opacity,2)
          }
        return spline:open-spline($points)=>geom:with-properties($properties)
      }
    )
}

Function: ribbon
declare function ribbon($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:ribbon(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $pivot := rand:select-random($curl=>geom:points())
  return
    this:ribbon(
      $origin,
      $minicurls,
      $pivot,
      $scale,
      $gradient-ix,
      $fade-scale,
      $randomizers,
      $parameters
    )
}

Function: ribbon
declare function ribbon($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, $pivot as map(xs:string,item()*), $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • pivot as map(xs:string,item()*)
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:ribbon(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $pivot as map(xs:string,item()*),
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $rotation := ($i - 1) * $angle-sep
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($pivot)||","||point:y($pivot)||")"
        }
     )
  )
}

Function: sheet
declare function sheet($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*, (: minicurl* :) $pivot-path as map(xs:string,item()*), (: path :) $scale as xs:double, $gradient-ix as xs:integer, $fade-scale as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • origin as map(xs:string,item()*)
  • minicurls as map(xs:string,item()*)*
  • pivot-path as map(xs:string,item()*)
  • scale as xs:double
  • gradient-ix as xs:integer
  • fade-scale as xs:double
  • randomizers as map(xs:string,item()*)
  • parameters as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:sheet(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*, (: minicurl* :)
  $pivot-path as map(xs:string,item()*), (: path :)
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $pivot-points :=
    let $n-edges := count($pivot-path=>geom:edges())
    return
      if ($n-edges <= $n-slices)
      then geom:interpolate($pivot-path, 1 + ($n-slices idiv $n-edges))=>geom:points()
      else $pivot-path=>geom:points()
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $pivot := $pivot-points[$i]
    let $rotation := ($i - 1) * $angle-sep
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($pivot)||","||point:y($pivot)||")"
        }
      )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : Curls: circle arc curls
 :
 : Parameters:
 :   curl.mode: what kinds of curls are we making?
 :     Acts as a master switch for default parameter settings
 :     smooth: fat-sinuous-curl
 :        tangent-connected arcs, sliced by rotation from origin with translation
 :     kinked: fat-kinked-curl
 :        rotated from origin, but buggy in an interesting way
 :     interpolated: fat-interpolated-curl
 :        sliced by splines created via tropism function
 :     smoke: smoky-curl
 :        sliced by messing with radius and angle of minicurls
 :     ribbon: ribbon
 :        tangent-connected arc, sliced by rotation from random connection point
 :   curl.scaling: basic sizing of curls
 :   curl.generations: mean number of generations
 :   curl.fickleness: standard deviation of number of generations (as fraction)
 :   curl.fade: how size of curls fades with generation
 :   curl.angle.min: minumum arc of curls
 :   curl.angle.max: maximum arc of curls
 :   curl.radius.min: minimum radius of curls
 :
 :   curl.n-slices: how many rendering slices to do
 :   curl.slice.extent: extent of slices
 :   curl.angle.extent: extent of angles of slices
 :   curl.interpolation: how much to interpolate points in paths
 :   curl.jitter.percent: how much to jitter interpolation
 :
 :   curl.gradients: gradient palettes to use in slicing
 :   curl.opacity: base opacity of curl slices
 :   curl.opacity.fade: fade of opacity across slices
 :   curl.opacity.mode: mode to apply opacity fade
 :     none: all slices have same opacity
 :     symmetric: slices fade to edges
 :     top: slices fade from first to last
 :     bottom: slices fade from last to first
 :   curl.colour.mode: colour selection mode
 :     singular: every slice the same colour from gradient
 :     multiple: every slice a random colour from gradient
 :     centered: every slice a random colour centered on single random colour
 :     sliced: colours sliced across gradient
 :
 :   curl.tropism.function: tropism function to use for slicing (interpolated)
 :
 : Randomizers:
 :   curl.angle: angle of extent for minicurls
 :   curl.radius: radius of minicurls
 :   curl.generations: how many minicurls
 :   curl.colour: colouring for curl slices
 :
 : Rendering parameters:
 :   colours.{1-5}.gradient, populated from curl.gradients
 :   stroke-opacity, defaults to empty to support slicing
 :
 : Copyright© Mary Holstege 2020-2025
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @custom:Status Stable
 :)
module namespace this="http://mathling.com/art/curl";

import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy";
import module namespace mmath="http://mathling.com/math"
       at "../math/math.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.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 box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.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 spline="http://mathling.com/geometric/spline"
       at "../geo/spline.xqy";
import module namespace curl="http://mathling.com/type/curl"
       at "../types/curl.xqy";
import module namespace defref="http://mathling.com/type/defref"
       at "../types/defref.xqy";
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy";

declare namespace svg="http://www.w3.org/2000/svg";
declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";
declare namespace art="http://mathling.com/art";

(:~
 : Map to use in components:expand in caller.
 :)
declare function this:component-map(
  $render as xs:boolean,
  $mode as xs:string
) as map(xs:string,item()*)
{
  map {
    "namespace": "http://mathling.com/art/curl",
    "render": true(),
    "mode": "default"
  }
};

declare function this:component-map(
  $render as xs:boolean
) as map(xs:string,item()*)
{
  this:component-map($render, "default")
};

declare function this:component-map(
) as map(xs:string,item()*)
{
  this:component-map(true())
};

declare function this:rendering-parameters(
  $canvas as map(xs:string,item()*),
  $algorithm-parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $all-gradients := core:parameter("curl.gradients", $algorithm-parameters)
  let $mode := core:parameter("curl.mode", $algorithm-parameters)
  return
    map {
      "meta-description": "Arc curls",
      "colours.1.gradient": $all-gradients[1],
      "colours.2.gradient": $all-gradients[2],
      "colours.3.gradient": $all-gradients[3],
      "colours.4.gradient": $all-gradients[4],
      "colours.5.gradient": $all-gradients[5],
      "stroke-opacity": ""
    }
};

declare function this:algorithm-mode-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string,item()*),
  $mode as xs:string,
  $reverse as xs:boolean
) as map(xs:string,item()*)
{
  let $basic-scaling := box:width(core:canvas($resolution)) idiv 7
  return
  switch ($mode)
  case "smooth" return
    map {
      "description": "Arc curls: smooth",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 10,
      "curl.angle.extent": 10,
      "curl.opacity": 1,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "interpolated" return
    map {
      "description": "Arc curls: interpolated",
      "curl.mode": $mode,
      "curl.scaling": 1.5 * $basic-scaling,
      "curl.generations": 10,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("curl"),
      "curl.n-slices": 50,
      "curl.slice.extent": 75,
      "curl.angle.extent": 20,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 10,
      "curl.jitter.percent": 0
    }
  case "kinked" return
    map {
      "description": "Arc curls: kinked",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5, (: 0.2 :)
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 20,
      "curl.angle.extent": 20,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "none",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "smoke" return
    map {
      "description": "Arc curls: smoke",
      "curl.mode": $mode,
      "curl.scaling": 0.1 * $basic-scaling,
      "curl.generations": 12,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 30,
      "curl.angle.max": 330,
      "curl.gradients": ("lajolla"),
      "curl.n-slices": 150,
      "curl.slice.extent": 15,
      "curl.angle.extent": 5,
      "curl.opacity": 0.6,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "top",
      "curl.colour.mode": "sliced",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  case "ribbon" return
    map {
      "description": "Arc curls: ribbon",
      "curl.mode": $mode,
      "curl.scaling":
        (: if ($reverse) then $basic-scaling * 0.1 else :) $basic-scaling
      ,
      "curl.generations": if ($reverse) then 12 else 15,
      "curl.fickleness": 0.5,
      "curl.fade": if ($reverse) then 1.25 else 0.7,
      "curl.radius.min": 10,
      "curl.angle.min": 10,
      "curl.angle.max": 30,
      "curl.gradients": ("lajolla", "turbidity"),
      "curl.n-slices": 200,
      "curl.slice.extent": 10,
      "curl.angle.extent": 10,
      "curl.opacity": 1,
      "curl.opacity.fade": 0.99,
      "curl.opacity.mode": "symmetric",
      "curl.colour.mode": "centered",
      "curl.tropism.function": this:tropism#4,
      "curl.interpolation": 0,
      "curl.jitter.percent": 0
    }
  default return map {}
};

declare function this:algorithm-mode-parameters(
  $mode-and-reverse as xs:string,
  $resolution as xs:string,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $mode := substring-before($mode-and-reverse,'-')
  let $reverse :=
    if (substring-after($mode-and-reverse,'-')="reverse") then true() else false()
  return this:algorithm-mode-parameters($resolution, $canvas, $mode, $reverse)
};

declare function this:algorithm-parameters(
  $resolution as xs:string,
  $canvas as map(xs:string, item()*)
) as map(xs:string,item()*)
{
  this:algorithm-mode-parameters($resolution, $canvas, "smooth", false())
};

declare function this:randomizers(
  $canvas as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  map {
    "curl.angle":
      dist:uniform(
        core:parameter("curl.angle.min", $parameters),
        core:parameter("curl.angle.max", $parameters)
      )=>dist:cast("integer")
    ,
    "curl.radius":
      dist:normal(
        core:parameter("curl.scaling", $parameters),
        core:parameter("curl.scaling", $parameters) idiv 10
      )=>dist:min(core:parameter("curl.radius.min", $parameters))
       =>dist:truncation("ceiling")
       =>dist:cast("integer")
    ,
    "curl.generations":
      let $generations := core:parameter("curl.generations", $parameters)
      return
      dist:normal(
        $generations,
        min((
          1,
          mmath:round(
            $generations *
            core:parameter("curl.fickleness", $parameters)
          )
        ))
      )=>dist:min(1)=>dist:cast("integer")
    ,
    (: Note: this gets std/max adjusted for additional gradients if necessary :)
    "curl.colour":
      let $n := gradient:n-gradient-colours((core:parameter("curl.gradients", $parameters))[1])
      return
        dist:normal(rand:get-last#2, $n idiv 20)=>
          dist:cast("integer")=>
          dist:min(1)=>
          dist:max($n)=>
          dist:truncation("ceiling")
  }
};

declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string?
{
  "Arc curls created by stitching together circle arcs at their tangents"
};

declare function this:metadata(
  $canvas as map(xs:string,item()*),
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
)
{
   ()
};

declare function this:minicurl($radius as xs:double, $angle as xs:double) as map(xs:string,item()*)
{
  map {
    "radius": $radius,
    "angle": $angle
  }
};

declare function this:minicurl($radius as xs:double, $angle as xs:double, $initial-angle as xs:double) as map(xs:string,item()*)
{
  this:minicurl($radius, $angle)=>
    map:put("initial-angle", $initial-angle)
};

declare function this:radius($minicurl as map(xs:string,item()*)) as xs:double
{
  $minicurl("radius")
};

declare function this:angle($minicurl as map(xs:string,item()*)) as xs:double
{
  $minicurl("angle")
};

declare function this:initial-angle($minicurl as map(xs:string,item()*)) as xs:double
{
  ($minicurl("initial-angle"),0)[1]
};

declare function this:describe($minicurls as map(xs:string,item()*)*) as xs:string
{
  string-join(
    for $mini in $minicurls return (
      this:radius($mini)||"∠"||this:angle($mini)||
      (if (this:initial-angle($mini)!=0)
       then "+"||this:initial-angle($mini)
       else "")
    )," "
  )
};

declare function this:minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $generations := $curl=>curl:generations()
  let $initial-angle := $curl=>curl:initial-angle()
  let $fade := $curl=>curl:fade()
  let $radii := curl:radii($generations, $curl)
  let $angles := curl:angles($generations, $curl)
  for $i in 1 to $generations
  let $radius :=
    mmath:round(math:pow($fade,($i - 1)) * $radii[$i])
  let $angle := mmath:round(math:pow($fade,($i - 1)) * $angles[$i])
  return
    if ($i=1)
    then this:minicurl($radius, $angle, curl:initial-angle($curl))
    else this:minicurl($radius, $angle)
};

(: Symmetrically faded, that is :)
declare function this:symmetric-minicurls($curl as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $generations := $curl=>curl:generations()
  let $initial-angle := $curl=>curl:initial-angle()
  let $fade := $curl=>curl:fade()
  let $radii := curl:radii($generations, $curl)
  let $angles := curl:angles($generations, $curl)
  for $i in 1 to $generations
  let $pow-fade := math:pow($fade,abs(($generations idiv 2) - ($i - 1)))
  let $radius := mmath:round($pow-fade * $radii[$i])
  let $angle := mmath:round($pow-fade * $angles[$i])
  return
    if ($i=1)
    then this:minicurl($radius, $angle, curl:initial-angle($curl))
    else this:minicurl($radius, $angle)
};

declare function this:minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      core:randomize("curl.angle", $randomizers),
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
};

declare function this:minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      $initial-angle,
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
};

declare function this:symmetric-minicurls($randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:symmetric-minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      core:randomize("curl.angle", $randomizers),
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
};

declare function this:symmetric-minicurls($initial-angle as xs:double, $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  this:symmetric-minicurls(
    curl:curl(
      core:randomize("curl.generations", $randomizers),
      $initial-angle,
      core:parameter("curl.fade", $parameters),
      core:randomizer("curl.radius", $randomizers),
      core:randomizer("curl.angle", $randomizers)
    )
  )
};

declare function this:curl($origin as map(xs:string,item()*), $minicurls as map(xs:string,item()*)*) as map(xs:string,item()*)?
{
  let $edges :=
    fold-left(1 to count($minicurls), (),
      function($edges as map(xs:string,item()*)*, $i as xs:integer) {
        $edges,
        let $last := $edges[last()]
        let $start := if (empty($edges)) then $origin else $last=>edge:end()
        let $lω :=
          if (empty($edges)) then $minicurls[$i]=>this:initial-angle()
          else mmath:round(point:angle($last=>edge:arc-center(), $last=>edge:end()))
        let $last-flipped :=
          if (empty($edges)) then true() else $last=>edge:arc-flipped()
        let $last-radius :=
          if (empty($edges))
          then 0
          else $last=>edge:arc-radius()
        let $last-center :=
          if (empty($edges))
          then $origin
          else $last=>edge:arc-center()
        let $α := $minicurls[$i]=>this:angle()
        let $radius := $minicurls[$i]=>this:radius()
        let $center :=
          point:destination($last-center, $lω, $last-radius + $radius)
        let $θ :=
          mmath:remap-degrees(point:angle($center, $start))
        let $end :=
          if ($last-flipped)
          then point:snap(point:destination($center, $θ + $α, $radius))
          else point:snap(point:destination($center, $θ - $α, $radius))
        return (
          edge:arc($center, $radius, $start, $end, not($last-flipped), $α > 180)
        )
      }
    )
  where exists($edges)
  return path:path($edges)
};

declare function this:fat-sinuous-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $rotation := mmath:decimal($i * $angle-sep, 2)
    let $translation := mmath:decimal($i * $slice-sep, 2)
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "translate("||$translation||","||$translation||") scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($origin)||","||point:y($origin)||")"
        }
      )
  )
};

declare function this:fat-kinked-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  for $i in 1 to $n-slices
  let $edges :=
    for $edge in $curl=>geom:edges()
    let $ends := $edge=>edge:arc-ends()
    let $start := geom:translate($ends[1], $i * $slice-sep, $i * $slice-sep)
    let $end := geom:translate($ends[2], $i * $slice-sep, $i * $slice-sep)
    return
       edge:arc(
         $edge=>edge:arc-center(),
         $edge=>edge:arc-radius() + $i * $slice-sep,
         $start,
         $end,
         $edge=>edge:arc-flipped(),
         $edge=>edge:arc-large()
       )
  let $rotated := geom:rotate(path:path($edges), $i * $angle-sep, $origin)
  let $scaled := geom:scale($rotated, $scale, $scale, $origin)
  let $opacity :=
    switch ($opacity-mode)
    case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
    case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
    case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
    default return $curl-opacity * $fade-scale
  let $stop :=
    switch($colour-mode)
    case "singular" return $single-stop
    case "multiple" return rand:select-random(1 to $n-colours)
    case "centered" return rand:randomize(1, $single-stop, $centered-colour)
    case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
    default return 1 + ceiling($gradient-sep*($i - 1))
  let $properties :=
    map {
      "gradient": $gradient-ix,
      "stop": $stop,
      "stroke-opacity": mmath:decimal($opacity,2)
    }
  return $scaled=>geom:with-properties($properties)
};

declare function this:smoky-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $angle-min := core:parameter("curl.angle.min", $parameters)
  let $radius-min := core:parameter("curl.radius.min", $parameters)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  for $i in 1 to $n-slices
  let $newcurls :=
    for $minicurl in $minicurls
    let $radius := rand:randomize(dist:normal($minicurl=>this:radius(), $i * $slice-sep)=>dist:min($radius-min))
    let $angle := rand:randomize(dist:normal($minicurl=>this:angle(), $i * $slice-sep)=>dist:min($angle-min))
    return this:minicurl($radius, $angle)
  let $curl := this:curl($origin, $newcurls)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $opacity :=
    switch ($opacity-mode)
    case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
    case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
    case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
    default return $curl-opacity * $fade-scale
  let $stop :=
    switch($colour-mode)
    case "singular" return $single-stop
    case "multiple" return rand:select-random(1 to $n-colours)
    case "centered" return rand:randomize(1, $single-stop, $centered-colour)
    case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
    default return 1 + ceiling($gradient-sep*($i - 1))
  let $properties :=
    map {
      "gradient": $gradient-ix,
      "stop": $stop,
      "stroke-opacity": mmath:decimal($opacity,2)
    }
  return $curl=>geom:with-properties($properties)
};

declare function this:tropism($pt as map(xs:string,item()*), $origin as map(xs:string,item()*), $i as xs:integer, $d as xs:double)
{
  let $θ := point:angle($origin, $pt)
  return
    point:snap(
      geom:translate($pt,
        $i * $d * math:cos(5*$θ) * math:cos(3*$θ),
        $i * $d * math:sin(5*$θ) * math:cos(3*$θ)
      )
    )
};

declare function this:fat-interpolated-curl(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $path :=
    if ($curl-interpolation > 0) then (
      if ($curl-jitter-percent > 0) then (
        geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
      ) else (
        geom:interpolate($curl, $curl-interpolation)
      )
    ) else (
      $curl=>geom:points()
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := count(gradient:gradient-colours($gradient))
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $tropism := core:parameter("curl.tropism.function", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  return
    fold-left(1 to $n-slices, spline:open-spline($path),
      function($paths as map(xs:string,item()*)*, $i as xs:integer) {
        $paths,
        let $points :=
          for $point in $path
          return $tropism($point, $origin, $i, $slice-sep)
        let $opacity :=
          switch ($opacity-mode)
          case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
          case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
          case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
          default return $curl-opacity * $fade-scale
        let $stop :=
          switch($colour-mode)
          case "singular" return $single-stop
          case "multiple" return rand:select-random(1 to $n-colours)
          case "centered" return rand:randomize(1, $single-stop, $centered-colour)
          case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
          default return 1 + ceiling($gradient-sep*($i - 1))
        let $properties :=
          map {
            "gradient": $gradient-ix,
            "stop": $stop,
            "stroke-opacity": mmath:decimal($opacity,2)
          }
        return spline:open-spline($points)=>geom:with-properties($properties)
      }
    )
};

declare function this:ribbon(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $pivot := rand:select-random($curl=>geom:points())
  return
    this:ribbon(
      $origin,
      $minicurls,
      $pivot,
      $scale,
      $gradient-ix,
      $fade-scale,
      $randomizers,
      $parameters
    )
};

declare function this:ribbon(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*,
  $pivot as map(xs:string,item()*),
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $rotation := ($i - 1) * $angle-sep
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($pivot)||","||point:y($pivot)||")"
        }
     )
  )
};

declare function this:sheet(
  $origin as map(xs:string,item()*),
  $minicurls as map(xs:string,item()*)*, (: minicurl* :)
  $pivot-path as map(xs:string,item()*), (: path :)
  $scale as xs:double,
  $gradient-ix as xs:integer,
  $fade-scale as xs:double,
  $randomizers as map(xs:string,item()*),
  $parameters as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $curl := this:curl($origin, $minicurls)
  let $curl-interpolation := core:parameter("curl.interpolation", $parameters)
  let $curl-jitter-percent := core:parameter("curl.jitter.percent", $parameters)
  let $curl :=
    if ($curl-interpolation > 0) then (
      path:path(edge:to-edges(
        if ($curl-jitter-percent > 0) then (
          geom:interpolate-jittered($curl, $curl-interpolation, $curl-jitter-percent)
        ) else (
          geom:interpolate($curl, $curl-interpolation)
        )
      ))
    ) else (
      $curl
    )
  let $all-gradients := core:parameter("curl.gradients", $parameters)
  let $gradient-ix := mmath:modix($gradient-ix, count($all-gradients))
  let $gradient := $all-gradients[$gradient-ix]
  let $n-colours := gradient:n-gradient-colours($gradient)
  let $_ := util:assert($n-colours!=0, "Missing gradient "||$gradient)
  let $curl-opacity := core:parameter("curl.opacity", $parameters)
  let $opacity-fade := core:parameter("curl.opacity.fade", $parameters)
  let $opacity-mode := core:parameter("curl.opacity.mode", $parameters)
  let $colour-mode := core:parameter("curl.colour.mode", $parameters)
  let $n-slices := core:parameter("curl.n-slices", $parameters)
  let $slice-extent := core:parameter("curl.slice.extent", $parameters)
  let $angle-extent := core:parameter("curl.angle.extent", $parameters)
  let $slice-sep := ($slice-extent * $fade-scale) div $n-slices
  let $angle-sep := ($angle-extent * $fade-scale) div $n-slices
  let $gradient-sep := $n-colours div $n-slices
  let $single-stop := rand:select-random(1 to $n-colours)
  let $centered-colour :=
    if ($gradient-ix=1) then core:randomizer("curl.colour", $randomizers)
    else (
      let $n := gradient:n-gradient-colours($gradient)
      return
        if ($n=$n-colours) then core:randomizer("curl.colour", $randomizers)
        else core:randomizer("curl.colour", $randomizers)=>dist:std($n idiv 20)=>dist:max($n)
    )
  let $pivot-points :=
    let $n-edges := count($pivot-path=>geom:edges())
    return
      if ($n-edges <= $n-slices)
      then geom:interpolate($pivot-path, 1 + ($n-slices idiv $n-edges))=>geom:points()
      else $pivot-path=>geom:points()
  let $id := rand:id("curl")
  return (
    defref:def($id, $curl),
    for $i in 1 to $n-slices
    let $pivot := $pivot-points[$i]
    let $rotation := ($i - 1) * $angle-sep
    let $opacity :=
      switch ($opacity-mode)
      case "symmetric" return $curl-opacity * $fade-scale * math:pow($opacity-fade, abs(($n-slices idiv 2) - $i))
      case "bottom" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($i idiv 2))
      case "top" return $curl-opacity * $fade-scale * math:pow($opacity-fade, ($n-slices - $i) idiv 2)
      default return $curl-opacity * $fade-scale
    let $stop :=
      switch($colour-mode)
      case "singular" return $single-stop
      case "multiple" return rand:select-random(1 to $n-colours)
      case "centered" return rand:randomize(1, $single-stop, $centered-colour)
      case "sliced" return 1 + ceiling($gradient-sep*($i - 1))
      default return 1 + ceiling($gradient-sep*($i - 1))
    return
      defref:ref($id,
        map {
          "gradient": $gradient-ix,
          "stop": $stop,
          "stroke-opacity": mmath:decimal($opacity,2),
          "transform":
            "scale("||$scale||","||$scale||") rotate("||$rotation||","||point:x($pivot)||","||point:y($pivot)||")"
        }
      )
  )
};