<div id="comparison-container">
  <div id="before" class="map"></div>
  <div id="after" class="map"></div>
</div>
body {
  overflow: hidden;
  margin: 0;
  padding: 0;
}

body * {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}
import SkeletonBuilder from "https://cdn.skypack.dev/straight-skeleton@1.0.1";
import {
  Point,
  Vector,
  Line,
  Polygon,
  Multiline
} from "https://cdn.skypack.dev/@flatten-js/core@1.3.5";
import earcut from "https://cdn.skypack.dev/earcut@2.2.4";

mapboxgl.accessToken =
  "pk.eyJ1IjoiYnJhbnpoYW5nIiwiYSI6ImNqM3FycmVldjAxZTUzM2xqMmllNnBjMHkifQ.Wv3ekbtia0BuUHGWVUGoFg";

const beforeMap = new mapboxgl.Map({
  container: "before",
  style: "mapbox://styles/mapbox/streets-v12",
  projection: "mercator",
  center: [114.31548, 30.565478],
  zoom: 15.3
});

const afterMap = new mapboxgl.Map({
  container: "after",
  style: "mapbox://styles/mapbox/streets-v12",
  projection: "mercator",
  center: [114.31548, 30.565478],
  zoom: 15.3
});

const container = "#comparison-container";

const map = new mapboxgl.Compare(beforeMap, afterMap, container, {});

afterMap.on("load", () => {
  afterMap.setLayoutProperty("water-shadow", "visibility", "none");

  const layer = new shadowLayer(
    {
      id: "new-water-shadow"
    },
    "water-shadow"
  );
  afterMap.addLayer(layer);
});

const lightGreen = [0, 0, 0, 0];
const darkGreen = [49 / 255, 165 / 255, 0, 0];
const middleGreen = [20 / 255, 20 / 255, 20 / 255, 0.15];

let bufferArray = new BufferArray();

class shadowLayer {
  constructor(options) {
    this.id = options.layerId || "shadow-layer";
    this.type = "custom";
    this.renderingMode = "3d";
  }

  onAdd(map, gl) {
    this.map = map;
    map.on("moveend", this.updateData.bind(this));
    this.updateData();

    const vert = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(
      vert,
      `
        uniform mat4 u_matrix;
        attribute vec4 a_pos;

        varying float v_distance;

        void main(void) {
            gl_Position = u_matrix * vec4(a_pos.xy, 0.0, 1.0);
            v_distance = a_pos[2];
        }
        `
    );
    gl.compileShader(vert);

    const frag = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(
      frag,
      `
        varying highp float v_distance;

        uniform highp float u_stroke_width;
        uniform highp vec4 u_stroke_colour;
        uniform highp vec4 u_fill_colour;
        uniform highp float u_stroke_offset;
        uniform highp vec4 u_inset_colour;
        uniform highp float u_inset_width;

        void main(void) {
            highp float inset_dist = (u_inset_width - v_distance) / u_inset_width;
            highp float inset_alpha = clamp(min(1.0, smoothstep(0.0, 1.5, inset_dist)), 0.0, 1.0);

            highp float fill_alpha = clamp(sign(v_distance + u_stroke_offset) - inset_alpha, 0.0, 1.0);
            gl_FragColor = u_inset_colour * inset_alpha + u_fill_colour * fill_alpha;
        }
        `
    );
    gl.compileShader(frag);

    this.program = gl.createProgram();
    gl.attachShader(this.program, vert);
    gl.attachShader(this.program, frag);
    gl.linkProgram(this.program);

    this.u_stroke_width = gl.getUniformLocation(this.program, "u_stroke_width");
    this.u_stroke_colour = gl.getUniformLocation(
      this.program,
      "u_stroke_colour"
    );
    this.u_fill_colour = gl.getUniformLocation(this.program, "u_fill_colour");
    this.u_stroke_offset = gl.getUniformLocation(
      this.program,
      "u_stroke_offset"
    );
    this.u_inset_width = gl.getUniformLocation(this.program, "u_inset_width");
    this.u_inset_colour = gl.getUniformLocation(this.program, "u_inset_colour");
    this.u_inset_blur = gl.getUniformLocation(this.program, "u_inset_blur");
    this.a_pos = gl.getAttribLocation(this.program, "a_pos");
  }

  render(gl, matrix) {
    gl.useProgram(this.program);

    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, bufferArray.arrayBuffer, gl.STATIC_DRAW);
    gl.enableVertexAttribArray(this.a_pos);
    gl.vertexAttribPointer(
      this.a_pos,
      4,
      gl.FLOAT,
      false,
      bufferArray.byteSize,
      0
    );

    gl.uniformMatrix4fv(
      gl.getUniformLocation(this.program, "u_matrix"),
      false,
      matrix
    );

    gl.uniform1f(this.u_stroke_width, 0);
    gl.uniform4fv(this.u_stroke_colour, darkGreen);
    gl.uniform4fv(this.u_fill_colour, lightGreen);
    gl.uniform1f(this.u_stroke_offset, 0);
    gl.uniform4fv(this.u_inset_colour, middleGreen);
    gl.uniform1f(this.u_inset_width, 0.000001);

    // gl.enable(gl.BLEND);
    // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.drawArrays(gl.TRIANGLES, 0, bufferArray.pos / bufferArray.byteSize);
  }

  updateData() {
    this.center = mapboxgl.MercatorCoordinate.fromLngLat(this.map.getCenter());
    try {
      const features = this.map.queryRenderedFeatures({ layers: ["water"] });

      if (features.length === 0) {
        return;
      }

      const data = turf.dissolve(
        turf.flatten({
          type: "FeatureCollection",
          features: features
        })
      );

      bufferArray = new BufferArray();

      data.features.forEach((feature) => {
        const coordinates = this.lngLatToMercator(feature.geometry.coordinates);

        coordinates.forEach((cc) => {
          cc.pop();
        });

        const skeleton = SkeletonBuilder.BuildFromGeoJSON([coordinates]);
        for (const poly of calculateRoundCorner(skeleton)) {
          for (const i of poly.indices) {
            const x = poly.flat[i * 3] / 1000000 + this.center.x;
            const y = poly.flat[i * 3 + 1] / 1000000 + this.center.y;
            const d = poly.flat[i * 3 + 2] / 1000000;
            bufferArray.add(x, y, d || 0);
          }
        }
      });
    } catch (e) {
      console.log(e);
    }
  }

  lngLatToMercator(coordinates) {
    if (typeof coordinates[0] === "number") {
      const coordinate = mapboxgl.MercatorCoordinate.fromLngLat({
        lng: coordinates[0],
        lat: coordinates[1]
      });
      return [
        (coordinate.x - this.center.x) * 1000000,
        (coordinate.y - this.center.y) * 1000000
      ];
    } else {
      return coordinates.map((c) => this.lngLatToMercator(c));
    }
  }
}

function BufferArray() {
  this.arrayBuffer = new ArrayBuffer(1024000);
  this.int16 = new Int16Array(this.arrayBuffer);
  this.float32 = new Float32Array(this.arrayBuffer);
  this.pos = 0;
  this.byteSize = 32;
}

BufferArray.prototype.add = function (x, y, dx, dy) {
  this.float32[this.pos / 4 + 0] = x;
  this.float32[this.pos / 4 + 1] = y;
  this.float32[this.pos / 4 + 2] = dx;
  this.pos += this.byteSize;
};

function calculateRoundCorner(skeleton) {
  const originalVertices = new Map();
  for (const [key, value] of skeleton.Distances) {
    originalVertices.set(`${key.X},${key.Y}`, value);
  }
  const output = [];

  // traverse every edge of original Polygon.
  for (const { Edge: e, Polygon: p } of skeleton.Edges) {
    const poly = new Polygon(p.map((po) => [po.X, po.Y]));

    let polygonCut = false;

    for (let i = 0; i < p.length - 1; i++) {
      if (
        skeleton.Distances.get(p[i]) === 0 &&
        skeleton.Distances.get(p[i + 1]) === 0
      ) {
        // this edge is coming from original Polygon.

        const cutLines = [];

        const previous = i > 0 ? p[i - 1] : p[p.length - 1];
        const next = i + 1 < p.length - 1 ? p[i + 2] : p[0];
        if (p[i + 1].Sub(p[i]).Dot(previous.Sub(p[i])) < 0) {
          // create a new line perpendicular to this edge
          // !! the definition of Norm in 'straight-skeleton' is different from 'flatten-js'.
          cutLines.push(
            new Line(new Point(p[i].X, p[i].Y), new Vector(e.Norm.X, e.Norm.Y))
          );
        }
        if (p[i].Sub(p[i + 1]).Dot(next.Sub(p[i + 1])) < 0) {
          cutLines.push(
            new Line(
              new Point(p[i + 1].X, p[i + 1].Y),
              new Vector(e.Norm.X, e.Norm.Y)
            )
          );
        }

        if (cutLines.length > 0) {
          polygonCut = true;

          const multiline = new Multiline(cutLines);

          for (let line of cutLines) {
            const ip = line.intersect(poly);
            const ipSorted = line.sortPoints(ip);
            multiline.split(ipSorted);
          }

          const splitPolygons = poly.cut(multiline);

          for (const splitPolygon of splitPolygons) {
            const sharePoints = sharePointsWithOriginalPolygon(
              splitPolygon,
              originalVertices
            );
            if (sharePoints.length === 2) {
              const copy = new Map();
              for (const [key, value] of originalVertices) {
                copy.set(key, value);
              }
              splitPolygon.vertices.forEach((vertex) => {
                if (!copy.has(`${vertex.x},${vertex.y}`)) {
                  copy.set(
                    `${vertex.x},${vertex.y}`,
                    new Point(vertex.x, vertex.y).distanceTo(
                      new Line(
                        new Point(sharePoints[0].x, sharePoints[0].y),
                        new Point(sharePoints[1].x, sharePoints[1].y)
                      )
                    )[0]
                  );
                }
              });
              output.push(triangulate(splitPolygon, copy));
            } else if (sharePoints.length === 1) {
              // recalculate distance
              const copy = new Map();
              for (const [key, value] of originalVertices) {
                copy.set(key, value);
              }
              splitPolygon.vertices.forEach((vertex) => {
                copy.set(
                  `${vertex.x},${vertex.y}`,
                  Math.sqrt(
                    (vertex.x - sharePoints[0].x) *
                      (vertex.x - sharePoints[0].x) +
                      (vertex.y - sharePoints[0].y) *
                        (vertex.y - sharePoints[0].y)
                  )
                );
              });
              output.push(triangulate(splitPolygon, copy));
            } else {
              console.log("maybe error");
              output.push(triangulate(splitPolygon, originalVertices));
            }
          }
        }
      }
    }

    if (!polygonCut) {
      output.push(triangulate(poly, originalVertices));
    }
  }

  return output;
}

function sharePointsWithOriginalPolygon(polygon, originalVertices) {
  return polygon.vertices.filter(
    (vertex) => originalVertices.get(`${vertex.x},${vertex.y}`) === 0
  );
}

function triangulate(polygon, originalVertices) {
  const flat = [];

  polygon.vertices.forEach((vertex) => {
    flat.push(
      vertex.x,
      vertex.y,
      originalVertices.get(`${vertex.x},${vertex.y}`)
    );
  });

  return {
    flat: flat,
    indices: earcut(flat, undefined, 3)
  };
}

External CSS

  1. https://api.mapbox.com/mapbox-gl-js/v2.11.0/mapbox-gl.css
  2. https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-compare/v0.4.0/mapbox-gl-compare.css

External JavaScript

  1. https://api.mapbox.com/mapbox-gl-js/v2.11.0/mapbox-gl.js
  2. https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-compare/v0.4.0/mapbox-gl-compare.js
  3. https://cdnjs.cloudflare.com/ajax/libs/Turf.js/6.5.0/turf.min.js