上一次我们拆了缩放控制器结尾我埋了个话头控件家族的老熟人还没走完下一个登场的是提示浮层控件——鼠标悬停时怎样在地球表面某个对象上方浮出一行说明文字而它和气泡#69的点哪弹哪又差着一层悬停的微妙。今天就把这层悬停拆开。顺带把进度摆一下地图控件这条线一路走来罗盘管朝哪翻、气泡管点哪咬哪、图表管把图焊哪、缩放管往哪推现在轮到提示浮层管鼠标走到哪、字就报到哪。它们四个全是纯 DOM 浮层不是 Cesium 的内部对象——这一点到这篇会更明显。 本文涉及的全部代码均已开源完整的仓库地址见文末可自行取用、随意魔改。你大概率在无数 GIS 网站见过它把鼠标移到地球上光标旁边立刻浮出一行经度 118.8° / 纬度 32.05°。看起来天经地义仿佛 Cesium 自带一个Tooltip类你只要viewer.tooltip.show()就完事。但和前面几篇一样第一件事还是先拆穿Cesium 的公开 API 里根本没有 Tooltip / TooltipWidget 这种东西。屏幕上那行字本质是一个普通的div加上一段监听鼠标移动 → 把屏幕坐标翻译成经纬度 → 把 div 摆到光标旁边的逻辑。Cesium 从头到尾只负责中间那一步——屏幕像素 → 地球坐标的翻译。其余全是你的 DOM 手艺。一、先拆穿提示浮层不是 Cesium 画的是鼠标 一个 div先看 demo 里那行浮层到底长什么样。它就是一个挂在地图容器里的divdivreftooltipElv-showtooltipVisibleclasscesium-tool-tip:style{ left: tooltipLeft px, top: tooltipTop px }{{ tooltipContent }}/divcesium-tool-tip的样式也很朴素position: absolute、pointer-events: none、深灰半透明底、一行白字。没有任何 Cesium 特有的标签——它就是网页里最普通的绝对定位浮层。关键在它怎么出现、怎么移动。气泡弹窗#69是点一下 → 弹出一个钉死在地表点的浮层靠postRender每帧把地理锚点重投影成屏幕坐标。而提示浮层是鼠标移到哪 → 浮层跟到哪它连postRender都不需要——因为鼠标移动事件本身就直接给你屏幕坐标浮层用的是同一套坐标系根本不用绕回地球再绕回来。这就引出它和气泡最重要的血缘差异气泡锚定的是地球上的点点不动地球转了它也得跟着转所以要 postRender提示浮层锚定的是鼠标光标光标在屏幕里动坐标天然是屏幕坐标地球转不转都无所谓。一个是追地球、一个是追光标看似都是浮层底层那道坐标题的解法却不一样。所以提示浮层控件的真身可以压成一句话一个 DOM div 一个 MOUSE_MOVE 监听 一道屏幕坐标 → 经纬度的翻译 把 div 摆到光标旁边。Cesium 只卖中间那道翻译浮层和监听都是你自己搭的。顺带把这个 demo 的两个开关讲清楚因为它们正好对应浮层要不要动的两种层级。启用提示开关切的是enabled——它为真时onEnableChange才把MOUSE_MOVE监听挂上canvas为假时直接removeInputAction摘掉监听并隐藏浮层这是彻底不干活。而跟随鼠标开关切的是followMouse——它只是让onMouseMove一进来就return并隐藏浮层监听还在只是不更新内容这是挂着但装死。两种关法体验上都是浮层没了底层的资源占用和事件链路却不一样生产里你常要的是第一种彻底摘监听省得空转。还有个细节demo 允许在输入框填customText填了之后浮层不再只报坐标而是显示你写的文字、再在后面自动追加(经度, 纬度)。这说明提示浮层的文本完全由你定义——它只是个 div你往里塞什么它都照显示坐标只是最经典的那个用例。把提示浮层和气泡#69摆在一起对照差异会更刺眼维度气泡弹窗#69提示浮层#72锚定对象固定的地球表面点鼠标光标需要 postRender是每帧重投影锚点否坐标直接来自鼠标定位算术top y - h - 14锚点上方left x 12, top y - 15光标右下方触发方式点击鼠标移动典型坑v-show 首帧测零错位浮层吞掉鼠标事件一句话气泡是点了才弹、钉死不动提示浮层是移了就报、跟着光标。它们共用div 屏幕→地球翻译的内核却在锚定策略和随之而来的坑上分道扬镳。顺带说一句为什么 Cesium 不内置。Cesium 的定位是渲染引擎 一堆地理原语——它给你球、相机、图元、拾取这些底层能力但 UI 层按钮、面板、浮层从来不是它的活。浮层本质是 HTML/CSS 的事在哪显示、什么样式、跟不跟鼠标全是浏览器 DOM 的管辖范围。Cesium 即便真给你一个Tooltip类最后也得往 DOM 塞文字、调定位——和你自己写 div 没两样还多一层 API 约束。所以没有 Tooltip 类不是疏漏是边界划得清楚翻译坐标归我怎么摆字归你。这一条对前面所有控件都成立气泡、图表、缩放、罗盘无一例外都是引擎给坐标、DOM 负责长相。二、MOUSE_MOVE 给你的到底是什么CSS 像素的 endPosition要摆浮层先得搞清楚鼠标在哪这件事 Cesium 是怎么告诉你的。ScreenSpaceEventHandler监听MOUSE_MOVE时回调拿到的movement对象里有个endPosition它就是鼠标当前的位置。但它是相对谁的坐标是相对整个浏览器窗口还是相对那块 canvas读ScreenSpaceEventHandler.js的getPositionL11 起能确认当事件处理器挂在某个具体元素这里是viewer.canvas上时它做的是constrectelement.getBoundingClientRect();result.xevent.clientX-rect.left;result.yevent.clientY-rect.top;也就是相对 canvas 左上角的偏移而且单位是 CSS 像素。这一点和前面比例尺#66、气泡#69完全同源——endPosition用的是canvas.clientWidth/clientHeight那一套 CSS 像素Retina 屏默认安全不用自己乘devicePixelRatio。为什么强调相对 canvas 左上角因为如果你的地图不是铺满整个窗口旁边有侧边栏、顶部有标题栏用clientX/clientY相对窗口去摆浮层就会整体偏移一个侧边栏宽度。demo 把handler直接挂在viewer.canvas上拿到的endPosition天然是画布内部坐标浮层又是绝对定位于画布容器两者坐标系对齐才摆得准。这点和 #66 卷帘控件踩过的handler 挂错元素导致坐标漂移是同一类坑只是这里恰恰挂对了。顺带记住endPosition是 CSS 像素、是相对画布的、是屏幕空间坐标。它和后面要翻译出的地球坐标是两套完全不同的东西整篇控制浮层的难都难在把这两套别搞混。这里还能顺手点破一个源码细节getPosition还有个element document的分支——如果你的事件处理器不是挂在 canvas、而是挂在document上它就不会减rect.left/top直接把clientX/clientY相对整个浏览器窗口交给你。所以endPosition 是相对 canvas 还是相对窗口这件事完全取决于你把ScreenSpaceEventHandler的第一参数传成了谁。demo 传的是viewer.canvas所以稳稳拿到画布内部坐标你若改成document就得自己再减一次 canvas 的偏移否则浮层会整体错位一个侧边栏宽度。这点和 #66 卷帘控件踩过的handler 挂错元素导致坐标漂移是同一类坑只是这里恰恰挂对了。三、从屏幕到地球三条拾取路径各管一摊拿到endPosition后真正的核心是把它翻成经纬度。Cesium 给了你三条路而且它们天差地别——这是整篇最该实测、也最容易写错的地方。demo 主路径widget-tooltip是这样写的if(scene.modeSceneMode.SCENE3D){constrayscene.camera.getPickRay(movement.endPosition);surfacePositionray?scene.globe.pick(ray,scene)??undefined:undefined;}else{surfacePositionscene.camera.pickEllipsoid(movement.endPosition,Ellipsoid.WGS84)??undefined;}三条路径逐一拆路径 AgetPickRayglobe.pick3D 最常用getPickRay(endPosition)从屏幕点发一条射线穿进场景globe.pick(ray, scene)求这条射线和地球表面的交点。读Globe.js的Globe.prototype.pickL823能确认它最终落到pickWorldCoordinates——射线打的是地球表面地形瓦片加载了就返地形真实高度没加载就落在椭球面高度≈0但它只认地球认不得 3D Tiles、Primitive 这些贴在地球上的物体。路径 Bscene.pickPosition深度拾取最全能mouse-tip那个进阶 demo 把这条路放在最前面if(scene.pickPositionSupported){try{positionscene.pickPosition(event.endPosition);}catch{positionundefined;}}读Picking.js的pickPositionL802 →pickPositionWorldCoordinatesL691能确认它依赖scene.context.depthTextureL702没有深度纹理就抛不支持也就是pickPositionSupported false从深度缓冲里反算世界坐标。这意味着它能捡到任何被渲染出来的东西上的真实坐标——3D Tiles 的楼顶、Primitive 的模型、以及加载了地形后的真实海拔。一句话pickPosition带真实高度且能越过地球去抓 3D 物体是三者里信息最全的。路径 Ccamera.pickEllipsoid2D/Columbus 用纯几何pickEllipsoid(endPosition, Ellipsoid.WGS84)直接求射线和参考椭球的交点。它看不见地形、看不见任何物体只是一道纯几何相交——射线打中椭球就返一个点高度永远 0打不中比如看向太空就返undefined。它最大的优点是稳不依赖深度纹理、不依赖地形瓦片2D 和 Columbus 视图下也能用平滑好算。把三条路径摆成一张表差异一眼就清路径返回高度认 3D 物体依赖适用getPickRay globe.pick地形加载则有、否则 0否地球表面3D 下最省心只想知道地表经纬度scene.pickPosition真实海拔是深度纹理pickPositionSupported要高度、要抓 3D Tiles/模型camera.pickEllipsoid永远 0否无纯几何2D/Columbus或不想依赖任何纹理再往深一层看pickPosition为什么全能pickPositionWorldCoordinatesPicking.js L691先用pickDepth.getDepth从深度缓冲读出这一像素的深度值L753再判断0 depth 1L761超出范围说明没命中任何东西最后用这个深度把屏幕坐标反算回世界坐标L782。也就是说它读的是这一像素实际画了多深自然能还原出真实海拔和任何被画出来的物体——这正是它和globe.pick的本质区别后者只问射线打没打到地球前者问屏幕这块像素底下到底画了什么、离相机多远。代价是它必须有一张深度纹理而深度纹理在部分老设备、或你主动关掉context深度相关选项的环境里可能缺失那时pickPositionSupported就是 false必须回退。如果你担心pickPosition在不支持环境翻车最稳的写法是mouse-tip那种三级回退先trypickPositioncatch 或undefined再globe.pick再undefined再pickEllipsoid。这样无论在 3D 带地形、3D 裸球、还是 2D都能至少拿到一个经纬度。三者的关系用一句话点破在裸椭球、没加载地形、也没 3D 物体的情况下三者给你的经纬度一致、高度都是 0可一旦叠了地形或 3D TilespickPosition透过深度缓冲返出带真实海拔的坐标globe.pick返出地形面前提是瓦片已加载而pickEllipsoid依旧永远是 0——它根本看不见地形。我用探针实测过这组几何相机在 (118.8°, 32.05°) 上方 260 km 垂直俯视裸椭球上rayEllipsoid的近交点start 260.0 km正好等于相机高度交点算回经纬度118.8000 / 32.0500、高度0.000——和期望的地表点分毫不差而把射线改成朝太空方向zrayEllipsoid直接返undefined。这正好解释为什么你的浮层在看向太空时会失联不是代码坏了是几何上那道射线确实没打到地球。也正是这个裸椭球基线藏着另一个新手最容易误判的现象很多 demo 明明飞到了高山上空浮层报的高度却始终是0.000。原因就在这——只要没加载地形globe.pick落点永远是椭球面h≈0pickEllipsoid更不用说了。你以为高度没算对其实它压根没去量山只是老老实实报了椭球面。要看到真实海拔两条路要么globe.pick之前先sampleTerrainMostDetailed把地形瓦片加载出来要么直接用pickPosition走深度缓冲前提是pickPositionSupported为真。这也是为什么我反复说——高度对不对不取决于你的浮层代码而取决于你选了哪条拾取路径、以及那条路径此刻有没有真实地形可量。至于选哪条demo 给了一个务实的答案3D 用globe.pick够用、最省心2D/Columbus 用pickEllipsoid别无选择把带真实高度、能抓 3D 物体的pickPosition留给那些真的需要海拔和物体拾取的进阶场景——mouse-tip就是这么做的它把pickPosition放第一优先级拾取失败再依次回退globe.pick、pickEllipsoid。四、把字摆到光标旁边定位算术与pointer-events:none的命门坐标翻出来了最后一步是把 div 摆对位置。气泡是钉在地表点上方所以top y - h - 14往锚点上方抬、留个尾尖而提示浮层是跟在光标旁边方向正好反过来functionupdateWindowCoord(pos){tooltipLeft.valuepos.x12;// 光标右侧 12pxtooltipTop.valuepos.y-15;// 光标上方 15px}left x 12、top y - 15让浮层落在光标的右下方既不被光标本身挡住也不压住你想看的那片地。CSS 里还用::before画了一个朝左的小三角border-right实心三角尖角正对光标视觉上像从光标冒出来一句话。这套摆法和气泡上方尾尖是镜像的两套审美别抄串了。但这里藏着一个比摆位更要命的坑很多新手第一次写浮层都会栽浮层必须pointer-events: none。想想看浮层就摆在光标右下方光标动一下、浮层跟着动——如果浮层能接收鼠标事件那它每时每刻都盖在光标前方一点点会把本该传给 canvas 的MOUSE_MOVE吞掉于是浮层一出现、鼠标事件就时断时续甚至自己触发自己的 hover陷入动了才显示、显示了又不动的死循环。pointer-events: none这一句就是让浮层看得见摸不着鼠标事件直接穿透它落到下面的 canvas 上监听链才不断。我特意把这点拎出来说是因为它不是优化项而是不开就废的开关。cesium-tool-tip的样式里那行pointer-events: none分量比它的背景色、圆角、字体加起来都重。再补两个摆位相关的细节免得你抄代码时漏掉。cesium-tool-tip还设了z-index: 30——它必须压在地图 canvas默认层级和左侧控制面板z-index 20之上否则浮层会被面板盖住、或半透明叠在地球上糊成一团。另一个细节是那个朝左的小三角::before它用border-right: 10px solid rgba(0,0,0,0.72)画一个实心向右的三角、再用left: -10px推到浮层左边缘之外、top: 5px让它落在文字行高的中间——视觉上像从光标方向戳过来一句话。这套右下方 左三角的摆法不是随便定的浮层在光标右下三角朝左指光标读者的视线从字顺着三角回到光标、再落到你想看的那片地动线最短要是把浮层摆在光标左上或正上三角方向和阅读动线就会打架。为什么不在光标正中、而是偏到右下方因为浮层若压在光标正下方那片地恰恰挡住你想看的坐标点——你移动鼠标是为了看那里是什么浮层却盖住那里本末倒置。右下方是既报得出、又不挡视线的折中这也是大多数 GIS 网站的默认摆法不是这个 demo 拍脑袋定的。还有个容易被忽略的使用场景浮层靠MOUSE_MOVE驱动而MOUSE_MOVE是鼠标悬停事件——在触屏设备上根本没有悬停这回事。手机上你手指拖地图时触发的是touchmove而不是mousemove原样搬这套逻辑到移动端浮层要么不出现、要么卡在最后一次鼠标位置。要兼容触屏得额外挂ScreenSpaceEventHandler的PINCH/touchmove或监听 pointer 事件把移动的来源从鼠标扩成任意指针。这也是为什么我说提示浮层锚定光标——它锚的是指针位置而指针在触屏上是个比鼠标更宽泛、也更难伺候的概念。你若哪天要把这套浮层搬上平板或手机第一件要改的就是这条事件源别等到上线了才发现安卓上浮层失灵。还有一个对比值得点破提示浮层不需要量自身尺寸。气泡为了居中要getBoundingClientRect量宽高、于是踩了v-show首帧display:none测零的坑#69 讲过而提示浮层直接跟鼠标坐标定位、不居中从根上就没碰那个坑。这也侧面说明浮层的摆位策略决定了你会不会撞上哪一类坑选对策略比事后打补丁重要。五、三个常被忽略的坑把前面几节串起来有三个坑几乎每篇讲浮层都要单独点名因为太容易想当然。坑一光标移出地球globe.pick会返undefined不处理就出 NaN。射线没打到地球看向太空、或移到地球边缘之外时globe.pick返回undefined。demo 两种处理风格widget-tooltip 用else if (!content)显示未拾取到地面点占位mouse-tip 用if (!position) tooltip.hide()直接藏掉。两种都行但绝不能拿undefined去cartesianToCartographic——那会抛错或给 NaN浮层瞬间显示一串NaN°。记住 #66 比例尺那篇的教训凡是打空的分支都要显式处理要么占位、要么隐藏。坑二pickPosition不是永远能用要先问pickPositionSupported。pickPosition依赖深度纹理老设备、某些禁用深度缓冲的环境会返回false。如果你像 mouse-tip 那样把pickPosition当第一优先级务必先if (scene.pickPositionSupported)判断、再try/catch兜底然后回退globe.pick、pickEllipsoid。widget-tooltip 干脆没走pickPosition——它只需要地表经纬度、不需要高度和 3D 拾取少一条路就少一份兼容负担这也是一种务实。选哪条路取决于你到底要不要那点海拔和物体信息。坑三提示浮层不需要postRender但代价是地球转了它不跟地表点走。这是和气泡最本质的取舍。气泡为了钉死地表点挂了postRender每帧重投影提示浮层因为锚定光标只在MOUSE_MOVE时更新一次地球自转或你拖动视角、光标没动浮层就停在原地——它报的是光标此刻屏幕位置对应的经纬度不是某个固定地标的经纬度。这反而有个隐藏好处开了requestRenderMode按需渲染时静止画面不重绘但浮层是 DOM、照样跟着鼠标更新比气泡省心气泡在那种模式下会被冻住#69 讲过。所以要不要 postRender不是优劣题而是你锚定的是光标还是地球点这道题的答案。把三个坑压成一句提醒打空要显式处理、pickPosition先问支不支持、锚光标就不挂 postRender——这三条漏掉任何一条浮层都会在你看不见的地方悄悄失灵。还有个容易被混为一谈的点enabled和followMouse两个开关在浮层消失这件事上效果一样但enabled关掉时是真把MOUSE_MOVE监听摘了removeInputActionfollowMouse关掉时监听还在、只是onMouseMove一进来就 return——前者在完全不用提示时更省后者在临时不想看、但随时可能想看时更跟手。这个区分在 #71 缩放按钮那篇也出现过v-show只是切 display、元素始终在 DOM控件家族里显隐到底摘不摘监听是个反复出现的设计题。最后提醒一句浮层文本里出现NaN°是新手最常见的翻车现场。根因几乎都是把undefined的拾取结果拿去cartesianToCartographic了——globe.pick打空、或pickPosition在不支持环境抛错、或pickEllipsoid看向太空这三种情况都会给你undefined务必在翻译前if (!position)挡一道。demo 里 widget-tooltip 用占位文字、mouse-tip 用直接隐藏都是挡法但先判空这一步谁都不能省。六、写在最后把这一篇收一下提示浮层的真身可以压成三句话先接住鼠标MOUSE_MOVE 的endPosition是相对 canvas 的 CSS 像素和气泡同源、再翻成坐标三条拾取路径各管一摊——globe.pick 打地表、pickPosition 带真实高度和 3D 物体、pickEllipsoid 纯椭球永远 h0、最后摆到光标旁leftx12, topy-15右下方避开光标且务必pointer-events: none让事件穿透。它和气泡#69最像——都是 div 浮层、都靠屏幕→地球的翻译——却又因为一个锚光标、一个锚地表点在 postRender、定位算术、踩坑清单上走上两条不同的路。控件家族这条线现在凑齐了五位常客罗盘读相机、气泡写浮层追地球坐标、图表焊图到地球坐标、缩放挪相机对准坐标、提示浮层跟光标报坐标——前三个站在地球一侧把信息贴上去或读出来后两个站在相机一侧把镜头推过去或把坐标读出来。看似各管一摊底层全是同一道题的变体如何在屏幕像素和地球坐标之间反复横跳且每次都不许漂、不许反、不许在边界翻车。落到工程上选哪条拾取路径其实有一句口诀只想要地表经纬度、且跑在 3Dglobe.pick最省心要真实海拔或要抓 3D Tiles/模型pickPosition是唯一的准2D/Columbus 或不想依赖任何纹理pickEllipsoid最稳。把这句口诀和打空必判空、浮层必 pointer-events:none、endPosition 是相对 canvas 的 CSS 像素这三句护身符一起记住提示浮层基本就焊死了。它和气泡、图表、缩放、罗盘一样看着是个小控件背后全是你和屏幕 ↔ 地球那道翻译的拉锯——控件家族写到这这条主轴你已经摸得差不多了。把五位常客往深里看其实是一根主轴的不同切面。这跟线还没走到头——下一个常客是位置信息栏控件它不跟光标、而是常驻在角落把当前相机的经纬度、视角、高度钉成一行常显的状态条和提示浮层这种鼠标走到哪报哪刚好是浮层的另一张脸。这条线往后的站咱们下回再拆。如果你也在 Cesium 上做过鼠标提示欢迎留言说说你选了哪条拾取路径——是图省心用globe.pick还是为了那点真实海拔和高度直接上pickPosition这两个选择基本决定了你后面要填多少打空分支和兼容性的坑。 项目已开源欢迎 Star你的 Star ⭐ 就是对我最大的支持仓库地址https://github.com/jianlei-wang/CESIUM-TREASURE-BOX代码在src/cases/widget-tooltip/目录下核心就一个WidgetTooltipDemo.vue。搬去自己项目时盯住三件事MOUSE_MOVE的endPosition是相对 canvas 的 CSS 像素、globe.pick打空要显式处理、浮层那句pointer-events: none一个字都不能省。Cesium酱の百宝箱 · 第 72 篇浮层本是寻常 div光标走到字便来2026 年的秋夜与你共探「鼠标怎么把地球读给眼睛听」