block by monfera 21be9bb116a8e5b2423b706155fdb718

Generative elevation map with SVG filters

Full Screen

Click for a new map and palette. Move the mouse sideways to change where the light source is.

[There is also a pure SVG map with elevation contours.]

This SVG filter generates elevation, flat waters and icy mesas. Also see my previous filter and twitter/@monfera for visualizations including glitchy shots from making this block. A couple of D3 color scales are added but otherwise there’s no dependency, it’s kept minimal.

This block has:

Sometimes we lean on D3 for things that the underlying standards already provide, or miss opportunities. I’m impressed by Nadieh Bremer’s work with filters which shows how much can be done beyond the basics.

Palette copyrights in the source. Palette authors:

  1. tv-a: Jim Mossman
  2. wiki-schwarzwald-cont: W-j-s and Jide
  3. viridis, magma: Stéfan van der Walt and Nathaniel Smith, via D3 by Mike Bostock (not meant for topography)

This is what I wanted:

I found that:

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>SVG generative map</title>
  <script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg width=960 height=500 version="1.1" xmlns="//www.w3.org/2000/svg">

  <defs>

    <filter id="bump">

      <!--generate noise-->
      <feTurbulence id="noise" seed="603883" type="fractalNoise"
                    baseFrequency=".001" numOctaves="12" result="turbulence" />

      <!--scale the noise and keep the alpha channel only-->
      <feComponentTransfer in="turbulence" result="bumpMap">
        <feFuncA type="table" tableValues="-1.9 2.3"/>
        <feFuncR type="gamma" amplitude="0" />
        <feFuncG type="gamma" amplitude="0" />
        <feFuncB type="gamma" amplitude="0" />
      </feComponentTransfer>


      <!--make a bump map out of the noise
      Safari chokes on multiline attrs so... split lines and use Chrome-->
      <feConvolveMatrix in="bumpMap" result="fineContours" order="3"
                        kernelMatrix="10 10 10   10 -80 10   10 10 10" />


      <!--convert the alpha channel to grayscale RGB channels for palette -->
      <feColorMatrix in="bumpMap" result="grayscaleBumpMap" mode="matrix"
                     values="0 0 0 1 0   0 0 0 1 0   0 0 0 1 0  0  0 0 0 1" />


      <!--pick up colors from the palette and map them into the channels-->
      <feComponentTransfer in="grayscaleBumpMap" result="coloredBumpMap">
        <feFuncR id="geoR" type="discrete" />
        <feFuncG id="geoG" type="discrete" />
        <feFuncB id="geoB" type="discrete" />
      </feComponentTransfer>


      <!--Elevation run color alterations 2.-->

      <!--desaturate the original palette a bit-->
      <feColorMatrix id="saturate" type="saturate" values="0.8"
                     in="coloredBumpMap" result="saturationAdjusted"/>

       <!--adjust the gamma for darker tones
         ... lighter tones would be better if there are overlays-->
      <feComponentTransfer in="saturationAdjusted" result="colorAdjusted">
        <feFuncR id="gammaR" type="gamma" amplitude="1" exponent="2.5"/>
        <feFuncG id="gammaG" type="gamma" amplitude="1" exponent="2.5"/>
        <feFuncB id="gammaB" type="gamma" amplitude="1" exponent="1.8"/>
      </feComponentTransfer>


      <!--join the elevation coloring, contour lines and original SVG input-->
      <feBlend in="colorAdjusted" in2="fineContours" result="topoMap"/>
      <feComposite in="SourceGraphic" in2="topoMap" result="blendedContents"/>


      <!--make light-->
      <feSpecularLighting in="bumpMap" lighting-color="#fff" surfaceScale="100"
                      specularConstant="1" specularExponent="2" result="light">
        <fePointLight id="specularPointLight" x="960" y="-500" z="500" />
      </feSpecularLighting>


      <!--output contents with light-->ec
      <feComposite in="blendedContents" in2="light"
                   operator = "arithmetic" k1="0" k2="1" k3="0.3" k4="0"/>


    </filter>

  </defs>

  <g style="filter: url('#bump'); transform: scale(0.5)"
     width="1920" height="1000">

    <rect id="r" width="1920" height="1000" style="fill:black;fill-opacity:0"/>
    <text x="60" y="980" style="font: bold 42px 'Arial Black'">@monfera</text>
    <text id="meta" x="1880" y="980" text-anchor="end"
          style="font: bold 42px 'Arial Black'"></text>

  </g>

  <script>

    // Elements

    var noise = document.getElementById('noise')
    var light = document.getElementById('specularPointLight')
    var sat = document.getElementById('saturate')
    var gamma = document.getElementById('gamma')
    var container = document.getElementById('r')
    var geom = document.getElementById('geom')
    var meta = document.getElementById('meta')

    // Elevation run colors

    //wiki-schwarzwald-cont
    //Copyright Jide and W-j-s (https://als.wikipedia.org/wiki/Benutzer:W-j-s)
    // soliton.vm.bytemark.co.uk
    //   /pub/cpt-city/wkp/schwarzwald/wiki-schwarzwald-cont.png.index.html
    // Creative commons attribution share-alike 3.0 unported

    function fromByte(d) { return d / 255}

    var wikiSchwarzwald = {
      name: "wiki-schwarzwald-cont by Jide and W-j-s",
      r: [
        174,175,176,176,177,176,176,178,181,186,192,198,204,210,217,224,231,
        238,245,250,248,238,226,213,198,184,170,154,140,125,110,94,77,62,49,
        39,30,24,18,14,9,7,12, 24,40,52,64,76,87,99,110,120,128,137,147,156,
        166,176,187,197,207,218,228,238,246,248,244,238,232,226,220,216,211,
        206,200,192,186,180,174,169,163,157,151,146,141,135,130,125,122,119,
        118,117,117,117,116,116,114,114,112,111,110,110,109,108,108,108,107,
        106,106,107,110,113,116,118,121,125,128,131,135,138,140,144,147,150,
        152,156,158,160,163,166,167,170,172,174,178,181,184,188,192,196,200,
        204,208,212,216,218,221,225,229,233,235
      ].map(fromByte),
      g: [
        239,240,242,242,242,243,244,246,246,247,247,248,249,250,250,251,252,
        252,252,252,249,244,240,235,228,222,216,211,205,199,194,188,182,176,
        171,165,160,154,148,142,137,132,130,130,132,136,140,142,146,148,150,
        154,156,160,162,164,166,170,173,176,177,179,180,182,182,176,166,155,
        144,132,122,111,102,92,84,74,66,58,49,42,36,30,23,18,14,8,5,4,8,13,16,
        18,20,21,22,24,26,29,31,33,35,36,38,40,40,42,44,44,46,48,52,57,62,66,
        70,74,79,85,90,96,101,106,111,116,122,129,135,141,147,154,160,167,172,
        174,178,181,184,188,192,196,200,204,206,210,214,216,219,223,227,231,233
      ].map(fromByte),
      b:  [
        213,211,208,202,196,190,186,181,178,178,178,178,178,177,178,178,178,
        179,179,178,172,162,151,140,128,118,108,98,89,82,74,66,57,50,44,42,43,
        46,49,52,56,60,63,63, 61,60,59,59,56,54,52,50,48,46,43,41,39,36,34,30,
        28,24,20,14,8,4,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,1,1,0,0,0,2,2,2,4,
        4,4,4,5,6,6,7,8,8,8,9,10,10,10,11,12,12,14,18,23,28,32,37,43,50,56,63,
        69,76,84,90,96,104,112,120,130,139,147,156,164,171,174,178,181,184,
        188,192,196,200,204,208,212,216,218,221,225,229,233,235
      ].map(fromByte),
      water: {r: 174/255, g: 213/255, b: 239/255},
      gamma: {r: 1, g: 1, b: 1},
      saturation: 0.8,
      cut: 26 // start with greens
    }


    //tv-a
    //Copyright Jim Mossman //www.esri.com/news/arcuser/0101/shademax.html
    // soliton.vm.bytemark.co.uk/pub/cpt-city/jm/tv/tn/tv-a.png.index.html
    // //soliton.vm.bytemark.co.uk/pub/cpt-city/jm/copying.html
    var tva = {
      name: "tv-a by Jim Mossman",
      r: [115,115,115,115,140,140,148,148,155,155,163,163,171,171,178,178,186,
        186,194,194,201,201,209,209,217,217,224,224,232,232,240,240,247,247,
        255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
        255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
        255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
        255,255,255,255,255,255,255
      ].map(fromByte),
      g: [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
        255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
        255,255,255,247,247,240,240,232,232,225,225,218,218,211,211,204,204,
        197,197,190,190,183,183,176,176,169,169,162,162,155,155,160,160,172,
        172,183,183,189,189,195,195,201,201,210,210,218,218,226,226,232,232,
        237,237,243,243,247,247,251,251,
      ].map(fromByte),
      b: [143,143,115,115,115,115,115,115,115,115,115,115,115,115,115,115,115,
        115,115,115,115,115,115,115,115,115,115,115,115,115,115,115,115,115,
        115,115,119,119,123,123,127,127,127,127,127,127,127,127,127,127,127,
        127,127,127,127,127,127,127,127,127,127,127,127,127,141,141,155,155,
        168,168,176,176,183,183,190,190,200,200,210,210,220,220,227,227,233,
        233,240,240,245,245,250,250
      ].map(fromByte),
      water: {r: 174/255, g: 213/255, b: 239/255},
      gamma: {r: 4, g: 4, b: 4},
      saturation: 0.8,
      cut: 0
    }

    function runToPalette(name, d3Palette) {
      var palette = {
        name: name,
        r: [],
        g: [],
        b: [],
        water: {r: 90/255, g: 105/255, b: 120/255},
        gamma: {r: 1, g: 1, b: 1},
        saturation: 1,
        cut: 0
      }
      var color, i
      for(i = 0; i <= 1000; i ++) {
        color = d3.color(d3Palette(i / 1000))
        palette.r.push(color.r / 255)
        palette.g.push(color.g / 255)
        palette.b.push(color.b / 255)
      }
      return palette
    }

    var viridis = runToPalette("Viridis run from D3", d3.interpolateViridis)
    var magma = runToPalette("Magma run from D3", d3.interpolateMagma)

    // ice effect
    var ice = {
      name: "grayscale/ice",
      r: [],
      g: [],
      b: [],
      gamma: {r: 1.5, g: 1.3, b: 1},
      saturation: 0.8,
      cut: 0
    }

    var coal = {
      name: "text invisible anyway",
      r: [0, 0],
      g: [0, 0],
      b: [0, 0],
      gamma: {r: 1, g: 1, b: 1},
      saturation: 1,
      cut: 0
    }

    var palettes = [tva, wikiSchwarzwald, ice, magma, viridis]
    var paletteIndex = 0

    function setPalette() {

      var p = palettes[paletteIndex++ % palettes.length]

      // add color for water bodies:
      var geoR= (p.water ? [p.water.r] : []).concat(p.r.slice(p.cut)).join(' ')
      var geoG= (p.water ? [p.water.g] : []).concat(p.g.slice(p.cut)).join(' ')
      var geoB= (p.water ? [p.water.b] : []).concat(p.b.slice(p.cut)).join(' ')

      // set the palette on the receiving SVG filter channels
      document.getElementById('geoR').setAttribute('tableValues', geoR)
      document.getElementById('geoG').setAttribute('tableValues', geoG)
      document.getElementById('geoB').setAttribute('tableValues', geoB)

      // set saturation
      sat.setAttribute("values", p.saturation)

      // set gamma
      document.getElementById('gammaR').setAttribute('exponent', p.gamma.r)
      document.getElementById('gammaG').setAttribute('exponent', p.gamma.g)
      document.getElementById('gammaB').setAttribute('exponent', p.gamma.b)

      // set palette name
      meta.innerHTML = p.name
    }


    //Interactions

    function moveLight(e) {
      light.setAttribute('x', 2 * e.x)
      //sat.setAttribute('values', .4 + .6 * Math.round(3 * (1 - e.y/500)) / 3)
    }

    function newMap() {
      setPalette()
      var newSeed = Math.round(1e6 * Math.random())
      noise.setAttribute('seed', newSeed)
      console.log("Seed: ", newSeed)
      // some good seeds: 453109 394778 221947 601567
    }

    container.addEventListener("mousemove", moveLight)
    container.addEventListener("click", newMap)

    // Initial palette
    setPalette()


  </script>
</svg>
</body>
</html>