Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <!--
  ArcGIS API for JavaScript, https://js.arcgis.com
  For more information about the custom-gl-visuals sample, read the original sample description at developers.arcgis.com.
  https://developers.arcgis.com/javascript/latest/sample-code/custom-gl-visuals/index.html
  -->
<title>Custom WebGL layer view - 4.14</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.14/esri/themes/light/main.css"
    />
    <script src="https://js.arcgis.com/4.14/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
      
      #addBtn {
        position: absolute;
        right: 10px;
        top: 10px;
      }
    </style>

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

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

        "esri/geometry/support/webMercatorUtils",

        "esri/layers/GraphicsLayer",
        "esri/layers/FeatureLayer",

        "esri/views/MapView",

        "esri/views/2d/layers/BaseLayerViewGL2D"
      ], function(
        Map,
        watchUtils,
        promiseUtils,
        webMercatorUtils,
        GraphicsLayer,
        FeatureLayer,
        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: {
                id: 1,
                NAME: "Los Angeles",
                POPULATION: 3792621
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // Dallas
                x: -96.797,
                y: 32.7767,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                id: 2,
                NAME: "Dallas",
                POPULATION: 1197816
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // Denver
                x: -104.9903,
                y: 39.7392,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                id: 3,
                NAME: "Denver",
                POPULATION: 600158
              }
            },
            {
              geometry: webMercatorUtils.geographicToWebMercator({
                // New York
                x: -74.006,
                y: 40.7128,
                type: "point",
                spatialReference: {
                  wkid: 4326
                }
              }),
              attributes: {
                id: 4,
                NAME: "New York",
                POPULATION: 8175133
              }
            }
          ]
        });
        
        var featureLayer = new FeatureLayer({
          source: layer.graphics,
          renderer: {
                type: 'simple',
                symbol: {
                    type: "simple-marker",  // autocasts as new SimpleMarkerSymbol()
                    size: "0px",  // pixels
                }
            },
            fields: [{
                name: 'id',
                type: 'oid'
            }, {
                name: 'POPULATION',
                type: 'integer'
            }, {
              name: 'NAME',
                type: 'string'
            }],
            objectIdField: 'id',
            geometryType: 'point',
            labelingInfo: [
                {
                    labelExpressionInfo: { expression: '$feature.POPULATION' },
                    symbol: {
                        type: 'text',
                        color: 'white',
                        font: {
                            size: 12,
                            weight: 'bold'
                        }
                    },
                    labelPlacement: 'center-center'
                }
            ]
        })

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

        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)
        );
        
        window.addBtn.onclick = () => {
          map.addMany([layer, featureLayer])
        }
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <button id="addBtn">Add Layers</button>
  </body>
</html>

              
            
!

CSS

              
                
              
            
!

JS

              
                
              
            
!
999px

Console