Assign various blending modes to a color layer.

Parameters
100% © Mapbox, © OpenStreetMap contributors, © NASA

Color layers can have different blending modes depending on the desired effect. The default blending mode (BlendingMode.Normal) is alpha blending, where the transparency of the pixels is used to blend the layer with the previous layer (or the background).

index.js
import { MapControls } from "three/examples/jsm/controls/MapControls.js";

import { Stroke, Style } from "ol/style.js";
import XYZ from "ol/source/XYZ.js";
import GeoJSON from "ol/format/GeoJSON.js";

import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import BlendingMode from "@giro3d/giro3d/core/layer/BlendingMode.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import StaticImageSource from "@giro3d/giro3d/sources/StaticImageSource.js";
import VectorSource from "@giro3d/giro3d/sources/VectorSource.js";

function bindNumericalDropDown(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLSelectElement)) {
    throw new Error(
      "invalid binding element: expected HTMLSelectElement, got: " +
        element.constructor.name,
    );
  }

  element.onchange = () => {
    onChange(parseInt(element.value));
  };

  const callback = (v) => {
    element.value = v.toString();
    onChange(parseInt(element.value));
  };

  return [callback, parseInt(element.value), element];
}

function bindButton(id, onClick) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLButtonElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.onclick = () => {
    onClick(element);
  };

  return element;
}

function bindColorPicker(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    // Let's change the classification color with the color picker value
    const hexColor = element.value;
    onChange(new Color(hexColor));
  };

  const externalFunction = (v) => {
    element.value = `#${new Color(v).getHexString()}`;
    onChange(element.value);
  };

  return [externalFunction, new Color(element.value), element];
}

function bindToggle(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.checked);
  };

  const callback = (v) => {
    element.checked = v;
    onChange(element.checked);
  };

  return [callback, element.checked, element];
}

const instance = new Instance({
  target: "view",
  crs: "EPSG:4326",
});

const extent = new Extent("EPSG:4326", -180, 180, -90, 90);

const map = new Map({ extent, backgroundColor: "blue" });

instance.add(map);

const key =
  "pk.eyJ1IjoidG11Z3VldCIsImEiOiJjbGJ4dTNkOW0wYWx4M25ybWZ5YnpicHV6In0.KhDJ7W5N3d1z3ArrsDjX_A";

// Create a satellite layer with no blending at all (layer is completely opaque)
const satellite = new ColorLayer({
  name: "satellite",
  blendingMode: BlendingMode.None,
  source: new TiledImageSource({
    source: new XYZ({
      url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${key}`,
      crossOrigin: "anonymous",
    }),
  }),
});
map.addLayer(satellite).catch((e) => console.error(e));

// Create a vector layer with normal blending mode.
const vector = new ColorLayer({
  name: "boundaries",
  blendingMode: BlendingMode.Normal,
  source: new VectorSource({
    data: {
      url: "https://3d.oslandia.com/giro3d/vectors/countries.geojson",
      format: new GeoJSON(),
    },
    style: new Style({
      stroke: new Stroke({ color: "red", width: 2 }),
    }),
    dataProjection: "EPSG:4326",
  }),
});
map.addLayer(vector).catch((e) => console.error(e));

// Create a cloud coverage layer with an additive blending mode
const cloud = new ColorLayer({
  name: "clouds",
  blendingMode: BlendingMode.Add,
  source: new StaticImageSource({
    source: "https://3d.oslandia.com/giro3d/images/cloud_cover.webp",
    extent,
  }),
});
map.addLayer(cloud).catch((e) => console.error(e));

instance.view.camera.position.set(0, 0, 230);

const controls = new MapControls(instance.view.camera, instance.domElement);

instance.view.setControls(controls);

// Example GUI

const [setBackground] = bindColorPicker("color", (v) => {
  map.backgroundColor = v;
  instance.notifyChange(map);
});
const setMode = (layer, mode) => {
  layer.blendingMode = mode;
  instance.notifyChange(layer);
};
const [setCloudMode] = bindNumericalDropDown("cloud", (v) => setMode(cloud, v));
const [setVectorMode] = bindNumericalDropDown("vector", (v) =>
  setMode(vector, v),
);
const [setSatelliteMode] = bindNumericalDropDown("satellite", (v) =>
  setMode(satellite, v),
);

const show = (layer, v) => {
  layer.visible = v;
  instance.notifyChange(layer);
};
const [showClouds] = bindToggle("show-cloud", (v) => show(cloud, v));
const [showSatellite] = bindToggle("show-satellite", (v) => show(satellite, v));
const [showVector] = bindToggle("show-vector", (v) => show(vector, v));
const [showBackground] = bindToggle("show-background", (v) => {
  map.backgroundOpacity = v ? 1 : 0;
  instance.notifyChange(map);
});

const reset = () => {
  setCloudMode(BlendingMode.Add);
  setVectorMode(BlendingMode.Normal);
  setSatelliteMode(BlendingMode.None);

  showClouds(true);
  showVector(true);
  showSatellite(true);

  setBackground("blue");

  showBackground(true);
};

bindButton("reset", reset);

reset();

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Blending modes</title>
    <meta charset="UTF-8" />
    <meta name="name" content="layer_blending_modes" />
    <meta
      name="description"
      content="Assign various blending modes to a color layer."
    />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="icon" href="https://giro3d.org/images/favicon.svg" />
    <link
      href="https://giro3d.org/assets/bootstrap-custom.css"
      rel="stylesheet"
    />
    <script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://giro3d.org/next/examples/css/example.css"
    />
  </head>

  <body>
    <div id="view" class="m-0 p-0 w-100 h-100"></div>
    <div
      id="inspector"
      class="position-absolute top-0 start-0 mh-100 overflow-auto"
    ></div>

    <div class="side-pane-with-status-bar">
      <!--Parameters -->
      <div class="card">
        <div class="card-header">
          Parameters
          <button
            type="button"
            id="reset"
            class="btn btn-sm btn-primary rounded float-end"
          >
            reset
          </button>
        </div>

        <div class="card-body">
          <!-- Cloud layer -->
          <div class="input-group mb-3">
            <div class="input-group-text">
              <input
                class="form-check-input"
                type="checkbox"
                checked="true"
                role="switch"
                id="show-cloud"
                autocomplete="off"
              />
            </div>
            <label class="input-group-text" style="width: 8rem" for="cloud"
              >Cloud layer</label
            >
            <select class="form-select" id="cloud" autocomplete="off">
              <option value="0">None</option>
              <option value="1">Normal</option>
              <option value="2" selected>Add</option>
              <option value="3">Multiply</option>
            </select>
          </div>

          <!-- Vector layer -->
          <div class="input-group mb-3">
            <div class="input-group-text">
              <input
                class="form-check-input"
                type="checkbox"
                checked="true"
                role="switch"
                id="show-vector"
                autocomplete="off"
              />
            </div>
            <label class="input-group-text" style="width: 8rem" for="vector"
              >Vector layer</label
            >
            <select class="form-select" id="vector" autocomplete="off">
              <option value="0">None</option>
              <option value="1" selected>Normal</option>
              <option value="2">Add</option>
              <option value="3">Multiply</option>
            </select>
          </div>

          <!-- Satellite layer -->
          <div class="input-group mb-3">
            <div class="input-group-text">
              <input
                class="form-check-input"
                type="checkbox"
                checked="true"
                role="switch"
                id="show-satellite"
                autocomplete="off"
              />
            </div>
            <label class="input-group-text" style="width: 8rem" for="satellite"
              >Satellite layer</label
            >
            <select class="form-select" id="satellite" autocomplete="off">
              <option value="0" selected>None</option>
              <option value="1">Normal</option>
              <option value="2">Add</option>
              <option value="3">Multiply</option>
            </select>
          </div>

          <!-- Background color -->
          <div class="input-group">
            <div class="input-group-text">
              <input
                class="form-check-input"
                type="checkbox"
                checked="true"
                role="switch"
                id="show-background"
                autocomplete="off"
              />
            </div>
            <label class="input-group-text" style="width: 8rem" for="color"
              >Background</label
            >
            <input
              type="color"
              class="form-control form-control-color"
              id="color"
              value="#2978b4"
              title="color"
            />
          </div>
        </div>
      </div>
    </div>

    <script type="module" src="index.js"></script>
    <script>
      /* activate popovers */
      const popoverTriggerList = [].slice.call(
        document.querySelectorAll('[data-bs-toggle="popover"]'),
      );
      popoverTriggerList.map(
        // bootstrap is used as script in the template, disable warning about undef
        // eslint-disable-next-line no-undef
        (popoverTriggerEl) =>
          new bootstrap.Popover(popoverTriggerEl, {
            trigger: "hover",
            placement: "left",
            content: document.getElementById(
              popoverTriggerEl.getAttribute("data-bs-content"),
            ).innerHTML,
            html: true,
          }),
      );
    </script>
  </body>
</html>
package.json
{
    "name": "layer_blending_modes",
    "dependencies": {
        "@giro3d/giro3d": "git+https://gitlab.com/giro3d/giro3d.git"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}