Install
$ agentstack add skill-saschabrunnerch-arcgis-maps-sdk-js-ai-context-arcgis-custom-rendering ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
ArcGIS Custom Rendering
Use this skill for creating custom layers, WebGL rendering in 2D/3D, custom tile sources, and post-processing effects in SceneView.
Import Patterns
ESM (npm)
// 3D custom rendering
import RenderNode from "@arcgis/core/views/3d/webgl/RenderNode.js";
// 2D custom rendering
import BaseLayerView2D from "@arcgis/core/views/2d/layers/BaseLayerView2D.js";
import BaseLayerViewGL2D from "@arcgis/core/views/2d/layers/BaseLayerViewGL2D.js";
// Custom layer bases
import BaseTileLayer from "@arcgis/core/layers/BaseTileLayer.js";
import BaseDynamicLayer from "@arcgis/core/layers/BaseDynamicLayer.js";
import BaseElevationLayer from "@arcgis/core/layers/BaseElevationLayer.js";
import Layer from "@arcgis/core/layers/Layer.js";
// Utilities
import esriRequest from "@arcgis/core/request.js";
import Color from "@arcgis/core/Color.js";
CDN (dynamic import)
const RenderNode = await $arcgis.import(
"@arcgis/core/views/3d/webgl/RenderNode.js",
);
const BaseTileLayer = await $arcgis.import(
"@arcgis/core/layers/BaseTileLayer.js",
);
const BaseLayerViewGL2D = await $arcgis.import(
"@arcgis/core/views/2d/layers/BaseLayerViewGL2D.js",
);
Custom Rendering Summary
| Class | 2D | 3D | Typical Use | | -------------------- | --- | --- | ------------------------------------ | | RenderNode | — | Yes | Post-processing effects in SceneView | | BaseLayerView2D | Yes | — | Custom 2D canvas rendering | | BaseLayerViewGL2D | Yes | — | WebGL-accelerated 2D rendering | | BaseTileLayer | Yes | Yes | Custom tile sources and processing | | BaseDynamicLayer | Yes | Yes | On-demand dynamic rendering | | BaseElevationLayer | — | Yes | Custom elevation/terrain data |
RenderNode (3D Post-Processing)
RenderNode creates custom rendering passes in SceneView's WebGL 2 pipeline.
Basic RenderNode
const LuminanceRenderNode = RenderNode.createSubclass({
constructor() {
this.consumes = { required: ["composite-color"] };
this.produces = "composite-color";
},
render(inputs) {
const input = inputs.find(({ name }) => name === "composite-color");
const colorTexture = input.getTexture();
const output = this.acquireOutputFramebuffer();
const gl = this.gl;
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.uniform1i(this.texLocation, 0);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Preserve depth from input
output.attachDepth(input.getAttachment(gl.DEPTH_STENCIL_ATTACHMENT));
return output;
},
destroy() {
if (this.program) this.gl?.deleteProgram(this.program);
if (this.vao) this.gl?.deleteVertexArray(this.vao);
},
});
// Add to SceneView
const view = new SceneView({ container: "viewDiv", map });
view.when(() => {
new LuminanceRenderNode({ view });
});
RenderNode Key Concepts
| Property/Method | Description | | ---------------------------- | ----------------------------------------------------------------------------------- | | consumes | Render targets this node needs as input (e.g., { required: ["composite-color"] }) | | produces | Name of the render target produced (set null to disable) | | gl | WebGL 2 context (read-only) | | view | Reference to SceneView | | render(inputs) | Override to implement rendering logic | | acquireOutputFramebuffer() | Get framebuffer to render into | | requestRender() | Request a new frame |
RenderNode Enable/Disable
const MyRenderNode = RenderNode.createSubclass({
properties: {
enabled: {
get() {
return this.produces != null;
},
set(value) {
this.produces = value ? "composite-color" : null;
this.requestRender();
},
},
},
});
GLSL ES 3.0 Fragment Shader Example
#version 300 es
precision highp float;
out lowp vec4 fragColor;
in vec2 uv;
uniform sampler2D colorTex;
void main() {
vec4 color = texture(colorTex, uv);
// Luminance (grayscale) conversion
fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722))), color.a);
}
Custom TileLayer
Basic Custom TileLayer
const TintLayer = BaseTileLayer.createSubclass({
properties: {
urlTemplate: null,
tint: {
value: null,
type: Color,
},
},
getTileUrl(level, row, col) {
return this.urlTemplate
.replace("{z}", level)
.replace("{x}", col)
.replace("{y}", row);
},
fetchTile(level, row, col, options) {
const url = this.getTileUrl(level, row, col);
return esriRequest(url, {
responseType: "image",
signal: options?.signal,
}).then((response) => {
const image = response.data;
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;
if (this.tint) {
context.fillStyle = this.tint.toCss();
context.fillRect(0, 0, width, height);
context.globalCompositeOperation = "difference";
}
context.drawImage(image, 0, 0, width, height);
return canvas;
});
},
});
const customLayer = new TintLayer({
urlTemplate: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
tint: new Color("#132178"),
title: "Custom Tinted Layer",
});
map.add(customLayer);
BaseTileLayer Key Methods
| Method | Description | | ------------------------------------- | --------------------------------------------------------- | | getTileUrl(level, row, col) | Return URL string for a tile | | fetchTile(level, row, col, options) | Fetch and optionally process tile; return canvas or image | | load() | Called when layer loads | | refresh() | Reload all tiles |
Watch Property Changes
const CustomLayer = BaseTileLayer.createSubclass({
properties: {
customProperty: null,
},
initialize() {
reactiveUtils.watch(
() => this.customProperty,
() => this.refresh(),
);
},
});
Custom DynamicLayer
const CustomDynamicLayer = BaseDynamicLayer.createSubclass({
properties: {
data: null,
},
getImageUrl(extent, width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
// Convert geo coords to pixel coords
const xScale = width / (extent.xmax - extent.xmin);
const yScale = height / (extent.ymax - extent.ymin);
this.data.forEach((point) => {
const x = (point.x - extent.xmin) * xScale;
const y = height - (point.y - extent.ymin) * yScale;
context.beginPath();
context.arc(x, y, 5, 0, Math.PI * 2);
context.fillStyle = "red";
context.fill();
});
return canvas.toDataURL("image/png");
},
});
Custom Elevation Layer
const ExaggeratedElevationLayer = BaseElevationLayer.createSubclass({
properties: {
exaggeration: 2,
},
load() {
this._elevation = new ElevationLayer({
url: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer",
});
this.addResolvingPromise(this._elevation.load());
},
fetchTile(level, row, col, options) {
return this._elevation.fetchTile(level, row, col, options).then((data) => {
const exaggeration = this.exaggeration;
for (let i = 0; i {
const decodedPixels = Lerc.decode(response.data);
const { width, height, pixels } = decodedPixels;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
for (let i = 0; i {
this.addResolvingPromise(layer.load());
});
},
fetchTile(level, row, col, options) {
const tilePromises = this.multiplyLayers.map((layer) =>
layer.fetchTile(level, row, col, options),
);
return Promise.all(tilePromises).then((images) => {
const width = this.tileInfo.size[0];
const height = this.tileInfo.size[1];
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(images[0], 0, 0, width, height);
ctx.globalCompositeOperation = "multiply";
for (let i = 1; i < images.length; i++) {
ctx.drawImage(images[i], 0, 0, width, height);
}
return canvas;
});
},
});
Common Pitfalls
- CORS: External tile servers must support CORS for canvas-based processing.
- Canvas size: Match tile size exactly with
tileInfo.size[0]— mismatches cause rendering artifacts.
- Memory management: Always clean up WebGL resources (programs, buffers, VAOs, textures) in
detach()ordestroy().
- Abort signals: Always pass
options?.signaltoesriRequestfor proper cancellation.
- Coordinate systems: Use
displayViewMatrix3for pixel-aligned rendering,viewMatrix3for map-coordinate rendering.
- RenderNode WebGL version: SceneView uses WebGL 2 — shaders must use
#version 300 es.
- createSubclass pattern: Use
BaseTileLayer.createSubclass({...})— do not use ES6class extends.
Reference Samples
layers-custom-tilelayer— Custom tinted tile layerlayers-custom-blendlayer— Multi-layer blendinglayers-custom-lerc-2d— LERC data decodinglayers-custom-elevation-exaggerated— Elevation exaggerationlayers-custom-dynamiclayer— Dynamic on-demand renderingcustom-gl-tiles— WebGL tile renderingcustom-gl-visuals— Complex WebGL visualizationscustom-gl-animated-lines— Animated line renderingcustom-render-node-color— RenderNode color post-processingcustom-render-node-dof— Depth of field effectcustom-render-node-windmills— 3D geometry renderingcustom-lv-deckgl— deck.gl integration
Related Skills
arcgis-3d-layers— SceneLayer, PointCloudLayer, VoxelLayerarcgis-scene-environment— Lighting, weather, atmospherearcgis-layers— Standard layer typesarcgis-performance— Rendering optimization
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: SaschaBrunnerCH
- Source: SaschaBrunnerCH/arcgis-maps-sdk-js-ai-context
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.