[{"data":1,"prerenderedAt":1332},["ShallowReactive",2],{"\u002Fblog\u002Fcesium-region-mask-overlay-implementation":3,"\u002Fblog\u002Fcesium-region-mask-overlay-implementation-surround":1321},{"id":4,"title":5,"author":6,"body":10,"date":1312,"description":1313,"extension":1314,"image":1315,"meta":1316,"minRead":157,"navigation":220,"path":1317,"seo":1318,"stem":1319,"__hash__":1320},"blog\u002Fblog\u002Fcesium-region-mask-overlay-implementation.md","Cesium 行政区反向遮罩的屏幕空间实现",{"name":7,"avatar":8},"Kevin",{"src":9,"alt":7},"\u002Favatar.jpg",{"type":11,"value":12,"toc":1303},"minimark",[13,17,21,24,32,35,38,49,52,55,58,79,98,105,111,114,246,249,252,488,491,741,748,922,928,1172,1175,1273,1276,1279,1282,1285,1288,1291,1297,1300],[14,15,16],"h2",{"id":16},"这篇文章解决什么问题",[18,19,20],"p",{},"行政区遮罩常见于指挥调度、态势感知和专题地图页面：业务关注区保持清晰，区域外统一压暗，再用高亮边界告诉用户当前视野的主角在哪里。",[18,22,23],{},"这个效果看起来像是在地图上挖了一个洞，但它不一定适合用真实地理裁剪来做。这里的目标只是视觉表达：不裁剪瓦片、不改造底图、不参与空间分析，也不要求区域外数据真的被过滤。",[18,25,26,27,31],{},"因此这个 Demo 最终采用屏幕空间反向遮罩：在 Cesium 画布上方叠加一个透明 canvas，每帧把 GeoJSON 面投影到屏幕坐标，再用 ",[28,29,30],"code",{},"evenodd"," 规则绘制“全屏遮罩 + 行政区洞口”。地图本身照常渲染，遮罩只负责改变用户看到的视觉层次。",[14,33,34],{"id":34},"核心难点",[18,36,37],{},"第一个难点是遮罩应该跟着相机变化。行政区边界存在于经纬度空间，但屏幕遮罩画在 DOM canvas 上。相机缩放、旋转、飞行之后，GeoJSON 点必须重新投影到当前屏幕坐标，否则洞口会和真实行政区错位。",[18,39,40,41,44,45,48],{},"第二个难点是面数据的结构。GeoJSON 里既可能是 ",[28,42,43],{},"Polygon","，也可能是 ",[28,46,47],{},"MultiPolygon","；一个面还可能有内洞。遮罩需要只消费面要素，并跳过线、点等非面几何，避免把业务数据里的其他几何误当成遮罩。",[18,50,51],{},"第三个难点是边界观感。单条 cyan 线很容易显得薄，图形化大屏里更常见的是外层发光加内层亮线。这个 Demo 使用两层 Cesium polyline：外层负责氛围发光，内层负责轮廓锐度。",[14,53,54],{"id":54},"实现思路",[18,56,57],{},"整体链路可以分成四步。",[18,59,60,61,64,65,68,69,72,73,75,76,78],{},"第一步，解析行政区 GeoJSON。实现支持 ",[28,62,63],{},"FeatureCollection","、",[28,66,67],{},"Feature"," 和 ",[28,70,71],{},"Geometry"," 输入，但只收集 ",[28,74,43],{}," \u002F ",[28,77,47],{},"，并把每个 ring 规范成可投影的经纬度点列。",[18,80,81,82,85,86,89,90,93,94,97],{},"第二步，创建屏幕遮罩 canvas。canvas 挂在 ",[28,83,84],{},"viewer.container"," 上方，",[28,87,88],{},"pointer-events"," 设为 ",[28,91,92],{},"none","，让它不影响 Cesium 原有交互；同时监听 ",[28,95,96],{},"scene.postRender","，在 Cesium 每次渲染后重画遮罩。",[18,99,100,101,104],{},"第三步，把经纬度投影到屏幕坐标。每个点先转成 ",[28,102,103],{},"Cartesian3","，再通过 Cesium 的场景转换 API 得到当前窗口坐标，最终写入 canvas path。",[18,106,107,108,110],{},"第四步，用 ",[28,109,30],{}," 填充。先画一个覆盖全屏的矩形，再画行政区 ring。canvas 使用奇偶填充规则后，外部会被遮罩色填充，行政区内部则成为透明洞口。",[14,112,113],{"id":113},"技术流程图",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","%%{init: {'theme':'tokyo-night', 'themeVariables':{'primaryColor':'#7aa2f7'}}}%%\nflowchart TD\n    A([开始]) --> B[读取行政区 GeoJSON]\n    B --> C{几何是否为 Polygon 或 MultiPolygon}\n    C -->|是| D[规范化外环和内洞点列]\n    C -->|否| E[跳过非面几何]\n    D --> F[创建覆盖 Cesium 容器的屏幕 canvas]\n    E --> F\n    F --> G[注册 scene.postRender 回调]\n    G --> H[每帧把经纬度点投影到窗口坐标]\n    H --> I[绘制全屏矩形路径]\n    I --> J[追加行政区 ring 路径]\n    J --> K[使用 evenodd 填充生成透明洞口]\n    K --> L[创建 Cesium 发光边界线]\n    L --> M([遮罩渲染完成])\n\n    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n    style M fill:#9ece6a,stroke:#9ece6a,color:#fff\n    style C fill:#e0af68,stroke:#e0af68,color:#1f2335\n    style K fill:#bb9af7,stroke:#bb9af7,color:#fff\n","mermaid","",[28,122,123,131,137,143,149,155,161,167,173,179,185,191,197,203,209,215,222,228,234,240],{"__ignoreMap":120},[124,125,128],"span",{"class":126,"line":127},"line",1,[124,129,130],{},"%%{init: {'theme':'tokyo-night', 'themeVariables':{'primaryColor':'#7aa2f7'}}}%%\n",[124,132,134],{"class":126,"line":133},2,[124,135,136],{},"flowchart TD\n",[124,138,140],{"class":126,"line":139},3,[124,141,142],{},"    A([开始]) --> B[读取行政区 GeoJSON]\n",[124,144,146],{"class":126,"line":145},4,[124,147,148],{},"    B --> C{几何是否为 Polygon 或 MultiPolygon}\n",[124,150,152],{"class":126,"line":151},5,[124,153,154],{},"    C -->|是| D[规范化外环和内洞点列]\n",[124,156,158],{"class":126,"line":157},6,[124,159,160],{},"    C -->|否| E[跳过非面几何]\n",[124,162,164],{"class":126,"line":163},7,[124,165,166],{},"    D --> F[创建覆盖 Cesium 容器的屏幕 canvas]\n",[124,168,170],{"class":126,"line":169},8,[124,171,172],{},"    E --> F\n",[124,174,176],{"class":126,"line":175},9,[124,177,178],{},"    F --> G[注册 scene.postRender 回调]\n",[124,180,182],{"class":126,"line":181},10,[124,183,184],{},"    G --> H[每帧把经纬度点投影到窗口坐标]\n",[124,186,188],{"class":126,"line":187},11,[124,189,190],{},"    H --> I[绘制全屏矩形路径]\n",[124,192,194],{"class":126,"line":193},12,[124,195,196],{},"    I --> J[追加行政区 ring 路径]\n",[124,198,200],{"class":126,"line":199},13,[124,201,202],{},"    J --> K[使用 evenodd 填充生成透明洞口]\n",[124,204,206],{"class":126,"line":205},14,[124,207,208],{},"    K --> L[创建 Cesium 发光边界线]\n",[124,210,212],{"class":126,"line":211},15,[124,213,214],{},"    L --> M([遮罩渲染完成])\n",[124,216,218],{"class":126,"line":217},16,[124,219,221],{"emptyLinePlaceholder":220},true,"\n",[124,223,225],{"class":126,"line":224},17,[124,226,227],{},"    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n",[124,229,231],{"class":126,"line":230},18,[124,232,233],{},"    style M fill:#9ece6a,stroke:#9ece6a,color:#fff\n",[124,235,237],{"class":126,"line":236},19,[124,238,239],{},"    style C fill:#e0af68,stroke:#e0af68,color:#1f2335\n",[124,241,243],{"class":126,"line":242},20,[124,244,245],{},"    style K fill:#bb9af7,stroke:#bb9af7,color:#fff\n",[14,247,248],{"id":248},"关键代码解析",[18,250,251],{},"GeoJSON 解析阶段只收集面几何。这样同一份行政区数据里即使带有辅助线、标注点或其他几何，也不会影响遮罩生成。",[115,253,257],{"className":254,"code":255,"language":256,"meta":120,"style":120},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","function collectPolygonCoordinates(input, target, stats) {\n  if (input.type === 'Polygon') {\n    target.push(input.coordinates)\n    return\n  }\n\n  if (input.type === 'MultiPolygon') {\n    const polygons = Array.isArray(input.coordinates) ? input.coordinates : []\n    target.push(...polygons)\n    return\n  }\n\n  stats.skippedGeometryCount += 1\n}\n","js",[28,258,259,294,331,353,358,363,367,391,436,454,458,462,466,483],{"__ignoreMap":120},[124,260,261,265,269,273,277,280,283,285,288,291],{"class":126,"line":127},[124,262,264],{"class":263},"spNyl","function",[124,266,268],{"class":267},"s2Zo4"," collectPolygonCoordinates",[124,270,272],{"class":271},"sMK4o","(",[124,274,276],{"class":275},"sHdIc","input",[124,278,279],{"class":271},",",[124,281,282],{"class":275}," target",[124,284,279],{"class":271},[124,286,287],{"class":275}," stats",[124,289,290],{"class":271},")",[124,292,293],{"class":271}," {\n",[124,295,296,300,304,307,310,313,316,319,322,325,328],{"class":126,"line":133},[124,297,299],{"class":298},"s7zQu","  if",[124,301,303],{"class":302},"swJcz"," (",[124,305,276],{"class":306},"sTEyZ",[124,308,309],{"class":271},".",[124,311,312],{"class":306},"type",[124,314,315],{"class":271}," ===",[124,317,318],{"class":271}," '",[124,320,43],{"class":321},"sfazB",[124,323,324],{"class":271},"'",[124,326,327],{"class":302},") ",[124,329,330],{"class":271},"{\n",[124,332,333,336,338,341,343,345,347,350],{"class":126,"line":139},[124,334,335],{"class":306},"    target",[124,337,309],{"class":271},[124,339,340],{"class":267},"push",[124,342,272],{"class":302},[124,344,276],{"class":306},[124,346,309],{"class":271},[124,348,349],{"class":306},"coordinates",[124,351,352],{"class":302},")\n",[124,354,355],{"class":126,"line":145},[124,356,357],{"class":298},"    return\n",[124,359,360],{"class":126,"line":151},[124,361,362],{"class":271},"  }\n",[124,364,365],{"class":126,"line":157},[124,366,221],{"emptyLinePlaceholder":220},[124,368,369,371,373,375,377,379,381,383,385,387,389],{"class":126,"line":163},[124,370,299],{"class":298},[124,372,303],{"class":302},[124,374,276],{"class":306},[124,376,309],{"class":271},[124,378,312],{"class":306},[124,380,315],{"class":271},[124,382,318],{"class":271},[124,384,47],{"class":321},[124,386,324],{"class":271},[124,388,327],{"class":302},[124,390,330],{"class":271},[124,392,393,396,399,402,405,407,410,412,414,416,418,420,423,426,428,430,433],{"class":126,"line":169},[124,394,395],{"class":263},"    const",[124,397,398],{"class":306}," polygons",[124,400,401],{"class":271}," =",[124,403,404],{"class":306}," Array",[124,406,309],{"class":271},[124,408,409],{"class":267},"isArray",[124,411,272],{"class":302},[124,413,276],{"class":306},[124,415,309],{"class":271},[124,417,349],{"class":306},[124,419,327],{"class":302},[124,421,422],{"class":271},"?",[124,424,425],{"class":306}," input",[124,427,309],{"class":271},[124,429,349],{"class":306},[124,431,432],{"class":271}," :",[124,434,435],{"class":302}," []\n",[124,437,438,440,442,444,446,449,452],{"class":126,"line":175},[124,439,335],{"class":306},[124,441,309],{"class":271},[124,443,340],{"class":267},[124,445,272],{"class":302},[124,447,448],{"class":271},"...",[124,450,451],{"class":306},"polygons",[124,453,352],{"class":302},[124,455,456],{"class":126,"line":181},[124,457,357],{"class":298},[124,459,460],{"class":126,"line":187},[124,461,362],{"class":271},[124,463,464],{"class":126,"line":193},[124,465,221],{"emptyLinePlaceholder":220},[124,467,468,471,473,476,479],{"class":126,"line":199},[124,469,470],{"class":306},"  stats",[124,472,309],{"class":271},[124,474,475],{"class":306},"skippedGeometryCount",[124,477,478],{"class":271}," +=",[124,480,482],{"class":481},"sbssI"," 1\n",[124,484,485],{"class":126,"line":205},[124,486,487],{"class":271},"}\n",[18,489,490],{},"屏幕 canvas 的关键不在样式，而在它和 Cesium 渲染循环的绑定。遮罩每帧根据当前相机重新计算，因此它不会在飞行或缩放后停留在旧位置。",[115,492,494],{"className":254,"code":493,"language":256,"meta":120,"style":120},"const canvas = document.createElement('canvas')\nObject.assign(canvas.style, {\n  position: 'absolute',\n  inset: '0',\n  width: '100%',\n  height: '100%',\n  pointerEvents: 'none'\n})\n\ncontainer.appendChild(canvas)\nthis.screenContext = canvas.getContext('2d')\nthis.screenRenderCallback = () => {\n  this.drawScreenMask()\n}\nthis.viewer.scene.postRender.addEventListener(this.screenRenderCallback)\n",[28,495,496,526,548,566,582,598,613,627,634,638,651,680,697,708,712],{"__ignoreMap":120},[124,497,498,501,504,507,510,512,515,517,519,522,524],{"class":126,"line":127},[124,499,500],{"class":263},"const",[124,502,503],{"class":306}," canvas ",[124,505,506],{"class":271},"=",[124,508,509],{"class":306}," document",[124,511,309],{"class":271},[124,513,514],{"class":267},"createElement",[124,516,272],{"class":306},[124,518,324],{"class":271},[124,520,521],{"class":321},"canvas",[124,523,324],{"class":271},[124,525,352],{"class":306},[124,527,528,531,533,536,539,541,544,546],{"class":126,"line":133},[124,529,530],{"class":306},"Object",[124,532,309],{"class":271},[124,534,535],{"class":267},"assign",[124,537,538],{"class":306},"(canvas",[124,540,309],{"class":271},[124,542,543],{"class":306},"style",[124,545,279],{"class":271},[124,547,293],{"class":271},[124,549,550,553,556,558,561,563],{"class":126,"line":139},[124,551,552],{"class":302},"  position",[124,554,555],{"class":271},":",[124,557,318],{"class":271},[124,559,560],{"class":321},"absolute",[124,562,324],{"class":271},[124,564,565],{"class":271},",\n",[124,567,568,571,573,575,578,580],{"class":126,"line":145},[124,569,570],{"class":302},"  inset",[124,572,555],{"class":271},[124,574,318],{"class":271},[124,576,577],{"class":321},"0",[124,579,324],{"class":271},[124,581,565],{"class":271},[124,583,584,587,589,591,594,596],{"class":126,"line":151},[124,585,586],{"class":302},"  width",[124,588,555],{"class":271},[124,590,318],{"class":271},[124,592,593],{"class":321},"100%",[124,595,324],{"class":271},[124,597,565],{"class":271},[124,599,600,603,605,607,609,611],{"class":126,"line":157},[124,601,602],{"class":302},"  height",[124,604,555],{"class":271},[124,606,318],{"class":271},[124,608,593],{"class":321},[124,610,324],{"class":271},[124,612,565],{"class":271},[124,614,615,618,620,622,624],{"class":126,"line":163},[124,616,617],{"class":302},"  pointerEvents",[124,619,555],{"class":271},[124,621,318],{"class":271},[124,623,92],{"class":321},[124,625,626],{"class":271},"'\n",[124,628,629,632],{"class":126,"line":169},[124,630,631],{"class":271},"}",[124,633,352],{"class":306},[124,635,636],{"class":126,"line":175},[124,637,221],{"emptyLinePlaceholder":220},[124,639,640,643,645,648],{"class":126,"line":181},[124,641,642],{"class":306},"container",[124,644,309],{"class":271},[124,646,647],{"class":267},"appendChild",[124,649,650],{"class":306},"(canvas)\n",[124,652,653,656,659,661,664,666,669,671,673,676,678],{"class":126,"line":187},[124,654,655],{"class":271},"this.",[124,657,658],{"class":306},"screenContext ",[124,660,506],{"class":271},[124,662,663],{"class":306}," canvas",[124,665,309],{"class":271},[124,667,668],{"class":267},"getContext",[124,670,272],{"class":306},[124,672,324],{"class":271},[124,674,675],{"class":321},"2d",[124,677,324],{"class":271},[124,679,352],{"class":306},[124,681,682,684,687,689,692,695],{"class":126,"line":193},[124,683,655],{"class":271},[124,685,686],{"class":267},"screenRenderCallback",[124,688,401],{"class":271},[124,690,691],{"class":271}," ()",[124,693,694],{"class":263}," =>",[124,696,293],{"class":271},[124,698,699,702,705],{"class":126,"line":199},[124,700,701],{"class":271},"  this.",[124,703,704],{"class":267},"drawScreenMask",[124,706,707],{"class":302},"()\n",[124,709,710],{"class":126,"line":205},[124,711,487],{"class":271},[124,713,714,716,719,721,724,726,729,731,734,736,738],{"class":126,"line":211},[124,715,655],{"class":271},[124,717,718],{"class":306},"viewer",[124,720,309],{"class":271},[124,722,723],{"class":306},"scene",[124,725,309],{"class":271},[124,727,728],{"class":306},"postRender",[124,730,309],{"class":271},[124,732,733],{"class":267},"addEventListener",[124,735,272],{"class":306},[124,737,655],{"class":271},[124,739,740],{"class":306},"screenRenderCallback)\n",[18,742,743,744,747],{},"投影阶段需要兼容 Cesium 版本差异。当前实现优先使用 ",[28,745,746],{},"worldToWindowCoordinates","，如果项目里仍有旧版本 Cesium，也保留后备转换路径。",[115,749,751],{"className":254,"code":750,"language":256,"meta":120,"style":120},"function worldToWindowCoordinates(Cesium, scene, cartesian) {\n  const sceneTransforms = Cesium.SceneTransforms\n\n  if (typeof sceneTransforms?.worldToWindowCoordinates === 'function') {\n    return sceneTransforms.worldToWindowCoordinates(scene, cartesian)\n  }\n\n  if (typeof scene.cartesianToCanvasCoordinates === 'function') {\n    return scene.cartesianToCanvasCoordinates(cartesian)\n  }\n\n  return null\n}\n",[28,752,753,779,797,801,829,850,854,858,885,902,906,910,918],{"__ignoreMap":120},[124,754,755,757,760,762,765,767,770,772,775,777],{"class":126,"line":127},[124,756,264],{"class":263},[124,758,759],{"class":267}," worldToWindowCoordinates",[124,761,272],{"class":271},[124,763,764],{"class":275},"Cesium",[124,766,279],{"class":271},[124,768,769],{"class":275}," scene",[124,771,279],{"class":271},[124,773,774],{"class":275}," cartesian",[124,776,290],{"class":271},[124,778,293],{"class":271},[124,780,781,784,787,789,792,794],{"class":126,"line":133},[124,782,783],{"class":263},"  const",[124,785,786],{"class":306}," sceneTransforms",[124,788,401],{"class":271},[124,790,791],{"class":306}," Cesium",[124,793,309],{"class":271},[124,795,796],{"class":306},"SceneTransforms\n",[124,798,799],{"class":126,"line":139},[124,800,221],{"emptyLinePlaceholder":220},[124,802,803,805,807,810,812,815,817,819,821,823,825,827],{"class":126,"line":145},[124,804,299],{"class":298},[124,806,303],{"class":302},[124,808,809],{"class":271},"typeof",[124,811,786],{"class":306},[124,813,814],{"class":271},"?.",[124,816,746],{"class":306},[124,818,315],{"class":271},[124,820,318],{"class":271},[124,822,264],{"class":321},[124,824,324],{"class":271},[124,826,327],{"class":302},[124,828,330],{"class":271},[124,830,831,834,836,838,840,842,844,846,848],{"class":126,"line":151},[124,832,833],{"class":298},"    return",[124,835,786],{"class":306},[124,837,309],{"class":271},[124,839,746],{"class":267},[124,841,272],{"class":302},[124,843,723],{"class":306},[124,845,279],{"class":271},[124,847,774],{"class":306},[124,849,352],{"class":302},[124,851,852],{"class":126,"line":157},[124,853,362],{"class":271},[124,855,856],{"class":126,"line":163},[124,857,221],{"emptyLinePlaceholder":220},[124,859,860,862,864,866,868,870,873,875,877,879,881,883],{"class":126,"line":169},[124,861,299],{"class":298},[124,863,303],{"class":302},[124,865,809],{"class":271},[124,867,769],{"class":306},[124,869,309],{"class":271},[124,871,872],{"class":306},"cartesianToCanvasCoordinates",[124,874,315],{"class":271},[124,876,318],{"class":271},[124,878,264],{"class":321},[124,880,324],{"class":271},[124,882,327],{"class":302},[124,884,330],{"class":271},[124,886,887,889,891,893,895,897,900],{"class":126,"line":175},[124,888,833],{"class":298},[124,890,769],{"class":306},[124,892,309],{"class":271},[124,894,872],{"class":267},[124,896,272],{"class":302},[124,898,899],{"class":306},"cartesian",[124,901,352],{"class":302},[124,903,904],{"class":126,"line":181},[124,905,362],{"class":271},[124,907,908],{"class":126,"line":187},[124,909,221],{"emptyLinePlaceholder":220},[124,911,912,915],{"class":126,"line":193},[124,913,914],{"class":298},"  return",[124,916,917],{"class":271}," null\n",[124,919,920],{"class":126,"line":199},[124,921,487],{"class":271},[18,923,924,925,927],{},"真正形成“外部压暗、内部透明”的是 ",[28,926,30],{},"。先把整屏矩形写入 path，再写入行政区边界；填充时 canvas 会把重叠次数为奇数的区域涂色，行政区内部则被反向镂空。",[115,929,931],{"className":254,"code":930,"language":256,"meta":120,"style":120},"context.clearRect(0, 0, size.width, size.height)\ncontext.beginPath()\ncontext.rect(0, 0, size.width, size.height)\n\nfor (const polygon of this.parsedPolygons) {\n  this.drawProjectedRing(polygon.outer.points)\n\n  for (const hole of polygon.holes) {\n    this.drawProjectedRing(hole.points)\n  }\n}\n\ncontext.fillStyle = colorToCssRgba(this.options.maskColor)\ncontext.fill('evenodd')\n",[28,932,933,971,982,1015,1019,1042,1066,1070,1097,1115,1119,1123,1127,1153],{"__ignoreMap":120},[124,934,935,938,940,943,945,947,949,952,954,957,959,962,964,966,968],{"class":126,"line":127},[124,936,937],{"class":306},"context",[124,939,309],{"class":271},[124,941,942],{"class":267},"clearRect",[124,944,272],{"class":306},[124,946,577],{"class":481},[124,948,279],{"class":271},[124,950,951],{"class":481}," 0",[124,953,279],{"class":271},[124,955,956],{"class":306}," size",[124,958,309],{"class":271},[124,960,961],{"class":306},"width",[124,963,279],{"class":271},[124,965,956],{"class":306},[124,967,309],{"class":271},[124,969,970],{"class":306},"height)\n",[124,972,973,975,977,980],{"class":126,"line":133},[124,974,937],{"class":306},[124,976,309],{"class":271},[124,978,979],{"class":267},"beginPath",[124,981,707],{"class":306},[124,983,984,986,988,991,993,995,997,999,1001,1003,1005,1007,1009,1011,1013],{"class":126,"line":139},[124,985,937],{"class":306},[124,987,309],{"class":271},[124,989,990],{"class":267},"rect",[124,992,272],{"class":306},[124,994,577],{"class":481},[124,996,279],{"class":271},[124,998,951],{"class":481},[124,1000,279],{"class":271},[124,1002,956],{"class":306},[124,1004,309],{"class":271},[124,1006,961],{"class":306},[124,1008,279],{"class":271},[124,1010,956],{"class":306},[124,1012,309],{"class":271},[124,1014,970],{"class":306},[124,1016,1017],{"class":126,"line":145},[124,1018,221],{"emptyLinePlaceholder":220},[124,1020,1021,1024,1026,1028,1031,1034,1037,1040],{"class":126,"line":151},[124,1022,1023],{"class":298},"for",[124,1025,303],{"class":306},[124,1027,500],{"class":263},[124,1029,1030],{"class":306}," polygon ",[124,1032,1033],{"class":271},"of",[124,1035,1036],{"class":271}," this.",[124,1038,1039],{"class":306},"parsedPolygons) ",[124,1041,330],{"class":271},[124,1043,1044,1046,1049,1051,1054,1056,1059,1061,1064],{"class":126,"line":157},[124,1045,701],{"class":271},[124,1047,1048],{"class":267},"drawProjectedRing",[124,1050,272],{"class":302},[124,1052,1053],{"class":306},"polygon",[124,1055,309],{"class":271},[124,1057,1058],{"class":306},"outer",[124,1060,309],{"class":271},[124,1062,1063],{"class":306},"points",[124,1065,352],{"class":302},[124,1067,1068],{"class":126,"line":163},[124,1069,221],{"emptyLinePlaceholder":220},[124,1071,1072,1075,1077,1079,1082,1085,1088,1090,1093,1095],{"class":126,"line":169},[124,1073,1074],{"class":298},"  for",[124,1076,303],{"class":302},[124,1078,500],{"class":263},[124,1080,1081],{"class":306}," hole",[124,1083,1084],{"class":271}," of",[124,1086,1087],{"class":306}," polygon",[124,1089,309],{"class":271},[124,1091,1092],{"class":306},"holes",[124,1094,327],{"class":302},[124,1096,330],{"class":271},[124,1098,1099,1102,1104,1106,1109,1111,1113],{"class":126,"line":175},[124,1100,1101],{"class":271},"    this.",[124,1103,1048],{"class":267},[124,1105,272],{"class":302},[124,1107,1108],{"class":306},"hole",[124,1110,309],{"class":271},[124,1112,1063],{"class":306},[124,1114,352],{"class":302},[124,1116,1117],{"class":126,"line":181},[124,1118,362],{"class":271},[124,1120,1121],{"class":126,"line":187},[124,1122,487],{"class":271},[124,1124,1125],{"class":126,"line":193},[124,1126,221],{"emptyLinePlaceholder":220},[124,1128,1129,1131,1133,1136,1138,1141,1143,1145,1148,1150],{"class":126,"line":199},[124,1130,937],{"class":306},[124,1132,309],{"class":271},[124,1134,1135],{"class":306},"fillStyle ",[124,1137,506],{"class":271},[124,1139,1140],{"class":267}," colorToCssRgba",[124,1142,272],{"class":306},[124,1144,655],{"class":271},[124,1146,1147],{"class":306},"options",[124,1149,309],{"class":271},[124,1151,1152],{"class":306},"maskColor)\n",[124,1154,1155,1157,1159,1162,1164,1166,1168,1170],{"class":126,"line":205},[124,1156,937],{"class":306},[124,1158,309],{"class":271},[124,1160,1161],{"class":267},"fill",[124,1163,272],{"class":306},[124,1165,324],{"class":271},[124,1167,30],{"class":321},[124,1169,324],{"class":271},[124,1171,352],{"class":306},[18,1173,1174],{},"边界没有画在 canvas 里，而是继续交给 Cesium entity。这样线宽、发光和贴地显示可以利用 Cesium 的 polyline 能力，也能跟地图深度关系保持一致。",[115,1176,1178],{"className":254,"code":1177,"language":256,"meta":120,"style":120},"polyline: {\n  positions: this.createBoundaryPositions(points),\n  width,\n  material: new Cesium.PolylineGlowMaterialProperty({\n    glowPower,\n    color\n  }),\n  clampToGround: true\n}\n",[28,1179,1180,1190,1210,1216,1237,1244,1249,1258,1269],{"__ignoreMap":120},[124,1181,1182,1186,1188],{"class":126,"line":127},[124,1183,1185],{"class":1184},"sBMFI","polyline",[124,1187,555],{"class":271},[124,1189,293],{"class":271},[124,1191,1192,1195,1197,1199,1202,1204,1206,1208],{"class":126,"line":133},[124,1193,1194],{"class":1184},"  positions",[124,1196,555],{"class":271},[124,1198,1036],{"class":271},[124,1200,1201],{"class":267},"createBoundaryPositions",[124,1203,272],{"class":302},[124,1205,1063],{"class":306},[124,1207,290],{"class":302},[124,1209,565],{"class":271},[124,1211,1212,1214],{"class":126,"line":139},[124,1213,586],{"class":306},[124,1215,565],{"class":271},[124,1217,1218,1221,1223,1226,1228,1230,1233,1235],{"class":126,"line":145},[124,1219,1220],{"class":1184},"  material",[124,1222,555],{"class":271},[124,1224,1225],{"class":271}," new",[124,1227,791],{"class":306},[124,1229,309],{"class":271},[124,1231,1232],{"class":267},"PolylineGlowMaterialProperty",[124,1234,272],{"class":302},[124,1236,330],{"class":271},[124,1238,1239,1242],{"class":126,"line":151},[124,1240,1241],{"class":306},"    glowPower",[124,1243,565],{"class":271},[124,1245,1246],{"class":126,"line":157},[124,1247,1248],{"class":306},"    color\n",[124,1250,1251,1254,1256],{"class":126,"line":163},[124,1252,1253],{"class":271},"  }",[124,1255,290],{"class":302},[124,1257,565],{"class":271},[124,1259,1260,1263,1265],{"class":126,"line":169},[124,1261,1262],{"class":1184},"  clampToGround",[124,1264,555],{"class":271},[124,1266,1268],{"class":1267},"sfNiH"," true\n",[124,1270,1271],{"class":126,"line":175},[124,1272,487],{"class":271},[14,1274,1275],{"id":1275},"效果与边界",[18,1277,1278],{},"这个方案最适合图形化大屏和业务专题图：区域内保持原始地图清晰度，区域外用统一色调压暗，边界通过 cyan 发光线加强识别。遮罩颜色、透明度、边界宽度和发光强度都可以作为视觉参数暴露给业务侧。",[18,1280,1281],{},"它的边界也很明确。屏幕遮罩不是地理裁剪，不会阻止用户点击区域外对象，也不会减少底图或瓦片加载。它只改变最终画面，因此实现成本低，和底图、地形、影像源之间的耦合也低。",[18,1283,1284],{},"另外，屏幕空间方案依赖当前相机投影。俯视地图场景下效果最稳定；如果相机大幅倾斜、行政区跨过地平线，屏幕投影得到的是当前可见画面里的形状，而不是严格的二维制图投影。",[14,1286,1287],{"id":1287},"小结",[18,1289,1290],{},"这类行政区遮罩的核心不是把 Cesium 地图真的裁开，而是把空间数据转换成屏幕上的反向镂空路径。",[18,1292,1293,1294,1296],{},"GeoJSON 负责提供行政区边界，Cesium 负责把经纬度点投影到当前窗口坐标，canvas 负责用 ",[28,1295,30],{}," 填充规则生成透明洞口，polyline 负责提供可视化边界强调。",[18,1298,1299],{},"当需求只是“突出一个行政区、压暗其他区域”时，屏幕空间遮罩比真实地表挖洞更直接，也更容易做出图形化大屏里常见的 HUD 视觉效果。",[543,1301,1302],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":120,"searchDepth":133,"depth":133,"links":1304},[1305,1306,1307,1308,1309,1310,1311],{"id":16,"depth":133,"text":16},{"id":34,"depth":133,"text":34},{"id":54,"depth":133,"text":54},{"id":113,"depth":133,"text":113},{"id":248,"depth":133,"text":248},{"id":1275,"depth":133,"text":1275},{"id":1287,"depth":133,"text":1287},"2026-05-07","解析如何把行政区 GeoJSON 投影到屏幕 canvas，用 evenodd 填充规则实现地图压暗与行政区透明洞口。","md","\u002Fimages\u002Fdemos\u002Fcesium-region-mask-overlay-cover.png",{},"\u002Fblog\u002Fcesium-region-mask-overlay-implementation",{"title":5,"description":1313},"blog\u002Fcesium-region-mask-overlay-implementation","EQcjCVo8mBGRuRnUIghHSyg0Ieh16L4b19GdLt4JfZo",[1322,1327],{"title":1323,"path":1324,"stem":1325,"description":1326,"children":-1},"Cesium 离屏渲染 Framebuffer 实现","\u002Fblog\u002Fcesium-offscreen-render-framebuffer","blog\u002Fcesium-offscreen-render-framebuffer","解析如何在 Cesium 中自建 Framebuffer，把指定视角渲染成离屏纹理并读回为 ImageData。",{"title":1328,"path":1329,"stem":1330,"description":1331,"children":-1},"Cesium 最高地形瓦片高度图实现","\u002Fblog\u002Fcesium-terrain-heightmap-implementation","blog\u002Fcesium-terrain-heightmap-implementation","解析如何从 Cesium Terrain 采样最高可用层级、生成灰度高度图，并用同一份高度数据构建带真实起伏的三角网格。",1778657555322]