SOURCE

console 命令行工具 X clear

                    
>
console
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>
      Custom WebGL layer view | Sample | ArcGIS API for JavaScript 4.18
    </title>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix.js"></script>

    <link
      rel="stylesheet"
      href="https://js.arcgis.com/4.18/esri/themes/light/main.css"
    />
    <script src="https://js.arcgis.com/4.18/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "esri/Map",

        "esri/core/watchUtils",
        "esri/core/promiseUtils",

        "esri/geometry/support/webMercatorUtils",

        "esri/layers/GraphicsLayer",

        "esri/views/MapView",

        "esri/views/2d/layers/BaseLayerViewGL2D"
      ], function (
        Map,
        watchUtils,
        promiseUtils,
        webMercatorUtils,
        GraphicsLayer,
        MapView,
        BaseLayerViewGL2D
      ) {
        // Subclass the custom layer view from BaseLayerViewGL2D.
        var CustomLayerView2D = BaseLayerViewGL2D.createSubclass({
          // Locations of the two vertex attributes that we use. They
          // will be bound to the shader program before linking.
          aPosition: 0,
          aOffset: 1,

          constructor: function () {
            // Geometrical transformations that must be recomputed
            // from scratch at every frame.
            this.transform = mat3.create();
            this.translationToCenter = vec2.create();
            this.screenTranslation = vec2.create();

            // Geometrical transformations whose only a few elements
            // must be updated per frame. Those elements are marked
            // with NaN.
            this.display = mat3.fromValues(NaN, 0, 0, 0, NaN, 0, -1, 1, 1);
            this.screenScaling = vec3.fromValues(NaN, NaN, 1);

            // Whether the vertex and index buffers need to be updated
            // due to a change in the layer data.
            this.needsUpdate = false;

            // We listen for changes to the graphics collection of the layer
            // and trigger the generation of new frames. A frame rendered while
            // `needsUpdate` is true may cause an update of the vertex and
            // index buffers.
            var requestUpdate = function () {
              this.needsUpdate = true;
              this.requestRender();
            }.bind(this);

            this.watcher = watchUtils.on(
              this,
              "layer.graphics",
              "change",
              requestUpdate,
              requestUpdate,
              requestUpdate
            );
          },

          // Called once a custom layer is added to the map.layers collection and this layer view is instantiated.
          attach: function () {
            var gl = this.context;

            // Define and compile shaders.
            var vertexSource =
              "precision highp float;" +
              "uniform mat3 u_transform;" +
              "uniform mat3 u_display;" +
              "attribute vec2 a_position;" +
              "attribute vec2 a_offset;" +
              "varying vec2 v_offset;" +
              "const float SIZE = 70.0;" +
              "void main() {" +
              "    gl_Position.xy = (u_display * (u_transform * vec3(a_position, 1.0) + vec3(a_offset * SIZE, 0.0))).xy;" +
              "    gl_Position.zw = vec2(0.0, 1.0);" +
              "    v_offset = a_offset;" +
              "}";

            var fragmentSource =
              "precision highp float;" +
              "uniform float u_current_time;" +
              "varying vec2 v_offset;" +
              "const float PI = 3.14159;" +
              "const float N_RINGS = 3.0;" +
              "const vec3 COLOR = vec3(0.23, 0.43, 0.70);" +
              "const float FREQ = 1.0;" +
              "void main() {" +
              "    float l = length(v_offset);" +
              "    float intensity = clamp(cos(l * PI), 0.0, 1.0) * clamp(cos(2.0 * PI * (l * 2.0 * N_RINGS - FREQ * u_current_time)), 0.0, 1.0);" +
              "    gl_FragColor = vec4(COLOR * intensity, intensity);" +
              "}";

            var vertexShader = gl.createShader(gl.VERTEX_SHADER);
            gl.shaderSource(vertexShader, vertexSource);
            gl.compileShader(vertexShader);
            var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
            gl.shaderSource(fragmentShader, fragmentSource);
            gl.compileShader(fragmentShader);

            // Create the shader program.
            this.program = gl.createProgram();
            gl.attachShader(this.program, vertexShader);
            gl.attachShader(this.program, fragmentShader);

            // Bind attributes.
            gl.bindAttribLocation(this.program, this.aPosition, "a_position");
            gl.bindAttribLocation(this.program, this.aOffset, "a_offset");

            // Link.
            gl.linkProgram(this.program);

            // Shader objects are not needed anymore.
            gl.deleteShader(vertexShader);
            gl.deleteShader(fragmentShader);

            // Retrieve uniform locations once and for all.
            this.uTransform = gl.getUniformLocation(
              this.program,
              "u_transform"
            );
            this.uDisplay = gl.getUniformLocation(this.program, "u_display");
            this.uCurrentTime = gl.getUniformLocation(
              this.program,
              "u_current_time"
            );

            // Create the vertex and index buffer. They are initially empty. We need to track the
            // size of the index buffer because we use indexed drawing.
            this.vertexBuffer = gl.createBuffer();
            this.indexBuffer = gl.createBuffer();

            // Number of indices in the index buffer.
            this.indexBufferSize = 0;

            // When certain conditions occur, we update the buffers and re-compute and re-encode
            // all the attributes. When buffer update occurs, we also take note of the current center
            // of the view state, and we reset a vector called `translationToCenter` to [0, 0], meaning that the
            // current center is the same as it was when the attributes were recomputed.
            this.centerAtLastUpdate = vec2.fromValues(
              this.view.state.center[0],
              this.view.state.center[1]
            );
          },

          // Called once a custom layer is removed from the map.layers collection and this layer view is destroyed.
          detach: function () {
            // Stop watching the `layer.graphics` collection.
            this.watcher.remove();

            var gl = this.context;

            // Delete buffers and programs.
            gl.deleteBuffer(this.vertexBuffer);
            gl.deleteBuffer(this.indexBuffer);
            gl.deleteProgram(this.program);
          },

          // Called every time a frame is rendered.
          render: function (renderParameters) {
            var gl = renderParameters.context;
            var state = renderParameters.state;

            // Update vertex positions. This may trigger an update of
            // the vertex coordinates contained in the vertex buffer.
            // There are three kinds of updates:
            //  - Modification of the layer.graphics collection ==> Buffer update
            //  - The view state becomes non-stationary ==> Only view update, no buffer update
            //  - The view state becomes stationary ==> Buffer update
            this.updatePositions(renderParameters);

            // If there is nothing to render we return.
            if (this.indexBufferSize === 0) {
              return;
            }

            // Update view `transform` matrix; it converts from map units to pixels.
            mat3.identity(this.transform);
            this.screenTranslation[0] = (state.pixelRatio * state.size[0]) / 2;
            this.screenTranslation[1] = (state.pixelRatio * state.size[1]) / 2;
            mat3.translate(
              this.transform,
              this.transform,
              this.screenTranslation
            );
            mat3.rotate(
              this.transform,
              this.transform,
              (Math.PI * state.rotation) / 180
            );
            this.screenScaling[0] = state.pixelRatio / state.resolution;
            this.screenScaling[1] = -state.pixelRatio / state.resolution;
            mat3.scale(this.transform, this.transform, this.screenScaling);
            mat3.translate(
              this.transform,
              this.transform,
              this.translationToCenter
            );

            // Update view `display` matrix; it converts from pixels to normalized device coordinates.
            this.display[0] = 2 / (state.pixelRatio * state.size[0]);
            this.display[4] = -2 / (state.pixelRatio * state.size[1]);

            // Draw.
            gl.useProgram(this.program);
            gl.uniformMatrix3fv(this.uTransform, false, this.transform);
            gl.uniformMatrix3fv(this.uDisplay, false, this.display);
            gl.uniform1f(this.uCurrentTime, performance.now() / 1000.0);
            gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
            gl.enableVertexAttribArray(this.aPosition);
            gl.enableVertexAttribArray(this.aOffset);
            gl.vertexAttribPointer(this.aPosition, 2, gl.FLOAT, false, 16, 0);
            gl.vertexAttribPointer(this.aOffset, 2, gl.FLOAT, false, 16, 8);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
            gl.drawElements(
              gl.TRIANGLES,
              this.indexBufferSize,
              gl.UNSIGNED_SHORT,
              0
            );

            // Request new render because markers are animated.
            this.requestRender();
          },

          // Called by the map view or the popup view when hit testing is required.
          hitTest: function (x, y) {
            // The map view.
            var view = this.view;

            if (this.layer.graphics.length === 0) {
              // Nothing to do.
              return promiseUtils.resolve(null);
            }

            // Compute screen distance between each graphic and the test point.
            var distances = this.layer.graphics.map(function (graphic) {
              var graphicPoint = view.toScreen(graphic.geometry);
              return Math.sqrt(
                (graphicPoint.x - x) * (graphicPoint.x - x) +
                  (graphicPoint.y - y) * (graphicPoint.y - y)
              );
            });

            // Find the minimum distance.
            var minIndex = 0;

            distances.forEach(function (distance, i) {
              if (distance < distances.getItemAt(minIndex)) {
                minIndex = i;
              }
            });

            var minDistance = distances.getItemAt(minIndex);

            // If the minimum distance is more than 35 pixel then nothing was hit.
            if (minDistance > 35) {
              return promiseUtils.resolve(null);
            }

            // Otherwise it is a hit; We set the layer as the source layer for the graphic
            // (required for the popup view to work) and we return a resolving promise to
            // the graphic.
            var graphic = this.layer.graphics.getItemAt(minIndex);
            graphic.sourceLayer = this.layer;
            return promiseUtils.resolve(graphic);
          },

          // Called internally from render().
          updatePositions: function (renderParameters) {
            var gl = renderParameters.context;
            var stationary = renderParameters.stationary;
            var state = renderParameters.state;

            // If we are not stationary we simply update the `translationToCenter` vector.
            if (!stationary) {
              vec2.sub(
                this.translationToCenter,
                this.centerAtLastUpdate,
                state.center
              );
              this.requestRender();
              return;
            }

            // If we are stationary, the `layer.graphics` collection has not changed, and
            // we are centered on the `centerAtLastUpdate`, we do nothing.
            if (
              !this.needsUpdate &&
              this.translationToCenter[0] === 0 &&
              this.translationToCenter[1] === 0
            ) {
              return;
            }

            // Otherwise, we record the new encoded center, which imply a reset of the `translationToCenter` vector,
            // we record the update time, and we proceed to update the buffers.
            this.centerAtLastUpdate.set(state.center);
            this.translationToCenter[0] = 0;
            this.translationToCenter[1] = 0;
            this.needsUpdate = false;

            var graphics = this.layer.graphics;

            // Generate vertex data.
            gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
            var vertexData = new Float32Array(16 * graphics.length);

            var i = 0;
            graphics.forEach(
              function (graphic) {
                var point = graphic.geometry;

                // The (x, y) position is relative to the encoded center.
                var x = point.x - this.centerAtLastUpdate[0];
                var y = point.y - this.centerAtLastUpdate[1];

                vertexData[i * 16 + 0] = x;
                vertexData[i * 16 + 1] = y;
                vertexData[i * 16 + 2] = -0.5;
                vertexData[i * 16 + 3] = -0.5;
                vertexData[i * 16 + 4] = x;
                vertexData[i * 16 + 5] = y;
                vertexData[i * 16 + 6] = 0.5;
                vertexData[i * 16 + 7] = -0.5;
                vertexData[i * 16 + 8] = x;
                vertexData[i * 16 + 9] = y;
                vertexData[i * 16 + 10] = -0.5;
                vertexData[i * 16 + 11] = 0.5;
                vertexData[i * 16 + 12] = x;
                vertexData[i * 16 + 13] = y;
                vertexData[i * 16 + 14] = 0.5;
                vertexData[i * 16 + 15] = 0.5;

                ++i;
              }.bind(this)
            );

            gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);

            // Generates index data.
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);

            var indexData = new Uint16Array(6 * graphics.length);
            for (var i = 0; i < graphics.length; ++i) {
              indexData[i * 6 + 0] = i * 4 + 0;
              indexData[i * 6 + 1] = i * 4 + 1;
              indexData[i * 6 + 2] = i * 4 + 2;
              indexData[i * 6 + 3] = i * 4 + 1;
              indexData[i * 6 + 4] = i * 4 + 3;
              indexData[i * 6 + 5] = i * 4 + 2;
            }

            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);

            // Record number of indices.
            this.indexBufferSize = indexData.length;
          }
        });

        // Subclass the custom layer view from GraphicsLayer.
        var CustomLayer = GraphicsLayer.createSubclass({
          createLayerView: function (view) {
            // We only support MapView, so we only need to return a
            // custom layer view for the `2d` case.
            if (view.type === "2d") {
              return new CustomLayerView2D({
                view: view,
                layer: this
              });
            }
          }
        });

        // Create an instance of the custom layer with 4 initial graphics.
        var layer = new CustomLayer({
          popupTemplate: {
            title: "{NAME}",
            content: "Population: {POPULATION}."
          },
          graphics: [
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // Los Angeles
                x: -118.2437,
                y: 34.0522,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                NAME: "Los Angeles",
                POPULATION: 3792621
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // Dallas
                x: -96.797,
                y: 32.7767,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                NAME: "Dallas",
                POPULATION: 1197816
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // Denver
                x: -104.9903,
                y: 39.7392,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                NAME: "Denver",
                POPULATION: 600158
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // New York
                x: -74.006,
                y: 40.7128,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                NAME: "New York",
                POPULATION: 8175133
              }
            }
          ]
        });

        // Create the map and the view.
        var map = new Map({
          basemap: "streets-night-vector",
          layers: [layer]
        });

        var view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-100, 40],
          zoom: 3
        });

        var lastFeatureId = 0;

        // Add new graphics on double click.
        view.on(
          "double-click",
          function (event) {
            event.stopPropagation();

            ++lastFeatureId;

            layer.graphics.add({
              geometry: event.mapPoint,
              attributes: {
                NAME: "Feature #" + lastFeatureId,
                POPULATION: 100000
              }
            });
          }.bind(this)
        );
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>