介绍
在前端可视化开发中,实现多边形区域的内发光效果是常见的需求。本文将介绍四种不同的实现方法,从最基础的贴图方案到高级的着色器方案,每种方案都有其特点和适用场景。
通过顶部贴图、边缘贴图、Canvas贴图和着色器编程这四种方式,我们可以根据实际项目需求选择最合适的实现方案。本文将详细介绍这四种方案的具体实现步骤,包括所需工具、代码示例以及注意事项,帮助开发者根据项目需求选择最适合的实现方案。

实现思路
在前端可视化开发中,多边形区域的内发光效果实现一直是一个有趣且具有挑战性的课题。经过深入研究和实践,我总结出了四种不同的实现方案,从简单的贴图方案到复杂的着色器编程。每种方案都各具特色,适用于不同的应用场景和技术需求。这些方案的实现难度逐步递增,但同时能够满足不同层次的开发需求。在选择具体方案时,需要权衡项目的性能要求、开发周期、维护成本等多个因素。接下来,让我们深入分析每种方案的具体实现方法、优势特点以及潜在的局限性。
方案 | 操作方法 | 优点 | 缺点 |
---|---|---|---|
1.顶部贴图 | 制作专属贴图贴在polygon上 | 贴图效果可高度定制 | 贴图无法复用;贴图无法以动态更新;放大模型时容易看到图片模糊 |
2.边缘贴图 | 在polygon边缘生成Meshline,对Meshline进行重复贴图 | 操作方便,可以快速更换各种边缘线效果 | 对polygon形状有要求,比边线太宽或者拐点太尖锐会出现视觉效果异常 |
3.canvas贴图 | 方案1的改进版,通过polygon顶点路径数据动态生成Canvas画布内容,并贴在polygon上 | 视觉效果好;动态更新方便 | 每一个polygon都需要先占用CPU资源绘制canvas,数量多的话可能有性能风险 |
4.编写着色器 | 计算polygon上每个点的有向距离场sdf,根据每个点sdf的值填色 | 动态更新方便;GPU渲染性能优化空间大 | 上手门槛较高,需要对计算机图形学有一定的了解 |
准备工作
技术栈
工具 | 版本 | 说明 |
---|---|---|
AMap JSAPI | 2.0 | 高德地图JsAPI实现GIS部分工作,使用map.customCoords的lngLatsToCoords实现坐标转换工作 |
three.js | 0.157 | 基础3d引擎,封装好的webGL,实现Mesh贴图靠它了 |
turf.js | 2.0 | GIS数据分析处理函数库 |
Cursor | 代码编辑器 | 代码提示功能逆天,可以大大减轻编码和理解代码的工作量,花点小钱让AI给我打工就是爽 |
创建几何体
实现一个完整的多边形Mesh需要几何体geometry和材质Material,我们先搞定geometry部分。
-
geometry的实现也可以有两种方式:
(1)计算geometry的矩形范围range,创建一个简单的矩形平面,后续把png格式的贴图贴上去就可以了,polygon范围外保持透明,这样从视觉看上去就是个多边形
(2)绘制真正的geometry,后续把贴图对齐,这样实现的Mesh具有真正的polygon边缘,如何还有其他物体存在的话,比较容易处理它与其他物体的关系,光标拾取检测也比较准确。
这里我们采用第二种方式,因为我们最后需要放置多个polyon在地图上。以下为具体代码:
jsx/** * 绘制多边形 * @private * @param {Array} path 路径 * @param {Object} properties 属性 */ drawPolygon({ path, properties }) { const { altitude, opacity } = this._conf // 获取多边形的范围 const range = this.getExtRange(path) // 创建几何体 const geometry = this.generateOneGeometry(range, path) // 创建材质,不同的方案实现方法不同 const material = ... const polygon = new THREE.Mesh(geometry, material) // 将多边形网格对象添加到场景中 const _scene = this.group || this.scene _scene.add(polygon) this._meshs.push(polygon) } /** * 初始化Polygon边界范围 * @private */ getExtRange(positions) { let minX = positions[0][0] let minY = positions[0][1] let maxX = positions[0][0] let maxY = positions[0][1] for (let i = 0; i < positions.length; i += 1) { const [x, y] = positions[i] if (x < minX) { minX = x } else if (x > maxX) { maxX = x } if (y < minY) { minY = y } else if (y > maxY) { maxY = y } } return { minX, minY, maxX, maxY } } /** * 创建多边形几何体 * @private * @param {Object} range 范围 * @param {Array} path 路径 * @return {THREE.BufferGeometry} */ generateOneGeometry(range, path) { // 创建多边形 const shape = new THREE.Shape() path.forEach(([x, y], index) => { if (index === 0) { shape.moveTo(x, y) } else { shape.lineTo(x, y) } }) // 创建几何体 let geometry = new THREE.ShapeGeometry(shape) // 创建 UV 属性并将其设置到几何体 const uvArray = this.getGeometryUV(geometry, range) const uvAttribute = new THREE.BufferAttribute(uvArray, 2) geometry.setAttribute('uv', uvAttribute) return geometry }
-
这里需要注意的点是如何调整好polygon顶部面的UV,相当于告诉机器要如何把纹理图纸铺到polygon面上,如图所示我们使用平铺的方式,因此需要将UV坐标归一化。
jsx/** * 获取几何体归一化之后的UV坐标 * @param geometry * @return {Float32Array} */ getGeometryUV(geometry, range) { // 获取所有顶点数量 const vertexCount = geometry.attributes.position.count // 创建 UV 坐标数组 const uvArray = new Float32Array(vertexCount * 2) const { minX, minY, maxX, maxY } = range // 设置 UV 坐标 for (let i = 0; i < vertexCount; i++) { const vertexIndex = i * 2 // UV坐标归一化 const u = (geometry.attributes.position.getX(i) - minX) / (maxX - minX) const v = (geometry.attributes.position.getY(i) - minY) / (maxY - minY) uvArray[vertexIndex] = u uvArray[vertexIndex + 1] = v } return uvArray }
-
创建好几何体后,我们创建一个测试的纹理覆盖上去看看是否贴图正常,就可以进行下一步工作了。
实现步骤
方案1.顶部贴图
-
获取多边形的顶点坐标geoJSON文件,将该文件转换为svg格式
-
把图片交给设计师进行二次创作了,除了绘制边缘和内发光,也可以定制一些效果,经过美术上的加工处理后转为png格式的图片
-
处理好的贴图直接贴到geometry上面,对齐贴图,设置好平铺、重复等参数即可
jsx/** * 生成纹理 * @param {string} url - 纹理图片的URL * @returns {Texture} - 生成的纹理对象 */ async function generateTexture(name = 'glow-line1.png') { const texture = await new THREE.TextureLoader().load(`./demo/images/${name}`) texture.wrapS = THREE.RepeatWrapping texture.wrapT = THREE.RepeatWrapping return texture } ... // 创建纹理实例 this._conf.style.polygonMap = generateTexture('textureName') /** * 绘制单个多边形Mesh * @private * @param {Array} path 路径 * @param {Object} properties 属性 */ drawPolygon({ path, properties }) { const { altitude, opacity } = this._conf ... // 创建材质 const material = new THREE.MeshBasicMaterial({ color: properties.color || '#0674F1', transparent: true, opacity: properties.opacity || opacity }) // 设置多边形纹理 const polygonMap = properties.polygonMap || this._conf.style.polygonMap if (polygonMap) { material.map = polygonMap } ... }
-
贴图完成后如果发现纹理没对,可以调整偏移值强行对齐。
jsx// 调整参数UI function initGUI(material) { gui = new dat.GUI() // [AIGC] 切换材质map的偏移 gui.add(guiControl, 'offsetX', -1, 1).step(0.01).name('offsetX') .onChange(async (value) => { await setLayerStyle('offsetX', value) }); // [AIGC] 切换材质map的偏移 gui.add(guiControl, 'offsetY', -1, 1).step(0.01).name('offsetY') .onChange(async (value) => { await setLayerStyle('offsetY', value) }); } // 设置图层样式 async function setLayerStyle(key, value) { const layer = layerManger.findLayerById('polygonGlow1'); switch (key) { case 'offsetX': // [AIGC] 设置材质map的偏移 layer._meshs.forEach(mesh => { mesh.material.map.offset.x = value mesh.material.needsUpdate = true }) break; case 'offsetY': // [AIGC] 设置材质map的偏移 layer._meshs.forEach(mesh => { mesh.material.map.offset.y = value mesh.material.needsUpdate = true }) break; default: break; } }
-
这个方案的绘制范围是polgyone从边缘到内部,因此我们也可以在范围内定制各种外观。但缺陷非常明显,就是纹理在放大后容易出现锯齿感。
方案2.边缘贴图
-
该方案其实受到了arcGIS实现内发光方案的启发。我们获取多边形顶点坐标,使用Meshline组件生成边缘线面geometry,通过在geometry贴发光材质实现内发光。
-
这里需要注意线面的宽度在整个图形的占比,太宽的话会出现"边缘毛刺"的情况。
-
为了保证后续的贴图更均匀且減少毛刺的情况,我们可以优化一下顶点的分布,让顶点之间的距离尽量是差不多的。我们可以用turf.js 实现顶点的均匀化,代码逻辑如下:
(1)设定顶点之间的距离spacing,获取整个polygon边缘的总长度length;
(2)通过spacing和length生成均匀分布的点,获取点坐标;
(3)更新polygon的顶点坐标。
jsx/** * [AIGC] * 生成均匀分布的点 * @param {Array} coordinates - 多边形坐标数组 * @param {number} spacing - 点之间的间距(单位:公里),默认0.5公里 * @returns {Array} - 均匀分布的点坐标数组 [[lng, lat], ...] */ function generateUniformPoints(coordinates, spacing = 0.5) { // 创建一个LineString几何体 const line = turf.lineString(coordinates); // 计算线的总长度(单位:公里) const length = turf.length(line); // 计算需要生成的点数 const pointCount = Math.ceil(length / spacing); // 生成均匀分布的点 const points = []; for (let i = 0; i <= pointCount; i++) { const distance = (i / pointCount) * length; // 获取分段点坐标 const point = turf.along(line, distance, { units: 'kilometers' }); points.push(point.geometry.coordinates); } console.log(`生成均匀分布的点: ${pointCount}/${coordinates.length}`) return points; }
-
创建贴图实例,并设置好贴图,通过调节lineWidth就可以改变内发光的范围。
jsx// 创建贴图实例 async function generateTexture(name = 'glow-line1.png') { const lineMap = await new THREE.TextureLoader().load(`./demo/images/${name}`) lineMap.wrapS = THREE.RepeatWrapping lineMap.wrapT = THREE.RepeatWrapping return lineMap } //... const lineMap = generateTexture('textureName') // 初始化贴图 initLineMaterial() { const { sizeAttenuation, lineWidth, lineColor, lineMap } = this._conf let option = { useMap: true, map: lineMap, transparent: true, depthTest: false, alphaTest: 0.0, sizeAttenuation: 1, lineWidth, dashOffset: 0 } this._lineMaterial = new MeshLineMaterial(option) return this._lineMaterial } // 给Polygon边缘线面贴图 drawLines({ path, properties }) { //... const line = new MeshLineGeometry() line.setPoints(points) const mesh = new THREE.Mesh(line, this._lineMaterial) //... }
-
使用meshline实现边缘内发光的一个好处,就是我们可以利用它自带的动态调整纹理offset的功能,使用不同的纹理和偏移方式创造不同的效果。
jsxconst guiControl = { texture: 'glow-line5.png', //纹理 lineWidth: 500, //范围 dashOffset: 0, offsetDirect: 'y', //偏移方向 offsetSpeed: 1 //偏移速度 } // 逐帧函数中更新纹理偏移值 layer2.on('update', (layer) => { const {_lineMaterial} = layer const {offsetDirect, offsetSpeed} = guiControl // 调整偏移量 _lineMaterial.uniforms.offset.value[offsetDirect] -= 0.01 * offsetSpeed _lineMaterial.uniforms.dashOffset.value -= 0.01 * offsetSpeed })
方案3.Canvas贴图
-
Canvas方案可以算是方案1.顶部贴图的升级版,为了规避方案1贴图复用性差,无法动态更新的问题,我们直接使用Canvas绘制贴图,我们依旧需要polygon的顶点坐标coordinates和矩形范围ExtRange两个数据。
jsx/** * 获取边界范围盒子 * @param {Array} positions 边界坐标 * @private */ getExtRange(positions) { let minX = positions[0][0] let minY = positions[0][1] let maxX = positions[0][0] let maxY = positions[0][1] for (let i = 0; i < positions.length; i += 1) { const [x, y] = positions[i] if (x < minX) { minX = x } else if (x > maxX) { maxX = x } if (y < minY) { minY = y } else if (y > maxY) { maxY = y } } return { minX, minY, maxX, maxY } }
-
绘制贴图纹理CanvasTexture,canvas的api里其实没有内发光的方法,我们直接沿着边缘绘制一条外阴影的线,并绘制多重阴影
a. 通过rang设置canvas画布大小,为了避免画布过大影响性能,还加了_CANVAS_MAX_LEN 限制画布的最大宽高 ```jsx generateCanvasTexture(range, path) { const polygonVertices = path.map(([x, y]) => { return new THREE.Vector3(x, y, 0); });
arduinoconst { minX, minY, maxX, maxY } = range; // 限制画布的最大宽和高 const scale = this._CANVAS_MAX_LEN / this._maxDimension; console.log('scale', scale) // 缩放多边形顶点的坐标 const scaledVertices = polygonVertices.map(vertex => { const x = (vertex.x - minX) * scale; const y = (vertex.y - minY) * scale; return new THREE.Vector3(x, y, vertex.z); }); // 计算调整后的画布大小 const width = Math.ceil((maxX - minX) * scale); const height = Math.ceil((maxY - minY) * scale); console.log(width,height) ```
b. 根据path开始绘制多边形边线,并给边线增加多种外阴影效果,为什么要多重绘制外阴影呢?因为glowBlur控制阴影的范围越大时,阴影会变得越"稀薄",为了同时提升阴影的范围和"浓度",我们需要多重绘制。
ini```jsx // 开始绘制多边形 context.beginPath(); context.moveTo(scaledVertices[0].x, scaledVertices[0].y); for (let i = 1; i < scaledVertices.length; i++) { context.lineTo(scaledVertices[i].x, scaledVertices[i].y); } context.closePath(); // 设置线条样式 context.lineWidth = lineWidth * 2; // 设置线条宽度 context.strokeStyle = lineColor; // 设置线条颜色 // 多重阴影的数量 const numShadows = 5; // 内发光强度 const baseIntensity = glowIntensity * 100.0; // 内发光颜色 const [r,g,b] = hexToArray(glowColor) for (let i = 1; i <= numShadows; i++) { context.shadowColor = `rgba(${r}, ${g}, ${b}, ${baseIntensity * (numShadows - i + 1) / numShadows})`; // 内发光颜色和透明度 context.shadowBlur = glowBlur * i; // 增加模糊程度 context.shadowOffsetX = 0; // 内发光水平偏移 context.shadowOffsetY = 0; // 内发光垂直偏移 context.stroke(); // 重新填充以应用内发光效果 } ```
- 生成CanvasTexture作为材质纹理贴图,当然每个形状的polygon需要单独渲染一块贴图
arduino```jsx const texture = this.generateCanvasTexture(range, path); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; const shaderMaterial = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide, opacity: 0.9 }); //... // 创建几何体 const geometry = this.generateOneGeometry(range, path) // 创建材质 const material = this.getMaterial(range, path) // 创建多边形的网格对象 const polygon = new THREE.Mesh(geometry, material) ```
- 最终可以得到相对理想的PolygonMesh
该方案的完整代码如下:
jsx/** * 通过边缘坐标数据获取灰度图纹理 * @private */ generateCanvasTexture(range, path) { const polygonVertices = path.map(([x, y]) => { return new THREE.Vector3(x, y, 0); }); const { minX, minY, maxX, maxY } = range; // 限制画布的最大宽和高 const scale = this._CANVAS_MAX_LEN / this._maxDimension; console.log('scale', scale) // 缩放多边形顶点的坐标 const scaledVertices = polygonVertices.map(vertex => { const x = (vertex.x - minX) * scale; const y = (vertex.y - minY) * scale; return new THREE.Vector3(x, y, vertex.z); }); // 计算调整后的画布大小 const width = Math.ceil((maxX - minX) * scale); const height = Math.ceil((maxY - minY) * scale); console.log(width,height) // 读取外观配置 const {lineWidth, lineColor, glowColor, glowIntensity, glowBlur} = this._conf.style const canvas = this.generateCanvas({ width, height }); const context = canvas.getContext('2d'); // 绘制多边形背景为黑色 context.fillStyle = 'transparent'; context.fillRect(0, 0, width, height); // 开始绘制多边形 context.beginPath(); context.moveTo(scaledVertices[0].x, scaledVertices[0].y); for (let i = 1; i < scaledVertices.length; i++) { context.lineTo(scaledVertices[i].x, scaledVertices[i].y); } context.closePath(); // 设置线条样式 context.lineWidth = lineWidth * 2; // 设置线条宽度 context.strokeStyle = lineColor; // 设置线条颜色 // 多重阴影的数量 const numShadows = 5; // 内发光强度 const baseIntensity = glowIntensity * 100.0; // 内发光颜色 const [r,g,b] = hexToArray(glowColor) for (let i = 1; i <= numShadows; i++) { context.shadowColor = `rgba(${r}, ${g}, ${b}, ${baseIntensity * (numShadows - i + 1) / numShadows})`; // 内发光颜色和透明度 context.shadowBlur = glowBlur * i; // 增加模糊程度 context.shadowOffsetX = 0; // 内发光水平偏移 context.shadowOffsetY = 0; // 内发光垂直偏移 context.stroke(); // 重新填充以应用内发光效果 } return new THREE.CanvasTexture(canvas, null, THREE.RepeatWrapping, THREE.RepeatWrapping); } /** * 获取材质 * @private * @param {*} range polygon边界矩形范围 * @param {*} path 边界路径 * @returns */ getMaterial(range, path) { // 通过边缘坐标数据获取灰度图纹理 const texture = this.generateCanvasTexture(range, path); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; const shaderMaterial = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide, opacity: 0.9 }); return shaderMaterial; } /** * 绘制多边形 * @private * @param {Array} path 路径 * @param {Object} properties 属性 */ drawPolygon({ path, properties }) { const { altitude, opacity } = this._conf // 获取多边形的范围 const range = this.getExtRange(path) // 创建几何体 const geometry = this.generateOneGeometry(range, path) // 创建材质 const material = this.getMaterial(range, path) // 创建多边形的网格对象 const polygon = new THREE.Mesh(geometry, material) polygon.position.z = altitude // 缓存属性 polygon._attrs = properties // 将多边形网格对象添加到场景中 const _scene = this.group || this.scene _scene.add(polygon) this._meshs.push(polygon) }
-
贴图到几何体上,我们可以借助dat.gui控件调整到最佳效果,从目前看,使用Canvas绘制的方法是相对省心,效果最好的
方案4.编写着色器
-
使用着色器材质的方式是最灵活且性能潜力最大的,毕竟是GPU渲染,问题就是难度太大,不过可以上shadertoy抄作业,前提需要懂GLSL和计算机图形学的基础知识
-
说实话写shader挺难的,但如果人人都会写,就体现不出个人能力优势了。这里我们参考距离场实现描边的方案。 a. shader核心逻辑就是针对矩形贴图中每个像素点,判断它是否在polygon内部,且跟polygon边缘的最短距离,根据这个信息确定其透明度a。如果点在polygon外部,a为0,即不显示;如果点在polygon内部,且与边缘距离d比在设定范围edgeWidth内,则根据d与edgeWidth的比例调整透明度a,如果在edgeWidth范围外,也不显示。
b. 如何获取Polygon内1个像素点P跟Polygon边缘的最小距离呢?其实是遍历了该像素点周围一圈的所有点之后确定的,如果这个点刚好在边缘上,就测出它与P的距离distance,如果distance比minDistance还小则替换minDistance
-
着色器的具体代码实现可以看这里
jsxconst shader = { uniforms: { inputImageTexture: { value: null }, // polygon灰度图 glowColor: { value: new THREE.Color(0x00ffff) }, //发光颜色 glowIntensity: { value: 2.0 }, //发光强度 stepWidth: { value: 1.0 }, //固定值,控制过渡的平滑度 resolution: { value: new THREE.Vector2(100, 100)},画布的宽高 md: { value: 10.0 },//固定值,控制过渡的平滑度 edgeWidth: { value: 0.08 },// 内发光范围占画布整体的百分比 textureOffsetX: { value: 0.0 },//纹理X偏移 textureOffsetY: { value: 0.0 }//纹理Y偏移 }, /** * [AIGC] 定义顶点着色器代码 */ vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, /** * [AIGC] 定义片元着色器代码 */ fragmentShader: ` uniform sampler2D inputImageTexture; uniform vec3 glowColor; uniform float glowIntensity; uniform float edgeWidth; uniform vec2 resolution uniform float textureOffsetX; uniform float textureOffsetY; varying vec2 vUv; // 在for循环中需要设置为静态值,该算法复杂度太高 width*height*md*md // todo: 待优化为双通道有向距离场生成 const float md = 20.0; const float stepWidth = 1.0; // 判断点是否在边界内 float source(vec2 uv) { vec2 textureOffset = vec2(textureOffsetX, textureOffsetY); vec2 finalUV = uv - textureOffset; return texture2D(inputImageTexture, finalUV).r - 0.7; } // 计算当前点到边界的最短距离 float distanceToEdge(vec2 point) { float minDistance = md; // 处于边界外 if(source(point) < 0.0){ return minDistance; } for (float x = -md ; x <= md; x += stepWidth) { for (float y = -md; y <= md; y += stepWidth) { // 采样点坐标 vec2 samplePoint = point + vec2(x, y) / resolution; // 判断采样点是否刚好在边界 if (source(samplePoint) < 0.0) { float distance = length(samplePoint - point); if (distance < minDistance) { minDistance = distance; } } } } return minDistance; } // void main() { // 计算到最近边界的距离 distance值应该是归一化后的值 float distance = distanceToEdge(vUv); // 计算透明度渐变: 距离越远alpha越小 float alphaFactor = smoothstep(0.0, 1.0, 1.0 - clamp( distance / edgeWidth, 0.0, 1.0)); // 应用发光效果 // float glowFactor = exp(-distance * glowIntensity); vec3 finalGlow = glowColor * glowIntensity; // 输出最终颜色 gl_FragColor = vec4(finalGlow, alphaFactor); } `, };
-
在代码实现上与其他三个方案最大的区别就是getMaterial材质的生成,核心代码已经给出,后面的依葫芦画瓢即可。
jsx/** * 获取材质 * @private * @param {*} range polygon边界矩形范围 * @param {*} path 边界路径 * @returns */ getMaterial(range, path) { // 通过边缘坐标数据获取灰度图纹理 const texture = this.generateAlphaMap(range, path); // 获取canvas的宽高 const { width, height } = texture.source.data // 着色器 const { fragmentShader, vertexShader } = PolyInnerGlowShader // 自定义材质 const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { inputImageTexture: { value: texture }, // polygon灰度图 glowColor: { value: new THREE.Color(0x00ffff) },//发光颜色 glowIntensity: { value: 2.0 }, //发光强度 stepWidth: { value: 1.0 },//固定值,控制过渡的平滑度 md: { value: 10.0 }, //固定值, 控制每个像素点检测范围 resolution: { value: new THREE.Vector2(width, height) },//画布尺寸 edgeWidth: { value: 0.02 }, // 内发光范围占画布整体的百分比 textureOffsetX: { value: 0.0 }, //纹理X偏移 textureOffsetY: { value: 0.0 } //纹理Y偏移 }, vertexShader, fragmentShader, transparent: true, side: THREE.DoubleSide }); return shaderMaterial; }
-
最后实现效果如下,shader方案的动态更新响应性能非常高,即使目前的代码仍有优化空间。
总结
最后综合考虑可读性、可维护性、实现难度等综合因素,我们选择了方案3Canvas贴图方案作为产品常用方案。
在鲁迅的小说中,孔乙己虽然掌握了茴字的四种写法,但他只是把知识作为炫耀自己的工具,而没有真正将知识用于启发他人或推动社会进步。这也提醒我们知识的价值不仅在于掌握,更在于传播和应用,这是我编写这边文章的初衷。今天就分享到这里吧。

相关链接
How to add an inner glow to polygons with the ArcGIS API for JavaScript