全網最全的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彈窗組件。
整合了有贊Vant及NutUI等熱門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可以創建播放組件對象,關閉視頻時能進行操作?;赩ue2使用video.js方法如下。
npm install video.js@6.13.0
import videoJs from 'video.js'
import 'video.js/dist/video-js.css'
Vue.prototype.videoJs = videoJs //注冊
創建<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
*請認真填寫需求信息,我們會在24小時內與您取得聯系。