http://mathling.com/geometric/pixels  library module

http://mathling.com/geometric/pixels


Module for drawing various things out onto pixel array. Not intended for
heavy rendering, more for creating seeds for diffusion-reaction or the like.

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

October 2023
Status: Bleeding edge

Imports

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/sdf
import module namespace sdf="http://mathling.com/sdf"
       at "../sdf/sdf.xqy"
http://mathling.com/core/callable
import module namespace f="http://mathling.com/core/callable"
       at "../core/callable.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Functions

Function: max
declare function max($pixels as array(array(*)), $other as array(array(*))) as array(array(*))


max()
Compute the maximum of the values at each corresponding point in the array
and return an array with those maximums.
A way to union greyscale values, for example.

Params
  • pixels as array(array(*)) pixel array
  • other as array(array(*)) another pixel array
Returns
  • array(array(*)): pixel array containing maximum value at every point
declare function this:max(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          max(($this, $that))
        }
      )
    }
  )
}

Function: avg
declare function avg($pixels as array(array(*)), $other as array(array(*))) as array(array(*))


avg()
Compute the average of the values at each corresponding point in the array
and return an array with those maximums.
A way to union greyscale values, for example.

Params
  • pixels as array(array(*)) pixel array
  • other as array(array(*)) another pixel array
Returns
  • array(array(*)): pixel array containing average value at every point
declare function this:avg(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          avg(($this, $that))
        }
      )
    }
  )
}

Function: mask
declare function mask($pixels as array(array(*)), $other as array(array(*))) as array(array(*))


mask()
Mask the pixels from one grid by another.

Params
  • pixels as array(array(*)) pixel array
  • other as array(array(*)) another pixel array
Returns
  • array(array(*)): pixel array containing the values of the first array minus the values of the second, clamped in [0,1] range
declare function this:mask(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          util:clamp($this - $that, 0.0, 1.0)
        }
      )
    }
  )
}

Function: edge-pixel-points
declare function edge-pixel-points($edges as map(xs:string,item()*)*) as map(xs:string,item()*)*


edge-pixel-points()
Determine which points we need to represent the straight edges.
Bresenham algorithm.

Params
  • edges as map(xs:string,item()*)* the edges (straight edges)
Returns
  • map(xs:string,item()*)*: the pixel points touching those edges
declare function this:edge-pixel-points(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $edge in $edges
  let $start := edge:start($edge)
  let $end := edge:end($edge)
  return (
    if (point:x($start)=point:x($end)) then (
      let $x := point:x($start)
      for $y in min((point:y($start), point:y($end))) to max((point:y($start), point:y($end)))
      return point:point($x, $y)
    ) else if (point:y($start)=point:y($end)) then (
      let $y := point:y($start)
      for $x in min((point:x($start), point:x($end))) to max((point:x($start), point:x($end)))
      return point:point($x, $y)
    ) else (
      let $x0 := point:x($start)
      let $x1 := point:x($end)
      let $y0 := point:y($start)
      let $y1 := point:y($end)
      let $dx := abs($x1 - $x0)
      let $sx := util:sign($x1 - $x0)
      let $dy := -abs($y1 - $y0)
      let $sy := util:sign($y1 - $y0)
      let $error := $dx + $dy
      return (
        util:until(
          function($x as xs:integer, $y as xs:integer, $error as xs:integer, $points as map(xs:string,item()*)*) as xs:boolean {
            $x = $x1 and $y = $y1
          },
          function($x as xs:integer, $y as xs:integer, $error as xs:integer, $points as map(xs:string,item()*)*) as item()* {
            if ($x = $x1 and $y = $y1) then (
               $x, $y, $error, ($points, point:point($x, $y))
            ) else (
              let $e2 := 2 * $error
              let $next-x := if ($e2 >= $dy) then (
                if ($x = $x1) then $x else $x + $dx
              ) else $x
              let $next-error := if ($e2 >= $dy) then (
                if ($x = $x1) then $error else $error + $dy
              ) else $error
              let $next-y := if ($e2 <= $dx) then (
                if ($y = $y1) then $y else $y + $sy
              ) else $y
              let $next-error := if ($e2 <= $dx) then (
                if ($y = $y1) then $next-error + $dx else $error
              ) else $next-error
              return (
                $next-x, $next-y, $next-error, ($points, point:point($x, $y))
              )
            )
          },
          $x0, $y0, $error, ()
        )
      )=>tail()=>tail()=>tail()
    )
  )=>point:with-properties(map {"cix": $edge("cix")})
}

Function: to-array
declare function to-array($canvas as map(xs:string,item()*), $regions as map(xs:string,item()*)*, $filled as xs:boolean, $base-grid as array(array(*))?, $colouring as function(xs:integer*, xs:double) as xs:double) as array(array(*))


to-array()
Render regions into pixel array. By default index values are 1 for the region presence, 0 for absence. The colouring function can modify this for non-zero values.
Renders borders unless filled is true.

Params
  • canvas as map(xs:string,item()*) space to turn into pixel grid
  • regions as map(xs:string,item()*)* to render
  • filled as xs:boolean whether to fill interior of regions
  • base-grid as array(array(*))? starting grid, if any
  • colouring as function(xs:integer*,xs:double)asxs:double function point and distance to index value
Returns
  • array(array(*)): array of arrays of index values
declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?,
  $colouring as function(xs:integer*, xs:double) as xs:double
) as array(array(*))
{
  let $singletons := (
    point:snap($regions[util:kind(.)="point"]),
    this:edge-pixel-points($regions[util:kind(.)="edge"])
  )
  let $grid := array {
    for $j in 1 to xs:integer(box:height($canvas)) return array {
      for $i in 1 to xs:integer(box:width($canvas)) return (
        if (some $p in $singletons satisfies $j = point:y($p) and $i = point:x($p))
        then $colouring(($i,$j), 1)
        else if (exists($base-grid)) then $base-grid($j)($i)
        else 0
      )
    }
  }
  let $others := $regions[ not(util:kind(.)=("point","edge")) ]
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and $fij <= 0) then $colouring(($i,$j), $fij)
              else if ($fij <= 1.42) then $colouring(($i,$j), $fij)
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
}

Function: to-array
declare function to-array($canvas as map(xs:string,item()*), $regions as map(xs:string,item()*)*, $filled as xs:boolean, $base-grid as array(array(*))?) as array(array(*))


to-array()
Render regions into pixel array using default colouring (1 for the region presence, 0 for absence).
Renders borders unless filled is true.

Params
  • canvas as map(xs:string,item()*) space to turn into pixel grid
  • regions as map(xs:string,item()*)* to render
  • filled as xs:boolean whether to fill interior of regions
  • base-grid as array(array(*))?
Returns
  • array(array(*)): array of arrays of index values
declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?
) as array(array(*))
{
  let $singletons := (
    point:snap($regions[util:kind(.)="point"]),
    this:edge-pixel-points($regions[util:kind(.)="edge"])
  )
  let $grid := array {
    for $j in 1 to xs:integer(box:height($canvas)) return array {
      for $i in 1 to xs:integer(box:width($canvas)) return (
        if (some $p in $singletons satisfies $j = point:y($p) and $i = point:x($p))
        then 1
        else if (exists($base-grid)) then $base-grid($j)($i)
        else 0
      )
    }
  }
  let $others := $regions[ not(util:kind(.)=("point","edge")) ]
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and head($fij) <= 0) then (tail($fij),1)[1]
              else if ($fij <= 1.42) then (tail($fij),1)[1]
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
}

Function: to-array
declare function to-array($canvas as map(xs:string,item()*), $regions as map(xs:string,item()*)*, $filled as xs:boolean) as array(array(*))

Params
  • canvas as map(xs:string,item()*)
  • regions as map(xs:string,item()*)*
  • filled as xs:boolean
Returns
  • array(array(*))
declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean
) as array(array(*))
{
  this:to-array($canvas, $regions, $filled, ())
}

Function: to-signed-array
declare function to-signed-array($canvas as map(xs:string,item()*), $regions as map(xs:string,item()*)*, $filled as xs:boolean, $base-grid as array(array(*))?) as array(array(*))

Params
  • canvas as map(xs:string,item()*)
  • regions as map(xs:string,item()*)*
  • filled as xs:boolean
  • base-grid as array(array(*))?
Returns
  • array(array(*))
declare function this:to-signed-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?
) as array(array(*))
{
  let $grid := (
    if (exists($base-grid)) then $base-grid
    else (
      array {
        for $j in 1 to xs:integer(box:height($canvas)) return array {
          for $i in 1 to xs:integer(box:width($canvas)) return 0
        }
      }
    )
  )
  let $others := $regions
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {"op": "sign-union", "colouring": true()})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and head($fij) <= 0) then (tail($fij),1)[1]
              else if ($fij = 0) then (tail($fij),1)[1]
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
}

Function: to-colour-array
declare function to-colour-array($canvas as map(xs:string,item()*), $regions as map(xs:string,item()*)*, $filled as xs:boolean, $colours as xs:string*) as array(array(*))


to-colour-array()
Render regions into pixel array mapping distinct colours into a colour index.
Renders borders unless filled is true.

Params
  • canvas as map(xs:string,item()*) space to turn into pixel grid
  • regions as map(xs:string,item()*)* to render
  • filled as xs:boolean whether to fill interior of regions
  • colours as xs:string* colours to map to index values
Returns
  • array(array(*)): array of arrays of index values
declare function this:to-colour-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $colours as xs:string*
) as array(array(*))
{
  let $regions := (
    for $region in $regions
    let $cix := (
      for $i in 1 to count($colours) where $region("colour")=$colours[$i] return $i
    )
    return (
      $region=>map:put("cix", $cix)
    )
  )
  return this:to-signed-array($canvas, $regions, $filled, ())
}

Original Source Code

xquery version "3.1";
(:~
 : Module for drawing various things out onto pixel array. Not intended for
 : heavy rendering, more for creating seeds for diffusion-reaction or the like.
 :
 : Copyright© Mary Holstege 2020-2024
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since October 2023
 : @custom:Status Bleeding edge
 :)
module namespace this="http://mathling.com/geometric/pixels";

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 f="http://mathling.com/core/callable"
       at "../core/callable.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.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 box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace sdf="http://mathling.com/sdf"
       at "../sdf/sdf.xqy";

declare namespace art="http://mathling.com/art";
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";

(:~
 : max()
 : Compute the maximum of the values at each corresponding point in the array
 : and return an array with those maximums.
 : A way to union greyscale values, for example.
 :
 : @param $pixels pixel array
 : @param $other another pixel array
 : @return pixel array containing maximum value at every point
 :)
declare function this:max(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          max(($this, $that))
        }
      )
    }
  )
};

(:~
 : avg()
 : Compute the average of the values at each corresponding point in the array
 : and return an array with those maximums.
 : A way to union greyscale values, for example.
 :
 : @param $pixels pixel array
 : @param $other another pixel array
 : @return pixel array containing average value at every point
 :)
declare function this:avg(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          avg(($this, $that))
        }
      )
    }
  )
};

(:~
 : mask()
 : Mask the pixels from one grid by another.
 :
 : @param $pixels pixel array
 : @param $other another pixel array
 : @return pixel array containing the values of the first array minus the values of the second, clamped in [0,1] range
 :)
declare function this:mask(
  $pixels as array(array(*)),
  $other as array(array(*))
) as array(array(*))
{
  if (array:size($pixels)=array:size($other)) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),
  if (array:size($pixels(1))=array:size($other(1))) then ()
  else errors:error("UTIL-MATRIXNOTCOMPAT", ("pixels:union", array:size($pixels), array:size($other), array:size($pixels(1)), array:size($other(1)))),

  array:for-each-pair($pixels, $other,
    function($row as array(*), $orow as array(*)) as array(*) {
      array:for-each-pair($row, $orow,
        function($this as xs:double, $that as xs:double) as xs:double {
          util:clamp($this - $that, 0.0, 1.0)
        }
      )
    }
  )
};

(:~
 : edge-pixel-points()
 : Determine which points we need to represent the straight edges.
 : Bresenham algorithm.
 :
 : @param $edges the edges (straight edges)
 : @return the pixel points touching those edges
 :)
declare function this:edge-pixel-points(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $edge in $edges
  let $start := edge:start($edge)
  let $end := edge:end($edge)
  return (
    if (point:x($start)=point:x($end)) then (
      let $x := point:x($start)
      for $y in min((point:y($start), point:y($end))) to max((point:y($start), point:y($end)))
      return point:point($x, $y)
    ) else if (point:y($start)=point:y($end)) then (
      let $y := point:y($start)
      for $x in min((point:x($start), point:x($end))) to max((point:x($start), point:x($end)))
      return point:point($x, $y)
    ) else (
      let $x0 := point:x($start)
      let $x1 := point:x($end)
      let $y0 := point:y($start)
      let $y1 := point:y($end)
      let $dx := abs($x1 - $x0)
      let $sx := util:sign($x1 - $x0)
      let $dy := -abs($y1 - $y0)
      let $sy := util:sign($y1 - $y0)
      let $error := $dx + $dy
      return (
        util:until(
          function($x as xs:integer, $y as xs:integer, $error as xs:integer, $points as map(xs:string,item()*)*) as xs:boolean {
            $x = $x1 and $y = $y1
          },
          function($x as xs:integer, $y as xs:integer, $error as xs:integer, $points as map(xs:string,item()*)*) as item()* {
            if ($x = $x1 and $y = $y1) then (
               $x, $y, $error, ($points, point:point($x, $y))
            ) else (
              let $e2 := 2 * $error
              let $next-x := if ($e2 >= $dy) then (
                if ($x = $x1) then $x else $x + $dx
              ) else $x
              let $next-error := if ($e2 >= $dy) then (
                if ($x = $x1) then $error else $error + $dy
              ) else $error
              let $next-y := if ($e2 <= $dx) then (
                if ($y = $y1) then $y else $y + $sy
              ) else $y
              let $next-error := if ($e2 <= $dx) then (
                if ($y = $y1) then $next-error + $dx else $error
              ) else $next-error
              return (
                $next-x, $next-y, $next-error, ($points, point:point($x, $y))
              )
            )
          },
          $x0, $y0, $error, ()
        )
      )=>tail()=>tail()=>tail()
    )
  )=>point:with-properties(map {"cix": $edge("cix")})
};

(:~
 : to-array()
 : Render regions into pixel array. By default index values are 1 for the region presence, 0 for absence. The colouring function can modify this for non-zero values.
 : Renders borders unless filled is true.
 :
 : @param $canvas space to turn into pixel grid
 : @param $regions to render
 : @param $filled whether to fill interior of regions
 : @param $base-grid starting grid, if any
 : @param $colouring function point and distance to index value
 : @return array of arrays of index values
 :)
declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?,
  $colouring as function(xs:integer*, xs:double) as xs:double
) as array(array(*))
{
  let $singletons := (
    point:snap($regions[util:kind(.)="point"]),
    this:edge-pixel-points($regions[util:kind(.)="edge"])
  )
  let $grid := array {
    for $j in 1 to xs:integer(box:height($canvas)) return array {
      for $i in 1 to xs:integer(box:width($canvas)) return (
        if (some $p in $singletons satisfies $j = point:y($p) and $i = point:x($p))
        then $colouring(($i,$j), 1)
        else if (exists($base-grid)) then $base-grid($j)($i)
        else 0
      )
    }
  }
  let $others := $regions[ not(util:kind(.)=("point","edge")) ]
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and $fij <= 0) then $colouring(($i,$j), $fij)
              else if ($fij <= 1.42) then $colouring(($i,$j), $fij)
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
};


(:~
 : to-array()
 : Render regions into pixel array using default colouring (1 for the region presence, 0 for absence).
 : Renders borders unless filled is true.
 :
 : @param $canvas space to turn into pixel grid
 : @param $regions to render
 : @param $filled whether to fill interior of regions
 : @return array of arrays of index values
 :)
declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?
) as array(array(*))
{
  let $singletons := (
    point:snap($regions[util:kind(.)="point"]),
    this:edge-pixel-points($regions[util:kind(.)="edge"])
  )
  let $grid := array {
    for $j in 1 to xs:integer(box:height($canvas)) return array {
      for $i in 1 to xs:integer(box:width($canvas)) return (
        if (some $p in $singletons satisfies $j = point:y($p) and $i = point:x($p))
        then 1
        else if (exists($base-grid)) then $base-grid($j)($i)
        else 0
      )
    }
  }
  let $others := $regions[ not(util:kind(.)=("point","edge")) ]
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and head($fij) <= 0) then (tail($fij),1)[1]
              else if ($fij <= 1.42) then (tail($fij),1)[1]
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
};

declare function this:to-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean
) as array(array(*))
{
  this:to-array($canvas, $regions, $filled, ())
};

declare function this:to-signed-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $base-grid as array(array(*))?
) as array(array(*))
{
  let $grid := (
    if (exists($base-grid)) then $base-grid
    else (
      array {
        for $j in 1 to xs:integer(box:height($canvas)) return array {
          for $i in 1 to xs:integer(box:width($canvas)) return 0
        }
      }
    )
  )
  let $others := $regions
  return (
    if (empty($others)) then $grid else (
      let $sdf := sdf:toSDF($others, map {"op": "sign-union", "colouring": true()})=>f:function()
      return (
        array {
          for $j in 1 to array:size($grid) return array {
            for $i in 1 to array:size($grid(1))
            let $fij := $sdf(($i, $j))
            return (
              if ($filled and head($fij) <= 0) then (tail($fij),1)[1]
              else if ($fij = 0) then (tail($fij),1)[1]
              else $grid($j)($i)
            )
          }
        }
      )
    )
  )
};

(:~
 : to-colour-array()
 : Render regions into pixel array mapping distinct colours into a colour index.
 : Renders borders unless filled is true.
 :
 : @param $canvas space to turn into pixel grid
 : @param $regions to render
 : @param $filled whether to fill interior of regions
 : @param $colours colours to map to index values
 : @return array of arrays of index values
 :)
declare function this:to-colour-array(
  $canvas as map(xs:string,item()*),
  $regions as map(xs:string,item()*)*,
  $filled as xs:boolean,
  $colours as xs:string*
) as array(array(*))
{
  let $regions := (
    for $region in $regions
    let $cix := (
      for $i in 1 to count($colours) where $region("colour")=$colours[$i] return $i
    )
    return (
      $region=>map:put("cix", $cix)
    )
  )
  return this:to-signed-array($canvas, $regions, $filled, ())
};