http://mathling.com/art/hyphae library module
http://mathling.com/art/hyphae
HYPHAE:
Parameters:
hyphae.n: Max number of nodes, iteration limit
hyphae.seeds: Initial number of seeds
hyphae.branches.max: Max branch attempts from node
hyphae.radius.max: 1/2 max path width
hyphae.radius.fade: Radius changes this-fold each branch
hyphae.radius.function: f($k) adjustment to radius fade
hyphae.search-angle.range: Standard deviation on search angle
hyphae.search-angle.damping: damping in angle variation per generation
hyphea.tropism.mode:
default=damped: use search angle with increasing variability with each generation
adhoc: use function indicated by hyphae.tropism.function (no default)
the tropism function takes $randomizers, $parameters and returns
a tropism that takes an entry and returns an angle
hyphae.colour.mode: Colouring scheme
default=generation: generation
k: based on parent index
random: random colour
distance: distance from center
width: radius with
single: one (random) colour
order: based on point index
adhoc: use function indicated by hyphae.colour.function (no default)
the colour function takes $entries, $randomizers, $parameters and
returns a function that takes an entry and returns a stop number
hyphae.stop.max: Expected maximum value for colour stops (used for some colour schemes)
hyphae.fatness: Relative width of strokes (0,1]
Randomizers:
hyphae.init-angles: initial angles
hyphae.search-angles: branching angles
hyphae.init-radii: radius multipliers (multiplied by max-radius)
Rendering parameters:
background.fill
signature.colour
default-stroke-width
dot-shape
graph.gradient
graph.edge-mode: circles|sand|brush|graph|stroke
sand.grains
sand.opacity
sand.size
Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)
Status: Stable
Imports
http://mathling.com/geometric/intersectionimport module namespace inter="http://mathling.com/geometric/intersection" at "../geo/intersection.xqy"http://mathling.com/type/distribution
import module namespace dist="http://mathling.com/type/distribution" at "../types/distributions.xqy"http://mathling.com/type/wrapper
import module namespace wrapper="http://mathling.com/type/wrapper" at "../types/wrapper.xqy"http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle" at "../geo/rectangle.xqy"http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric" at "../geo/euclidean.xqy"http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"http://mathling.com/geometric/graph
import module namespace graph="http://mathling.com/geometric/graph" at "../geo/graph.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/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/core/sparse
import module namespace sparse="http://mathling.com/core/sparse" at "../core/sparse.xqy"http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"
Functions
Function: components
declare function components() as map(xs:string, map(xs:string,item()*))
declare function components() as map(xs:string, map(xs:string,item()*))
Standard callback: Subcomponent map.
Returns
- map(xs:string,map(xs:string,item()*))
declare function this:components() as map(xs:string, map(xs:string,item()*)) { map { } }
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()*)
declare function rendering-parameters($canvas as map(xs:string,item()*), $algorithm-parameters as map(xs:string,item()*)) as map(xs:string,item()*)
Standard callback: Set of rendering parameters.
Params
- canvas as map(xs:string,item()*): drawing canvas
- algorithm-parameters as map(xs:string,item()*): set of algorithm parameters
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()*) { map { "background.fill": "black", "signature.colour": "white", "default-stroke-width": 1, "dot-shape": "none", "graph.gradient": "plasma", "graph.edge-mode": "circles", (: circles|sand|brush|graph|stroke :) "sand.grains": 10, "sand.opacity": 0.5, "sand.size": 1 } }
Function: algorithm-mode-parameters
declare function algorithm-mode-parameters($mode as xs:string,
$resolution as xs:string,
$canvas as map(xs:string,item()*)) as map(xs:string,item()*)
declare function algorithm-mode-parameters($mode as xs:string, $resolution as xs:string, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)
Standard callback: Set of algorithm parameters specific to a particular mode.
Params
- mode as xs:string: selected mode
- resolution as xs:string: defined resolution
- canvas as map(xs:string,item()*): drawing canvas
Returns
- map(xs:string,item()*)
declare function this:algorithm-mode-parameters( $mode as xs:string, $resolution as xs:string, $canvas as map(xs:string,item()*) ) as map(xs:string,item()*) { switch ($mode) case "default" return map {} default return errors:error("ML-BADPARMS", ("mode", $mode)) }
Function: algorithm-parameters
declare function algorithm-parameters($resolution as xs:string,
$canvas as map(xs:string, item()*)) as map(xs:string,item()*)
declare function algorithm-parameters($resolution as xs:string, $canvas as map(xs:string, item()*)) as map(xs:string,item()*)
Standard callback: Set of default algorithm parameters.
Params
- resolution as xs:string: defined resolution
- canvas as map(xs:string,item()*): drawing canvas
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()*) { map { "description": "Non-overlapping hyphae", "hyphae.n": 25000, (: Max number of nodes :) "hyphae.seeds": 9, (: Initial number of seeds :) "hyphae.branches.max": 15, (: Max branch attempts from node :) "hyphae.radius.max": 40, (: 1/2 max path width :) "hyphae.radius.fade": 0.92, (: Radius change per branch:) "hyphae.radius.function": this:identity#1, (: f(k) adjustment :) "hyphae.search-angle.range": 180, (: Standard deviation in search angle :) "hyphae.search-angle.damping": 0.1, (: 1 - 1/pow(gen,damp) = multiplier of δangle :) "hyphea.fatness": 0.35, (: Relative thickness of lines :) "hyphae.colour.mode": "generation", (: How to calculate colour stops :) "hyphae.stop.max": 512, (: Constraint on colour stops :) "hyphae.tropism.mode": "damped" (: How to grow :) } }
Function: randomizers
declare function randomizers($canvas as map(xs:string,item()*),
$parameters as map(xs:string,item()*)) as map(xs:string,item()*)
declare function randomizers($canvas as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)
Standard callback: Set of randomizers.
Params
- canvas as map(xs:string,item()*): drawing canvas
- parameters as map(xs:string,item()*): algorithm parameter bundle
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 { "hyphae.init-angles": dist:uniform(0, math:pi()*2), "hyphae.search-angles": dist:normal(0, core:parameter("hyphae.search-angle.range", $parameters)), (: ($RAD + 0.2*$RAD*(1 - 2*rand:randomize($rand:STD-UNIFORM))) :) (: Multiply by $RAD to get results :) "hyphae.init-radii": dist:uniform(0.8, 1.2) } }
Function: colophon
declare function colophon($parameters as map(xs:string,item()*)) as xs:string?
declare function colophon($parameters as map(xs:string,item()*)) as xs:string?
Standard callback: component colophon (string attached to signature).
Params
- parameters as map(xs:string,item()*): algorithm parameter bundle
Returns
- xs:string?
declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string? { () }
Function: metadata
declare function metadata($canvas as map(xs:string,item()*),
$randomizers as map(xs:string,item()*),
$parameters as map(xs:string,item()*))
declare function metadata($canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*))
Standard callback: component metadata (in addition to dump of randomizers
and parameters)
Params
- canvas as map(xs:string,item()*): drawing canvas
- randomizers as map(xs:string,item()*): active randomizers for component
- parameters as map(xs:string,item()*): active parameters for component
declare function this:metadata( $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) { () }
Function: identity
declare function identity($k as xs:integer) as xs:double
declare function identity($k as xs:integer) as xs:double
identity()
Default radius adjustment function (no adjustment).
Params
- k as xs:integer: parent node ID
Returns
- xs:double: adjustment
declare function this:identity($k as xs:integer) as xs:double { $k cast as xs:double }
Function: sin
declare function sin($k as xs:integer) as xs:double
declare function sin($k as xs:integer) as xs:double
sin()
Radius adjustment function that expands and contracts.
Params
- k as xs:integer: parent node ID
Returns
- xs:double: adjustment
declare function this:sin($k as xs:integer) as xs:double { math:sin($k div 500) }
Function: hyphae
declare function hyphae($init-boundaries as map(xs:string,item()*)*,
$boundaries as map(xs:string,item()*)*,
$canvas as map(xs:string,item()*),
$randomizers as map(xs:string,item()*),
$parameters as map(xs:string,item()*)) as map(xs:string,item()*)
declare function hyphae($init-boundaries as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)
hyphae()
Non-overlapping hyphae growth.
Params
- init-boundaries as map(xs:string,item()*)*: bounding regions for initial seeds
- boundaries as map(xs:string,item()*)*: bounding regions for growth
- canvas as map(xs:string,item()*)
- randomizers as map(xs:string,item()*)
- parameters as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:hyphae( $init-boundaries as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { let $num_seeds := core:parameter("hyphae.seeds", $parameters) (: : All calculations are done in unit box [0,1]X[0,1] so scale : boundaries to that :) let $init-boundaries := $init-boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $seeds := this:unit-seeds($num_seeds, $init-boundaries) let $boundaries := $boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters) return ( wrapper:wrapper( this:graph($nodes, $canvas, $randomizers, $parameters), map { "hyphae.init-boundaries": geom:quote($init-boundaries), "hyphae.boundaries": geom:quote($boundaries) } ) ) }
Function: hyphae-seeded
declare function hyphae-seeded($seeds as map(xs:string,item()*)*,
$boundaries as map(xs:string,item()*)*,
$canvas as map(xs:string,item()*),
$randomizers as map(xs:string,item()*),
$parameters as map(xs:string,item()*)) as map(xs:string,item()*)
declare function hyphae-seeded($seeds as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*)) as map(xs:string,item()*)
hyphae-seeded()
Non-overlapping hyphae growth with pre-determined seeds.
Params
- seeds as map(xs:string,item()*)*: initial seeds
- boundaries as map(xs:string,item()*)*: bounding regions for growth
- canvas as map(xs:string,item()*)
- randomizers as map(xs:string,item()*)
- parameters as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:hyphae-seeded( $seeds as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { (: : All calculations are done in unit box [0,1]X[0,1] so scale : boundaries and seeds to that :) let $boundaries := $boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $seeds := $seeds=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters) return ( wrapper:wrapper( this:graph($nodes, $canvas, $randomizers, $parameters), map { "hyphae.boundaries": geom:quote($boundaries) } ) ) }
Original Source Code
xquery version "3.1"; (:~ : HYPHAE: : : Parameters: : hyphae.n: Max number of nodes, iteration limit : hyphae.seeds: Initial number of seeds : hyphae.branches.max: Max branch attempts from node : hyphae.radius.max: 1/2 max path width : hyphae.radius.fade: Radius changes this-fold each branch : hyphae.radius.function: f($k) adjustment to radius fade : hyphae.search-angle.range: Standard deviation on search angle : hyphae.search-angle.damping: damping in angle variation per generation : hyphea.tropism.mode: : default=damped: use search angle with increasing variability with each generation : adhoc: use function indicated by hyphae.tropism.function (no default) : the tropism function takes $randomizers, $parameters and returns : a tropism that takes an entry and returns an angle : hyphae.colour.mode: Colouring scheme : default=generation: generation : k: based on parent index : random: random colour : distance: distance from center : width: radius with : single: one (random) colour : order: based on point index : adhoc: use function indicated by hyphae.colour.function (no default) : the colour function takes $entries, $randomizers, $parameters and : returns a function that takes an entry and returns a stop number : hyphae.stop.max: Expected maximum value for colour stops (used for some colour schemes) : hyphae.fatness: Relative width of strokes (0,1] : : Randomizers: : hyphae.init-angles: initial angles : hyphae.search-angles: branching angles : hyphae.init-radii: radius multipliers (multiplied by max-radius) : : Rendering parameters: : background.fill : signature.colour : default-stroke-width : dot-shape : graph.gradient : graph.edge-mode: circles|sand|brush|graph|stroke : sand.grains : sand.opacity : sand.size : : Copyright© Mary Holstege 2020-2023 : CC-BY (https://creativecommons.org/licenses/by/4.0/) : @since February 2023 : @custom:Status Stable :) module namespace this="http://mathling.com/art/hyphae"; import module namespace core="http://mathling.com/art/core" at "../art/core.xqy"; import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"; import module namespace rand="http://mathling.com/core/random" at "../core/random.xqy"; import module namespace sparse="http://mathling.com/core/sparse" at "../core/sparse.xqy"; import module namespace dist="http://mathling.com/type/distribution" at "../types/distributions.xqy"; import module namespace wrapper="http://mathling.com/type/wrapper" at "../types/wrapper.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 inter="http://mathling.com/geometric/intersection" at "../geo/intersection.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 graph="http://mathling.com/geometric/graph" at "../geo/graph.xqy"; import module namespace box="http://mathling.com/geometric/rectangle" at "../geo/rectangle.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"; declare namespace art="http://mathling.com/art"; (:~ : Standard callback: Subcomponent map. :) declare function this:components() as map(xs:string, map(xs:string,item()*)) { map { } }; (:~ : Standard callback: Set of rendering parameters. : @param $canvas: drawing canvas : @param $algorithm-parameters: set of algorithm parameters :) declare function this:rendering-parameters( $canvas as map(xs:string,item()*), $algorithm-parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { map { "background.fill": "black", "signature.colour": "white", "default-stroke-width": 1, "dot-shape": "none", "graph.gradient": "plasma", "graph.edge-mode": "circles", (: circles|sand|brush|graph|stroke :) "sand.grains": 10, "sand.opacity": 0.5, "sand.size": 1 } }; (:~ : Standard callback: Set of algorithm parameters specific to a particular mode. : @param $mode: selected mode : @param $resolution: defined resolution : @param $canvas: drawing canvas :) declare function this:algorithm-mode-parameters( $mode as xs:string, $resolution as xs:string, $canvas as map(xs:string,item()*) ) as map(xs:string,item()*) { switch ($mode) case "default" return map {} default return errors:error("ML-BADPARMS", ("mode", $mode)) }; (:~ : Standard callback: Set of default algorithm parameters. : @param $resolution: defined resolution : @param $canvas: drawing canvas :) declare function this:algorithm-parameters( $resolution as xs:string, $canvas as map(xs:string, item()*) ) as map(xs:string,item()*) { map { "description": "Non-overlapping hyphae", "hyphae.n": 25000, (: Max number of nodes :) "hyphae.seeds": 9, (: Initial number of seeds :) "hyphae.branches.max": 15, (: Max branch attempts from node :) "hyphae.radius.max": 40, (: 1/2 max path width :) "hyphae.radius.fade": 0.92, (: Radius change per branch:) "hyphae.radius.function": this:identity#1, (: f(k) adjustment :) "hyphae.search-angle.range": 180, (: Standard deviation in search angle :) "hyphae.search-angle.damping": 0.1, (: 1 - 1/pow(gen,damp) = multiplier of δangle :) "hyphea.fatness": 0.35, (: Relative thickness of lines :) "hyphae.colour.mode": "generation", (: How to calculate colour stops :) "hyphae.stop.max": 512, (: Constraint on colour stops :) "hyphae.tropism.mode": "damped" (: How to grow :) } }; (:~ : Standard callback: Set of randomizers. : @param $canvas: drawing canvas : @param $parameters: algorithm parameter bundle :) declare function this:randomizers( $canvas as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { map { "hyphae.init-angles": dist:uniform(0, math:pi()*2), "hyphae.search-angles": dist:normal(0, core:parameter("hyphae.search-angle.range", $parameters)), (: ($RAD + 0.2*$RAD*(1 - 2*rand:randomize($rand:STD-UNIFORM))) :) (: Multiply by $RAD to get results :) "hyphae.init-radii": dist:uniform(0.8, 1.2) } }; (:~ : Standard callback: component colophon (string attached to signature). : @param $parameters: algorithm parameter bundle :) declare function this:colophon($parameters as map(xs:string,item()*)) as xs:string? { () }; (:~ : Standard callback: component metadata (in addition to dump of randomizers : and parameters) : : @param $canvas: drawing canvas : @param $randomizers: active randomizers for component : @param $parameters: active parameters for component :) declare function this:metadata( $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) { () }; (: ====================================================================== : Hyphae: non-overlapping growth : ====================================================================== :) declare %private function this:near_zone_inds($pt as map(xs:string,item()*), $zones as xs:integer, $Z as array(*)) as xs:integer* { (: : Z is array of arrays of points: Z[z] = points in zone #z : ZONES is the scaled zone width SIZE idiv ZONEWIDTH : So i and j are the max offset from this point in a zone : Z operates as a 2D array with enough space allocated for the maximum : span of points index (get_z) is (1+x*ZONES)*ZONES + 1+y*ZONES : so we're getting all zone combinations of (i-1,i,i+1) and (j-1,j,j+1) : i.e. adjacent points in the zone arrays :) distinct-values( let $i := (1 + point:px($pt)*$zones) cast as xs:integer let $j := (1 + point:py($pt)*$zones) cast as xs:integer for $a in ($i - 1, $i, $i + 1) for $b in ($j - 1, $j, $j + 1) let $ij := $a*$zones + $b where $ij > 1 return array:flatten($Z($ij)) ) }; (:~ : The zone for this point : ZONES is the scaled zone width SIZE idiv ZONEWIDTH : So i and j are the max offset from this point in a zone : i*ZONES+j is essentially treating the zone array as a 2D array allocating : enough space in each portion for the max zone size :) declare %private function this:get_z($zones as xs:integer, $pt as map(xs:string,item()*)) as xs:integer { let $i := (1 + point:px($pt)*$zones) cast as xs:integer let $j := (1 + point:py($pt)*$zones) cast as xs:integer return $i*$zones+$j }; declare %private function this:node( $node-data as map(xs:string,item()*), $k as xs:integer, (: parent to branch from :) $num as xs:integer, (: current number of nodes :) $boundaries as map(xs:string,item()*)*, $tropism as function (map(xs:string,item()*)) as xs:double ) as map(xs:string,item()*) { let $one := $node-data("one") let $zones := $node-data("zones") let $max-branches := $node-data("max-branches") let $radius-fade := $node-data("radius-fade") * $node-data("radius-adjustment")($k) let $NODES := $node-data("NODES") let $Z := $node-data("Z") let $entry := $NODES=>sparse:get($k) let $entry := $entry=>map:put("br", $entry("br")+1) let $NODES := $NODES=>sparse:put($k, $entry) let $node-data := $node-data=>map:put("NODES", $NODES) return ( if ($entry("br") > $max-branches) then ( (: It's dead, Jim :) $node-data=> map:put("got-node", false()) ) else ( (: Alternative r := RAD + rand:randomize($rand:STD-UNIFORM)*ONE*R_RAND_SIZE :) let $Dk := $entry("d") let $Rk := $entry("r") let $r := if ($Dk > -1 ) then $Rk*$radius-fade else $Rk return ( if ($r < $one) then ( (: Kill this node :) $node-data=> map:put("NODES", $NODES=>sparse:put($k, $entry=>map:put("br", $max-branches + 1) ) )=>map:put("got-node", false()) ) else ( let $GEk := $entry("ge") let $ge := if ($Dk > -1) then $GEk+1 else $GEk let $θ := $tropism($entry) let $PTSk := $entry("pt") let $pt2 := point:destination($PTSk, $θ, $r) return ( (: stop nodes outside of bounds :) (: remember to set initial node inside boundary :) let $good := if ( every $boundary in $boundaries satisfies not(inter:region-contains($boundary, $pt2)) ) then ( (: node is outside bounds :) false() ) else ( let $Pk := $entry("p") let $inds := this:near_zone_inds($pt2,$zones,$Z) let $inds := $inds[not(. = $k) and not(. = $Pk)] return ( if (count($inds) > 0) then ( every $ind in $inds satisfies ( let $node := $NODES=>sparse:get($ind) let $PTSind := $node("pt") let $Rind := $node("r") return (: i.e. the new point is a full circle away from the old :) 2 * point:distance($PTSind, $pt2) > $Rind + $r ) ) else ( true() ) ) ) return ( if ($good) then ( let $new-entry := util:merge-into($NODES=>sparse:get($num), map { "pt": $pt2, "r": $r, "θ": $θ, "ge": $ge, "p": $k } ) let $new-entry := if ($Dk < 0) then $new-entry=>map:put("d", $num) else $new-entry let $NODES := $NODES=>sparse:put($num, $new-entry) let $z := this:get_z($zones, $pt2) let $Z := $Z=>array:put($z, $Z=>array:get($z)=>array:append($num)) let $node-data := $node-data=> map:put("NODES", $NODES)=> map:put("Z", $Z)=> map:put("got-node", true()) return $node-data ) else ( $node-data=> map:put("got-node", false()) ) ) ) ) (: else: r>1 :) ) ) (: else: c < max-branches :) ) }; declare %private function this:nodes( $seeds as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(*) { let $iterations := core:parameter("hyphae.n", $parameters) let $search-angles := core:randomizer("hyphae.search-angles", $randomizers) let $max-branches := core:parameter("hyphae.branches.max", $parameters) let $radius-fade := core:parameter("hyphae.radius.fade", $parameters) let $radius-adjustment := core:parameter("hyphae.radius.function", $parameters) let $damping := core:parameter("hyphae.search-angle.damping", $parameters) let $size := box:diagonal($canvas) let $one := 1 div $size (: one scaled to canvas :) let $rad := core:parameter("hyphae.radius.max", $parameters) * $one let $zonewidth := 2 * ($rad div $one) let $zones := $size idiv $zonewidth let $n-seeds := count($seeds) let $Z := array { for $i in 1 to ($zones+2)*($zones+2) return array{ () } } let $NODES := sparse:array( map { "pt": $point:ORIGIN, (: location on the unit square :) "r": 0, (: radius of line :) "θ": 0, (: current angle :) "ge": 0, (: generation :) "p": -1, (: parent point index :) "br": 0, (: branches attempted :) "d": -1 (: point index :) } ) let $radii := core:randomize($n-seeds, "hyphae.init-radii", $randomizers)!(. *$rad) let $angles := core:randomize($n-seeds, "hyphae.init-angles", $randomizers) let $Z := fold-left(1 to $n-seeds, $Z, function($Z as array(*), $i as xs:integer) as array(*) { let $z := this:get_z($zones, $seeds[$i]) return $Z=>array:put($z, $Z=>array:get($z)=>array:append($i)) } ) let $NODES := fold-left(1 to $n-seeds, $NODES, function($NODES as map(*), $i as xs:integer) as map(*) { $NODES=>sparse:put($i, map { "pt": $seeds[$i], "r": $radii[$i], "θ": $angles[$i], "ge": 1, "p": -1, "br": 0, "d": -1 } ) } ) let $num := $n-seeds let $node-data := map { "NODES": $NODES, "Z": $Z, "one": $one, "zones": $zones, "max-branches": $max-branches, "radius-fade": $radius-fade, "radius-adjustment": $radius-adjustment } let $tropism := (: entry => angle :) switch (core:parameter("hyphae.tropism.mode", $parameters)) case "adhoc" return ( core:parameter("hyphae.tropism.function", $parameters)($randomizers, $parameters) ) default (: damped :) return ( function ($entry as map(*)) as xs:double { let $Dk := $entry("d") let $GEk := $entry("ge") let $ge := if ($Dk > -1) then $GEk+1 else $GEk let $θk := $entry("θ") let $θ := $θk + (1 - 1 div math:pow($ge + 1, $damping))*rand:randomize($search-angles) return $θ } ) let $node-data := ( fold-left(1 to $iterations, ($n-seeds, $node-data), function($info as item()*, $i as xs:integer) as item()* { let $num := head($info) let $node-data := tail($info) let $k := rand:uniform(1, $num)=>rand:cast("integer") let $node-data := this:node($node-data, $k, $num, $boundaries, $tropism) return ( if ($node-data("got-node")) then ( $num + 1, $node-data ) else ( $num, $node-data ) ) } )=>tail() ) return $node-data("NODES") }; declare %private function this:graph( $nodes as map(*), $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { let $scale-x := box:width($canvas) let $scale-y := box:height($canvas) let $fatness := core:parameter("hyphae.fatness", $parameters) let $rscale := (box:width($canvas) idiv 3) * $fatness let $entries := $nodes=>sparse:collection() let $stop-fn := let $colour-mode := core:parameter("hyphae.colour.mode", $parameters) let $stop-max := core:parameter("hyphae.stop.max", $parameters) return switch ($colour-mode) case "distance" return ( let $center := point:point(0.5, 0.5) return function($entry as map(*)) as xs:integer { util:intmix(1, $stop-max, point:distance($entry("pt"), $center) div 0.5) } ) case "random" return ( let $dist := dist:uniform(1, $stop-max)=>dist:cast("integer") return function($entry as map(*)) as xs:integer { rand:randomize($dist) } ) case "single" return ( let $stop := rand:uniform(1, $stop-max)=>rand:cast("integer") return function($entry as map(*)) as xs:integer { $stop } ) case "width" return ( let $radius-max := max($entries!(.("r"))) let $radius-min := min($entries!(.("r"))) let $radius-min := if ($radius-max = $radius-min) then 0 else $radius-min return function($entry as map(*)) as xs:integer { util:intmix(1, $stop-max, 1 - ($entry("r") - $radius-min) div ($radius-max - $radius-min)) } ) case "order" return ( function($entry as map(*)) as xs:integer { $entry("d") } ) case "k" return ( function($entry as map(*)) as xs:integer { $entry("p") } ) case "adhoc" return ( core:parameter("hyphae.colour.function", $parameters)( $entries, $randomizers, $parameters ) ) default (: generation :) return ( let $max-gen := max($entries!(.("ge"))) return function($entry as map(*)) as xs:integer { util:intmix(1, $stop-max, $entry("ge") div $max-gen) } ) let $edges := for $entry in $entries let $k := $entry("p") let $parent := if ($k = -1) then () else ($nodes=>sparse:get($k))("pt") let $stop := $stop-fn($entry) where $k != -1 return ( edge:edge( $parent, $entry("pt"), util:round($entry("r")*$rscale), map {"stop": $stop} )=>geom:scale($scale-x, $scale-y) ) let $points := for $entry in $entries return ( $entry("pt")=>geom:scale($scale-x, $scale-y) ) let $properties := map {"no-vertices": true()} return ( util:log("nodes="||count($entries)||" edges="||count($edges)), graph:graph($points, $edges, $properties) ) }; declare %private function this:unit-seeds( $n-seeds as xs:integer, $bounds as map(xs:string,item()*)* ) as map(xs:string,item()*)* { let $unit := dist:uniform(0.0, 1.0) return if ($n-seeds = 0) then () else ( let $seeds := geom:random-points($n-seeds, $unit, $unit)[ some $boundary in $bounds satisfies $boundary=>inter:region-contains(.) ] let $n := count($seeds) return ( if ($n=$n-seeds) then $seeds else ( $seeds, this:unit-seeds($n-seeds - $n, $bounds) ) ) ) }; (:~ : identity() : Default radius adjustment function (no adjustment). : @param $k: parent node ID : @return adjustment :) declare function this:identity($k as xs:integer) as xs:double { $k cast as xs:double }; (:~ : sin() : Radius adjustment function that expands and contracts. : @param $k: parent node ID : @return adjustment :) declare function this:sin($k as xs:integer) as xs:double { math:sin($k div 500) }; (:~ : hyphae() : Non-overlapping hyphae growth. : : @param $init-boundaries: bounding regions for initial seeds : @param $boundaries: bounding regions for growth : @param: $canvas: drawing canvas : @param: $randomizers: randomizer bundle : @param: $parameters: parameter bundle :) declare function this:hyphae( $init-boundaries as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { let $num_seeds := core:parameter("hyphae.seeds", $parameters) (: : All calculations are done in unit box [0,1]X[0,1] so scale : boundaries to that :) let $init-boundaries := $init-boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $seeds := this:unit-seeds($num_seeds, $init-boundaries) let $boundaries := $boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters) return ( wrapper:wrapper( this:graph($nodes, $canvas, $randomizers, $parameters), map { "hyphae.init-boundaries": geom:quote($init-boundaries), "hyphae.boundaries": geom:quote($boundaries) } ) ) }; (:~ : hyphae-seeded() : Non-overlapping hyphae growth with pre-determined seeds. : : @param $seeds: initial seeds : @param $boundaries: bounding regions for growth : @param: $canvas: drawing canvas : @param: $randomizers: randomizer bundle : @param: $parameters: parameter bundle :) declare function this:hyphae-seeded( $seeds as map(xs:string,item()*)*, $boundaries as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $randomizers as map(xs:string,item()*), $parameters as map(xs:string,item()*) ) as map(xs:string,item()*) { (: : All calculations are done in unit box [0,1]X[0,1] so scale : boundaries and seeds to that :) let $boundaries := $boundaries=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $seeds := $seeds=> geom:scale(1 div box:width($canvas), 1 div box:height($canvas)) let $nodes := this:nodes($seeds, $boundaries, $canvas, $randomizers, $parameters) return ( wrapper:wrapper( this:graph($nodes, $canvas, $randomizers, $parameters), map { "hyphae.boundaries": geom:quote($boundaries) } ) ) };