当前位置: 首页 > news >正文

Cesium实现标注动画

继续我们的Cesium系列,这次来整点有意思的——给地图上的标注加点动画效果。静态的点线面看久了总觉得少点什么,动起来的标注不仅更吸引眼球,在实际项目中也能更好地突出重要信息。

动画实现的核心原理

在Cesium中实现标注动画主要有两种方式:

  • CallbackProperty方式:通过回调函数动态计算属性值,适合简单的动画效果
  • GPU着色器方式:使用GLSL着色器在GPU上计算,适合复杂的视觉效果,性能更好

基础动画效果

1. 呼吸动画

最经典的动画效果,点的大小按正弦波规律变化。通过Math.sin()函数控制大小的周期性变化,就像呼吸一样有节奏感。

2. 闪烁动画

通过快速改变透明度来实现闪烁效果,特别适合用来标识警告信息或需要紧急关注的位置。使用条件判断让颜色在两个状态间快速切换。

3. 颜色渐变

利用HSL颜色空间,让标注的颜色在整个色环上连续变化,产生彩虹般的渐变效果。这种效果在展示数据变化或状态转换时特别有用。

4. 弹跳动画

控制标签的垂直位置,让文字上下弹跳。使用Math.abs(Math.sin())确保弹跳始终向上,增加趣味性的同时也能有效吸引注意。

5. 旋转动画

对Billboard图标应用旋转变换,适合展示方向性信息。旋转速度可控,可以表示风向、车辆朝向等动态信息。

6. 组合动画

将多种动画效果组合使用,比如同时改变大小、颜色、位置和透明度,创造更丰富的视觉效果。这种方式虽然复杂,但视觉冲击力更强。

7.波纹扩散效果

使用GPU着色器技术实现真正的水波涟漪效果。不同于之前用多个点模拟的方法,这种实现方式性能更好,效果也更自然。

<template><div id="cesiumContainer" style="width: 100%; height: 100vh;"></div>
</template><script setup>
import { onMounted } from 'vue';
import {Ion,Viewer,Cartesian3,Color,PointGraphics,LabelGraphics,BillboardGraphics,CallbackProperty,Math as CesiumMath,JulianDate,ScreenSpaceEventType,defined,HorizontalOrigin,VerticalOrigin,LabelStyle,HeightReference,Material,MaterialAppearance,EllipseGeometry,GeometryInstance,Primitive
} from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";onMounted(() => {// 设置 Cesium Ion 访问令牌Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJhNmQ5NDYyNi1lZTdhLTRiYTItODFiZi1mYzNiYWNjNDFjMzgiLCJpZCI6NTk3MTIsImlhdCI6MTY2MDE4MDAyNX0.bDTaHEah0hRjUyJWz0hyxIL0Fg63awPXV26OmQ5MCdM';const viewer = new Viewer('cesiumContainer', {animation: false, // 移除动画控件timeline: false, // 移除时间轴控件geocoder: false, // 移除地理编码控件homeButton: false, // 移除主页按钮sceneModePicker: false, // 移除场景模式选择器selectionIndicator: false, // 移除选择指示器fullscreenButton: false, // 移除全屏按钮vrButton: false // 移除 VR 按钮});// ==================== 动画相关变量 ====================let startTime = performance.now(); // 动画开始时间// ==================== 1. 呼吸动画点标注 ====================// 使用 CallbackProperty 创建动态属性const breathingSize = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000; // 经过的秒数const breathingFactor = Math.sin(elapsed * 2) * 0.5 + 1; // 0.5-1.5之间变化return 15 * breathingFactor; // 基础大小15,动态变化}, false);viewer.entities.add({id: 'breathing-point',name: '呼吸动画点',position: Cartesian3.fromDegrees(114.0579, 22.5431),point: new PointGraphics({pixelSize: breathingSize, // 动态大小color: Color.RED,outlineColor: Color.WHITE,outlineWidth: 2,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '呼吸动画点',font: '16pt Arial',fillColor: Color.WHITE,outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cartesian3(0, -50, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 2. 闪烁动画点标注 ====================const blinkingAlpha = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const blinkFactor = Math.sin(elapsed * 4) > 0 ? 1.0 : 0.3; // 快速闪烁return blinkFactor;}, false);viewer.entities.add({id: 'blinking-point',name: '闪烁动画点',position: Cartesian3.fromDegrees(113.2278, 23.1291),point: new PointGraphics({pixelSize: 20,color: new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const alpha = Math.sin(elapsed * 4) > 0 ? 1.0 : 0.3;return Color.BLUE.withAlpha(alpha);}, false),outlineColor: Color.YELLOW,outlineWidth: 3,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '闪烁动画点',font: '16pt Arial',fillColor: Color.YELLOW,outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cartesian3(0, -50, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 3. 颜色渐变动画点标注 ====================const gradientColor = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const hue = (elapsed * 50) % 360; // 色相循环变化return Color.fromHsl(hue / 360, 1.0, 0.5); // HSL颜色空间}, false);viewer.entities.add({id: 'gradient-point',name: '颜色渐变点',position: Cartesian3.fromDegrees(108.9478, 34.2317),point: new PointGraphics({pixelSize: 25,color: gradientColor,outlineColor: Color.WHITE,outlineWidth: 2,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '颜色渐变点',font: '16pt Arial',fillColor: gradientColor, // 标签颜色也跟着变化outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cartesian3(0, -50, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 4. 弹跳动画点标注 ====================const bounceOffset = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const bounce = Math.abs(Math.sin(elapsed * 3)) * 80; // 弹跳高度0-80像素return new Cartesian3(0, -50 - bounce, 0);}, false);viewer.entities.add({id: 'bounce-point',name: '弹跳动画点',position: Cartesian3.fromDegrees(104.0614, 30.6515), // 成都point: new PointGraphics({pixelSize: 18,color: Color.GREEN,outlineColor: Color.DARKGREEN,outlineWidth: 3,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '弹跳动画点',font: '16pt Arial',fillColor: Color.GREEN,outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: bounceOffset, // 动态偏移horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 5. 旋转动画Billboard标注 ====================// 创建一个简单的箭头图标SVGconst arrowSvg = `<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><polygon points="20,5 35,25 25,25 25,35 15,35 15,25 5,25" fill="orange" stroke="black" stroke-width="1"/></svg>`;const arrowDataUri = 'data:image/svg+xml;base64,' + btoa(arrowSvg);const rotationAngle = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;return elapsed * CesiumMath.PI_OVER_TWO; // 每2秒旋转90度}, false);viewer.entities.add({id: 'rotation-billboard',name: '旋转Billboard',position: Cartesian3.fromDegrees(87.6177, 43.7928),billboard: new BillboardGraphics({image: arrowDataUri,scale: 1.0,rotation: rotationAngle, // 动态旋转角度pixelOffset: new Cartesian3(0, -20, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '旋转动画标注',font: '16pt Arial',fillColor: Color.ORANGE,outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cartesian3(0, -80, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 6. 组合动画标注 ====================// 综合多种动画效果的复杂标注const complexSize = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const sizeFactor = Math.sin(elapsed * 1.5) * 0.3 + 1; // 缓慢的呼吸return 20 * sizeFactor;}, false);const complexColor = new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const alpha = Math.sin(elapsed * 2) * 0.3 + 0.7; // 透明度变化return Color.PURPLE.withAlpha(alpha);}, false);viewer.entities.add({id: 'complex-animation',name: '组合动画标注',position: Cartesian3.fromDegrees(91.1406, 29.6522),point: new PointGraphics({pixelSize: complexSize,color: complexColor,outlineColor: Color.WHITE,outlineWidth: 2,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '组合动画',font: '18pt Arial',fillColor: new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const hue = (elapsed * 30) % 360;return Color.fromHsl(hue / 360, 0.8, 0.6);}, false),outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new CallbackProperty(function(time, result) {const elapsed = (performance.now() - startTime) / 1000;const wobble = Math.sin(elapsed * 4) * 10; // 左右摆动return new Cartesian3(wobble, -60, 0);}, false),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 7. 波纹扩散效果====================// 创建波纹扩散函数function createRippleEffect(viewer, longitude, latitude, radius, color, speed) {const instance = new GeometryInstance({geometry: new EllipseGeometry({center: Cartesian3.fromDegrees(longitude, latitude, 0),semiMinorAxis: radius,semiMajorAxis: radius,}),});const appearance = new MaterialAppearance({material: new Material({fabric: {uniforms: {color: Color.fromCssColorString(color),speed: speed,},source: `czm_material czm_getMaterial(czm_materialInput materialInput) {czm_material material = czm_getDefaultMaterial(materialInput);material.diffuse = 1.5 * color.rgb;vec2 st = materialInput.st;float dis = distance(st, vec2(0.5, 0.5));// 创建三道波纹,每道间隔0.3的时间float per1 = fract(czm_frameNumber * 0.032 * speed);float per2 = fract((czm_frameNumber * 0.032 * speed) - 0.3);float per3 = fract((czm_frameNumber * 0.032 * speed) - 0.6);// 计算每道波纹的透明度float pass1 = step(per1 * 0.3, dis) == 0.0? color.a * dis / per1: 0.0;float pass2 = step(per2 * 0.3, dis) == 0.0? color.a * dis / per2: 0.0;float pass3 = step(per3 * 0.3, dis) == 0.0? color.a * dis / per3: 0.0;// 实现渐变消失效果pass1 = pass1 * (1.0 - per1) * 2.0;pass2 = pass2 * (1.0 - per2) * 2.0;pass3 = pass3 * (1.0 - per3) * 2.0;// 取最大值作为最终透明度material.alpha = max(max(pass1, pass2), pass3);return material;}`,}}),});return viewer.scene.primitives.add(new Primitive({geometryInstances: instance,appearance: appearance}));}// 添加波纹效果// 波纹 - 哈尔滨 - 青色createRippleEffect(viewer, 126.5382, 45.8036, 100000, 'rgba(0, 255, 255, 0.8)', 0.2);// 为波纹中心添加标注点和标签viewer.entities.add({id: 'ripple-center-1',name: '哈尔滨波纹中心',position: Cartesian3.fromDegrees(126.5382, 45.8036),point: new PointGraphics({pixelSize: 8,color: Color.CYAN,outlineColor: Color.WHITE,outlineWidth: 2,heightReference: HeightReference.CLAMP_TO_GROUND}),label: new LabelGraphics({text: '哈尔滨-波纹扩散',font: '14pt Arial',fillColor: Color.CYAN,outlineColor: Color.BLACK,outlineWidth: 2,style: LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cartesian3(0, -60, 0),horizontalOrigin: HorizontalOrigin.CENTER,verticalOrigin: VerticalOrigin.BOTTOM})});// ==================== 设置相机视角 ====================/*viewer.camera.setView({destination: Cartesian3.fromDegrees(116, 30, 3000000),orientation: {heading: 0.0,pitch: -0.99,roll: 0.0}});*/// 使用 flyTo 方法,自动计算最佳视角viewer.flyTo(viewer.entities);// ==================== 点击事件处理 ====================viewer.cesiumWidget.screenSpaceEventHandler.setInputAction(function onLeftClick(event) {const pickedObject = viewer.scene.pick(event.position);if (defined(pickedObject) && defined(pickedObject.id)) {console.log('点击的动画标注:', pickedObject.id.name || pickedObject.id.id);// 可以在这里添加点击后的交互效果if (pickedObject.id.name) {alert(`点击了: ${pickedObject.id.name}`);}}}, ScreenSpaceEventType.LEFT_CLICK);// ==================== 动画控制函数 ====================// 可以添加暂停/恢复动画的功能window.pauseAnimations = function() {startTime = performance.now() - (performance.now() - startTime);};window.resumeAnimations = function() {startTime = performance.now();};// 在控制台输出提示信息console.log('标注动画示例已加载!');console.log('可以使用 pauseAnimations() 和 resumeAnimations() 来控制动画');
});
</script><style>
/* 隐藏页面底部的 Cesium logo 和数据归属 */
.cesium-viewer .cesium-widget-credits {display: none !important;
}/* 隐藏右上角的 Imagery 和 Navigation instructions */
.cesium-viewer .cesium-viewer-toolbar {display: none !important;
}
</style>

源码

相关文章:

  • get_rga_thread线程和low_camera_venc_thread线程获取低分辨率VENC码流数据
  • WES(二)——数据预处理
  • 美颜SDK功能模块化设计实战:滤镜、贴纸与人脸识别的协同实现
  • YOLOv8 区域计数系统:基于计算机视觉的智能物体计数方案
  • 各类效果名称收集
  • Nacos 服务注册发现案例:nacos-spring-cloud-example 详解
  • Django实现文件上传
  • 数据结构 -- 树相关面试题
  • 网络出版服务许可证年检
  • go实例化结构体的方式
  • 无法发布到PowerBI?试试拆分它
  • 天数计算卡 报错和解决记录
  • 玻纤效应的时序偏差
  • 有无D6完全是两个强度的主角——以撒(Isaac)
  • 基于Springboot + vue3实现的图书管理系统
  • 时间序列预测算法中的预测概率化笔记
  • Tesseract 字库介绍与训练指南
  • 【案例95】“小”问题引发的“大”发现---记一次环境修复
  • RTOS 完整概述与实战应用:从基础原理到产业实情
  • 从技术到实践:ArcGIS 与 HEC-RAS 解析洪水危险性及风险评估
  • 做游戏出租的网站好/互联网推广渠道
  • 微信优惠券网站怎么做的/qq群推广网站
  • wordpress开启curl/域名seo查询
  • 怎么做收费网站/北京网
  • wordpress 去掉index.php/南京seo顾问
  • 山东网站建设公司哪家专业/互联网营销师课程