我们之前实现过一个echarts和地图图片叠加实现数据渲染的效果,今天我们要实现一个效果就是给地图底图增加一个流光效果。
动态效果
在线演示
帧动画资源文件比较大,加载时间会比较长。
准备图片
首先我们需要准备地图底图,地图尺寸1023*783,文件大小705kb
图片处理
我们需要创建一个图层,图层是当前图片宽高的3倍,也就是3069×2349,之所以这样创建是给流光增加展示空间,避免我们创建的流光因为展示空间不足被硬生生截断。
ae处理
将我们的图片拖入到ae中,使用钢笔工具将地图轮廓勾勒出来,然后添加saber流光插件
添加saber配置
这里我们使用saber创建来实现我们的动画效果,下面是基本的配置参数
帧动画导出
这里我们的帧动画图片一共有8秒,200张图片,这个图片本身有705k,加上流光大搞800多k,200张帧图片的话,导出图片就要高达160M,单单这个帧动画就这么大的话,前端就没办法正常渲染了,所以我们需要优化,我们将地图和流光分为两层,导出的时候,我们只导出流光,我们导出为200张流光图,大小一共为32.9M,虽然依然很大,但相比于160M地图,这里已经很小了。
前端渲染
图片导出后,我们需要可以正常渲染,保证地图渲染出来不会出现错位。下面是帧动画的渲染,我们将200张帧动画图片渲染,因为图片四周各有100%的空白区域,所以我们需要调整一下定位。因为帧动画有空白区域的影响,可能会干扰到其他区域,这里我们需要让这一层不会干扰到鼠标点击,这里给这一层增加一个鼠标穿透pointer-events: none;
<template>
<div class="canvas-container">
<div class="canvas-wrapper">
<canvas ref="animationCanvas" class="animation_canvas" id="animation_canvas"></canvas>
</div>
</div>
</template>
<script>
export default {
props: {
fileLength: {
type: Number,
default: 199
},
IntervalTime: {
type: Number,
default: 60
}
},
data() {
return {
animationCanvas: null,
isLoading: true
};
},
methods: {
// 加载所有图片
async loadImages2() {
const sources = [];
for (let i = 0; i <= this.fileLength; i++) {
const image = await import(`./bg/${i}.png`);
sources.push(image.default);
}
return sources;
},
// 预加载图片
loadImages(sources) {
return new Promise((resolve) => {
let loadedImages = 0;
const numImages = sources.length;
const images = [];
for (let i = 0; i < numImages; i++) {
images[i] = new Image();
images[i].onload = () => {
loadedImages++;
if (loadedImages >= numImages) {
resolve(images);
}
};
images[i].src = sources[i];
}
});
},
// 播放动画
playImages(images, ctx, width, height) {
let imageNow = 0;
return setInterval(() => {
ctx.clearRect(0, 0, width, height);
ctx.drawImage(images[imageNow], 0, 0, width, height);
imageNow = (imageNow + 1) % images.length;
}, this.IntervalTime);
},
// 初始化画布
initCanvas() {
const canvas = this.$refs.animationCanvas;
if (!canvas) return null;
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Failed to get canvas context');
return null;
}
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
canvas.width = width;
canvas.height = height;
return { ctx, width, height };
}
},
async mounted() {
try {
// 初始化画布
const canvasData = this.initCanvas();
if (!canvasData) return;
const { ctx, width, height } = canvasData;
// 加载所有图片源
const sources = await this.loadImages2();
// 预加载所有图片
const images = await this.loadImages(sources);
// 开始播放动画
this.playImages(images, ctx, width, height);
} catch (error) {
console.error('Animation initialization error:', error);
}
}
};
</script>
<style scoped>
.canvas-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: calc(100% + 200%);
height: calc(100% + 200%);
overflow: hidden;
z-index: -1;
pointer-events: none;
}
.animation_canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}
</style>
echarts地图代码渲染
下面是地图的渲染,我们需要调整层级,最下面时地图图片层,中间是流光帧动画层,最上面是echarts层。
<template>
<div class="item1" :style="{width:width?width+'px':'100%',height:height?height+'px':'100%'}">
<div class="mapPopWin" ref="mapPopWin" v-if="popShow"
:style="{left:left-(popWidth/2)+'px',top:top-(popHeight/2)+'px'}">
<img class="close" @click="popShow=false" src="../assets/close.png" alt="">
<h3>{{ address }}</h3>
<div class="info2">
<p>接入车辆数:{{ num }}</p>
</div>
</div>
<div class="centerMap" :style="{width:width+'px',height:height+'px'}" ref="centerMap" id="centerMap">
</div>
<div class="map map2">
<sequence></sequence>
</div>
<div class="map">
<img ref="img" src="../assets/centerMap.png" alt="">
</div>
</div>
</template>
<script>
import * as echarts from "echarts";
import data from '../assets/data.json'
import mapTag from '../assets/labelImg.png'
import sequence from '../sequence/index.vue'
export default {
name: "item1",
data() {
return {
data,
height: 0,
width: 0,
popShow: false,
left: 0,
top: 0,
address: '',
num: 10,
popWidth: 0,
popHeight: 0,
}
},
components: {sequence},
watch: {
popShow() {
this.getPopInfo()
}
},
mounted() {
var that = this;
const viewElem = document.body;
// 监听窗口变化,重绘echarts
const resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
that.drawEcharts();
}, 300)
});
resizeObserver.observe(viewElem);
},
methods: {
getPopInfo() {
this.$nextTick(() => {
this.popWidth = this.$refs?.mapPopWin?.offsetWidth
this.popHeight = this.$refs?.mapPopWin?.offsetHeight * 2
})
},
drawEcharts() {
this.width = this.$refs.img.offsetWidth
this.height = this.$refs.img.offsetHeight
this.$nextTick(() => {
this.getEcharts()
})
},
getEcharts() {
var that = this;
var chartDom = that.$refs.centerMap;
var myChart = echarts.init(chartDom);
myChart.clear()
myChart.resize()
var nameMap = '地图数据';
var geoCoordMap = {};
var mapData = [];
var serverdata = []
// 图标数据
var iconData = [];
echarts.registerMap(nameMap, this.data);
var mapFeatures = echarts.getMap(nameMap).geoJson.features;
myChart.hideLoading();
var mapName = ''
var mapInfo = []
mapFeatures.forEach(function (v, index) {
// 地区名称
mapData.push({
name: v.properties.name,
value: Math.random() * 100
});
geoCoordMap[v.properties.name] = v.properties.center;
mapName = mapName + (mapName ? ',' : '') + v.properties.name
mapInfo.push({
name: v.properties.name,
code: v.properties.adcode
})
var data = {
"value": v.properties.center,
"id": index,
"name": v.properties.name,
"num": (Math.random() * 100).toFixed(0)
}
iconData.push(data)
});
// 生成地图图标
iconData.forEach((type, index) => {
var datamap = {
type: 'scatter',
tooltip: {
show: true,
formatter: function (params) {
return params.data.name;
}
},
name: type.name,
coordinateSystem: 'geo',
symbol: 'image://' + mapTag,
symbolSize: [68, 28],
symbolOffset: [-0, -0],
label: {
normal: {
show: true,
position: 'top',
offset: [0, 25],
formatter: function (params) {
console.log(params)
var text = `{num|${params.data.num}}\n{name|${params.name}}`
return text
},
color: '#fff',
rich: {
name: {
padding: [0, 0],
color: '#FEFEFE',
fontSize: 17,
fontWeight: 500,
fontFamily: 'YouSheBiaoTiHei'
},
num: {
padding: [10, 0],
color: '#11fffe',
fontSize: 20,
fontWeight: 500,
textAlign: 'center',
fontFamily: 'DIN-Bold'
},
},
},
},
hoverAnimation: true,
z: 6,
data: [type]
}
serverdata.push(datamap)
});
var optionMap = {
geo: {
map: nameMap,
show: true,
aspectScale: 0.80,
layoutCenter: ["50%", "65%"],
layoutSize: '120%',
roam: false,
itemStyle: {
normal: {
borderColor: 'rgba(147, 235, 248, 0)',
borderWidth: 0.5,
areaColor: 'rgba(147, 235, 248, 1)',
opacity: 0,
},
emphasis: {
borderColor: 'rgba(147, 235, 248, 0)',
borderWidth: 0.5,
areaColor: 'rgba(147, 235, 248, 0)',
opacity: 0,
}
},
z: 0,
label: {
normal: {
show: false
},
emphasis: {
show: false
}
}
},
series: [
...serverdata,
]
};
myChart.clear()
myChart.resize()
myChart.setOption(optionMap);
myChart.off('click')
myChart.on('click', function (params) {
console.log(params)
that.left = params.event.event.offsetX;
that.top = params.event.event.offsetY;
that.popShow = false
that.$nextTick(() => {
that.popShow = true
})
that.address = params.name
that.num = params.data.num
let data = myChart.convertFromPixel('geo', [that.left, that.top])
// that.getPositionByLonLats(data[0], data[1])
// myChart.off('click')
// myChart.setOption(optionMap, false)
// myChart.off('click')
})
that.address = '四川'
let datas = myChart.convertToPixel('geo', [102.02347029661686, 30.232391836704323]);
that.left = datas[0];
that.top = datas[1];
that.popShow = false
iconData.forEach((type) => {
if (type.name == '四川') {
that.num = type.num
}
});
that.$nextTick(() => {
that.popShow = true
})
}
},
}
</script>
<style lang="scss" scoped>
.item1 {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
margin: auto;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
}
.map {
//background: url("../../../../../assets/centerMap.png") center center no-repeat;
//background-size: 1024px 783px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
margin: 0 auto;
position: absolute;
z-index: 0;
img {
height: 100%;
max-width: 100%;
max-height: 100%;
}
}
.centerMap {
width: 100%;
height: 100%;
position: absolute;
z-index: 2;
top: 0;
}
.titleTop {
position: absolute;
top: 0;
width: 100%;
left: 0;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: column;
align-content: flex-start;
:deep(.titleTopDesc) {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
.real-time-num {
font-size: 22px;
font-family: DIN;
font-weight: 500;
width: auto;
margin: 0;
color: #1AFFFF;
}
}
.unit {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
margin-top: -3px;
margin-left: 3px;
}
.titleTopDesc {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
}
}
.infoWin {
background: red;
width: 100px;
height: 100px;
}
.mapPopWin {
position: absolute;
left: 0;
top: 0;
background: url("../assets/activeIcon.png") no-repeat;
background-size: 100% 100%;
width: 220PX;
height: 141PX;
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: nowrap;
flex-direction: row;
align-content: flex-start;
z-index: 10000;
.close {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
width: 20px;
}
h3 {
margin-left: 15px;
font-size: 22px;
font-family: PangMenZhengDao;
font-weight: 400;
color: #FEFEFE;
}
.info2 {
font-size: 14px;
font-family: PingFang;
font-weight: 500;
color: #FFFFFF;
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: nowrap;
flex-direction: column;
margin-top: 24px;
margin-left: 10px;
align-content: flex-start;
p {
margin: 0px;
}
}
}
.map2{
z-index: 1;
pointer-events: none;
}
</style>
实例下载
代码包括ae源文件
vue vite js 项目实例代码