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

http://mathling.com/shape/patterns


Various simple patterns.

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

February 2023
Status: Active

Imports

http://mathling.com/geometric/delaunay
import module namespace delaunay="http://mathling.com/geometric/delaunay"
       at "../geo/delaunay.xqy"
http://mathling.com/type/reach
import module namespace reach="http://mathling.com/type/reach"
       at "../types/reach.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/type/slot
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.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/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.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/geometric/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.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"

Functions

Function: hvpath
declare function hvpath($moves as xs:integer*) as map(xs:string,item()*)


Flip-invert-reverse
A path of alternating horizonal and vertical distances
Apply a series of mutations (flip/invert/reverse)
Draw with various symmetries

Params
  • moves as xs:integer*
Returns
  • map(xs:string,item()*)
declare function this:hvpath(
  $moves as xs:integer* (: distances :)
) as map(xs:string,item()*)
{
  map {
    "kind": "hv-path",
    "dirs": 
      for $move in 1 to count($moves)
      return if ($move mod 2 = 1) then "h" else "v"
    ,
    "moves": $moves
    ,
    "draw": this:hvdraw#3
  }
}

Function: flip
declare function flip($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:flip($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>map:put("moves",
    for $move at $i in $hvpath("moves")
    return -1 * $move
  )
}

Function: flipv
declare function flipv($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:flipv($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $dirs := $hvpath("dirs")
  return (
    $hvpath=>map:put("moves",
      for $move at $i in $hvpath("moves")
      return if ($dirs[$i]="h") then $move else -1 * $move
    )
  )
}

Function: fliph
declare function fliph($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:fliph($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $dirs := $hvpath("dirs")
  return (
    $hvpath=>map:put("moves",
      for $move at $i in $hvpath("moves")
      return if ($dirs[$i]="v") then $move else -1 * $move
    )
  )
}

Function: invert
declare function invert($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:invert($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>map:put("dirs",
    for $dir at $i in $hvpath("dirs")
    return if ($dir="v") then "h" else "v"
  )
}

Function: reverse
declare function reverse($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:reverse($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>
    map:put("dirs", reverse($hvpath("dirs")))=>
    map:put("moves", reverse($hvpath("moves")))
}

Function: mutate
declare function mutate($hvpath as map(xs:string,item()*), $operations as xs:string*) as map(xs:string,item()*)

Params
  • hvpath as map(xs:string,item()*)
  • operations as xs:string*
Returns
  • map(xs:string,item()*)
declare function this:mutate(
  $hvpath as map(xs:string,item()*),
  $operations as xs:string*
) as map(xs:string,item()*)
{
  fold-left($operations, $hvpath,
    function($hvpath as map(xs:string,item()*), $op as xs:string) as map(xs:string,item()*) {
      switch ($op)
      case "flip" return this:flip($hvpath)
      case "flipv" return this:flipv($hvpath)
      case "fliph" return this:fliph($hvpath)
      case "invert" return this:invert($hvpath)
      case "reverse" return this:reverse($hvpath)
      default return $hvpath
    }
  )
}

Function: hvdraw
declare function hvdraw($hvpath as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,function(*)?))

Params
  • hvpath as map(xs:string,item()*)
  • properties as map(xs:string,item()*)
  • drawing as map(xs:string,function(*)?)
declare function this:hvdraw(
  $hvpath as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
)
{
  let $style-properties := 
    util:merge-into((
      $drawing("draw:svg-style")($properties),
      $drawing("draw:svg-style")(util:exclude($hvpath, ("kind","dirs","moves","start")))
    ))
  let $start := head(($hvpath("start"), $point:ORIGIN))
  let $d :=
    "M "||point:px($start)||" "||point:py($start)||
    string-join(
      let $moves := $hvpath("moves")
      for $d at $i in $hvpath("dirs")
      return $d||" "||$moves[$i]
    )
  return
    <svg:path d="{$d}" class="path">{
      util:as-attributes($style-properties)
    }</svg:path>
}

Function: hvpoints
declare function hvpoints($hvpaths as map(xs:string,item()*)*) as map(xs:string,item()*)*

Params
  • hvpaths as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:hvpoints(
  $hvpaths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $hvpath in $hvpaths return
  if ($hvpath("kind") = "slot") then this:hvpoints(slot:body($hvpath)) else
  let $moves := $hvpath("moves")
  let $dirs := $hvpath("dirs")
  let $start := head(($hvpath("start"), $point:ORIGIN))
  return (
    fold-left(1 to count($moves), $start,
      function($points as map(xs:string,item()*)*, $i as xs:integer) {
        $points,
        point:destination($points[last()],
          if ($dirs[$i]="h") then 0 else 90,
          $moves[$i]
        )
      }
    )
  )
}

Function: fir
declare function fir($start as map(xs:string,item()*), $moves as xs:integer*, $operations as xs:string*, $symmetries as xs:integer*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


fir()
Create a flip-invert-reverse pattern. Each pattern starts with a sequence of
alternating horizontal and vertical lines of the given lengths. Then we apply
a sequence of operations to it. Finally we render out symmetric versions of
the line also.

Params
  • start as map(xs:string,item()*): starting point
  • moves as xs:integer*: sequence of lengths
  • operations as xs:string*: sequence of operations reverse: reverse the sequence of steps invert: horizontal steps <=> vertical steps fliph: reverse the direction of horizonal steps flipv: reverse the direction of vertical steps flip: reverse the direction of all steps
  • symmetries as xs:integer*: a sequence of numbers in the range 3 to 8; if the given number is in the sequence, we get that symmetry variant included. We always get symmetry variants 1 (base path) and 2 (base path with operations applied) 3: 1+flipv 4: 2+flipv 5: 1+fliph 6: 2+fliph 7: 1+flip 8: 2+flip
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:fir(
  $start as map(xs:string,item()*),
  $moves as xs:integer*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot :)
{
  let $base-path := this:hvpath($moves)
  let $p1 := $base-path=>map:put("start", $start)
  let $p2 := this:mutate($p1, $operations)
  let $p3 := if ($symmetries=3) then this:mutate($p1, "flipv") else ()
  let $p4 := if ($symmetries=4) then this:mutate($p2, "flipv") else ()
  let $p5 := if ($symmetries=5) then this:mutate($p1, "fliph") else ()
  let $p6 := if ($symmetries=6) then this:mutate($p2, "fliph") else ()
  let $p7 := if ($symmetries=7) then this:mutate($p1, "flip") else ()
  let $p8 := if ($symmetries=8) then this:mutate($p2, "flip") else ()
  return (
    slot:slot(
      ($p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8)=>util:with-properties($properties)
    )
  )
}

Function: bounded-fir
declare function bounded-fir($bounds as map(xs:string,item()*), (: bounding box :) $moves as xs:numeric*, $operations as xs:string*, $symmetries as xs:integer*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • bounds as map(xs:string,item()*)
  • moves as xs:numeric*
  • operations as xs:string*
  • symmetries as xs:integer*
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:bounded-fir(
  $bounds as map(xs:string,item()*), (: bounding box :)
  $moves as xs:numeric*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: clip def + slot with clip-path :)
{
  let $reach := reach:reach(rand:id("clip"), $bounds)
  let $scaling :=  box:diagonal($bounds) div count($moves)
  let $moves := $moves!(util:round(. * $scaling))
  return (
    $reach,
    this:fir(box:center($bounds), $moves, $operations, $symmetries, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: bounded-fir
declare function bounded-fir($bounds as map(xs:string,item()*), (: bounding box :) $moves as xs:numeric*, $operations as xs:string*, $symmetries as xs:integer*, $properties as map(xs:string,item()*), $clip-id as xs:string?) as map(xs:string,item()*)*

Params
  • bounds as map(xs:string,item()*)
  • moves as xs:numeric*
  • operations as xs:string*
  • symmetries as xs:integer*
  • properties as map(xs:string,item()*)
  • clip-id as xs:string?
Returns
  • map(xs:string,item()*)*
declare function this:bounded-fir(
  $bounds as map(xs:string,item()*), (: bounding box :)
  $moves as xs:numeric*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: clip def + slot with clip-path :)
{
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  let $scaling :=  box:diagonal($bounds) div count($moves)
  let $moves := $moves!(util:round(. * $scaling))
  return (
    $reach,
    this:fir(box:center($bounds), $moves, $operations, $symmetries, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: nested-arcs
declare function nested-arcs($start as map(xs:string,item()*), $n-arcs as xs:integer, $n-starts as xs:integer, $n-lines as xs:integer, $scaling as xs:integer, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


nested-arcs()

Params
  • start as map(xs:string,item()*): starting point (minimum point of arrangement)
  • n-arcs as xs:integer: number of nested arcs in each piece of design
  • n-starts as xs:integer: number of the largest arc on each line
  • n-lines as xs:integer: number of lines of arcs
  • scaling as xs:integer: size of arcs
  • properties as map(xs:string,item()*): drawing properties for the arcs: you'll want to set fill=background for best look
Returns
  • map(xs:string,item()*)
declare function this:nested-arcs(
  $start as map(xs:string,item()*),
  $n-arcs as xs:integer,
  $n-starts as xs:integer,
  $n-lines as xs:integer,
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot with clip-path :)
{
  let $arc-scales := util:linspace($n-arcs + 1, 0, 1)=>tail()=>reverse()=>tail()
  return (
    slot:slot(
      for $line in 1 to $n-lines
      let $offset := if ($line mod 2 = 0) then -1 else 0
      for $st in 1 to $n-starts - $offset
      let $start-pt :=
        $start=>
          geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, $line * $scaling)
      let $end-pt := $start-pt=>geom:translate(3 * $scaling, 0)
      let $mid-pt := $start-pt=>geom:translate(1.5 * $scaling, -$scaling)
      let $scale-pt := $start-pt=>geom:translate(1.5 * $scaling, 0)
      let $circle := ellipse:circle-from($start-pt, $end-pt, $mid-pt)
      let $arc := 
        edge:arc(ellipse:center($circle), ellipse:radius($circle), $start-pt, $end-pt, false(), false())=>
          geom:with-properties($properties)
      order by $st mod 2
      return (
        $arc=>geom:snap(),
        for $arc-scale in $arc-scales
        return $arc=>geom:scale($arc-scale, $arc-scale, $scale-pt)=>geom:snap()
      )
    )
  )
}

Function: bounded-nested-arcs
declare function bounded-nested-arcs($n-arcs as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $properties as map(xs:string,item()*), $clip-id as xs:string?) as map(xs:string,item()*)*

Params
  • n-arcs as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • properties as map(xs:string,item()*)
  • clip-id as xs:string?
Returns
  • map(xs:string,item()*)*
declare function this:bounded-nested-arcs(
  $n-arcs as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:nested-arcs(box:min-point($bounds), $n-arcs, $n-starts, $n-lines, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: bounded-nested-arcs
declare function bounded-nested-arcs($n-arcs as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • n-arcs as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:bounded-nested-arcs(
  $n-arcs as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:nested-arcs(box:min-point($bounds), $n-arcs, $n-starts, $n-lines, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: diamonds
declare function diamonds($start as map(xs:string,item()*), $n-diamonds as xs:integer, $n-starts as xs:integer, $n-lines as xs:integer, $scaling as xs:integer, $alternating as xs:boolean, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


diamonds()
Like nested-arcs with straight lines

Params
  • start as map(xs:string,item()*): starting point (minimum point of arrangement)
  • n-diamonds as xs:integer: number of nested angles in each piece of design
  • n-starts as xs:integer: number of the largest angled line on each line
  • n-lines as xs:integer: number of lines of angles
  • scaling as xs:integer: size of angles
  • alternating as xs:boolean: set to false() to get a different (but nice) variation of the pattern: instead of nested diamonds you get some overlaps
  • properties as map(xs:string,item()*): drawing properties for the angles
Returns
  • map(xs:string,item()*)
declare function this:diamonds(
  $start as map(xs:string,item()*),
  $n-diamonds as xs:integer,
  $n-starts as xs:integer,
  $n-lines as xs:integer,
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot with clip-path :)
{
  let $diamond-scales := util:linspace($n-diamonds + 1, 0, 1)=>tail()=>reverse()=>tail()
  return (
    slot:slot(
      for $line in 1 to $n-lines
      let $offset := if ($line mod 2 = 0) then -1 else 0
      for $st in 1 to $n-starts - $offset
      (: Keeping same start point for every line makes nice intersection pattern :)
      let $start-pt :=
        if ($line mod 2 = 1 or not($alternating)) then (
          $start=>
            geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, $line * $scaling)
        ) else (
          $start=>
            geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, ($line - 1) * $scaling)
        )
      let $end-pt := $start-pt=>geom:translate(3 * $scaling, 0)
      let $mid-pt :=
        if ($line mod 2 = 1)
        then $start-pt=>geom:translate(1.5 * $scaling, -$scaling)
        else $start-pt=>geom:translate(1.5 * $scaling, $scaling)
      let $scale-pt := $start-pt=>geom:translate(1.5 * $scaling, 0)
      let $diamond :=
        path:path((
          edge:edge($start-pt, $mid-pt),
          edge:edge($mid-pt, $end-pt)
        ))=>geom:with-properties($properties)
      order by $st
      return (
        $diamond=>geom:snap(),
        for $diamond-scale in $diamond-scales
        return $diamond=>geom:scale($diamond-scale, $diamond-scale, $scale-pt)=>geom:snap()
      )
    )
  )
}

Function: bounded-diamonds
declare function bounded-diamonds($n-diamonds as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $alternating as xs:boolean, $properties as map(xs:string,item()*), $clip-id as xs:string?) as map(xs:string,item()*)*

Params
  • n-diamonds as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • alternating as xs:boolean
  • properties as map(xs:string,item()*)
  • clip-id as xs:string?
Returns
  • map(xs:string,item()*)*
declare function this:bounded-diamonds(
  $n-diamonds as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:diamonds(box:min-point($bounds), $n-diamonds, $n-starts, $n-lines, $scaling, $alternating, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: bounded-diamonds
declare function bounded-diamonds($n-diamonds as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $alternating as xs:boolean, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • n-diamonds as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • alternating as xs:boolean
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:bounded-diamonds(
  $n-diamonds as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:diamonds(box:min-point($bounds), $n-diamonds, $n-starts, $n-lines, $scaling, $alternating, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: voronoi
declare function voronoi($n-repetitions as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


voronoi()
Nested random Voronoi regions.

Params
  • n-repetitions as xs:integer: number of nestings of each region
  • bounds as map(xs:string,item()*): boundary in which the points are placed
  • scaling as xs:integer: basic scaling of the regions
  • properties as map(xs:string,item()*): drawing properties for the regions
Returns
  • map(xs:string,item()*)
declare function this:voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $region-scales := util:linspace($n-repetitions + 1, 0, 1)=>tail()=>reverse()=>tail()
  let $n-points := 1 + box:width($bounds) idiv $scaling
  let $points := geom:random-points-in($n-points, $bounds)
  let $regions :=  delaunay:voronoi($points, $bounds)
  return (
    slot:slot(
      for $region in $regions
      let $center := point:centroid(geom:vertices($region))
      return (
        $region=>geom:snap()=>geom:with-properties($properties),
        for $region-scale in $region-scales return (
          $region=>geom:scale($region-scale, $region-scale, $center)=>
            geom:snap()=>geom:with-properties($properties)
        )
      )
    )
  )
}

Function: bounded-voronoi
declare function bounded-voronoi($n-repetitions as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $properties as map(xs:string,item()*), $clip-id as xs:string?) as map(xs:string,item()*)*

Params
  • n-repetitions as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • properties as map(xs:string,item()*)
  • clip-id as xs:string?
Returns
  • map(xs:string,item()*)*
declare function this:bounded-voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:voronoi($n-repetitions, $bounds, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Function: bounded-voronoi
declare function bounded-voronoi($n-repetitions as xs:integer, $bounds as map(xs:string,item()*), (: bounding box :) $scaling as xs:integer, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • n-repetitions as xs:integer
  • bounds as map(xs:string,item()*)
  • scaling as xs:integer
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:bounded-voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:voronoi($n-repetitions, $bounds, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
}

Original Source Code

xquery version "3.1";
(:~
 : Various simple patterns.
 :
 : Copyright© Mary Holstege 2022-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since February 2023
 : @custom:Status Active
 :)
module namespace this="http://mathling.com/shape/patterns"; 

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 rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace reach="http://mathling.com/type/reach"
       at "../types/reach.xqy";
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.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 ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy";
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace delaunay="http://mathling.com/geometric/delaunay"
       at "../geo/delaunay.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";

(:~
 : Flip-invert-reverse
 : A path of alternating horizonal and vertical distances
 : Apply a series of mutations (flip/invert/reverse)
 : Draw with various symmetries
 :)
 
declare function this:hvpath(
  $moves as xs:integer* (: distances :)
) as map(xs:string,item()*)
{
  map {
    "kind": "hv-path",
    "dirs": 
      for $move in 1 to count($moves)
      return if ($move mod 2 = 1) then "h" else "v"
    ,
    "moves": $moves
    ,
    "draw": this:hvdraw#3
  }
};

declare function this:flip($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>map:put("moves",
    for $move at $i in $hvpath("moves")
    return -1 * $move
  )
};

declare function this:flipv($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $dirs := $hvpath("dirs")
  return (
    $hvpath=>map:put("moves",
      for $move at $i in $hvpath("moves")
      return if ($dirs[$i]="h") then $move else -1 * $move
    )
  )
};

declare function this:fliph($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $dirs := $hvpath("dirs")
  return (
    $hvpath=>map:put("moves",
      for $move at $i in $hvpath("moves")
      return if ($dirs[$i]="v") then $move else -1 * $move
    )
  )
};

declare function this:invert($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>map:put("dirs",
    for $dir at $i in $hvpath("dirs")
    return if ($dir="v") then "h" else "v"
  )
};

declare function this:reverse($hvpath as map(xs:string,item()*)) as map(xs:string,item()*)
{
  $hvpath=>
    map:put("dirs", reverse($hvpath("dirs")))=>
    map:put("moves", reverse($hvpath("moves")))
};

declare function this:mutate(
  $hvpath as map(xs:string,item()*),
  $operations as xs:string*
) as map(xs:string,item()*)
{
  fold-left($operations, $hvpath,
    function($hvpath as map(xs:string,item()*), $op as xs:string) as map(xs:string,item()*) {
      switch ($op)
      case "flip" return this:flip($hvpath)
      case "flipv" return this:flipv($hvpath)
      case "fliph" return this:fliph($hvpath)
      case "invert" return this:invert($hvpath)
      case "reverse" return this:reverse($hvpath)
      default return $hvpath
    }
  )
};

declare function this:hvdraw(
  $hvpath as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
)
{
  let $style-properties := 
    util:merge-into((
      $drawing("draw:svg-style")($properties),
      $drawing("draw:svg-style")(util:exclude($hvpath, ("kind","dirs","moves","start")))
    ))
  let $start := head(($hvpath("start"), $point:ORIGIN))
  let $d :=
    "M "||point:px($start)||" "||point:py($start)||
    string-join(
      let $moves := $hvpath("moves")
      for $d at $i in $hvpath("dirs")
      return $d||" "||$moves[$i]
    )
  return
    <svg:path d="{$d}" class="path">{
      util:as-attributes($style-properties)
    }</svg:path>
};

declare function this:hvpoints(
  $hvpaths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $hvpath in $hvpaths return
  if ($hvpath("kind") = "slot") then this:hvpoints(slot:body($hvpath)) else
  let $moves := $hvpath("moves")
  let $dirs := $hvpath("dirs")
  let $start := head(($hvpath("start"), $point:ORIGIN))
  return (
    fold-left(1 to count($moves), $start,
      function($points as map(xs:string,item()*)*, $i as xs:integer) {
        $points,
        point:destination($points[last()],
          if ($dirs[$i]="h") then 0 else 90,
          $moves[$i]
        )
      }
    )
  )
};

(:~
 : fir()
 : Create a flip-invert-reverse pattern. Each pattern starts with a sequence of
 : alternating horizontal and vertical lines of the given lengths. Then we apply
 : a sequence of operations to it. Finally we render out symmetric versions of
 : the line also. 
 :
 : @param $start: starting point
 : @param $moves: sequence of lengths
 : @param $operations: sequence of operations 
 :   reverse: reverse the sequence of steps
 :   invert: horizontal steps <=> vertical steps
 :   fliph: reverse the direction of horizonal steps
 :   flipv: reverse the direction of vertical steps
 :   flip: reverse the direction of all steps
 : @param $symmetries: a sequence of numbers in the range 3 to 8; if the given number is
 :   in the sequence, we get that symmetry variant included. We always get symmetry
 :   variants 1 (base path) and 2 (base path with operations applied)
 :     3: 1+flipv     4: 2+flipv
 :     5: 1+fliph     6: 2+fliph
 :     7: 1+flip      8: 2+flip
 :)    
declare function this:fir(
  $start as map(xs:string,item()*),
  $moves as xs:integer*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot :)
{
  let $base-path := this:hvpath($moves)
  let $p1 := $base-path=>map:put("start", $start)
  let $p2 := this:mutate($p1, $operations)
  let $p3 := if ($symmetries=3) then this:mutate($p1, "flipv") else ()
  let $p4 := if ($symmetries=4) then this:mutate($p2, "flipv") else ()
  let $p5 := if ($symmetries=5) then this:mutate($p1, "fliph") else ()
  let $p6 := if ($symmetries=6) then this:mutate($p2, "fliph") else ()
  let $p7 := if ($symmetries=7) then this:mutate($p1, "flip") else ()
  let $p8 := if ($symmetries=8) then this:mutate($p2, "flip") else ()
  return (
    slot:slot(
      ($p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8)=>util:with-properties($properties)
    )
  )
};

declare function this:bounded-fir(
  $bounds as map(xs:string,item()*), (: bounding box :)
  $moves as xs:numeric*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: clip def + slot with clip-path :)
{
  let $reach := reach:reach(rand:id("clip"), $bounds)
  let $scaling :=  box:diagonal($bounds) div count($moves)
  let $moves := $moves!(util:round(. * $scaling))
  return (
    $reach,
    this:fir(box:center($bounds), $moves, $operations, $symmetries, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

declare function this:bounded-fir(
  $bounds as map(xs:string,item()*), (: bounding box :)
  $moves as xs:numeric*,
  $operations as xs:string*,
  $symmetries as xs:integer*,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: clip def + slot with clip-path :)
{
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  let $scaling :=  box:diagonal($bounds) div count($moves)
  let $moves := $moves!(util:round(. * $scaling))
  return (
    $reach,
    this:fir(box:center($bounds), $moves, $operations, $symmetries, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};


(:
 : Nested arcs
 : Overlapping sets of nested arcs
 :)

(:~
 : nested-arcs()
 :
 : @param $start: starting point (minimum point of arrangement)
 : @param $n-arcs: number of nested arcs in each piece of design
 : @param $n-starts: number of the largest arc on each line
 : @param $n-lines: number of lines of arcs
 : @param $scaling: size of arcs
 : @param $properties: drawing properties for the arcs: you'll want to set fill=background
 :    for best look
 :)
declare function this:nested-arcs(
  $start as map(xs:string,item()*),
  $n-arcs as xs:integer,
  $n-starts as xs:integer,
  $n-lines as xs:integer,
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot with clip-path :)
{
  let $arc-scales := util:linspace($n-arcs + 1, 0, 1)=>tail()=>reverse()=>tail()
  return (
    slot:slot(
      for $line in 1 to $n-lines
      let $offset := if ($line mod 2 = 0) then -1 else 0
      for $st in 1 to $n-starts - $offset
      let $start-pt :=
        $start=>
          geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, $line * $scaling)
      let $end-pt := $start-pt=>geom:translate(3 * $scaling, 0)
      let $mid-pt := $start-pt=>geom:translate(1.5 * $scaling, -$scaling)
      let $scale-pt := $start-pt=>geom:translate(1.5 * $scaling, 0)
      let $circle := ellipse:circle-from($start-pt, $end-pt, $mid-pt)
      let $arc := 
        edge:arc(ellipse:center($circle), ellipse:radius($circle), $start-pt, $end-pt, false(), false())=>
          geom:with-properties($properties)
      order by $st mod 2
      return (
        $arc=>geom:snap(),
        for $arc-scale in $arc-scales
        return $arc=>geom:scale($arc-scale, $arc-scale, $scale-pt)=>geom:snap()
      )
    )
  )
};

declare function this:bounded-nested-arcs(
  $n-arcs as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:nested-arcs(box:min-point($bounds), $n-arcs, $n-starts, $n-lines, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

declare function this:bounded-nested-arcs(
  $n-arcs as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:nested-arcs(box:min-point($bounds), $n-arcs, $n-starts, $n-lines, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

(:
 : Diamond patterns
 :)


(:~
 : diamonds()
 : Like nested-arcs with straight lines
 :
 : @param $start: starting point (minimum point of arrangement)
 : @param $n-diamonds: number of nested angles in each piece of design
 : @param $n-starts: number of the largest angled line on each line
 : @param $n-lines: number of lines of angles
 : @param $scaling: size of angles
 : @param $alternating: set to false() to get a different (but nice) variation of the pattern:
 :    instead of nested diamonds you get some overlaps
 : @param $properties: drawing properties for the angles
 :)
declare function this:diamonds(
  $start as map(xs:string,item()*),
  $n-diamonds as xs:integer,
  $n-starts as xs:integer,
  $n-lines as xs:integer,
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*) (: slot with clip-path :)
{
  let $diamond-scales := util:linspace($n-diamonds + 1, 0, 1)=>tail()=>reverse()=>tail()
  return (
    slot:slot(
      for $line in 1 to $n-lines
      let $offset := if ($line mod 2 = 0) then -1 else 0
      for $st in 1 to $n-starts - $offset
      (: Keeping same start point for every line makes nice intersection pattern :)
      let $start-pt :=
        if ($line mod 2 = 1 or not($alternating)) then (
          $start=>
            geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, $line * $scaling)
        ) else (
          $start=>
            geom:translate(($st - 1) * (2 * $scaling) + $offset * $scaling, ($line - 1) * $scaling)
        )
      let $end-pt := $start-pt=>geom:translate(3 * $scaling, 0)
      let $mid-pt :=
        if ($line mod 2 = 1)
        then $start-pt=>geom:translate(1.5 * $scaling, -$scaling)
        else $start-pt=>geom:translate(1.5 * $scaling, $scaling)
      let $scale-pt := $start-pt=>geom:translate(1.5 * $scaling, 0)
      let $diamond :=
        path:path((
          edge:edge($start-pt, $mid-pt),
          edge:edge($mid-pt, $end-pt)
        ))=>geom:with-properties($properties)
      order by $st
      return (
        $diamond=>geom:snap(),
        for $diamond-scale in $diamond-scales
        return $diamond=>geom:scale($diamond-scale, $diamond-scale, $scale-pt)=>geom:snap()
      )
    )
  )
};

declare function this:bounded-diamonds(
  $n-diamonds as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:diamonds(box:min-point($bounds), $n-diamonds, $n-starts, $n-lines, $scaling, $alternating, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

declare function this:bounded-diamonds(
  $n-diamonds as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $alternating as xs:boolean,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $n-starts := 1 + box:width($bounds) idiv ($scaling * 2)
  let $n-lines := 1 + box:height($bounds) idiv $scaling
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:diamonds(box:min-point($bounds), $n-diamonds, $n-starts, $n-lines, $scaling, $alternating, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

(:
 : Voronoi
 :)

(:~
 : voronoi()
 : Nested random Voronoi regions.
 :
 : @param $n-repetitions: number of nestings of each region
 : @param $bounds: boundary in which the points are placed
 : @param $scaling: basic scaling of the regions
 : @param $properties: drawing properties for the regions
 :)
declare function this:voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $region-scales := util:linspace($n-repetitions + 1, 0, 1)=>tail()=>reverse()=>tail()
  let $n-points := 1 + box:width($bounds) idiv $scaling
  let $points := geom:random-points-in($n-points, $bounds)
  let $regions :=  delaunay:voronoi($points, $bounds)
  return (
    slot:slot(
      for $region in $regions
      let $center := point:centroid(geom:vertices($region))
      return (
        $region=>geom:snap()=>geom:with-properties($properties),
        for $region-scale in $region-scales return (
          $region=>geom:scale($region-scale, $region-scale, $center)=>
            geom:snap()=>geom:with-properties($properties)
        )
      )
    )
  )
};

declare function this:bounded-voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*),
  $clip-id as xs:string?
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $clip-id := ($clip-id, rand:id("clip"))[1]
  let $reach := reach:reach($clip-id, $bounds)
  return (
    $reach,
    this:voronoi($n-repetitions, $bounds, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};

declare function this:bounded-voronoi(
  $n-repetitions as xs:integer,
  $bounds as map(xs:string,item()*), (: bounding box :)
  $scaling as xs:integer,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)* (: reach + slot :)
{
  let $reach := reach:reach(rand:id("clip"), $bounds)
  return (
    $reach,
    this:voronoi($n-repetitions, $bounds, $scaling, $properties)=>
      geom:with-properties(
        map {"clip-path": reach:ref($reach)}
      )
  )
};