http://mathling.com/svg/effects  library module

http://mathling.com/svg/effects


Module with functions providing some filter-building support.
Note: this provides hooks to reference Inkscape filters by name
too, but you'll need to install your own version of Inkscape and
point at the filter file (/usr/share/inkscape/filters/filters.svg)
for that to work. By default we expect to find it at ../EFFECTS/inkscape_effects.svg

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

December 2021
Status: Active

Imports

http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/svg/gradients
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy"
http://mathling.com/type/distribution
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/core/sequences
import module namespace seq="http://mathling.com/core/sequences"
       at "../core/sequences.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Variables

Variable: $BUILTIN-EFFECTS as xs:string*

Variable: $NAMED-EFFECTS as xs:string*

Variable: $INKSCAPE-ALIASES as map(xs:string,xs:string)

Variable: $KNOWN-ALIASES as xs:string*

Variable: $PARAMETERIZED-PREFIXES as xs:string*

Variable: $PREFIX-REGEX as xs:string

Variable: $PARAMETERIZED-SUFFIXES as xs:string*

Variable: $SUFFIX-REGEX as xs:string

Variable: $MODIFIER-SUFFIXES as xs:string*

Variable: $MODIFIER-REGEX as xs:string

Variable: $GRAIN-MATRIX as xs:double*

Variable: $WATERGRAIN-MATRIX as xs:double*

Functions

Function: fetch-effect
declare function fetch-effect($effect as xs:string) as element()?


fetch-effect()
Get the filter definition of a known named filter.
Empty otherwise.

Params
  • effect as xs:string
Returns
  • element()?
declare function this:fetch-effect($effect as xs:string) as element()?
{
  (: SLEAZY HACK ALERT :)
  if ($effect = $this:BUILTIN-EFFECTS) then (
    (: Note: may depend on Inkscape effects :)
    switch ($effect)
    case "sandstone" return this:perturb(this:effect("rough_paper"), 1.0, 0.25)=>this:rename($effect)
    case "sandpaper" return this:perturb(this:effect("linen_canvas"), 1.0, 2.0)=>this:rename($effect)
    default return ()
  )
  else if ($effect = $this:NAMED-EFFECTS) then (
    (: Is is a known effect in the effects stylesheet? :)
    (: trace((), "Named effect "||$effect), :)
    let $def :=
      try {
        doc("../stylesheets/effects.xsl")//xsl:param[@name=$effect]/svg:filter
      } catch * { () }
    return (
      if (exists($def)) then $def else (
        try {
          doc("../EFFECTS/"||$effect||".svg")//svg:defs/svg:filter
        } catch * { () }
      )
    )
  ) else if ($effect = $this:INKSCAPE-ALIASES=>map:keys()) then (
    (: Is it a known Inkscape effect, as named in our mapping? :)
    trace((), "Inkscape alias effect "||$effect),
    try {
      let $def := doc("../EFFECTS/inkscape_effects.svg")//svg:filter[@id=$this:INKSCAPE-ALIASES($effect)]
      return (
        if (exists($def)) then (
          <svg:filter id="{$effect}">{
            $def/@* except ($def/@id|$def/@inkscape:*),
            $def/node()
          }</svg:filter>
        ) else ()
      )
    } catch * { () }
  ) else (
    (: Is it a known Inkscape effect, as named by Inkscape? :)
    trace((), "Inkscape effect "||$effect),
    try {
      let $def := doc("../EFFECTS/inkscape_effects.svg")//svg:filter[@id=$effect]
      return (
        if (exists($def)) then (
          <svg:filter>{
            $def/@* except $def/@inkscape:*,
            $def/node()
          }</svg:filter>
        ) else ()
      )
    } catch * {()}
  )
}

Function: get-effect-definition
declare function get-effect-definition($effect as xs:string) as element()?


get-effect-definition()
Get the effect definition of the name, allowing for variants.

Params
  • effect as xs:string
Returns
  • element()?
declare function this:get-effect-definition($effect as xs:string) as element()?
{
  let $effect-pieces := tokenize($effect, "·") (: middle dot :)
  let $effects :=
    for $effect in $effect-pieces return (
    if (($effect = $this:NAMED-EFFECTS) or ($effect = $this:KNOWN-ALIASES)) then (
      this:fetch-effect($effect)
    ) else if (matches($effect, $this:MODIFIER-REGEX)) then (
      let $base-name := replace($effect, $this:MODIFIER-REGEX, '')
      let $modifier := substring-after($effect, $base-name||"-")
      let $base := this:get-effect-definition($base-name)
      return switch($modifier)
      case "overlay" return this:overlay-source($base)=>this:rename($effect)
      case "blend-normal" return this:blend-source($base, "normal")=>this:rename($effect)
      case "blend-multiply" return this:blend-source($base, "multiply")=>this:rename($effect)
      case "blend-screen" return this:blend-source($base, "screen")=>this:rename($effect)
      case "blend-darken" return this:blend-source($base, "darken")=>this:rename($effect)
      case "blend-lighten" return this:blend-source($base, "lighten")=>this:rename($effect)
      case "compose-over" return this:compose-source($base, "over")=>this:rename($effect)
      case "compose-in" return this:compose-source($base, "in")=>this:rename($effect)
      case "compose-out" return this:compose-source($base, "out")=>this:rename($effect)
      case "compose-atop" return this:compose-source($base, "atop")=>this:rename($effect)
      case "compose-xor" return this:compose-source($base, "xor")=>this:rename($effect)
      default return (util:log("Modifier not found "||$modifier||" to "||$base))
    ) else if (matches($effect, $this:PREFIX-REGEX)) then (
      let $parameters := replace($effect, $this:PREFIX-REGEX, '')
      let $operation := substring-before($effect, "-"||$parameters)
      return switch($operation)
      case "shadow" return this:shadow($effect, xs:integer($parameters))
      case "fuzz" return this:fuzz($effect, xs:integer($parameters))
      case "speckled" return this:speckled($effect, 0.25, number($parameters))
      case "wavy" return this:wavy($effect, (0.04, 0.2), xs:integer($parameters))
      default return (util:log("Prefix not found "||$operation||" with "||$parameters))
    ) else if (matches($effect, $this:SUFFIX-REGEX)) then (
      let $parameters := replace($effect, $this:SUFFIX-REGEX, '')
      let $operation := substring-after($effect, $parameters||"-")
      return switch ($operation)
      case "softglow" return this:glow($effect, $parameters, 0.4, 1, 3)
      case "hardglow" return this:glow($effect, $parameters, 1, 5, 11)
      case "glow" return this:glow($effect, $parameters, 1, 2, 5)
      case "roughpaper" return this:texture($effect, $parameters, 0.04)
      case "stone" return this:texture($effect, $parameters, 0.02)
      case "grain" return this:grain($effect, $parameters, $this:GRAIN-MATRIX)
      case "watergrain" return this:grain($effect, $parameters, $this:WATERGRAIN-MATRIX)
      case "glint" return this:glint($effect, $parameters, point:point(200, 200, 50))
      case "filigree" return this:filigree($effect, $parameters)
      default return (util:log("Suffix not found "||$operation||"+"||$parameters))
    ) else (
      util:log("Unknown effect")
    )
  )
  return (
    if (count($effects) le 1) then $effects
    else if (count($effects) = count($effect-pieces)) then (
      this:merge($effect, $effects)
    ) else (
      util:log("Unprocessed effect piece: no effect generated")
    )
  )
}

Function: effect
declare function effect($effect as xs:string) as element()?

Params
  • effect as xs:string
Returns
  • element()?
declare function this:effect($effect as xs:string) as element()?
{
  this:get-effect-definition($effect)
}

Function: ref
declare function ref($effect as xs:string) as xs:string

Params
  • effect as xs:string
Returns
  • xs:string
declare function this:ref($effect as xs:string) as xs:string
{
  if ($effect=("none","")) then "none"
  else if (starts-with($effect, "url(")) then $effect
  else "url(#"||$effect||")"
}

Function: shadow
declare function shadow($name as xs:string, $height as xs:integer) as element(svg:filter)

Params
  • name as xs:string
  • height as xs:integer
Returns
  • element(svg:filter)
declare function this:shadow(
  $name as xs:string,
  $height as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="-0.25" y="-0.25" width="1.5" height="1.5">
    <svg:feGaussianBlur in="SourceAlpha" stdDeviation="2.5" result="blur"/>
    <svg:feColorMatrix result="bluralpha" type="matrix" values=
                   "1 0 0 0   0
                    0 1 0 0   0
                    0 0 1 0   0
                    0 0 0 0.4 0 "/>
    <svg:feOffset in="bluralpha" dx="3" dy="{$height}" result="offsetBlur"/>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="offsetBlur"/>
      <svg:feMergeNode in="SourceGraphic"/>
    </svg:feMerge>
  </svg:filter>
}

Function: fuzz
declare function fuzz($name as xs:string, $width as xs:integer) as element(svg:filter)

Params
  • name as xs:string
  • width as xs:integer
Returns
  • element(svg:filter)
declare function this:fuzz(
  $name as xs:string,
  $width as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0%" y="0%" width="100%" height="100%">
    <svg:feGaussianBlur stdDeviation="{$width}" result="{rand:id('filter')}"/>
  </svg:filter>
}

Function: glow
declare function glow($name as xs:string, $colour as xs:string, $strength as xs:double, (: [0,1] :) $size as xs:integer, $spread as xs:integer) as element(svg:filter)

Params
  • name as xs:string
  • colour as xs:string
  • strength as xs:double
  • size as xs:integer
  • spread as xs:integer
Returns
  • element(svg:filter)
declare function this:glow(
  $name as xs:string,
  $colour as xs:string,
  $strength as xs:double, (: [0,1] :)
  $size as xs:integer,
  $spread as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="{-100*$size}%" y="{-100*$size}%" width="{2*100*$size}%" height="{2*100*$size}%">
    <svg:feFlood result="flood" flood-color="{gradient:colour($colour)}" flood-opacity="{$strength}"></svg:feFlood>
    <svg:feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></svg:feComposite>
    <svg:feMorphology in="mask" result="dilated" operator="dilate" radius="{$size}"></svg:feMorphology>
    <svg:feGaussianBlur in="dilated" result="blurred" stdDeviation="{$spread}"></svg:feGaussianBlur>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="blurred"></svg:feMergeNode>
      <svg:feMergeNode in="SourceGraphic"></svg:feMergeNode>
    </svg:feMerge>
  </svg:filter>
}

Function: texture
declare function texture($name as xs:string, $colour as xs:string, $roughness as xs:double) as element(svg:filter)

Params
  • name as xs:string
  • colour as xs:string
  • roughness as xs:double
Returns
  • element(svg:filter)
declare function this:texture(
  $name as xs:string,
  $colour as xs:string,
  $roughness as xs:double
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="0%" y="0%" width="100%" height="100%">
    <svg:feTurbulence type="fractalNoise" baseFrequency="{$roughness}" numOctaves="5" result="noise"/>
    <svg:feDiffuseLighting in="noise" lighting-color="{gradient:colour($colour)}" surfaceScale="2" result="{rand:id('filter')}">
      <svg:feDistantLight azimuth="45" elevation="35"/>
    </svg:feDiffuseLighting>
  </svg:filter>
}

Function: grain
declare function grain($name as xs:string, $colour as xs:string, $matrix as xs:double*) as element(svg:filter)

Params
  • name as xs:string
  • colour as xs:string
  • matrix as xs:double*
Returns
  • element(svg:filter)
declare function this:grain(
  $name as xs:string,
  $colour as xs:string,
  $matrix as xs:double*
) as element(svg:filter)
{
  <svg:filter id="{$name}" >
    <svg:feFlood flood-color="{gradient:colour($colour)}" result="one"/>
    <svg:feTurbulence baseFrequency=".004,.25" numOctaves="1"
                  seed="403" result="turb"/>
    <svg:feColorMatrix type="matrix" result="turb1" values="{string-join($matrix!string(.),' ')}"/>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="turb"/>
      <svg:feMergeNode in="one"/>
      <svg:feMergeNode in="turb1"/> 
    </svg:feMerge>
  </svg:filter>
}

Function: filigree
declare function filigree($name as xs:string, $colour as xs:string) as element(svg:filter)

Params
  • name as xs:string
  • colour as xs:string
Returns
  • element(svg:filter)
declare function this:filigree(
  $name as xs:string,
  $colour as xs:string
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0" y="0" width="110%" height="110%"
              style="color-interpolation-filters:sRGB;">
    <svg:feFlood flood-color="{$colour}" result="result9"/>
    <svg:feGaussianBlur stdDeviation="7" in="SourceGraphic" result="result8"/>
    <svg:feTurbulence type="turbulence" numOctaves="1" baseFrequency="0.09" result="result7"/>
    <svg:feColorMatrix result="result5"
      values="1 0 0 0 0 
              0 1 0 0 0 
              0 0 1 0 0 
              0 0 0 4 0 "/>
    <svg:feComposite in="result8" in2="result8" operator="in" result="result6"/>
    <svg:feDisplacementMap in="result5" in2="result6" 
      xChannelSelector="A" yChannelSelector="A" scale="100" result="result4"/>
    <svg:feComposite in="result8" in2="result4" operator="in" result="result2"/>
    <svg:feComposite operator="in" in="SourceGraphic" in2="result2" result="fbSourceGraphic"/>
    <svg:feMerge result="result10">
       <svg:feMergeNode in="result9"/>
       <svg:feMergeNode in="fbSourceGraphic"/>
    </svg:feMerge>
    <svg:feComposite in2="SourceGraphic" operator="in" result="result11"/>
  </svg:filter>
}

Function: glint
declare function glint($name as xs:string, $colour as xs:string, $light as map(xs:string,item()*)) as element(svg:filter)

Params
  • name as xs:string
  • colour as xs:string
  • light as map(xs:string,item()*)
Returns
  • element(svg:filter)
declare function this:glint(
  $name as xs:string,
  $colour as xs:string,
  $light as map(xs:string,item()*)
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0" y="0" width="100%" height="100%">
    <svg:feSpecularLighting in="SourceGraphic"
        surfaceScale="5" specularConstant="0.5" specularExponent="90"
        lighting-color="{gradient:colour($colour)}"
        result="specOut">
      <svg:fePointLight x="{point:x($light)}" y="{point:y($light)}" z="{point:z($light)}"/>
    </svg:feSpecularLighting>
    <svg:feComposite
        in="specOut" in2="SourceAlpha" operator="in" result="specOut"/>
    <svg:feComposite result="{rand:id('filter')}"
        in="SourceGraphic" in2="specOut" operator="arithmetic" 
        k1="0" k2="1" k3="1" k4="0"/>
  </svg:filter>
}

Function: speckled
declare function speckled($name as xs:string, $frequency as xs:double, $scale as xs:double) as element(svg:filter)

Params
  • name as xs:string
  • frequency as xs:double
  • scale as xs:double
Returns
  • element(svg:filter)
declare function this:speckled(
  $name as xs:string,
  $frequency as xs:double,
  $scale as xs:double
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="0%" y="0%" width="100%" height="100%">
    <svg:feTurbulence type="turbulence" baseFrequency="{$frequency}"
                      numOctaves="3" result="turbulence"/>
    <svg:feDisplacementMap result="{rand:id('filter')}"
                           in2="turbulence" in="SourceGraphic" scale="{$scale}"
                           xChannelSelector="R" yChannelSelector="G"/>
  </svg:filter>
}

Function: wavy
declare function wavy($name as xs:string, $frequencies as xs:double*, (: x,y or x=y one value :) $scale as xs:integer) as element(svg:filter)

Params
  • name as xs:string
  • frequencies as xs:double*
  • scale as xs:integer
Returns
  • element(svg:filter)
declare function this:wavy(
  $name as xs:string,
  $frequencies as xs:double*, (: x,y or x=y one value :)
  $scale as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}">
    <svg:feTurbulence baseFrequency="{string-join($frequencies!string(.),',')}"
                      numOctaves="2" result="turb"/>
    <svg:feDisplacementMap result="{rand:id('filter')}"
      in="SourceGraphic" in2="turb" scale="{$scale}"
      xChannelSelector="R" yChannelSelector="B"/>
  </svg:filter>
}

Function: effect-ref
declare function effect-ref($name as xs:string, $base-effect-name as xs:string) as element(svg:filter)

Params
  • name as xs:string
  • base-effect-name as xs:string
Returns
  • element(svg:filter)
declare function this:effect-ref(
  $name as xs:string,
  $base-effect-name as xs:string
) as element(svg:filter)
{
  let $def := this:effect($base-effect-name)
  return (
    <svg:filter id="{$name}" xlink:href="#{$base-effect-name}"/>
  )
}

Function: merge
declare function merge($name as xs:string, $effects as element(svg:filter)*) as element(svg:filter)


merge()
Merge several filters into one. Take the min (x%, y%) and max (height%, width%)
as the new extent.

Params
  • name as xs:string: name for new filter @parm $effects: set of filters to combine
  • effects as element(svg:filter)*
Returns
  • element(svg:filter): new filter
declare function this:merge(
  $name as xs:string,
  $effects as element(svg:filter)*
) as element(svg:filter)
{
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter id="{$name}">{
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feMerge result="{rand:id('filter')}">{
        for $effect in $bodies return (
          <svg:feMergeNode in="{$effect/*[last()]/@result}"/>
        )
      }</svg:feMerge>
    }</svg:filter>
  )
}

Function: overlay-source
declare function overlay-source($base as element(svg:filter)) as element(svg:filter)


this:overlay-source()
Construct a filter that overlays the source graphic. Useful for filters
like 'dust' that completely replace the source graphic.

Params
  • base as element(svg:filter): base effect to modify
Returns
  • element(svg:filter)
declare function this:overlay-source(
  $base as element(svg:filter)
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feMerge>
      <svg:feMergeNode in="SourceGraphic"/>
      <svg:feMergeNode/>
    </svg:feMerge>
  }</svg:filter>
}

Function: blend
declare function blend($effect1 as element(svg:filter), $effect2 as element(svg:filter), $mode as xs:string) as element(svg:filter)


this:blend()
Construct a filter that blends the outputs of the two effects.
Take the min (x%, y%) and max (height%, width%) as the new extent.

Params
  • effect1 as element(svg:filter): first effect
  • effect2 as element(svg:filter): second effect
  • mode as xs:string: blend mode (normal|multiply|screen|darken|lighten)
Returns
  • element(svg:filter): new filter
declare function this:blend(
  $effect1 as element(svg:filter),
  $effect2 as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  let $effects := ($effect1, $effect2)
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter>{
      $effect1/@id,
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feBlend result="{rand:id('filter')}" mode="{$mode}"
        in="{$bodies[1]/*[last()]/@result}"
        in2="{$bodies[2]/*[last()]/@result}"/>
    }</svg:filter>
  )
}

Function: blend-source
declare function blend-source($base as element(svg:filter), $mode as xs:string) as element(svg:filter)


this:blend-source()
Construct a filter that blends source graphic with output of base effect.

Params
  • base as element(svg:filter): base effect to modify
  • mode as xs:string: blend mode (normal|multiply|screen|darken|lighten)
Returns
  • element(svg:filter): new filter
declare function this:blend-source(
  $base as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feBlend mode="{$mode}" in="SourceGraphic"/>
  }</svg:filter>
}

Function: compose-source
declare function compose-source($effect1 as element(svg:filter), $effect2 as element(svg:filter), $mode as xs:string) as element(svg:filter)


this:compose-source()
Construct a filter that composes the outputs of the two effects

Params
  • effect1 as element(svg:filter): first effect2: second effect
  • effect2 as element(svg:filter)
  • mode as xs:string: composition mode (over|in|out|atop|xor)
Returns
  • element(svg:filter): new filter
declare function this:compose-source(
  $effect1 as element(svg:filter),
  $effect2 as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  let $effects := ($effect1, $effect2)
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter>{
      $effect1/@id,
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feComposite result="{rand:id('filter')}" operator="{$mode}"
        in="{$bodies[1]/*[last()]/@result}"
        in2="{$bodies[2]/*[last()]/@result}"/>
    }</svg:filter>
  )
}

Function: compose-source
declare function compose-source($base as element(svg:filter), $mode as xs:string) as element(svg:filter)

Params
  • base as element(svg:filter)
  • mode as xs:string
Returns
  • element(svg:filter)
declare function this:compose-source(
  $base as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feComposite operator="{$mode}" in="SourceGraphic"/>
  }</svg:filter>
}

Function: rename
declare function rename($base as element(svg:filter), $name as xs:string) as element(svg:filter)


rename()
Rename the filter

Params
  • base as element(svg:filter): base effect to modify
  • name as xs:string: new name of effect
Returns
  • element(svg:filter)
declare function this:rename(
  $base as element(svg:filter),
  $name as xs:string
) as element(svg:filter)
{
  <svg:filter id="{$name}">{
    $base/(@* except @id),
    $base/node()
  }</svg:filter>
}

Function: extent
declare function extent($base as element(svg:filter), $extent as map(xs:string,item()*), $percent as xs:boolean) as element(svg:filter)


extent()
Create a new effect with different extent (x/y, width/height) as a base effect

Params
  • base as element(svg:filter): base effect to modify
  • extent as map(xs:string,item()*): a box defining the extent min-point => x/y width/height => width/height Example: [-10, -10, 120, 120] => x="-10%" y="-10%" width="130%" height="130%"
  • percent as xs:boolean: interpret extent as percentages (default=true())
Returns
  • element(svg:filter): new filter
declare function this:extent(
  $base as element(svg:filter),
  $extent as map(xs:string,item()*),
  $percent as xs:boolean
) as element(svg:filter)
{
  let $pc := if ($percent) then "%" else ""
  return
  <svg:filter x="{point:x(box:min-point($extent))}{$pc}"
              y="{point:y(box:min-point($extent))}{$pc}"
              width="{box:width($extent)}{$pc}"
              height="{box:height($extent)}{$pc}"
  >{
    $base/(@* except (@x|@y|@width|@height)),
    $base/node()
  }</svg:filter>
}

Function: extent
declare function extent($base as element(svg:filter), $extent as map(xs:string,item()*)) as element(svg:filter)

Params
  • base as element(svg:filter)
  • extent as map(xs:string,item()*)
Returns
  • element(svg:filter)
declare function this:extent(
  $base as element(svg:filter),
  $extent as map(xs:string,item()*)
) as element(svg:filter)
{
  this:extent($base, $extent, true())
}

Function: colour
declare function colour($base as element(svg:filter), $colours as xs:string*) as element(svg:filter)


colour()
Create a new effect with colours replaced with a new colour. All the
lighting-color or flood-color attributes on svg:feFlood, svg:feSpecularLighting,
and svg:feDiffuseLighting elements will be changed to the new colour. If
more than one colour is given, the colours will be selected in rotation.

Params
  • base as element(svg:filter): base effect to modify
  • colours as xs:string*: new colours
Returns
  • element(svg:filter): new filter
declare function this:colour(
  $base as element(svg:filter),
  $colours as xs:string*
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-colour($base/node(), $colours!gradient:colour(.), ())
  }</svg:filter>
}

Function: colour
declare function colour($base as element(svg:filter), $colours as xs:string*, $opacity as xs:double?) as element(svg:filter)

Params
  • base as element(svg:filter)
  • colours as xs:string*
  • opacity as xs:double?
Returns
  • element(svg:filter)
declare function this:colour(
  $base as element(svg:filter),
  $colours as xs:string*,
  $opacity as xs:double?
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-colour($base/node(), $colours!gradient:colour(.), $opacity)
  }</svg:filter>
}

Function: point-light
declare function point-light($base as element(svg:filter), $point as map(xs:string,item()*)) as element(svg:filter)


point-light()
Create a new effect with the lightsource replaced with a point light source.
svg:feDistantLight, svg:fePointLight, svg:feSpotLight in
svg:feDiffuseLighting or svg:feSpecularLighting elements will be replaced
with the new point light source.

Params
  • base as element(svg:filter): base effect to modify
  • point as map(xs:string,item()*): new point light source (should have a z value)
Returns
  • element(svg:filter): new filter
declare function this:point-light(
  $base as element(svg:filter),
  $point as map(xs:string,item()*)
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-light($base/node(), $point)
  }</svg:filter>
}

Function: distant-light
declare function distant-light($base as element(svg:filter), $elevation as xs:double, $azimuth as xs:double) as element(svg:filter)


distant-light()
Create a new effect with the lightsource replaced with a distant light source.
svg:feDistantLight, svg:fePointLight, svg:feSpotLight in
svg:feDiffuseLighting or svg:feSpecularLighting elements will be replaced
with the new light source.

Params
  • base as element(svg:filter): base effect to modify
  • elevation as xs:double: new elevation of the light source
  • azimuth as xs:double: new azimuth of the light source
Returns
  • element(svg:filter): new filter
declare function this:distant-light(
  $base as element(svg:filter),
  $elevation as xs:double,
  $azimuth as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-light($base/node(), $elevation, $azimuth)
  }</svg:filter>
}

Function: perturb
declare function perturb($base as element(svg:filter), $octaves as xs:double, $frequency as xs:double) as element(svg:filter)


perturb()
Create a new effect with turbulence adjusted by new values.
Existing baseFrequency and numOctave attributes on existing svg:feTurbulence
elements will be multiplied by the modifiers.

Params
  • base as element(svg:filter): base effect to modify
  • octaves as xs:double: octave multiplier
  • frequency as xs:double: frequency multiplier
Returns
  • element(svg:filter)
declare function this:perturb(
  $base as element(svg:filter),
  $octaves as xs:double,
  $frequency as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-turbulence($base/node(), $octaves, $frequency)
  }</svg:filter>
}

Function: blur
declare function blur($base as element(svg:filter), $blurring as xs:double) as element(svg:filter)


blur()
Create a new effect with Gaussian blur adjusted by new values.
Existing stdDevision values on existing svg:feGaussianBlur
elements will be multiplied by the modifiers.

Params
  • base as element(svg:filter): base effect to modify
  • blurring as xs:double: std deviation multiplier
Returns
  • element(svg:filter)
declare function this:blur(
  $base as element(svg:filter),
  $blurring as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-blur($base/node(), $blurring)
  }</svg:filter>
}

Function: stretch
declare function stretch($base as element(svg:filter), $scale-x as xs:double, $scale-y as xs:double) as element(svg:filter)


stretch()
Create a new effect with offset adjusted by new values.
Existing dx and dy attributes on existing svg:feOffset
elements will be multiplied by the modifiers.

Params
  • base as element(svg:filter): base effect to modify
  • scale-x as xs:double: dx multiplier
  • scale-y as xs:double: dy multiplier
Returns
  • element(svg:filter)
declare function this:stretch(
  $base as element(svg:filter),
  $scale-x as xs:double,
  $scale-y as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-offset($base/node(), $scale-x, $scale-y)
  }</svg:filter>
}

Function: displace
declare function displace($base as element(svg:filter), $rescale as xs:double) as element(svg:filter)


displace()
Create a new effect with displacement scale offset adjusted by new value.

Params
  • base as element(svg:filter): base effect to modify
  • rescale as xs:double: scale multiplier
Returns
  • element(svg:filter)
declare function this:displace(
  $base as element(svg:filter),
  $rescale as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-scale($base/node(), $rescale)
  }</svg:filter>
}

Original Source Code

xquery version "3.1";
(:~
 : Module with functions providing some filter-building support.
 : Note: this provides hooks to reference Inkscape filters by name
 : too, but you'll need to install your own version of Inkscape and
 : point at the filter file (/usr/share/inkscape/filters/filters.svg)
 : for that to work. By default we expect to find it at ../EFFECTS/inkscape_effects.svg
 :
 : Copyright© Mary Holstege 2021-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since December 2021
 : @custom:Status Active
 :)
module namespace this="http://mathling.com/svg/effects"; 
declare namespace svg="http://www.w3.org/2000/svg";
declare namespace xsl="http://www.w3.org/1999/XSL/Transform";
declare namespace xhtml="http://www.w3.org/1999/xhtml";
declare namespace xlink="http://www.w3.org/1999/xlink";
declare namespace inkscape="http://www.inkscape.org/namespaces/inkscape";
declare namespace map="http://www.w3.org/2005/xpath-functions/map";

import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace seq="http://mathling.com/core/sequences"
       at "../core/sequences.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy";

declare copy-namespaces no-preserve, inherit;

declare variable $this:BUILTIN-EFFECTS as xs:string* := (
  "sandstone",
  "sandpaper"
);

declare variable $this:NAMED-EFFECTS as xs:string* := (
  "wavy", "little-wavy", "warped", "fuzz",
  "blurEnhance", "blur3D", "downBlur3D", "blurShift", "blurShiftSource",
  "watercolour", "stained-glass",
  "splotchify", "dust", "speckled", "flame",
  "incised", "extruded",
  "selfGlow", "softbloom", "topography",
  $this:BUILTIN-EFFECTS
);


  (: "frost", "organic", "pixel_smear", "bark", "scotland", "dragee",
  "garden_of_delights", "enamel_jewelry", "thick_acrylic",
  "metal_casting", "wood3D", "embossed_leather", "bloom" :)
    
declare variable $this:INKSCAPE-ALIASES as map(xs:string,xs:string) := map {
  (: Bevels :)
  "smart_jelly": "f001",     (: Same as Matte jelly but with more controls :)
  "metal_casting": "f002",   (: Smooth drop-like bevel with metallic finish :)
  "jigsaw_piece": "f005",    (: Low, sharp bevel :)
  "bloom": "f009",           (: Soft, cushion-like bevel with matte highlights :)
  "ridged_border": "f010",   (: Ridged border with inner bevel :)
  "button": "f030",          (: Soft bevel, slightly depressed middle :)
  "neon": "f038",            (: Neon light effect :)
  "molten_metal": "f039",    (: Melting parts of object together, with a glossy bevel and a glow :)
  "pressed_steel": "f040",   (: Pressed metal with a rolled edge :)
  "matte_bevel": "f041",     (: Soft, pastel-colored, blurry bevel :)
  "glowing_metal": "f044",   (: Glowing metal texture :)
  "translucent": "f046",     (: Illuminated translucent plastic or glass effect :)
  "raised_border": "f058",   (: Strongly raised border around a flat surface :)
  "fat_oil": "f060",         (: Fat oil with some adjustable turbulence :)
  "electronic_microscopy": "f077", (: Bevel, crude light, discoloration and glow like in electronic microscopy :)
  "stained_glass": "f086",   (: Illuminated stained glass effect :)
  "dark_glass": "f087",      (: Illuminated glass effect with light coming from beneath :)
  "bright_metal": "f161",    (: Bright metallic effect for any color :)
  "deep_colors_plastic": "f162",   (: Transparent plastic with deep colors :)
  "melted_jelly_matte": "f163",    (: Matte bevel with blurred edges :)
  "melted_jelly": "f164",    (: Glossy bevel with blurred edges :)
  "combined_lighting": "f165",     (: Basic specular bevel to use for building textures :)

  (: Blurs :)
  "apparition": "f003",      (: Edges are partly feathered out :)
  "evanescent": "f093",      (: Blur the contents of objects, preserving the outline and adding progressive transparency at edges :)
  "blur_double": "f193",     (: Overlays two copies with different blur amounts and modifiable blend and composite :)

  (: Overlays :)
  "rubber_stamp": "f006",    (: Random whiteouts inside :)
  "speckle": "f012",         (: Fill object with sparse translucent specks :)
  "oil_slick": "f013",       (: Rainbow-colored semitransparent oily splotches :)
  "frost": "f014",           (: Flake-like white splotches :)
  "zebra": "f016",           (: Irregular vertical dark stripes (loses object's own color) :)
  "clouds": "f017",          (: Airy, fluffy, sparse white clouds :)
  "barbed_wire": "f027",     (: Gray bevelled wires with drop shadows :)
  "swiss_cheese": "f028",    (: Random inner-bevel holes :)
  "blue_cheese": "f029",     (: Marble-like bluish speckles :)
  "tartan": "f078",          (: Checkered tartan pattern :)
  "shaken_liquid": "f083",   (: Colorizable filling with flow inside like transparency :)
  "people": "f095",          (: Colorized blotches, like a crowd of people :)
  "scotland": "f096",        (: Colorized mountain tops out of the fog :)
  "garden_of_delights": "f097",   (: Phantasmagorical turbulent wisps, like Hieronymus Bosch's Garden of Delights :)
  "wavy_tartan": "f113",     (: Tartan pattern with a wavy displacement and bevel around the edges :)
  "tiger_fur": "f117",       (: Tiger fur pattern with folds and bevel around the edges :)
  "dots_transparency": "f134",        (: Gives a pointillist HSL sensitive transparency :)
  "canvas_transparency": "f135",      (: Gives a canvas like HSL sensitive transparency. :)
  "smear_transparency": "f136",       (: Paint objects with a transparent turbulence which turns around color edges :)
  "carnaval": "f140",        (: White splotches evocating carnaval masks :)
  "rough_transparency": "f143",       (: Adds a turbulent transparency which displaces pixels at the same time :)
  "growing_cells": "f169",   (: Random rounded living cells like fill :)
  "silhouette_marbled": "f189",       (: Basic noise transparency texture :)
  "alpha_monochrome_cracked": "f201", (: Basic noise fill texture; adjust color in Flood :)
  "alpha_turbulent": "f202",  (: Basic noise fill texture; adjust color in Flood :)
  "colorize_turbulent": "f203",       (: Basic noise fill texture; adjust color in Flood :)
  "cross_noise_b": "f204",    (: Adds a small scale crossy graininess :)
  "cross_noise": "f205",      (: Adds a small scale screen like graininess :)
  "duotone_turbulent": "f206",        (: Basic noise fill texture; adjust color in Flood :)
  "light_eraser_cracked": "f207",     (: Basic noise fill texture; adjust color in Flood :)
  "poster_turbulent": "f208", (: Basic noise fill texture; adjust color in Flood :)
  "tartan_smart": "f209",     (: Highly configurable checkered tartan pattern :)
  "liquid": "f216",           (: Colorizable filling with liquid transparency :)

  (: Protrusions :)
  "ink_bleed": "f007",        (: Inky splotches underneath the object :)
  "fire": "f008",             (: Edges of object are on fire :)
  "dripping": "f032",         (: Random paint streaks downwards :)
  "chewing_gum": "f127",      (: Creates colorizable blotches which smoothly flow over the edges of the lines at their crossings :)

  (: Distort :)
  "ripple": "f011",           (: Horizontal rippling of edges :)
  "pixel_smear": "f034",      (: Van Gogh painting effect for bitmaps :)
  "torn_edges": "f090",       (: Displace the outside of shapes and pictures without altering their content :)
  "roughen_inside": "f092",   (: Roughen all inside shapes :)
  "chalk_and_sponge": "f094", (: Low turbulence gives sponge look and high turbulence chalk :)
  "torn_edges2": "f104",      (: Displace the outside of shapes and pictures without altering their content :)
  "rough_and_dilate": "f131", (: Create a turbulent contour around :)
  "lapping": "f151",          (: Something like a water noise :)
  "swirl": "f185",            (: Paint objects with a transparent turbulence which wraps around color edges :)

  (: Materials :)
  "leopard_fur": "f015",      (: Leopard spots (loses object's own color) :)
  "iridescent_beeswax": "f047",       (: Waxy texture which keeps its iridescence through color fill change :)
  "eroded_metal": "f048",     (: Eroded metal texture with ridges, grooves, holes and bumps :)
  "cracked_lava": "f049",     (: A volcanic texture, a little like leather :)
  "lizard_skin": "f051",      (: Stylized reptile skin texture :)
  "metallized_paint": "f056", (: Metallized effect with a soft lighting, slightly translucent at the edges :)
  "peel_off": "f066",         (: Peeling painting on a wall :)
  "gold_splatter": "f067",    (: Splattered cast metal, with golden highlights :)
  "gold_paste": "f068",       (: Fat pasted cast metal, with golden highlights :)
  "enamel_jewelry": "f070",   (: Slightly cracked enameled texture :)
  "flex_metal": "f112",       (: Bright, polished uneven metal casting, colorizable :)
  "marble_3d": "f114",        (: 3D warped marble texture :)
  "wood_3d": "f115",          (: 3D warped, fibered wood texture :)
  "mother_of_pearl_3d": "f116",       (: 3D warped, iridescent pearly shell texture :)

  (: Image Effects :)
  "sharpen": "f018",          (: Sharpen edges and boundaries within the object, force=0.15 :)
  "sharpen_more": "f019",     (: Sharpen edges and boundaries within the object, force=0.3 :)
  "age": "f025",              (: Imitate aged photograph :)
  "soft_focus_lens": "f085",  (: Glowing image content without blurring it :)
  "film_grain": "f122",       (: Adds a small scale graininess :)

  (: Image Paint and Draw :)
  "oil_painting": "f020",     (: Simulate oil painting style :)
  "pencil": "f021",           (: Detect color edges and retrace them in grayscale :)
  "blueprint": "f022",        (: Detect color edges and retrace them in blue :)
  "old_postcard": "f132",     (: Slightly posterize and draw edges like on old printed postcards :)
  "alpha_engraving": "f145",  (: Gives a transparent engraving effect with rough line and filling :)
  "alpha_draw_liquid": "f146",        (: Gives a transparent fluid drawing effect with rough line and filling :)
  "liquid_drawing": "f147",   (: Gives a fluid and wavy expressionist drawing effect to images :)
  "marbled_ink": "f148",      (: Marbled transparency effect which conforms to image detected edges :)
  "alpha_engraving_b": "f150",        (: Gives a controllable roughness engraving effect to bitmaps and materials :)
  "image_drawing_basic": "f194",      (: Enhance and redraw color edges in 1 bit black and white :)
  "poster_draw": "f195",      (: Enhance and redraw edges around posterized areas :)
  "cross_noise_poster": "f197",       (: Overlay with a small scale screen like noise :)
  "cross_noise_poster_b": "f198",     (: Adds a small scale screen like noise locally :)
  "poster_color_fun": "f199", (: Poster Color Fun :)
  "poster_rough": "f200",     (: Adds roughness to one of the two channels of the Poster paint filter :)
  "light_contour": "f210",    (: Uses vertical specular light to draw lines :)
  "chromolitho_alternate": "filter81", (: Old chromolithographic effect :)
  "litho": "filter169",       (: Create a two colors lithographic effect :)

  (: Textures :)
  "organic": "f026",          (: Bulging, knotty, slick 3D surface :)
  "jam_spread": "f033",       (: Glossy clumpy jam spread :)
  "cracked_glass": "f035",    (: Under a cracked glass :)
  "bark": "f050",             (: Bark texture, vertical; use with deep colors :)
  "stone_wall": "f052",       (: Stone wall texture to use with not too saturated colors :)
  "silk_carpet": "f053",      (: Silk carpet texture, horizontal stripes :)
  "crumpled_plastic": "f069", (: Crumpled matte plastic, with melted edge :)
  "rough_paper": "f071",      (: Aquarelle paper effect which can be used for pictures as for objects :)
  "rough_and_glossy": "f072", (: Crumpled glossy paper effect which can be used for pictures as for objects :)
  "blotting_paper": "f101",   (: Inkblot on blotting paper :)
  "wax_print": "f102",        (: Wax print on tissue texture :)
  "watercolor": "f107",       (: Cloudy watercolor effect :)
  "felt": "f108",             (: Felt like texture with color turbulence and slightly darker at the edges :)
  "ink_paint": "f109",        (: Ink paint on paper with some turbulent color shift :)
  "tinted_rainbow": "f110",   (: Smooth rainbow colors melted along the edges and colorizable :)
  "melted_rainbow": "f111",   (: Smooth rainbow colors slightly melted along the edges :)
  "warped_rainbow": "f130",   (: Smooth rainbow colors warped along the edges and colorizable :)
  "burst": "f138",            (: Burst balloon texture crumpled and with holes :)
  "gouache": "f144",          (: Partly opaque water color effect with bleed :)
  "riddled": "f155",          (: Riddle the surface and add bump to images :)

  (: Shadows and Glows :)
  "inset": "f031",            (: Shadowy outer bevel :)
  "in_and_out": "f073",       (: Inner colorized shadow, outer black shadow :)
  "cutout_glow": "f098",      (: In and out glow with a possible offset and colorizable flood :)
  "dark_and_glow": "f128",    (: Darkens the edge with an inner blur and adds a flexible glow :)
  "emergence": "filter127",   (: Cut out, add inner shadow and colorize some parts of an image :)

  (: Bumps :)
  "bubbly_bumps": "f036",     (: Flexible bubbles effect with some displacement :)
  "hsl_bumps_alpha": "f088",  (: Same as HSL Bumps but with transparent highlights :)
  "bubbly_bumps_alpha": "f089",       (: Same as Bubbly Bumps but with transparent highlights :)
  "dark_emboss": "f099",      (: Emboss effect : 3D relief where white is replaced by black :)
  "bubbly_bumps_matte": "f100",       (: Same as Bubbly Bumps but with a diffuse light instead of a specular one :)
  "plaster_color": "f123",    (: Colored plaster emboss effect :)
  "velvet_bumps": "f124",     (: Gives Smooth Bumps velvet like :)
  "thick_paint": "f137",      (: Thick painting effect with turbulence :)
  "embossed_leather": "f139", (: Combine a HSL edges detection bump with a leathery or woody and colorizable texture :)
  "plastify": "f141",         (: HSL edges detection bump with a wavy reflective surface effect and variable crumple :)
  "plaster": "f142",          (: Combine a HSL edges detection bump with a matte and crumpled surface effect :)
  "thick_acrylic": "f149",    (: Thick acrylic paint texture with high texture depth :)
  "wrinkled_varnish": "f156", (: Thick glossy and translucent paint texture with high depth :)
  "canvas_bumps": "f157",     (: Canvas texture with an HSL sensitive height map :)
  "canvas_bumps_matte": "f158",       (: Same as Canvas Bumps but with a diffuse light instead of a specular one :)
  "canvas_bumps_alpha": "f159",       (: Same as Canvas Bumps but with transparent highlights :)
  "tinfoil": "f166",          (: Metallic foil effect combining two lighting types and variable crumple :)
  "relief_print": "f168",     (: Bumps effect with a bevel, color flood and complex lighting :)
  "basic_diffuse_bump": "f173",       (: Matte emboss effect :)
  "basic_specular_bump": "f174",      (: Specular emboss effect :)
  "basic_two_lights_bump": "f175",    (: Two types of lighting emboss effect :)
  "linen_canvas": "f176",     (: Painting canvas emboss effect :)
  "plasticine": "f177",       (: Matte modeling paste emboss effect :)
  "rough_canvas_painting": "f178",    (: Painting canvas emboss effect :)
  "paper_bump": "f179",       (: Paper like emboss effect :)
  "jelly_bump": "f180",       (: Convert pictures to thick jelly :)
  "bump_engraving": "filter53",       (: Carving emboss effect :)
  "convoluted_bump": "filter109",     (: Convoluted emboss effect :)

  (: Ridges :)
  "glowing_bubble": "f037",   (: Bubble effect with refraction and glow :)
  "thin_membrane": "f042",    (: Thin like a soap membrane :)
  "matte_ridge": "f043",      (: Soft pastel ridge :)
  "refractive_gel_a": "f054", (: Gel effect with light refraction :)
  "refractive_gel_b": "f055", (: Gel effect with strong refraction :)
  "dragee": "f057",                   (: Gel Ridge with a pearlescent look :)
  "metallized_ridge": "f059", (: Gel Ridge metallized at its top :)

  (: Scatter :)
  "leaves": "f045",           (: Leaves on the ground in Fall, or living foliage :)
  "cubes": "f065",            (: Scattered cubes; adjust the Morphology primitive to vary size :)
  "air_spray": "f074",        (: Convert to small scattered particles with some thickness :)
  "pointillism": "f188",      (: Gives a turbulent pointillist HSL sensitive transparency :)

  (: Morphology :)
  "black_hole": "f063",       (: Creates a black light inside and outside :)
  "warm_inside": "f075",      (: Blurred colorized contour, filled inside :)
  "cool_outside": "f076",     (: Blurred colorized contour, empty inside :)
  "contouring_table": "filter581",    (: Blurred multiple contours for objects :)
  "posterized_blur": "filter609",     (: Converts blurred contour to posterized steps :)
  "contouring_discrete": "filter672", (: Sharp multiple contour for objects :)

  (: Color :)
  "black_light": "f119",      (: Light areas turn to black :)
  "soft_colors": "f167",      (: Adds a colorizable edges glow inside objects and pictures :)
  "fluorescence": "f170",     (: Oversaturate colors which can be fluorescent in real world :)
  "blend_opposites": "f181",  (: Blend an image with its hue opposite :)
  "hue_to_white": "f182",     (: Fades hue progressively to white :)
  "paint_channels": "filter291",      (: Colorize separately the three color channels :)
  "trichrome": "filter499",   (: Like Duochrome but with three colors :)
  "simulate_cmy": "filter106",        (: Render Cyan, Magenta and Yellow channels with a colorizable background :)

  (: Non-Realistic 3D Shaders :)
  "comics_cream": "f125",     (: Comics shader with creamy waves transparency :)
  "aluminium": "f217",        (: Aluminium effect with sharp brushed reflections :)
  "comics": "f218",           (: Comics cartoon drawing effect :)
  "comics_draft": "f219",     (: Draft painted cartoon shading with a glassy look :)
  "comics_fading": "f220",    (: Cartoon paint style with some fading at the edges :)
  "brushed_metal": "f221",    (: Satiny metal surface effect :)
  "opaline": "f222",          (: Contouring version of smooth shader :)
  "chrome": "f223",           (: Bright chrome effect :)
  "deep_chrome": "f224",      (: Dark chrome effect :)
  "emboss_shader": "f225",    (: Combination of satiny and emboss effect :)
  "sharp_metal": "f226",      (: Chrome effect with darkened edges :)
  "brush_draw": "f227",       (: Draft painted cartoon shading with a glassy look :)
  "chrome_emboss": "f228",    (: Embossed chrome effect :)
  "contour_emboss": "f229",   (: Satiny and embossed contour effect :)
  "sharp_deco": "f230",       (: Unrealistic reflections with sharp edges :)
  "deep_metal": "f231",       (: Deep and dark metal shading :)
  "aluminium_emboss": "f232", (: Satiny aluminium effect with embossing :)
  "refractive_glass": "f233", (: Double reflection through glass with some refraction :)
  "frosted_glass": "f234",    (: Satiny glass effect :)

  (: Fill and Transparency :)
  "fast_crop": "f000", (: Does not filter but adds a filter region :)
  "monochrome_transparency": "f152",  (: Convert to a colorizable transparent positive or negative :)
  "saturation_map": "f154",   (: Creates an approximative semi-transparent and colorizable image of the saturation levels :)
  "fill_background": "f190",  (: Adds a colorizable opaque background :)
  "flatten_transparency": "f191",     (: Adds a white opaque background :)
  "posterized_light_eraser": "filter451",     (: Create a semi transparent posterized image :)

  (: Pixel Tools :)
  "pixellize": "f171"         (: Reduce or remove antialiasing around shapes :)
};

declare variable $this:KNOWN-ALIASES as xs:string* := (
  $this:INKSCAPE-ALIASES=>map:keys(),
  util:map-entries($this:INKSCAPE-ALIASES)
);

declare variable $this:PARAMETERIZED-PREFIXES as xs:string* := (
  "shadow-", "fuzz-", "speckled-", "wavy-"
);

declare variable $this:PREFIX-REGEX as xs:string :=
  "^("||string-join($this:PARAMETERIZED-PREFIXES, "|")||")"
;

declare variable $this:PARAMETERIZED-SUFFIXES as xs:string* := (
  "-softglow", "-hardglow", "-glow", 
  "-roughpaper", "-stone", 
  "-grain", "-watergrain", "-glint", "-filigree"
);

declare variable $this:SUFFIX-REGEX as xs:string :=
  "("||string-join($this:PARAMETERIZED-SUFFIXES, "|")||")$"
;

declare variable $this:MODIFIER-SUFFIXES as xs:string* := (
  "-overlay",
  "-blend-normal",
  "-blend-multiply",
  "-blend-screen",
  "-blend-darken",
  "-blend-lighten",
  "-compose-over",
  "-compose-in",
  "-compose-out",
  "-compose-atop",
  "-compose-xor"
);

declare variable $this:MODIFIER-REGEX as xs:string :=
  "("||string-join($this:MODIFIER-SUFFIXES, "|")||")$"
;


declare variable $this:GRAIN-MATRIX as xs:double* := (
   0.4E0,    0E0,   0E0, -1E0, 0.4E0,
     0E0,   -1E0,   0E0, -1E0, 0.3E0,
     0E0,    0E0,  -1E0, -1E0, 0.2E0,
  -0.4E0, -0.2E0, 0.4E0,  1E0, 0.8E0
);

declare variable $this:WATERGRAIN-MATRIX as xs:double* := (
     1E0,    0E0,   0E0, -1E0,   0E0,
     0E0,   -1E0,   0E0, -1E0, 0.5E0,
     0E0,    0E0,  -1E0, -1E0, 0.5E0,
  -0.5E0, -0.5E0,   1E0,  0E0, 0.5E0
);

(:~
 : fetch-effect()
 : Get the filter definition of a known named filter.
 : Empty otherwise.
 :)
declare function this:fetch-effect($effect as xs:string) as element()?
{
  (: SLEAZY HACK ALERT :)
  if ($effect = $this:BUILTIN-EFFECTS) then (
    (: Note: may depend on Inkscape effects :)
    switch ($effect)
    case "sandstone" return this:perturb(this:effect("rough_paper"), 1.0, 0.25)=>this:rename($effect)
    case "sandpaper" return this:perturb(this:effect("linen_canvas"), 1.0, 2.0)=>this:rename($effect)
    default return ()
  )
  else if ($effect = $this:NAMED-EFFECTS) then (
    (: Is is a known effect in the effects stylesheet? :)
    (: trace((), "Named effect "||$effect), :)
    let $def :=
      try {
        doc("../stylesheets/effects.xsl")//xsl:param[@name=$effect]/svg:filter
      } catch * { () }
    return (
      if (exists($def)) then $def else (
        try {
          doc("../EFFECTS/"||$effect||".svg")//svg:defs/svg:filter
        } catch * { () }
      )
    )
  ) else if ($effect = $this:INKSCAPE-ALIASES=>map:keys()) then (
    (: Is it a known Inkscape effect, as named in our mapping? :)
    trace((), "Inkscape alias effect "||$effect),
    try {
      let $def := doc("../EFFECTS/inkscape_effects.svg")//svg:filter[@id=$this:INKSCAPE-ALIASES($effect)]
      return (
        if (exists($def)) then (
          <svg:filter id="{$effect}">{
            $def/@* except ($def/@id|$def/@inkscape:*),
            $def/node()
          }</svg:filter>
        ) else ()
      )
    } catch * { () }
  ) else (
    (: Is it a known Inkscape effect, as named by Inkscape? :)
    trace((), "Inkscape effect "||$effect),
    try {
      let $def := doc("../EFFECTS/inkscape_effects.svg")//svg:filter[@id=$effect]
      return (
        if (exists($def)) then (
          <svg:filter>{
            $def/@* except $def/@inkscape:*,
            $def/node()
          }</svg:filter>
        ) else ()
      )
    } catch * {()}
  )
};

(:~ 
 : get-effect-definition()
 : Get the effect definition of the name, allowing for variants.
 :)
declare function this:get-effect-definition($effect as xs:string) as element()?
{
  let $effect-pieces := tokenize($effect, "·") (: middle dot :)
  let $effects :=
    for $effect in $effect-pieces return (
    if (($effect = $this:NAMED-EFFECTS) or ($effect = $this:KNOWN-ALIASES)) then (
      this:fetch-effect($effect)
    ) else if (matches($effect, $this:MODIFIER-REGEX)) then (
      let $base-name := replace($effect, $this:MODIFIER-REGEX, '')
      let $modifier := substring-after($effect, $base-name||"-")
      let $base := this:get-effect-definition($base-name)
      return switch($modifier)
      case "overlay" return this:overlay-source($base)=>this:rename($effect)
      case "blend-normal" return this:blend-source($base, "normal")=>this:rename($effect)
      case "blend-multiply" return this:blend-source($base, "multiply")=>this:rename($effect)
      case "blend-screen" return this:blend-source($base, "screen")=>this:rename($effect)
      case "blend-darken" return this:blend-source($base, "darken")=>this:rename($effect)
      case "blend-lighten" return this:blend-source($base, "lighten")=>this:rename($effect)
      case "compose-over" return this:compose-source($base, "over")=>this:rename($effect)
      case "compose-in" return this:compose-source($base, "in")=>this:rename($effect)
      case "compose-out" return this:compose-source($base, "out")=>this:rename($effect)
      case "compose-atop" return this:compose-source($base, "atop")=>this:rename($effect)
      case "compose-xor" return this:compose-source($base, "xor")=>this:rename($effect)
      default return (util:log("Modifier not found "||$modifier||" to "||$base))
    ) else if (matches($effect, $this:PREFIX-REGEX)) then (
      let $parameters := replace($effect, $this:PREFIX-REGEX, '')
      let $operation := substring-before($effect, "-"||$parameters)
      return switch($operation)
      case "shadow" return this:shadow($effect, xs:integer($parameters))
      case "fuzz" return this:fuzz($effect, xs:integer($parameters))
      case "speckled" return this:speckled($effect, 0.25, number($parameters))
      case "wavy" return this:wavy($effect, (0.04, 0.2), xs:integer($parameters))
      default return (util:log("Prefix not found "||$operation||" with "||$parameters))
    ) else if (matches($effect, $this:SUFFIX-REGEX)) then (
      let $parameters := replace($effect, $this:SUFFIX-REGEX, '')
      let $operation := substring-after($effect, $parameters||"-")
      return switch ($operation)
      case "softglow" return this:glow($effect, $parameters, 0.4, 1, 3)
      case "hardglow" return this:glow($effect, $parameters, 1, 5, 11)
      case "glow" return this:glow($effect, $parameters, 1, 2, 5)
      case "roughpaper" return this:texture($effect, $parameters, 0.04)
      case "stone" return this:texture($effect, $parameters, 0.02)
      case "grain" return this:grain($effect, $parameters, $this:GRAIN-MATRIX)
      case "watergrain" return this:grain($effect, $parameters, $this:WATERGRAIN-MATRIX)
      case "glint" return this:glint($effect, $parameters, point:point(200, 200, 50))
      case "filigree" return this:filigree($effect, $parameters)
      default return (util:log("Suffix not found "||$operation||"+"||$parameters))
    ) else (
      util:log("Unknown effect")
    )
  )
  return (
    if (count($effects) le 1) then $effects
    else if (count($effects) = count($effect-pieces)) then (
      this:merge($effect, $effects)
    ) else (
      util:log("Unprocessed effect piece: no effect generated")
    )
  )
};

declare function this:effect($effect as xs:string) as element()?
{
  this:get-effect-definition($effect)
};

declare function this:ref($effect as xs:string) as xs:string
{
  if ($effect=("none","")) then "none"
  else if (starts-with($effect, "url(")) then $effect
  else "url(#"||$effect||")"
};

(:======================================================================
 : Basic effect constructions
 :======================================================================:)

declare function this:shadow(
  $name as xs:string,
  $height as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="-0.25" y="-0.25" width="1.5" height="1.5">
    <svg:feGaussianBlur in="SourceAlpha" stdDeviation="2.5" result="blur"/>
    <svg:feColorMatrix result="bluralpha" type="matrix" values=
                   "1 0 0 0   0
                    0 1 0 0   0
                    0 0 1 0   0
                    0 0 0 0.4 0 "/>
    <svg:feOffset in="bluralpha" dx="3" dy="{$height}" result="offsetBlur"/>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="offsetBlur"/>
      <svg:feMergeNode in="SourceGraphic"/>
    </svg:feMerge>
  </svg:filter>
};

declare function this:fuzz(
  $name as xs:string,
  $width as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0%" y="0%" width="100%" height="100%">
    <svg:feGaussianBlur stdDeviation="{$width}" result="{rand:id('filter')}"/>
  </svg:filter>
};

declare function this:glow(
  $name as xs:string,
  $colour as xs:string,
  $strength as xs:double, (: [0,1] :)
  $size as xs:integer,
  $spread as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="{-100*$size}%" y="{-100*$size}%" width="{2*100*$size}%" height="{2*100*$size}%">
    <svg:feFlood result="flood" flood-color="{gradient:colour($colour)}" flood-opacity="{$strength}"></svg:feFlood>
    <svg:feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></svg:feComposite>
    <svg:feMorphology in="mask" result="dilated" operator="dilate" radius="{$size}"></svg:feMorphology>
    <svg:feGaussianBlur in="dilated" result="blurred" stdDeviation="{$spread}"></svg:feGaussianBlur>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="blurred"></svg:feMergeNode>
      <svg:feMergeNode in="SourceGraphic"></svg:feMergeNode>
    </svg:feMerge>
  </svg:filter>
};

declare function this:texture(
  $name as xs:string,
  $colour as xs:string,
  $roughness as xs:double
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="0%" y="0%" width="100%" height="100%">
    <svg:feTurbulence type="fractalNoise" baseFrequency="{$roughness}" numOctaves="5" result="noise"/>
    <svg:feDiffuseLighting in="noise" lighting-color="{gradient:colour($colour)}" surfaceScale="2" result="{rand:id('filter')}">
      <svg:feDistantLight azimuth="45" elevation="35"/>
    </svg:feDiffuseLighting>
  </svg:filter>
};

declare function this:grain(
  $name as xs:string,
  $colour as xs:string,
  $matrix as xs:double*
) as element(svg:filter)
{
  <svg:filter id="{$name}" >
    <svg:feFlood flood-color="{gradient:colour($colour)}" result="one"/>
    <svg:feTurbulence baseFrequency=".004,.25" numOctaves="1"
                  seed="403" result="turb"/>
    <svg:feColorMatrix type="matrix" result="turb1" values="{string-join($matrix!string(.),' ')}"/>
    <svg:feMerge result="{rand:id('filter')}">
      <svg:feMergeNode in="turb"/>
      <svg:feMergeNode in="one"/>
      <svg:feMergeNode in="turb1"/> 
    </svg:feMerge>
  </svg:filter>
};

declare function this:filigree(
  $name as xs:string,
  $colour as xs:string
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0" y="0" width="110%" height="110%"
              style="color-interpolation-filters:sRGB;">
    <svg:feFlood flood-color="{$colour}" result="result9"/>
    <svg:feGaussianBlur stdDeviation="7" in="SourceGraphic" result="result8"/>
    <svg:feTurbulence type="turbulence" numOctaves="1" baseFrequency="0.09" result="result7"/>
    <svg:feColorMatrix result="result5"
      values="1 0 0 0 0 
              0 1 0 0 0 
              0 0 1 0 0 
              0 0 0 4 0 "/>
    <svg:feComposite in="result8" in2="result8" operator="in" result="result6"/>
    <svg:feDisplacementMap in="result5" in2="result6" 
      xChannelSelector="A" yChannelSelector="A" scale="100" result="result4"/>
    <svg:feComposite in="result8" in2="result4" operator="in" result="result2"/>
    <svg:feComposite operator="in" in="SourceGraphic" in2="result2" result="fbSourceGraphic"/>
    <svg:feMerge result="result10">
       <svg:feMergeNode in="result9"/>
       <svg:feMergeNode in="fbSourceGraphic"/>
    </svg:feMerge>
    <svg:feComposite in2="SourceGraphic" operator="in" result="result11"/>
  </svg:filter>
};

declare function this:glint(
  $name as xs:string,
  $colour as xs:string,
  $light as map(xs:string,item()*)
) as element(svg:filter)
{
  <svg:filter id="{$name}" filterUnits="userSpaceOnUse"
              x="0" y="0" width="100%" height="100%">
    <svg:feSpecularLighting in="SourceGraphic"
        surfaceScale="5" specularConstant="0.5" specularExponent="90"
        lighting-color="{gradient:colour($colour)}"
        result="specOut">
      <svg:fePointLight x="{point:x($light)}" y="{point:y($light)}" z="{point:z($light)}"/>
    </svg:feSpecularLighting>
    <svg:feComposite
        in="specOut" in2="SourceAlpha" operator="in" result="specOut"/>
    <svg:feComposite result="{rand:id('filter')}"
        in="SourceGraphic" in2="specOut" operator="arithmetic" 
        k1="0" k2="1" k3="1" k4="0"/>
  </svg:filter>
};

declare function this:speckled(
  $name as xs:string,
  $frequency as xs:double,
  $scale as xs:double
) as element(svg:filter)
{
  <svg:filter id="{$name}" x="0%" y="0%" width="100%" height="100%">
    <svg:feTurbulence type="turbulence" baseFrequency="{$frequency}"
                      numOctaves="3" result="turbulence"/>
    <svg:feDisplacementMap result="{rand:id('filter')}"
                           in2="turbulence" in="SourceGraphic" scale="{$scale}"
                           xChannelSelector="R" yChannelSelector="G"/>
  </svg:filter>
};

declare function this:wavy(
  $name as xs:string,
  $frequencies as xs:double*, (: x,y or x=y one value :)
  $scale as xs:integer
) as element(svg:filter)
{
  <svg:filter id="{$name}">
    <svg:feTurbulence baseFrequency="{string-join($frequencies!string(.),',')}"
                      numOctaves="2" result="turb"/>
    <svg:feDisplacementMap result="{rand:id('filter')}"
      in="SourceGraphic" in2="turb" scale="{$scale}"
      xChannelSelector="R" yChannelSelector="B"/>
  </svg:filter>
};

(:======================================================================
 : Effect modifications
 :======================================================================:)

declare function this:effect-ref(
  $name as xs:string,
  $base-effect-name as xs:string
) as element(svg:filter)
{
  let $def := this:effect($base-effect-name)
  return (
    <svg:filter id="{$name}" xlink:href="#{$base-effect-name}"/>
  )
};

(:~
 : merge()
 : Merge several filters into one. Take the min (x%, y%) and max (height%, width%)
 : as the new extent.
 : @param $name: name for new filter
 : @parm $effects: set of filters to combine
 : @return new filter
 :)
declare function this:merge(
  $name as xs:string,
  $effects as element(svg:filter)*
) as element(svg:filter)
{
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter id="{$name}">{
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feMerge result="{rand:id('filter')}">{
        for $effect in $bodies return (
          <svg:feMergeNode in="{$effect/*[last()]/@result}"/>
        )
      }</svg:feMerge>
    }</svg:filter>
  )
};

(:~
 : this:overlay-source()
 : Construct a filter that overlays the source graphic. Useful for filters
 : like 'dust' that completely replace the source graphic.
 :
 : @param $base: base effect to modify
 :)
declare function this:overlay-source(
  $base as element(svg:filter)
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feMerge>
      <svg:feMergeNode in="SourceGraphic"/>
      <svg:feMergeNode/>
    </svg:feMerge>
  }</svg:filter>
};

(:~
 : this:blend()
 : Construct a filter that blends the outputs of the two effects.
 : Take the min (x%, y%) and max (height%, width%) as the new extent.
 :
 : @param $effect1: first effect
 : @param $effect2: second effect
 : @param $mode: blend mode (normal|multiply|screen|darken|lighten)
 : @return new filter
 :)
declare function this:blend(
  $effect1 as element(svg:filter),
  $effect2 as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  let $effects := ($effect1, $effect2)
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter>{
      $effect1/@id,
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feBlend result="{rand:id('filter')}" mode="{$mode}"
        in="{$bodies[1]/*[last()]/@result}"
        in2="{$bodies[2]/*[last()]/@result}"/>
    }</svg:filter>
  )
};

(:~
 : this:blend-source()
 : Construct a filter that blends source graphic with output of base effect.
 :
 : @param $base: base effect to modify
 : @param $mode: blend mode (normal|multiply|screen|darken|lighten)
 : @return new filter
 :)
declare function this:blend-source(
  $base as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feBlend mode="{$mode}" in="SourceGraphic"/>
  }</svg:filter>
};

(:~
 : this:compose-source()
 : Construct a filter that composes the outputs of the two effects
 :
 : @param $effect1: first effect
 : @param $effect12: second effect
 : @param $mode: composition mode (over|in|out|atop|xor)
 : @return new filter
 :)
declare function this:compose-source(
  $effect1 as element(svg:filter),
  $effect2 as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  let $effects := ($effect1, $effect2)
  let $min-x := min(
    for $x in $effects/@x[ends-with(.,"%")]
    return number(substring-before($x,"%"))
  )
  let $min-y := min(
    for $y in $effects/@x[ends-with(.,"%")]
    return number(substring-before($y,"%"))
  )
  let $max-width := max(
    for $width in $effects/@width[ends-with(.,"%")]
    return number(substring-before($width,"%"))
  )
  let $max-height := max(
    for $height in $effects/@height[ends-with(.,"%")]
    return number(substring-before($height,"%"))
  )
  let $bodies :=
    for $effect in $effects return (
      <body>{
      if (exists($effect/*[last()]/@result)) then (
        $effect/node()
      ) else (
        $effect/*[position() < last()],
        let $last := $effect/*[last()]
        return (
          $effect/node()[position() < last()],
          element {node-name($last)} {
            $last/@*,
            attribute result {rand:id("step")},
            $last/node()
          }
        )
      )
      }</body>
    )
  return (
    <svg:filter>{
      $effect1/@id,
      if (exists($min-x)) then attribute x {$min-x||"%"} else (),
      if (exists($min-y)) then attribute y {$min-y||"%"} else (),
      if (exists($max-width)) then attribute width {$max-width||"%"} else (),
      if (exists($max-height)) then attribute height {$max-height||"%"} else (),
      $bodies/*,
      <svg:feComposite result="{rand:id('filter')}" operator="{$mode}"
        in="{$bodies[1]/*[last()]/@result}"
        in2="{$bodies[2]/*[last()]/@result}"/>
    }</svg:filter>
  )
};

declare function this:compose-source(
  $base as element(svg:filter),
  $mode as xs:string
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    $base/node(),
    <svg:feComposite operator="{$mode}" in="SourceGraphic"/>
  }</svg:filter>
};

(:~
 : rename()
 : Rename the filter
 :
 : @param $base: base effect to modify
 : @param $name: new name of effect
 :)
declare function this:rename(
  $base as element(svg:filter),
  $name as xs:string
) as element(svg:filter)
{
  <svg:filter id="{$name}">{
    $base/(@* except @id),
    $base/node()
  }</svg:filter>
};

(:~
 : extent()
 : Create a new effect with different extent (x/y, width/height) as a base effect
 :
 : @param $base: base effect to modify
 : @param $extent: a box defining the extent
 :   min-point => x/y 
 :   width/height => width/height 
 :   Example: [-10, -10, 120, 120] => x="-10%" y="-10%" width="130%" height="130%"
 : @param $percent: interpret extent as percentages (default=true())
 : @return new filter
 :)
declare function this:extent(
  $base as element(svg:filter),
  $extent as map(xs:string,item()*),
  $percent as xs:boolean
) as element(svg:filter)
{
  let $pc := if ($percent) then "%" else ""
  return
  <svg:filter x="{point:x(box:min-point($extent))}{$pc}"
              y="{point:y(box:min-point($extent))}{$pc}"
              width="{box:width($extent)}{$pc}"
              height="{box:height($extent)}{$pc}"
  >{
    $base/(@* except (@x|@y|@width|@height)),
    $base/node()
  }</svg:filter>
};

declare function this:extent(
  $base as element(svg:filter),
  $extent as map(xs:string,item()*)
) as element(svg:filter)
{
  this:extent($base, $extent, true())
};


(:~
 : colour()
 : Create a new effect with colours replaced with a new colour. All the 
 : lighting-color or flood-color attributes on svg:feFlood, svg:feSpecularLighting,
 : and svg:feDiffuseLighting elements will be changed to the new colour. If
 : more than one colour is given, the colours will be selected in rotation.
 :
 : @param $base: base effect to modify
 : @param $colours: new colours
 : @return new filter
 :)
declare function this:colour(
  $base as element(svg:filter),
  $colours as xs:string*
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-colour($base/node(), $colours!gradient:colour(.), ())
  }</svg:filter>
};

declare function this:colour(
  $base as element(svg:filter),
  $colours as xs:string*,
  $opacity as xs:double?
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-colour($base/node(), $colours!gradient:colour(.), $opacity)
  }</svg:filter>
};

declare %private function this:replace-colour(
  $nodes as node()*,
  $colours as xs:string*,
  $opacity as xs:double?
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:feFlood) return
    element {node-name($node)} {
      $node/(@* except (@flood-color|@flood-opacity)),
      attribute flood-color {head($colours)},
      if (empty($opacity)) then $node/@flood-opacity
      else attribute flood-opacity {$opacity}
      ,
      this:replace-colour($node/*, seq:rotate($colours,1), $opacity)
    }
  case element(svg:feDiffuseLighting) return
    element {node-name($node)} {
      $node/(@* except @lighting-color),
      attribute lighting-color {head($colours)},
      this:replace-colour($node/*, seq:rotate($colours,1), $opacity)
    }
  case element(svg:feSpecularLighting) return
    element {node-name($node)} {
      $node/(@* except @lighting-color),
      attribute lighting-color {head($colours)},
      this:replace-colour($node/*, seq:rotate($colours,1), $opacity)
    }
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-colour($node/*, $colours, $opacity)
    }
  default return $node
};

(:~
 : point-light()
 : Create a new effect with the lightsource replaced with a point light source.
 : svg:feDistantLight, svg:fePointLight, svg:feSpotLight in
 : svg:feDiffuseLighting or svg:feSpecularLighting elements will be replaced
 : with the new point light source.
 :
 : @param $base: base effect to modify
 : @param $point: new point light source (should have a z value)
 : @return new filter
 :)
declare function this:point-light(
  $base as element(svg:filter),
  $point as map(xs:string,item()*)
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-light($base/node(), $point)
  }</svg:filter>
};

declare %private function this:replace-light(
  $nodes as node()*,
  $point as map(xs:string,item()*)
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:fePointLight) return
    <svg:fePointLight x="{point:x($point)}" y="{point:y($point)}" z="{point:z($point)}"/>
  case element(svg:feDistantLight) return
    <svg:fePointLight x="{point:x($point)}" y="{point:y($point)}" z="{point:z($point)}"/>
  case element(svg:feSpotLight) return
    <svg:fePointLight x="{point:x($point)}" y="{point:y($point)}" z="{point:z($point)}"/>
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-light($node/*, $point)
    }
  default return $node
};

(:~
 : distant-light()
 : Create a new effect with the lightsource replaced with a distant light source.
 : svg:feDistantLight, svg:fePointLight, svg:feSpotLight in
 : svg:feDiffuseLighting or svg:feSpecularLighting elements will be replaced
 : with the new light source.
 :
 : @param $base: base effect to modify
 : @param $elevation: new elevation of the light source
 : @param $azimuth: new azimuth of the light source
 : @return new filter
 :)
declare function this:distant-light(
  $base as element(svg:filter),
  $elevation as xs:double,
  $azimuth as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-light($base/node(), $elevation, $azimuth)
  }</svg:filter>
};

declare %private function this:replace-light(
  $nodes as node()*,
  $elevation as xs:double,
  $azimuth as xs:double
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:fePointLight) return
    <svg:feDistantLight elevation="{$elevation}" azimuth="{$azimuth}"/>
  case element(svg:feDistantLight) return
    <svg:feDistantLight elevation="{$elevation}" azimuth="{$azimuth}"/>
  case element(svg:feSpotLight) return
    <svg:feDistantLight elevation="{$elevation}" azimuth="{$azimuth}"/>
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-light($node/*, $elevation, $azimuth)
    }
  default return $node
};

(:~
 : perturb()
 : Create a new effect with turbulence adjusted by new values.
 : Existing baseFrequency and numOctave attributes on existing svg:feTurbulence
 : elements will be multiplied by the modifiers.
 :
 : @param $base: base effect to modify
 : @param $octaves: octave multiplier
 : @param $frequency: frequency multiplier
 :)
declare function this:perturb(
  $base as element(svg:filter),
  $octaves as xs:double,
  $frequency as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-turbulence($base/node(), $octaves, $frequency)
  }</svg:filter>
};

declare %private function this:replace-turbulence(
  $nodes as node()*,
  $octaves as xs:double,
  $frequency as xs:double
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:feTurbulence) return
    element {node-name($node)} {
      $node/(@* except (@numOctaves|@baseFrequency)),
      attribute numOctaves {util:round($node/@numOctaves * $octaves)},
      attribute baseFrequency {
        string-join(
          for $n in tokenize($node/@baseFrequency,"[\p{Z},]+")[. ne ""]
          return string(xs:double($n)*$frequency)
        ," ")
      },
      this:replace-turbulence($node/*, $octaves, $frequency)
    }
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-turbulence($node/*, $octaves, $frequency)
    }
  default return $node
};

(:~
 : blur()
 : Create a new effect with Gaussian blur adjusted by new values.
 : Existing stdDevision values on existing svg:feGaussianBlur
 : elements will be multiplied by the modifiers.
 :
 : @param $base: base effect to modify
 : @param $blurring: std deviation multiplier
 :)
declare function this:blur(
  $base as element(svg:filter),
  $blurring as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-blur($base/node(), $blurring)
  }</svg:filter>
};

declare %private function this:replace-blur(
  $nodes as node()*,
  $blurring as xs:double
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:feGaussianBlur) return
    element {node-name($node)} {
      $node/(@* except (@stdDeviation)),
      attribute stdDeviation {
        string-join(
          for $n in tokenize($node/@stdDeviation,"[\p{Z},]+")[. ne ""]
          return string(xs:double($n)*$blurring)
        ," ")
      },
      this:replace-blur($node/*, $blurring)
    }
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-blur($node/*, $blurring)
    }
  default return $node
};

(:~
 : stretch()
 : Create a new effect with offset adjusted by new values.
 : Existing dx and dy attributes on existing svg:feOffset
 : elements will be multiplied by the modifiers.
 :
 : @param $base: base effect to modify
 : @param $scale-x: dx multiplier
 : @param $scale-y: dy multiplier
 :)
declare function this:stretch(
  $base as element(svg:filter),
  $scale-x as xs:double,
  $scale-y as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-offset($base/node(), $scale-x, $scale-y)
  }</svg:filter>
};

declare %private function this:replace-offset(
  $nodes as node()*,
  $scale-x as xs:double,
  $scale-y as xs:double
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:feOffset) return
    element {node-name($node)} {
      $node/(@* except (@dx|@dy)),
      attribute dx {util:decimal($node/@dx * $scale-x, 1)},
      attribute dy {util:decimal($node/@dy * $scale-y, 1)},
      this:replace-offset($node/*, $scale-x, $scale-y)
    }
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-offset($node/*, $scale-x, $scale-y)
    }
  default return $node
};

(:~
 : displace()
 : Create a new effect with displacement scale offset adjusted by new value.
 :
 : @param $base: base effect to modify
 : @param $rescale: scale multiplier
 :)
declare function this:displace(
  $base as element(svg:filter),
  $rescale as xs:double
) as element(svg:filter)
{
  <svg:filter>{
    $base/@*,
    this:replace-scale($base/node(), $rescale)
  }</svg:filter>
};

declare %private function this:replace-scale(
  $nodes as node()*,
  $rescale as xs:double
) as node()*
{
  for $node in $nodes return
  typeswitch ($node)
  case element(svg:feDisplacementMap) return
    element {node-name($node)} {
      $node/(@* except (@scale)),
      attribute scale {util:decimal($node/@scale * $rescale, 1)},
      this:replace-scale($node/*, $rescale)
    }
  case element() return
    element {node-name($node)} {
      $node/@*,
      this:replace-scale($node/*, $rescale)
    }
  default return $node
};