先上效果圖
之前在工作中需要給可視化大屏寫(xiě)些動(dòng)畫(huà)效果,其中就有上圖展示的多段路徑效果,寫(xiě)的時(shí)候也踩了些坑茶敏,避免大家后續(xù)工作中遇到相似功能不好下手,這里分享給小伙伴們缚俏。
組件使用如下惊搏,可以看到,主要就是在背景圖上寫(xiě)的動(dòng)畫(huà):
實(shí)現(xiàn)原理:
使用的是echarts的路徑圖忧换,也是就是type:‘lines’這個(gè)系列恬惯。可先看下我發(fā)布的這個(gè)“基礎(chǔ)版本”基礎(chǔ)-多段線-路徑圖亚茬,考慮到多個(gè)頁(yè)面會(huì)使用到當(dāng)前效果酪耳,因此對(duì)“基礎(chǔ)版本”封裝成了一個(gè)比較通用的組件,注意echarts版本為4.4.0及其以上刹缝。
使echarts 渲染盒子和背景圖片(可以是img標(biāo)簽)寬度高度一致碗暗,echarts 渲染盒子的層級(jí)z-index高于要寫(xiě)動(dòng)畫(huà)的圖片,以左下角為原點(diǎn)建立坐標(biāo)系(這樣方便測(cè)量坐標(biāo))梢夯,整個(gè)坐標(biāo)系寬高(即xAxis和yAxis的最大值)為圖片寬高言疗,然后量好各個(gè)點(diǎn)的坐標(biāo),結(jié)合基礎(chǔ)-多段線-路徑圖實(shí)現(xiàn)最終動(dòng)畫(huà)颂砸。
最后對(duì)該組件升級(jí)以滿足更多需求洲守,如頁(yè)面縮放時(shí)疑务,保證點(diǎn)不錯(cuò)位,如使組件支持多段點(diǎn)分別配置單獨(dú)的顏色梗醇、速度知允,如下:
下面進(jìn)行具體實(shí)現(xiàn),分v1.0和v2.0兩個(gè)版本叙谨,不想看的可直接翻到最后查看最終實(shí)現(xiàn)代碼
路徑組件v1.0版本開(kāi)發(fā)要求
1.核心功能就是上面的基礎(chǔ)-多段線-路徑圖
2.因?yàn)槭窃诒尘皥D上(也可以是img標(biāo)簽温鸽,只要保證圖片和組件寬高一致即可)寫(xiě)一層箭頭運(yùn)動(dòng)的動(dòng)畫(huà),就要考慮到圖片拉伸問(wèn)題手负,圖片拉伸需要保證動(dòng)畫(huà)始終在正確位置上涤垫,不會(huì)錯(cuò)位。
3.使用組件時(shí)要方便竟终,配置點(diǎn)位要簡(jiǎn)單蝠猬。
路徑組件1.0版本-代碼如下:
<template>
<div class="chart-box" :id="id"></div>
</template>
<script>
export default {
name: 'linesChartAnimate',
props: {
id: {
type: String,
default: 'ChartBox'
},
imgWH: {
type: Object,
default(){
return {
width: 882, // 當(dāng)前這張圖是 882*602的圖
height: 602
}
}
},
dotsArr: { // 運(yùn)動(dòng)點(diǎn)集合
type: Array,
default(){
return [
[ // 這個(gè)括號(hào)里代表的一組數(shù)據(jù)的運(yùn)動(dòng),即從點(diǎn)[205, 275]運(yùn)動(dòng)到點(diǎn)[263, 275]
[205, 275],
[263, 275],
],
[ // 這組點(diǎn)里有四個(gè)點(diǎn)
[206, 267],
[284, 267],
[284, 413],
[295, 413],
],
]
}
},
speed: { // 轉(zhuǎn)速
type: Number,
default: 7
}
},
data () {
return {
myChart: '',
// 注意:因?yàn)閳D片在現(xiàn)實(shí)的時(shí)候可能會(huì)拉伸,所以設(shè)置actualWH和imgWH兩個(gè)變量
actualWH: {
width: 0,
height: 0
}
}
},
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
this.draw()
},
methods: {
getLines(){
return {
type: 'lines',
coordinateSystem: 'cartesian2d',
// symbol:'arrow',
zlevel: 1,
symbol: ['none', 'none'],
polyline: true,
silent: true,
effect: {
symbol: 'arrow',
show: true,
period: this.speed, // 箭頭指向速度统捶,值越小速度越快
trailLength: 0.01, // 特效尾跡長(zhǎng)度[0,1]值越大榆芦,尾跡越長(zhǎng)重
symbolSize: 5, // 圖標(biāo)大小
},
lineStyle: {
width: 1,
normal: {
opacity: 0,
curveness: 0.4, // 曲線的彎曲程度
color: '#3be3ff'
}
}
}
},
getOption () {
// 點(diǎn)合集-在圖片上一個(gè)一個(gè)量的,注意以渲染盒子左下角為原點(diǎn)喘鸟,點(diǎn)取值方法:以圖片左下角為原點(diǎn)匆绣,量幾個(gè)線段點(diǎn)的(x,y)
let dotsArr = this.dotsArr
// 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 start
dotsArr.map(item => {
item.map(sub => {
sub[0] = (this.actualWH.width / this.imgWH.width) * sub[0] // x值
sub[1] = (this.actualWH.height / this.imgWH.height) * sub[1] // y值
})
})
// 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 end
// 散點(diǎn)圖和lines繪制 start
let scatterData = []
let linesData = [] // 默認(rèn)路徑圖點(diǎn)的路徑
let seriesLines = [] // 路徑圖
dotsArr.map(item => {
scatterData = scatterData.concat(item) // 散點(diǎn)圖data
linesData.push({
coords: item
})
})
// 默認(rèn)路徑圖
linesData && linesData.length && seriesLines.push({
...this.getLines(),
data: linesData
})
// 散點(diǎn)圖和lines繪制 end
let option = {
backgroundColor: 'transparent',
xAxis: {
// type: 'category',
type: 'value',
show: false,
min: 0,
max: this.actualWH.width,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.height,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// type: 'category'
},
grid: {
left: '0%',
right: '0%',
top: '0%',
bottom: '0%',
containLabel: false
},
series: [
{
zlevel: 2,
symbolSize: 0,
data: scatterData,
type: 'scatter'
},
...seriesLines
]
};
return option
},
// 繪制圖表
draw () {
this.myChart.clear()
this.resetChartData()
},
// 刷新數(shù)據(jù)
resetChartData () {
this.myChart.setOption(this.getOption(), true)
}
},
}
</script>
<style scoped>
.chart-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
注意上面兩個(gè)變量:imgWH和actualWH,imgWH是在測(cè)量點(diǎn)坐標(biāo)時(shí)的寬高什黑,actualWH是指頁(yè)面渲染時(shí)的實(shí)際寬高崎淳,初始時(shí)在mounted 中獲取。
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
},
在渲染圖形前先將點(diǎn)位坐標(biāo)根據(jù)比例換算為實(shí)際坐標(biāo)
結(jié)合下面的option配置愕把,到這里最終實(shí)現(xiàn)了不同大小圖片在初始時(shí)動(dòng)畫(huà)能準(zhǔn)確的定位
不知道小伙伴們看懂沒(méi)拣凹,這里總結(jié)下這步操作:
首先渲染圖表的盒子和背景圖(可以是img)大小完全一致,然后配置echarts的option的x軸和y軸分別盒子的寬高恨豁,注意x咐鹤,y軸的類型都為"value",然后grid配置上下左右都為0圣絮,再設(shè)置containLabel:false排除坐標(biāo)軸的影響祈惶,這就實(shí)現(xiàn)了在圖片上建立坐標(biāo)系的完美對(duì)齊。
在測(cè)量點(diǎn)位的時(shí)候扮匠,無(wú)論是哪個(gè)寬高量的點(diǎn)(量點(diǎn)的時(shí)候也是左下角開(kāi)始) 捧请,比如下面這個(gè)點(diǎn)的坐標(biāo)就是 [305,76],我量的時(shí)候是按照背景圖1000 * 280(這就是imgWH的值)的大小量的,但頁(yè)面實(shí)際渲染時(shí)盒子的大小實(shí)際是800 * 188(這就是actualWH獲取到的值)棒搜,直接使用點(diǎn)[305,76]肯定是不行的疹蛉,因此需要按等比縮放計(jì)算出現(xiàn)在的值也就是[800 / 1000 * 305,188 / 280 * 75],這才是現(xiàn)在的實(shí)際點(diǎn)位。
可以看到代碼中配置echarts的option里有scatter這個(gè)系列力麸,按理說(shuō)這部分代碼完全是多余的可款,但是實(shí)踐測(cè)試育韩,必須要有這項(xiàng)配置lines才能跑得起來(lái),而且scatter至少要有一個(gè)點(diǎn)闺鲸。其他代碼沒(méi)什么說(shuō)的筋讨,看代碼也能看懂,至此路徑圖簡(jiǎn)陋版v1.0開(kāi)發(fā)完畢摸恍。
路徑組件v2.0版本升級(jí)
1.在1.0版本上加上了頁(yè)面resize事件悉罕,頁(yè)面resize則echarts resize
2.1.0版本配置的顏色、運(yùn)動(dòng)速度等是通用的立镶,這里擴(kuò)展數(shù)據(jù)配置項(xiàng)壁袄,以支持對(duì)單條路徑的配置,比如:箭頭顏色媚媒、運(yùn)行速度等
這里只貼部分關(guān)鍵代碼嗜逻,完整代碼請(qǐng)移步頁(yè)面底部
解決問(wèn)題1,data中定義timer缭召,然后定義如下方法:
在頁(yè)面初始時(shí)調(diào)用
離開(kāi)頁(yè)面時(shí)銷毀
然后優(yōu)化交互
至此栈顷,問(wèn)題1解決,到這里按住ctr+鼠標(biāo)滾輪縮放頁(yè)面時(shí)恼琼,可實(shí)現(xiàn)適配妨蛹。
解決問(wèn)題2:
數(shù)據(jù)更改屏富,向下兼容晴竞,第一項(xiàng)為Object時(shí)可配置當(dāng)前這組點(diǎn)的表現(xiàn)行為
核心實(shí)現(xiàn),針對(duì)配置項(xiàng)單獨(dú)生成一個(gè)series狠半,這里小伙伴可能有疑問(wèn): 不能在一個(gè)series的lines中實(shí)現(xiàn)嗎噩死,為什么要每次單獨(dú)配置一段路徑動(dòng)畫(huà)都得push一個(gè)lines?答案是:不能神年,因?yàn)閑ffect項(xiàng)只能針對(duì)每個(gè)lines已维。
至此問(wèn)題二得到解決。
最后已日,我的項(xiàng)目是vue開(kāi)發(fā)的垛耳,封裝的vue組件-最終實(shí)現(xiàn)2.0版本-代碼如下,可直接使用飘千。若是react或者其他方式開(kāi)發(fā)的堂鲜,可參考代碼自行開(kāi)發(fā)。
<!--
路徑圖組件护奈,針對(duì)圖片上點(diǎn)需要有路徑動(dòng)畫(huà)的情況
若圖片有變化:
1.修改 imgWH 的寬高為最新圖片的寬高
2.重新在原圖上量出點(diǎn)合集并賦值給dotsArr
-->
<template>
<div class="chart-box" :id="id" v-show="!this.timer"></div>
</template>
<script>
// const merge = require('webpack-merge');
export default {
name: 'linesChartAnimate',
props: {
id: {
type: String,
default: 'ChartBox'
},
imgWH: {
type: Object,
default(){
return {
width: 882, // 當(dāng)前這張圖是 882*602的圖
height: 602
}
}
},
dotsArr: {
type: Array,
default(){
return [
// eg: [
// [140,338], // 點(diǎn)運(yùn)動(dòng)起點(diǎn) -- [x,y]
// [202,338], // 點(diǎn)運(yùn)動(dòng)終點(diǎn)
// ]
// 左上點(diǎn)合集
[
{ // 第一項(xiàng)可為對(duì)象缔莲,是當(dāng)前這組點(diǎn)的配置
color: 'red', // 顏色
symbol:'rect', // 類型-'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
speed: 3 // 運(yùn)動(dòng)時(shí)間
},
[140, 338],
[202, 338],
[202, 329],
],
[
[141, 227],
[160, 227],
[196, 100],
[202, 100],
[202, 107],
],
// 上中點(diǎn)
[
[205, 275],
[263, 275],
],
[
[206, 267],
[284, 267],
[284, 413],
[295, 413],
],
[
[208, 257],
[605, 257],
[605, 262],
],
[
[486, 272],
[582, 272],
[582, 307],
],
[
[563, 486],
[582, 486],
[582, 440],
],
// 底部點(diǎn)合集
[
[113, 123],
[113, 59],
[625, 59],
],
[
[677, 59],
[727, 59],
[727, 67],
[813, 67],
]
]
}
},
speed: { // 速度
type: Number,
default: 7
}
},
data () {
return {
myChart: '',
// 注意:因?yàn)閳D片在現(xiàn)實(shí)的時(shí)候可能會(huì)拉伸,所以設(shè)置actualWH和imgWH兩個(gè)變量
actualWH: {
width: 0,
height: 0
},
timer: null
}
},
mounted () {
this.actualWH = { // 渲染盒子的大小
width: this.$el.clientWidth,
height: this.$el.clientHeight
}
this.myChart = this.$echarts.init(document.getElementById(this.id))
this.draw()
this.eventListener(true)
},
methods: {
getLines(){
return {
type: 'lines',
coordinateSystem: 'cartesian2d',
// symbol:'arrow',
zlevel: 1,
symbol: ['none', 'none'],
polyline: true,
silent: true,
effect: {
symbol: 'arrow',
show: true,
period: this.speed, // 箭頭指向速度霉旗,值越小速度越快
trailLength: 0.01, // 特效尾跡長(zhǎng)度[0,1]值越大痴奏,尾跡越長(zhǎng)重
symbolSize: 5, // 圖標(biāo)大小
},
lineStyle: {
width: 1,
normal: {
opacity: 0,
curveness: 0.4, // 曲線的彎曲程度
color: '#3be3ff'
}
},
}
},
getOption () {
// 點(diǎn)合集-在圖片上一個(gè)一個(gè)量的蛀骇,注意以渲染盒子左下角為原點(diǎn),點(diǎn)取值方法:以圖片左下角為原點(diǎn)读拆,量幾個(gè)線段點(diǎn)的(x,y)
let dotsArr = this.dotsArr
// 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 start
dotsArr.map(item => {
item.map(sub => {
if (Object.prototype.toString.call(sub) !== '[object Object]') { // item可能配置了當(dāng)前這組點(diǎn)的運(yùn)動(dòng)時(shí)間
sub[0] = (this.actualWH.width / this.imgWH.width) * sub[0] // x值
sub[1] = (this.actualWH.height / this.imgWH.height) * sub[1] // y值
}
})
})
// 點(diǎn)的處理-量圖上距離轉(zhuǎn)換為在渲染盒子中的距離 end
// 散點(diǎn)圖和lines繪制 start
let scatterData = []
let linesData = [] // 默認(rèn)路徑圖點(diǎn)的路徑
let seriesLines = [] // 路徑圖
dotsArr.map(item => {
if (Object.prototype.toString.call(item[0]) === '[object Object]') { // 單獨(dú)配置路徑
let cArr = item.slice(1)
if (!cArr.length) return // 無(wú)數(shù)據(jù)跳過(guò)
scatterData = scatterData.concat(cArr) // 散點(diǎn)圖data
let opt = {
...this.getLines(),
zlevel: 2,
data: [{
coords: cArr
}]
}
// 配置
item[0]['symbol'] && (opt.effect.symbol = item[0]['symbol'])
item[0]['speed'] && (opt.effect.period = item[0]['speed'])
item[0]['color'] && (opt.lineStyle.normal.color = item[0]['color'])
// 可以更改成下面這種-傳入配置項(xiàng)
// opt = merge(opt,item[0])
seriesLines.push(opt)
} else { // 使用默認(rèn)路徑配置
scatterData = scatterData.concat(item) // 散點(diǎn)圖data
linesData.push({
coords: item
})
}
})
// 默認(rèn)路徑圖
linesData && linesData.length && seriesLines.push({
...this.getLines(),
data: linesData
})
// 散點(diǎn)圖和lines繪制 end
let option = {
backgroundColor: 'transparent',
xAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.width,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
},
yAxis: {
type: 'value',
show: false,
min: 0,
max: this.actualWH.height,
axisLine: {
lineStyle: {
color: 'red'
}
},
splitLine: {
lineStyle: {
color: 'red'
}
}
// type: 'category'
},
grid: {
left: '0%',
right: '0%',
top: '0%',
bottom: '0%',
containLabel: false
},
series: [
// 多段點(diǎn)
{
zlevel: 2,
symbolSize: 0,
data: scatterData,
type: 'scatter'
},
...seriesLines
]
};
return option
},
// 繪制圖表
draw () {
this.myChart.clear()
this.resetChartData()
},
// 刷新數(shù)據(jù)
resetChartData () {
this.myChart.setOption(this.getOption(), true)
},
// 擅憔。。建椰。雕欺。。 resize 相關(guān)優(yōu)化 start 棉姐。屠列。。伞矩。笛洛。。
clearTimer(){
this.timer && clearTimeout(this.timer)
this.timer = null
},
eventListener(bool){
if (!bool) { // 銷毀
window.removeEventListener('resize', this._eventHandle)
this.clearTimer()
} else {
window.addEventListener('resize', this._eventHandle, false)
}
},
// 優(yōu)化-添加resize
_eventHandle(){
this.clearTimer()
this.timer = setTimeout(() => {
this.clearTimer();
this.$nextTick(() => {
this.myChart && this.myChart.resize()
})
}, 500)
},
// 乃坤。苛让。。湿诊。狱杰。 resize 相關(guān)優(yōu)化 end 。厅须。仿畸。。朗和。错沽。
},
beforeDestroy () {
this.myChart && this.myChart.dispose()
this.eventListener() // 銷毀
}
}
</script>
<style scoped>
.chart-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
頁(yè)面報(bào)錯(cuò)
如果有的小伙伴引入報(bào)錯(cuò),看下是不是echarts未引入眶拉。注意我代碼中是用的this.$echarts
千埃,因?yàn)槲业膃charts是全局引入的,如果你要每次都引入可改為let echarts = require("echarts")
或者 import * as echarts from 'echarts';
然后下面的this.$echarts
改為echarts
即可忆植。還有echarts版本是否為4.4.0及其以上放可。
寫(xiě)在最后
總算寫(xiě)完了,這是我的第一篇博客朝刊,但不會(huì)是最后一篇耀里,如果對(duì)你有幫助的話請(qǐng)留個(gè)關(guān)注,謝謝啦坞古。