Blend Modes

Shows how to change the canvas compositing / blending mode in post- and precompose eventhandlers.

This example shows how to change the canvas compositing / blending mode in post- and precompose event handlers. The Canvas 2D API provides the property globalCompositeOperation with which one can influence which composition operation will be used when drawing on the canvas. The various options are well described on the MDN documentation page.

In this example three circles on the corners of an equilateral triangle are drawn with red, green or blue styles respectively. By setting the globalCompositeOperation you can change how these colors turn out when they are combined on the map.

You can select an operation in the select-field and you can also control which layers will be affected by the chosen operation through the layer checkboxes.

<!DOCTYPE html>
<html>
  <head>
    <title>Blend Modes</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v4.6.4/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
    <script src="https://openlayers.org/en/v4.6.4/build/ol.js"></script>
    <style>
      .map{
        background-repeat: repeat;
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAApSURBVBiVY7x///5/BjSgqKjIiC7GhC6ACwygQgxHMzAwMGDz4FDwDAD5/wevjSk4mwAAAABJRU5ErkJggg==);
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <form class="form-horizontal">
      <label>
        <select id="blend-mode" class="form-control">
          <option value="source-over">source-over (default)</option>
          <option>source-in</option>
          <option>source-out</option>
          <option>source-atop</option>
          <option>destination-over</option>
          <option>destination-in</option>
          <option>destination-out</option>
          <option>destination-atop</option>
          <option>lighter</option>
          <option>copy</option>
          <option>xor</option>
          <option>multiply</option>
          <option>screen</option>
          <option>overlay</option>
          <option>darken</option>
          <option>lighten</option>
          <option>color-dodge</option>
          <option>color-burn</option>
          <option>hard-light</option>
          <option>soft-light</option>
          <option selected>difference</option>
          <option>exclusion</option>
          <option>hue</option>
          <option>saturation</option>
          <option>color</option>
          <option>luminosity</option>
        </select>
        Canvas compositing / blending mode
      </label>
      <label>
        <input type="checkbox" id="affect-red" checked>
        Red circle affected
      </label>
      <label>
        <input type="checkbox" id="affect-green" checked>
        Green circle affected
      </label>
      <label>
        <input type="checkbox" id="affect-blue" checked>
        Blue circle affected
      </label>
    </form>
    <script>
      // Create separate layers for red, green an blue circles.
      //
      // Every layer has one feature that is styled with a circle, together the
      // features form the corners of an equilateral triangle and their styles overlap
      var redLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          features: [new ol.Feature(new ol.geom.Point([0, 0]))]
        }),
        style: new ol.style.Style({
          image: new ol.style.Circle({
            fill: new ol.style.Fill({
              color: 'rgba(255,0,0,0.8)'
            }),
            stroke: new ol.style.Stroke({
              color: 'rgb(255,0,0)',
              width: 15
            }),
            radius: 120
          })
        })
      });
      var greenLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          // 433.013 is roughly 250 * Math.sqrt(3)
          features: [new ol.Feature(new ol.geom.Point([250, 433.013]))]
        }),
        style: new ol.style.Style({
          image: new ol.style.Circle({
            fill: new ol.style.Fill({
              color: 'rgba(0,255,0,0.8)'
            }),
            stroke: new ol.style.Stroke({
              color: 'rgb(0,255,0)',
              width: 15
            }),
            radius: 120
          })
        })
      });
      var blueLayer = new ol.layer.Vector({
        source: new ol.source.Vector({
          features: [new ol.Feature(new ol.geom.Point([500, 0]))]
        }),
        style: new ol.style.Style({
          image: new ol.style.Circle({
            fill: new ol.style.Fill({
              color: 'rgba(0,0,255,0.8)'
            }),
            stroke: new ol.style.Stroke({
              color: 'rgb(0,0,255)',
              width: 15
            }),
            radius: 120
          })
        })
      });

      // Create the map, the view is centered on the triangle. Zooming and panning is
      // restricted to a sane area
      var map = new ol.Map({
        layers: [
          redLayer,
          greenLayer,
          blueLayer
        ],
        target: 'map',
        view: new ol.View({
          center: [250, 220],
          extent: [0, 0, 500, 500],
          resolution: 4,
          minResolution: 2,
          maxResolution: 32
        })
      });

      // Get the form elements and bind the listeners
      var select = document.getElementById('blend-mode');
      var affectRed = document.getElementById('affect-red');
      var affectGreen = document.getElementById('affect-green');
      var affectBlue = document.getElementById('affect-blue');


      /**
       * This method sets the globalCompositeOperation to the value of the select
       * field and it is bound to the precompose event of the layers.
       *
       * @param {ol.render.Event} evt The render event.
       */
      var setBlendModeFromSelect = function(evt) {
        evt.context.globalCompositeOperation = select.value;
      };


      /**
       * This method resets the globalCompositeOperation to the default value of
       * 'source-over' and it is bound to the postcompose event of the layers.
       *
       * @param {ol.render.Event} evt The render event.
       */
      var resetBlendModeFromSelect = function(evt) {
        evt.context.globalCompositeOperation = 'source-over';
      };


      /**
       * Bind the pre- and postcompose handlers to the passed layer.
       *
       * @param {ol.layer.Vector} layer The layer to bind the handlers to.
       */
      var bindLayerListeners = function(layer) {
        layer.on('precompose', setBlendModeFromSelect);
        layer.on('postcompose', resetBlendModeFromSelect);
      };


      /**
       * Unind the pre- and postcompose handlers to the passed layers.
       *
       * @param {ol.layer.Vector} layer The layer to unbind the handlers from.
       */
      var unbindLayerListeners = function(layer) {
        layer.un('precompose', setBlendModeFromSelect);
        layer.un('postcompose', resetBlendModeFromSelect);
      };


      /**
       * Handler for the click event of the 'affect-XXX' checkboxes.
       *
       * @this {HTMLInputElement}
       */
      var affectLayerClicked = function() {
        var layer;
        if (this.id == 'affect-red') {
          layer = redLayer;
        } else if (this.id == 'affect-green') {
          layer = greenLayer;
        } else {
          layer = blueLayer;
        }
        if (this.checked) {
          bindLayerListeners(layer);
        } else {
          unbindLayerListeners(layer);
        }
        map.render();
      };


      // Rerender map when blend mode changes
      select.addEventListener('change', function() {
        map.render();
      });

      // Unbind / bind listeners depending on the checked state when the checkboxes
      // are clicked
      affectRed.addEventListener('click', affectLayerClicked);
      affectGreen.addEventListener('click', affectLayerClicked);
      affectBlue.addEventListener('click', affectLayerClicked);

      // Initially bind listeners
      bindLayerListeners(redLayer);
      bindLayerListeners(greenLayer);
      bindLayerListeners(blueLayer);
    </script>
  </body>
</html>