整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

全网最全的Cesium跟随弹窗的全框架实现原理剖析

全网最全的Cesium跟随弹窗的全框架实现原理剖析

---

**引言:揭开Cesium三维地球弹窗跟随的秘密面纱**

Cesium是一个开源的Web 3D地球可视化库,常用于地理信息系统、虚拟现实和导航等领域。而在Cesium中实现地图上的弹窗跟随功能,即当视角发生变化时,弹窗始终保持在目标对象附近的固定相对位置,是一种常见且实用的需求。本文将全面解析如何在Cesium中实现跟随弹窗的全框架,深入挖掘其背后的实现原理,并通过详尽的代码示例,让您切实掌握这一技术难点。

---

**【第一部分】Cesium基础环境搭建**

**标题:Cesium入门与基本地图加载**

首先,我们需要引入Cesium库,并创建一个基本的地图视图。以下是简单的HTML结构与JavaScript代码:

```html

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<title>Cesium跟随弹窗示例</title>

<link href="https://cesium.com/downloads/cesiumjs/releases/1.87/Build/Cesium/Widgets/widgets.css" rel="stylesheet">

</head>

<body>

<div id="cesiumContainer"></div>

<script src="https://cesium.com/downloads/cesiumjs/releases/1.87/Build/Cesium/Cesium.js"></script>

<script src="app.js"></script>

</body>

</html>

```

```javascript

// app.js

var viewer = new Cesium.Viewer('cesiumContainer', {

imageryProvider: Cesium.createWorldImagery(),

baseLayerPicker: false,

geocoder: false,

homeButton: false,

infoBox: false,

sceneModePicker: false,

navigationHelpButton: false,

timeline: false,

animation: false

});

// 加载地球模型或地标等

viewer.entities.add({

position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),

billboard: {

image: 'marker.png',

heightReference: Cesium.HeightReference.CLAMP_TO_GROUND

}

});

```

---

**【第二部分】实现跟随弹窗的核心原理**

**标题:利用实体与视锥体裁剪技术**

跟随弹窗的核心是保持弹窗相对于屏幕的位置不变,这就需要监控视角的变化,并依据视角计算弹窗的位置。主要步骤包括:

1. **获取目标对象的世界坐标**:通过Cesium的实体(Entity)获取其世界坐标。

2. **投影到视口坐标**:利用`scene.mapProjection.project`方法将世界坐标转换为视口坐标。

3. **利用视锥体裁剪**:当视角变化时,通过监听`viewer.camera.moveEnd`事件,判断弹窗是否在视锥体内,若不在,则将其重新定位到视锥体边缘附近。

```javascript

let targetEntity; // 初始化目标实体

let popupElement; // 弹窗元素

// 监听视角变化

viewer.camera.moveEnd.addEventListener(function () {

const targetCartesian = targetEntity.position.getValue(Cesium.JulianDate.now());

const targetPixel = viewer.scene.mapProjection.project(targetCartesian);

// 判断弹窗是否在视口范围内

const isInViewport = isElementInViewport(popupElement, targetPixel);

if (!isInViewport) {

// 重新计算并设置弹窗位置

const newPosition = calculatePopupPosition(targetPixel);

setPopupPosition(newPosition);

}

});

```

---

**【第三部分】弹窗元素定位与样式**

**标题:CSS与JavaScript联动实现弹窗跟随**

为了让弹窗在屏幕上正确显示并跟随目标对象,需要对弹窗元素进行CSS样式设置,并在JavaScript中动态调整其位置。

```css

.popup {

position: absolute;

background-color: white;

border-radius: 5px;

padding: 10px;

pointer-events: none;

z-index: 1000;

}

```

```javascript

// 创建并添加弹窗元素

popupElement = document.createElement('div');

popupElement.className = 'popup';

document.body.appendChild(popupElement);

// 计算并设置弹窗位置函数

function calculatePopupPosition(targetPixel) {

// 根据目标像素位置和屏幕尺寸计算弹窗位置

// ...

return newPosition;

}

function setPopupPosition(position) {

popupElement.style.left = position.x + 'px';

popupElement.style.top = position.y + 'px';

}

```

---

**【第四部分】优化与拓展**

**标题:处理边界条件与适应多目标跟随**

在实际应用中,还需考虑边界条件,如弹窗超出屏幕边界时的处理,以及多个目标对象时如何分配弹窗位置等。可通过计算视口边界、设置最大偏移量等方式优化跟随效果。

---

**结语:领略Cesium跟随弹窗的魅力**

通过上述步骤,我们不仅掌握了在Cesium中实现跟随弹窗的基础方法,还洞察到了其背后的数学原理与逻辑思考。跟随弹窗的实现不仅仅是简单的坐标转换,更是一次Web 3D空间思维与前端开发技术的深度融合。希望本文能帮助广大开发者在Cesium开发道路上走得更远,创造出更多有趣而实用的应用场景。

很早之前就有写过一个wcPop.js弹窗插件,并且在h5酒店预订、h5聊天室项目中都有使用过,效果还不错。当初想着有时间整合一个Vue版本,刚好趁着国庆节空闲时间捣鼓了个vue.js版自定义模态弹出框组件VPopup

v-popup 一款聚合Msg、Dialog、Popup、ActionSheet、Toast等功能的轻量级移动端Vue弹窗组件。

整合了有赞VantNutUI等热门Vue组件库中Popup弹出层、Toast轻提示、Notify消息提示、Dialog对话框及ActionSheet动作面板等功能。

使用组件

// 在main.js中全局引入
import Vue from 'vue'

import Popup from './components/popup'
Vue.use(Popup)

支持如下两种方式调用组件。

  • 标签式调用
<v-popup
  v-model="showPopup"
  title="标题内容" 
  content="弹窗内容,告知当前状态、信息和解决方法,描述文字尽量控制在三行内"
  type="android"
  shadeClose="false"
  xclose
  z-index="2000"
  :btns="[
    {...},
    {...},
  ]"
/>
  • 函数式调用

this.$vpopup({...}),传入参数即可使用,该函数会返回弹窗组件实例。

let $el = this.$vpopup({
  title: '标题内容',
  content: '弹窗内容,描述文字尽量控制在三行内',
  type: 'android',
  shadeClose: false,
  xclose: true,
  zIndex: 2000,
  btns: [
    {text: '取消'},
    {
      text: '确认',
      style: 'color:#f60;',
      click: () => {
        $el.close()
      }
    },
  ]
});

你可根据喜好或项目需要任意选择一种调用方式即可。下面就开始讲解下组件的实现。

在components目录下新建popup.vue页面。

组件参数配置

<!-- Popup 弹出层模板 -->
<template>
  <div v-show="opened" class="nuxt__popup" :class="{'nuxt__popup-closed': closeCls}" :id="id">
    <div v-if="JSON.parse(shade)" class="nuxt__overlay" @click="shadeClicked" :style="{opacity}"></div>
    <div class="nuxt__wrap">
      <div class="nuxt__wrap-section">
        <div class="nuxt__wrap-child" :class="['anim-'+anim, type&&'popui__'+type, round&&'round', position]" :style="popupStyle">
          <div v-if="title" class="nuxt__wrap-tit" v-html="title"></div>
          <div v-if="type=='toast'&&icon" class="nuxt__toast-icon" :class="['nuxt__toast-'+icon]" v-html="toastIcon[icon]"></div>
          <template v-if="$slots.content">
            <div class="nuxt__wrap-cnt"><slot name="content" /></div>
          </template>
          <template v-else>
            <div v-if="content" class="nuxt__wrap-cnt" v-html="content"></div>
          </template>
          <slot />
          <div v-if="btns" class="nuxt__wrap-btns">
            <span v-for="(btn,index) in btns" :key="index" class="btn" :class="{'btn-disabled': btn.disabled}" :style="btn.style" @click="btnClicked($event,index)" v-html="btn.text"></span>
          </div>
          <span v-if="xclose" class="nuxt__xclose" :class="xposition" :style="{'color': xcolor}" @click="close"></span>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  // 弹窗索引,遮罩次数,定时器
  let $index = 0, $lockCount = 0, $timer = {};
  export default {
    props: {
      ...
    },
    data() {
      return {
        opened: false,
        closeCls: '',
        toastIcon: {
          loading: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',
          success: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M75.712 445.712l240.176 185.52s13.248 6.624 29.808 0l591.36-493.872s84.272-17.968 68.64 71.488c-57.04 57.968-638.464 617.856-638.464 617.856s-38.096 21.536-74.544 0C256.272 790.256 12.816 523.568 12.816 523.568s-6.672-64.592 62.896-77.856z"/></svg>',
          fail: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M450.602 665.598a62.464 62.464 0 0 0 122.88 0l40.96-563.198A102.615 102.615 0 0 0 512.042 0a105.256 105.256 0 0 0-102.4 112.64l40.96 552.958zm61.44 153.6a102.4 102.4 0 1 0 102.4 102.4 96.74 96.74 0 0 0-102.4-102.4z"/></svg>',
        }
      }
    },
    watch: {
      value(val) {
        const type = val ? 'open' : 'close';
        this[type]();
      },
    },
    methods: {
      // 打开弹窗
      open() {
        if(this.opened) return;
        this.opened = true;
        this.$emit('open');
        typeof this.onOpen === 'function' && this.onOpen();
        this.$el.style.zIndex = this.getZIndex() + 1;
        
        if(JSON.parse(this.shade)) {
          if(!$lockCount) {
            document.body.classList.add('nt-overflow-hidden');
          }
          $lockCount++;
        }
        
        // 倒计时关闭
        if(this.time) {
          $index++;
          // 防止重复点击
          if($timer[$index] !== null) clearTimeout($timer[$index])
          $timer[$index] = setTimeout(() => {
            this.close();
          }, parseInt(this.time) * 1000);
        }

        // 长按/右键弹窗
        if(this.follow) {
          // 避免获取不到弹窗宽高
          this.$nextTick(() => {
            let obj = this.$el.querySelector('.nuxt__wrap-child');
            let oW, oH, winW, winH, pos;

            oW = obj.clientWidth;
            oH = obj.clientHeight;
            winW = window.innerWidth;
            winH = window.innerHeight;
            pos = this.getPos(this.follow[0], this.follow[1], oW, oH, winW, winH);

            obj.style.left = pos[0] + 'px';
            obj.style.top = pos[1] + 'px';
          });
        }
      },
      // 关闭弹窗
      close() {
        if(!this.opened) return;
        
        this.closeCls = true;
        setTimeout(() => {
          this.opened = false;
          this.closeCls = false;
          if(JSON.parse(this.shade)) {
            $lockCount--;
            if(!$lockCount) {
              document.body.classList.remove('nt-overflow-hidden');
            }
          }
          if(this.time) {
            $index--;
          }
          this.$emit('input', false);
          this.$emit('close');
          typeof this.onClose === 'function' && this.onClose();
        }, 200);
      },

      // 点击遮罩层
      shadeClicked() {
        if(JSON.parse(this.shadeClose)) {
          this.close();
        }
      },
      // 按钮事件
      btnClicked(e, index) {
        let btn = this.btns[index];
        if(!btn.disabled) {
          typeof btn.click === 'function' && btn.click(e)
        }
      },
      // 获取弹窗层级
      getZIndex() {
        for(var $idx = parseInt(this.zIndex), $el = document.getElementsByTagName('*'), i = 0, len = $el.length; i < len; i++)
          $idx = Math.max($idx, $el[i].style.zIndex)
        return $idx;
      },
      // 获取弹窗坐标点
      getPos(x, y, ow, oh, winW, winH) {
        let l = (x + ow) > winW ? x - ow : x;
        let t = (y + oh) > winH ? y - oh : y;
        return [l, t];
      }
    },
  }
</script>

通过监听v-model值调用open和close方法。

watch: {
    value(val) {
        const type = val ? 'open' : 'close';
        this[type]();
    },
},

如果想要实现函数式调用this.$vpopup({...}),则需要使用到Vue.extend扩展实例构造器。

import Vue from 'vue';
import VuePopup from './popup.vue';

let PopupConstructor = Vue.extend(VuePopup);

let $instance;

let VPopup = function(options = {}) {
    // 同一个页面中,id相同的Popup的DOM只会存在一个
    options.id = options.id || 'nuxt-popup-id';

    $instance = new PopupConstructor({
        propsData: options
    });

    $instance.vm = $instance.$mount();
    
    let popupDom = document.querySelector('#' + options.id);
    if(options.id && popupDom) {
        popupDom.parentNode.replaceChild($instance.$el, popupDom);
    } else {
        document.body.appendChild($instance.$el);
    }

    Vue.nextTick(() => {
        $instance.value = true;
    })

    return $instance;
}

VPopup.install = () => {
    Vue.prototype['$vpopup'] = VPopup;
    Vue.component('v-popup', VuePopup);
}

export default VPopup;

这样就实现了引入 Popup 组件后,会自动在 Vue 的 prototype 上挂载 $vpopup 方法和注册 v-popup 组件。

下面就可以愉快的使用标签式及函数式调用组件了。

  • 设置圆角及关闭按钮

<v-popup v-model="showActionPicker" anim="footer" type="actionsheetPicker" round title="标题内容"
    :btns="[
        {text: '取消', click: () => showActionPicker=false},
        {text: '确定', style: 'color:#00e0a1;', click: () => null},
    ]"
>
    <ul class="goods-list" style="padding:50px;text-align:center;">
        <li>双肩包</li>
        <li>鞋子</li>
        <li>运动裤</li>
    </ul>
</v-popup>

<v-popup v-model="showBottom" position="bottom" round xclose title="标题内容">
    <ul class="goods-list" style="padding:50px;text-align:center;">
        <li>双肩包</li>
        <li>鞋子</li>
        <li>运动裤</li>
    </ul>
</v-popup>
  • 设置按钮禁用状态

按钮设置disabled: true即可禁用按钮事件。

<v-popup v-model="showActionSheet" anim="footer" type="actionsheet" :z-index="2020"
    content="弹窗内容,描述文字尽量控制在三行内"
    :btns="[
        {text: '拍照', style: 'color:#09f;', disabled: true, click: handleInfo},
        {text: '从手机相册选择', style: 'color:#00e0a1;', click: handleInfo},
        {text: '保存图片', style: 'color:#e63d23;', click: () => null},
        {text: '取消', click: () => showActionSheet=false},
    ]"
/>

另外还支持自定义slot插槽内容,当 content 和 自定义插槽 内容同时存在,只显示插槽内容。

<v-popup v-model="showComponent" xclose xposition="bottom" content="这里是内容信息"
    :btns="[
        {text: '确认', style: 'color:#f60;', click: () => showComponent=false},
    ]"
    @open="handleOpen" @close="handleClose"
>
    <template #content>当 content 和 自定义插槽 内容同时存在,只显示插槽内容!</template>
    <div style="padding:30px 15px;">
        <img src="assets/apple3.jpg" style="width:100%;" @click="handleContextPopup" />
    </div>
</v-popup>

好了,就分享到这里。希望对大家有所帮助。目前该组件正在项目中实战测试,后续会分享相关使用情况。

视频采集和管理是多模态大数据应用场景必不可少的环节,在基于Vue2前端框架实现的Web界面如何进行视频的展示和播放是开发人员会遇到的一个主要技术问题。本文提供基于Vue2+video.js实现视频的预览的方案。

采集的视频数据在前端视频管理模块列表中展示,然后用弹窗查看视频详情并预览播放。最开始使用 vue-mini-player 组件,可轻松实现视频在编辑界面的弹窗中播放,但是遇到两个问题:1)弹窗中播放着视频,关闭窗口后,视频流不会停止。2)关闭窗口,重新打开新的视频编辑窗口后,依旧是继续播放之前的视频。其原因应该是关闭旧的窗口后,视频播放的控件没有销毁,导致新打开的控件其实还是旧控件的实例。查了很多关于vue-mini-player的文档和使用样例,没有找到如何销毁vue-mini-player控件。

视频列表

单条视频数据编辑界面

video.js 是一个通用的可嵌入网页的视频播放器JS库,在Vue2中引用video.js可以创建播放组件对象,关闭视频时能进行操作。基于Vue2使用video.js方法如下。

  1. 安装video.js:
npm install video.js@6.13.0
  1. main.js注册
import videoJs from 'video.js'
import 'video.js/dist/video-js.css'
Vue.prototype.videoJs = videoJs //注册
  1. vue代码文件中使用组件

创建<video>组件,可放在弹窗中任何需要的地方。重点是给出id值,设置属性时需要用到。

<template>
  <el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
      <video id="casvideoplayer" ref="videoPlayerRef" class="video-js">
          <source :src="playUrl" type="video/mp4">
      </video>
      ...
      <div slot="footer" class="dialog-footer">
        	<el-button type="primary" @click="submitForm">确 定</el-button>
        	<el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
</template>
export default {
data() {
    return {
    // 使用video.js播放视频配置
    videoJsPlayer: null,
    playUrl:"", 			//视频文件链接
    videoPlayerOption: {
        controls: true, 			//确定播放器是否具有用户可以与之交互的控件。没有控件,启动视频播放的唯一方法是使用autoplay属性或通过Player API。
        // url: "", 							//要嵌入的视频资源url(不起作用?)
        poster: '',   					//封面
         autoplay: false, 	//自动播放属性, true/false/"muted"(静音播放)
        muted: false, 				//静音播放
        preload: 'none', 		//建议浏览器是否应在<video>加载元素后立即开始下载视频数据。
        fluid: false, 					//是否自适应布局,播放器将会有流体体积。换句话说,它将缩放以适应容器。
        width: "850px", 		//视频播放器的显示宽度(以像素为单位)(fluid=false时起作用)
        height: "600px", 		//视频播放器的显示高度(以像素为单位)(fluid=false时起作用)
     },
};
methods: {
   // 视频列表的“修改”按钮,点击后显示修改弹窗
   handleUpdate(row) {
        // 从后台获取视频信息
        getVedio(row.id).then(response => {
            this.form = response.data;    //修改弹窗其他字段信息赋值
            this.title = "修改视频管理";
            this.open = true;    								// 显示修改弹窗

            // video.js组件播放视频
            this.videoPlayerOption.poster = response.data.avator;
            this.playUrl = response.data.contentsOrg;
            this.showVideoWindow();   //设置视频播放控件
        });
     },
       
    //(重点是这里)
    // 使用video.js组件播放视频
     showVideoWindow(){
        // 如果视频播放控件已经存在,切换视频url,重新播放;如果控件不存在,创建
        if(this.videoJsPlayer){
            this.videoJsPlayer.src([
                 {
                    src: this.playUrl,
                    type: "video/mp4"
                 }
             ]);
            // 如何图片不为空,设置视频封面
            if(this.videoPlayerOption.poster != null && this.videoPlayerOption.poster != ""){
            		this.videoJsPlayer.poster(this.videoPlayerOption.poster);
             }
            this.videoJsPlayer.load(this.playUrl);
            // this.videoJsPlayer.play();      //自动播放(打开后,切换视频后需自动播放)
         }else{
            // 最开始创建一次视频播放组件
            this.$nextTick(() => {
                this.videoJsPlayer = this.videoJs(
                    "casvideoplayer", 				//播放器控件id
                    this.videoPlayerOption //播放器设置项(这里设置的poster属性不生效,需要在后面单独设置)
                 );
                this.videoJsPlayer.poster(this.videoPlayerOption.poster);  //貌似不生效?
             })
         }
     },
       
    // 编辑弹窗页面的“取消”按钮
    cancel() {
        // 重置视频控件数据(video.js组件)
        if(this.videoJsPlayer){
            this.videoJsPlayer.reset();
         }
					this.reset();
 		},
}

以上代码实现了在Vue2弹窗中播放视频组件的功能,注意关闭弹窗时要使用“取消”按钮。如果通过点击弹窗右上角X关闭弹窗,视频还可以在后台继续播放,但是打开一个新的视频修改弹窗后,播放的视频会终止,并切换到新视频播放界面。即使这样,目前的功能已经不影响用户正常使用。

video.js还有一个强大功能,看到喜欢的画面点击右键可以保存视频帧,另外支持画中画、设备投放等功能。

video.js右键功能

后续优化改进工作包括:1)把video.js视频播放功能做成Vue组件,方便在不同的Vue代码文件中调用。2)捕获窗口关闭的事件(如点击X关闭,或者鼠标失焦点后关闭),关闭视频流。

video.js

Vue

【参考材料】

video.js官方网站:https://videojs.com/

其他编码材料:

https://blog.csdn.net/qq_60533482/article/details/128015308

https://blog.csdn.net/Uookic/article/details/116131535

https://www.cnblogs.com/DL-CODER/p/16833222.html