Custom BlendLayer

Explore in the sandboxView live

This sample demonstrates how to create a custom tile layer by applying the multiply blending operation to the ArcGIS World Hillshade and National Geographic World Map services. The multiply blend mode multiplies the values of each pixel of the top layer with the corresponding pixel value of the bottom layer. In this sample, multiplying the hillshade tiles with the National Geographic tiles creates a more detailed representation of the terrain than you see from the default National Geographic basemap. You can toggle the blended layer on and off using the LayerList widget in the sample. The BlendLayer is toggled on by default.

National Geographic DefaultCustom Blend Layer (ArcGIS Hillshade + National Geographic)
custom-layers-no-blend custom-layers-blend

To create a custom tile layer, you must call createSubclass() on BaseTileLayer. BaseTileLayer is a class designed specifically for creating custom tile layers, such as the BlendLayer created in this sample. When creating a BlendLayer we define a property called multiplyLayers, which is an array of tile layers that will be blended together.

const BlendLayer = BaseTileLayer.createSubclass({
  // multiplyLayers stores tile layers
  // used as the basis for creating
  // a blend layer. Layers stored in this property
  // will be blended using "multiply" operation.

  properties: {
    multiplyLayers: null

The tile layers added to the multiplyLayers array are loadable resources. If a custom tile layer requires loadable resources, then you must load them in the layer using the load() method. This ensures that all loadable resources required for the layer to function are loaded prior to the custom layer resolving and becoming loaded.

const BlendLayer = BaseTileLayer.createSubclass({
  properties: {
    multiplyLayers: null

  // Called when the layer is added to the map
  // prior to it being rendered in the view.
  load: function() {
    // iterate through each layer in multiplyLayers property
    this.multiplyLayers.forEach((layer) => {
      // call load method on each layer and add as
      // a resolving promise of the custom layer.
      // The tile layers must load() prior to the BlendLayer
      // resolving and moving to the "loaded" status.
    }, this);

Once the layer is loaded with its required resources, we must override the fetchTile() method of the BlendLayer. Within this method, call fetchTile() on each tile layer returned in the multiplyLayers property. Once each tile layer returns the tile(s) visible in the view, we apply multiply operation to the tiles so that the BlendLayer will show the blended image.

// Fetches the tile(s) visible in the view
fetchTile: function (level, row, col, options) {
  const tilePromises = => {
    // calls fetchTile() on the tile layers returned in multiplyLayers property
    // for the tiles visible in the view
    return layer.fetchTile(level, row, col, options);

  return promiseUtils.eachAlways(tilePromises)
    .then((results) => {
      // Reject with abort error if the request was aborted.
      // It is expected that `fetchTile` will already have rejected with abort errors
      // in that case but those errors are caught by `eachAlways`, so we need to re-
      // throw it
      if (options && options.signal && options.signal.aborted) {
        throw new EsriError("AbortError");
      // create a canvas
      const width = this.tileInfo.size[0];
      const height = this.tileInfo.size[0];
      const canvas = document.createElement("canvas");
      const context = canvas.getContext("2d");

      canvas.width = width;
      canvas.height = height;

      // multiply - multiplies the numbers for each pixel of the top layer (nat geo)
      // with the corresponding pixel for the bottom layer (hillshade).
      context.globalCompositeOperation = "multiply";
      results.forEach((result) => {
        const image = result.value;
        context.drawImage(image, 0, 0, width, height);

      return canvas;