大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第17/100篇文章;
小散开胃
作为一个7年多的老前端
,对开发二维静态页面早已倦怠,每天面对枯燥的增删改查,未来的路不知该何去何从...
然而,一次工作的变换,让我接触到了有意思的3D开发
,从此,我的生活里又有了光,充满了希望,每天都是正能量满满!
前言
好了,言归正传,今天我将给大家带来一个3D弹窗
从零到一的组件封装教程,使用框架:cesiumjs + vue3
,并结合vue3的createApp动态渲染。
打点BillboardCollection
常见的业务场景,一般是现在三维场景中进行撒点,然后当点击每个点位的时候,再弹一个3D弹窗展示该点位的一些重要信息。
cesium中提供了BillboardCollection
大类,官方叫广告牌集合
API一览直达:cesium.xin/cesium/cn/D…
其实就是我们常说的POI点位标志
,所以我们先进行打点:
const store = useStore();
// viewer就是cesium实例化之后的场景示例,我把他存在了vuex的store中
const { viewer } = store.state;
// 先把广告牌实例化,然后再添加到场景中
const billboardsCollection = viewer.scene.primitives.add(
new Cesium.BillboardCollection()
);
// 点位特性信息集合
let pointFeatures = [];
// 先获取点位的json信息
const getJson = () => {
getGeojson("/json/chuzhong.geojson").then(({ res }) => {
const { features } = res;
pointFeatures = features;
formatData(features);
});
};
const formatData = (features) => {
for (let i = 0; i < features.length; i++) {
const feature = features[i];
// 每个点位的坐标
const coordinates = feature.geometry.coordinates;
// 将坐标处理成3D笛卡尔点
const position = Cesium.Cartesian3.fromDegrees(
coordinates[0],
coordinates[1],
1000
);
const name = feature.properties.name;
// 带图片的点
billboardsCollection._id = `mark`;
// add的是Billboard,将一个个Billboard添加到集合当中
billboardsCollection.add({
image: "/images/mark-icon.png",
width: 32,
height: 32,
position,
});
}
};
// 执行打点
getJson()
概念解释
Cartesian3: 在Cesium中,Cartesian3是一个用于表示三维空间
中点或向量
的类。它主要用于处理地理空间应用中的各种计算和操作,每个点有三个坐标值:x、y、z。这些坐标通常用于表示地球表面
上的位置或空间
中的任意点
OK,这样,点位就在我们的场景中撒好了。
POI点位事件交互
弹窗需要点击点位弹出,所以,我们来给点位添加事件交互,这就用到了Cesium中的ScreenSpaceEventHandler:处理用户输入事件
API一览直达:cesium.xin/cesium/cn/D…
const scene = viewer.scene;
// ScreenSpaceEventHandler的参数是要添加事件的元素,直接给整个画布添加
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction((e) => {
// 获取点击的实体
const pick = scene.pick(e.position);
// 判断点击的是不是点位
if (Cesium.defined(pick) && pick.collection._id.indexOf("mark") > -1) {
// ...
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK); // 监听屏幕鼠标左键点击
事件添加好了,还缺弹窗。
封装3D弹窗组件
这里我们把弹窗封装成了一个Dialog
大类,方便在cesium的多个场景中共用,并且运用了vue3的元素动态创建
能力,用到了vue3的createApp
和h
这两个方法,不了解的可以先去了解下。
弹窗js逻辑
代码中的注释已经写的很详细了,需要注意的是这个3D弹窗最终是要和cesium场景结合起来的,因为它需要跟随场景移动而跟随变换位置,所以需要接收viwer
场景参数。
import * as Cesium from "cesium";
import { createApp, h } from "vue";
import Popup from "@/components/Dialog/Popup.vue";
export default class Dialog {
constructor(opts) {
const { viewer, position, ...rest } = opts;
this.viewer = viewer;
// 点位的空间位置信息
this.position = position._value;
const { vmInstance } = createDialog({
...rest, // 主要是弹窗内容
closeEvent: this.windowClose.bind(this),
});
if (this.vmInstance) {
this.windowClose.bind(this);
} else {
this.vmInstance = vmInstance;
}
// 将弹窗元素添加到渲染cesium的容器中
viewer.cesiumWidget.container.appendChild(vmInstance.$el);
this.addPostRender();
}
//添加场景事件
addPostRender() {
this.viewer.scene.postRender.addEventListener(this.postRender, this);
}
postRender() {
if (!this.vmInstance.$el || !this.vmInstance.$el.style) return;
// 画布高度
const canvasHeight = this.viewer.scene.canvas.height;
// 实例化屏幕坐标
const windowPosition = new Cesium.Cartesian2();
// 将WGS84 经纬度坐标转换成屏幕坐标,这通常用于将 HTML 元素放置在与场景中的对象相同的屏幕位置。
Cesium.SceneTransforms.wgs84ToWindowCoordinates(
this.viewer.scene,
this.position,
windowPosition
);
// 调整弹窗的位置
this.vmInstance.$el.style.bottom =
canvasHeight - windowPosition.y + 260 + "px";
const elWidth = this.vmInstance.$el.offsetWidth;
this.vmInstance.$el.style.left =
windowPosition.x - elWidth / 2 + 110 + "px";
const camerPosition = this.viewer.camera.position;
// 控制边界值
let height =
this.viewer.scene.globe.ellipsoid.cartesianToCartographic(
camerPosition
).height;
height += this.viewer.scene.globe.ellipsoid.maximumRadius;
if (
!(Cesium.Cartesian3.distance(camerPosition, this.position) > height) &&
this.viewer.camera.positionCartographic.height < 50000000
) {
this.vmInstance.$el.style.display = "block";
} else {
this.vmInstance.$el.style.display = "none";
}
}
//关闭弹窗
windowClose() {
if (this.vmInstance) {
this.vmInstance.$el.remove();
}
this.viewer.scene.postRender.removeEventListener(this.postRender, this); //移除事件监听
}
}
let parentNode = null;
/**
* @description: 渲染弹窗组件并插入div中
* @param {*} opts 弹窗内容
* @return {*}
*/
const createDialog = (opts) => {
if (parentNode) {
document.body.removeChild(parentNode);
parentNode = null;
}
const app = createApp({
render() {
return h(
Popup,
{
...opts,
},
{
title: () => opts.slotTitle,
content: () => opts.slotContent,
}
);
},
});
parentNode = document.createElement("div");
// 将Popup组件挂载到父级div中,生成弹窗实例
const instance = app.mount(parentNode);
document.body.appendChild(parentNode);
return {
vmInstance: instance,
};
};
弹窗模板
接下来我们看下Popup.vue的实现,我们把它封装成公共弹窗样式组件,并给它添加一些酷炫的特效。
<script setup>
import { ref } from 'vue'
const props = defineProps({
title: {
type: String,
default: "标题"
},
content: {
type: String,
default: "内容"
},
closeEvent: Function
})
const closeClick = () => {
props?.closeEvent()
}
</script>
<template>
<div class="popup-container">
<div class="pine"></div>
<div class="box-wrap">
<div class="close" @click="closeClick">X</div>
<div class="area">
<div class="area-title fontColor">
<template v-if="props.title">{{ props.title }}</template>
<slot v-else name="title"></slot>
</div>
</div>
<div class="content">
<div class="data-li">
<div class="data-label textColor">地址:</div>
<div class="data-value">
<span class="label-num yellowColor">
<template v-if="props.content">{{ props.content }}</template>
<slot v-else name="content"></slot>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang='less' scoped>
.popup-container {
width: 200px;
position: relative;
bottom: 0;
left: 0;
}
.close {
position: absolute;
color: #fff;
top: 1px;
right: 10px;
text-shadow: 2px 2px 2px #022122;
cursor: pointer;
animation: fontColor 1s;
}
.box-wrap {
position: absolute;
left: 21%;
top: 0;
width: 100%;
height: 163px;
border-radius: 50px 0px 50px 0px;
border: 1px solid #38e1ff;
background-color: #38e1ff4a;
box-shadow: 0 0 10px 2px #29baf1;
animation: slide 2s;
}
.box-wrap .area {
position: absolute;
top: 20px;
right: 0;
width: 95%;
height: 30px;
background-image: linear-gradient(to left, #4cdef9, #4cdef96b);
border-radius: 30px 0px 0px 0px;
animation: area 1s;
}
.pine {
position: absolute;
width: 100px;
height: 100px;
box-sizing: border-box;
line-height: 120px;
text-indent: 5px;
}
.pine::before {
content: "";
position: absolute;
left: 0;
bottom: -83px;
width: 40%;
height: 60px;
box-sizing: border-box;
border-bottom: 1px solid #38e1ff;
transform-origin: bottom center;
transform: rotateZ(135deg) scale(1.5);
animation: slash 0.5s;
filter: drop-shadow(1px 0px 2px #03abb4);
}
.area .area-title {
text-align: center;
line-height: 30px;
}
.textColor {
font-size: 14px;
font-weight: 600;
color: #ffffff;
text-shadow: 1px 1px 5px #002520d2;
animation: fontColor 1s;
}
.yellowColor {
font-size: 14px;
font-weight: 600;
color: #f09e28;
text-shadow: 1px 1px 5px #002520d2;
animation: fontColor 1s;
}
.fontColor {
font-size: 16px;
font-weight: 800;
color: #ffffff;
text-shadow: 1px 1px 5px #002520d2;
animation: fontColor 1s;
}
.content {
padding: 55px 10px 10px 10px;
}
.content .data-li {
display: flex;
}
@keyframes fontColor {
0% {
color: #ffffff00;
text-shadow: 1px 1px 5px #00252000;
}
40% {
color: #ffffff00;
text-shadow: 1px 1px 5px #00252000;
}
100% {
color: #ffffff;
text-shadow: 1px 1px 5px #002520d2;
}
}
@keyframes slide {
0% {
border: 1px solid #38e1ff00;
background-color: #38e1ff00;
box-shadow: 0 0 10px 2px #29baf100;
}
100% {
border: 1px solid #38e1ff;
background-color: #38e1ff4a;
box-shadow: 0 0 10px 2px #29baf1;
}
}
@keyframes area {
0% {
width: 0%;
}
25% {
width: 0%;
}
100% {
width: 95%;
}
}
@keyframes slash {
0% {
transform: rotateZ(135deg) scale(0);
}
100% {
transform: rotateZ(135deg) scale(1.5);
}
}
</style>
弹窗使用
OK,弹窗已经封装好了,我们看下如何使用。
刚刚我们添加了handler.setInputAction事件交互方法,我们接着来。
import Dialog from "@/utils/cesiumCtrl/dialog";
// 首先需要定义弹窗实例
const dialogs = ref();
handler.setInputAction((e) => {
// 省略...
if (Cesium.defined(pick) && pick.collection._id.indexOf("mark") > -1) {
// 拿到点位的属性信息
const property = pointFeatures[pick.primitive._index];
// 弹窗所需的参数
const opts = {
viewer, // cesium的场景
position: {
_value: pick.primitive.position,
},
title: property.properties.name, // 弹窗标题
content: property.properties.address, // 弹窗内容
};
if (dialogs.value) {
// 只允许一个弹窗出现
dialogs.value.windowClose();
}
// 实例化弹窗
dialogs.value = new Dialog(opts);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
最后
OK,完事了,这样一个栩栩如生的3D弹窗就开发完成了,简单总结下:
- BillboardCollection打POI点位;
- 创建cesium中的事件交互ScreenSpaceEventHandler,监听点位点击;
- 创建Dialog弹窗大类,支持弹窗动态创建,并跟随viewer场景移动,弹窗模板单独创建;
【开源地址】:github.com/tingyuxuan2…
有需要进技术产品开发交流群(可视化&GIS)可以加我:brown_7778,也欢迎
数字孪生可视化领域
的交流合作。
【往期推荐】
可视化大屏开发,知道了这些经验以及解决方案,效率至少提升2倍!(上)
# 可视化大屏开发,知道这些解决方案,效率至少提升2倍!(中)
# 可视化大屏开发,知道了这些经验以及解决方案,效率至少提升2倍!(完结篇)
前端开发不会写动画?分享4个动画库助你打造视觉盛宴
# threejs实战数字孪生园区开源(threejs+vue3+vite)
最后,如果觉得文章对你有帮助,也希望可以一键三连👏👏👏,你的鼓励是支持我持续分享下去的动力~