|
<!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 - 4.11</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.11/esri/themes/light/main.css" |
|
/> |
|
<script src="https://js.arcgis.com/4.11/"></script> |
|
|
|
<style> |
|
html, |
|
body, |
|
#viewDiv { |
|
padding: 0; |
|
margin: 0; |
|
height: 100%; |
|
width: 100%; |
|
} |
|
</style> |
|
|
|
<script> |
|
require([ |
|
"esri/Map", |
|
"esri/core/watchUtils", |
|
"esri/geometry/support/webMercatorUtils", |
|
"esri/layers/GraphicsLayer", |
|
"esri/views/MapView", |
|
"esri/views/2d/layers/BaseLayerViewGL2D", |
|
"esri/layers/FeatureLayer" |
|
], function( |
|
Map, |
|
watchUtils, |
|
webMercatorUtils, |
|
GraphicsLayer, |
|
MapView, |
|
BaseLayerViewGL2D, |
|
FeatureLayer |
|
) { |
|
// 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] = (devicePixelRatio * state.size[0]) / 2; |
|
this.screenTranslation[1] = (devicePixelRatio * 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] = devicePixelRatio / view.state.resolution; |
|
this.screenScaling[1] = -devicePixelRatio / view.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 / (devicePixelRatio * state.size[0]); |
|
this.display[4] = -2 / (devicePixelRatio * 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 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 |
|
}); |
|
} |
|
} |
|
}); |
|
|
|
//Pass the graphics to the customLayer |
|
const myLayer = new FeatureLayer({ |
|
// URL to the service |
|
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/0" |
|
}); |
|
|
|
|
|
|
|
|
|
// Create an instance of the custom layer with 4 initial graphics. |
|
var layer = new CustomLayer({ |
|
graphics: [] |
|
}); |
|
|
|
|
|
myLayer.queryFeatures({where: '1=1', returnGeometry: true, num: 100, outFields: ['*']}).then(results => { |
|
results.features.forEach(feature => { |
|
feature.geometry = webMercatorUtils.geographicToWebMercator(feature.geometry) |
|
layer.graphics.push(feature); |
|
}) |
|
}); |
|
|
|
// 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 |
|
}); |
|
|
|
//map.add(myLayer); |
|
|
|
// Add new graphics on click. |
|
view.on( |
|
"click", |
|
function(event) { |
|
layer.graphics.add({ |
|
geometry: event.mapPoint |
|
}); |
|
}.bind(this) |
|
); |
|
}); |
|
</script> |
|
</head> |
|
|
|
<body> |
|
<div id="viewDiv"></div> |
|
</body> |
|
</html> |