使用canvas结合贝塞尔曲线实现,效果如下
<template><div class="box"><div class="mapBox"><div class="map"><img src="/img/dataCockpit/map.png" alt="" /><div class="dot"><div class="title">地图</div><img src="/img/dataCockpit/dotIcon.png" alt="" /></div></div><!-- 线 --><canvas id="canvas" class="canvas"></canvas><!-- 点 --><canvas id="canvasPoint" class="canvas"></canvas><!-- 动态效果 --><canvas id="canvasMove" class="canvas"></canvas><img class="airport" id="airportIcon" src="" alt="" /></div></div>
</template><script>
export default {name: "homePage",data() {return {canvas: null,canvasPoint: null,canvasMove: null,center: {}, // 迁徙线起点位置directionArr: [], // 迁徙线终点位置endKeep: [], // 保存一下各个迁徙线起点end: [], // 运动中的各迁徙线时间p时所在位置p: 0, // 时间记录,每到1时变为0step: 0.005, // 时间每次递增量animationSpeed: 0.03, // 点动画效果圆圈每次增加量dotNumber: 25, // 动画迁徙线 动态的线的部分由多少个点组成rate: 1.053, // 1.033 贝塞尔曲线计算时用到的参数requestAnimationFrameName: "",compareData: [// 用于临时计算各终点位置的参数// { x: 0.65, y: 0.89 },// { x: 0.094, y: 0.76 },// { x: 0.95, y: 0.28 },// { x: 0.19, y: 0.19 },// { x: 0.49, y: 0.08 },],radius: 1, // 航路点半径radiusRing: 1,radiusRingMin: 1,radiusRingMax: 25, // 最大设为25时,涟漪消失的不会很突兀dotColor: "243,254,193",ringColor: "rgba(236,210,32,0.5)",plane: null,};},mounted() {this.plane = document.getElementById("airportIcon");this.init();},methods: {init() {// 获取需要画布达到的宽高数据const mapBox = document.getElementsByClassName("mapBox")[0];const width = mapBox.offsetWidth;const height = mapBox.offsetHeight;// 拿到三个画布,给定宽高const canvas = document.getElementById("canvas");const canvasPoint = document.getElementById("canvasPoint");const canvasMove = document.getElementById("canvasMove");canvas.width = width;canvas.height = height;canvasPoint.width = width;canvasPoint.height = height;canvasMove.width = width;canvasMove.height = height;this.canvas = canvas.getContext("2d");this.canvasPoint = canvasPoint.getContext("2d");this.canvasMove = canvasMove.getContext("2d");// 找到所有迁徙线起点,this.center = {x: Math.ceil(width * 0.66),y: Math.ceil(height * 0.74),};// 迁徙线终点this.directionArr = [{// 双湖县x: Math.ceil(width * 0.32),y: Math.ceil(height * 0.46),},{// 尼玛县x: Math.ceil(width * 0.14),y: Math.ceil(height * 0.66),},{// 申扎县x: Math.ceil(width * 0.36),y: Math.ceil(height * 0.76),},{// 班戈县x: Math.ceil(width * 0.5),y: Math.ceil(height * 0.76),},{// 安多县x: Math.ceil(width * 0.56),y: Math.ceil(height * 0.56),},{// 聂荣县x: Math.ceil(width * 0.74),y: Math.ceil(height * 0.6),},{// 巴青县县x: Math.ceil(width * 0.84),y: Math.ceil(height * 0.64),},{// 索县x: Math.ceil(width * 0.93),y: Math.ceil(height * 0.72),},{// 比如县x: Math.ceil(width * 0.8),y: Math.ceil(height * 0.72),},{// 嘉黎县x: Math.ceil(width * 0.73),y: Math.ceil(height * 0.82),},];// 各线终点 以下仅为参考,具体以项目要求为准for (let i = 0; i <= 10; i++) {// this.directionArr[i] = {// x: Math.ceil(width * this.compareData[i].x),// y: Math.ceil(height * this.compareData[i].y),// };this.endKeep[i] = {x: this.center.x,y: this.center.y,};}this.end = JSON.parse(JSON.stringify(this.endKeep));// 画线开始this.drawAllLine();},drawAllLine() {// 根据每个点分别画线this.directionArr.forEach((item) => {this.drawLine(item);});this.drawMove();},drawLine({ x, y }) {this.canvas.beginPath();this.canvas.moveTo(this.center.x, this.center.y); // 起始点(x,y)// 计算贝塞尔曲线控制点位置const coord = this.calcCp([x, y], [this.center.x, this.center.y]);this.canvas.quadraticCurveTo(coord.x, coord.y, x, y); //创建二次贝塞尔曲线// 线宽1this.canvas.lineWidth = 2.5;// 线颜色this.canvas.strokeStyle = "#5cb85c";this.canvas.stroke();this.canvas.closePath();},drawPoint(x1, y1) {// 最里圈小圆this.canvasPoint.fillStyle = `rgba(${this.dotColor}, 1)`;this.canvasPoint.beginPath();this.canvasPoint.arc(x1, y1, this.radius, 0, 2 * Math.PI);this.canvasPoint.closePath();this.canvasPoint.fill();// 外层小圆this.canvasPoint.fillStyle = `rgba(${this.dotColor}, 0.3)`;this.canvasPoint.beginPath();this.canvasPoint.arc(x1, y1, this.accAdd(this.radius, 3), 0, 2 * Math.PI);this.canvasPoint.closePath();this.canvasPoint.fill();// 以下为涟漪部分if (this.radiusRing >= this.radiusRingMax) {this.radiusRing = this.radiusRingMin;}this.canvasPoint.fillStyle = this.ringColor;this.canvasPoint.beginPath();this.canvasPoint.arc(x1, y1, this.radiusRing, 0, 2 * Math.PI);this.canvasPoint.closePath();this.canvasPoint.fill();// this.radiusRing += 0.03;this.radiusRing += this.animationSpeed;this.ringColor =this.ringColor.split(",").slice(0, 3).join(",") +"," +(0.5 - (this.radiusRing - this.radiusRingMin) * 0.02) +")";},drawMivie(index) {// 获取当前时间p时贝塞尔曲线的x, y点const coord = this.calcCp([this.directionArr[index].x, this.directionArr[index].y],[this.center.x, this.center.y]);const x = this.calcRightNow(this.p,this.center.x,coord.x,this.directionArr[index].x);const y = this.calcRightNow(this.p,this.center.y,coord.y,this.directionArr[index].y);this.canvasMove.beginPath();this.canvasMove.moveTo(this.end[index].x, this.end[index].y);this.canvasMove.lineTo(x, y);const gnt1 = this.canvasMove.createLinearGradient(this.end[index].x,this.end[index].y,x,y);gnt1.addColorStop(0, "#fff");gnt1.addColorStop(1, "#ECD220");this.canvasMove.strokeStyle = gnt1;this.canvasMove.lineWidth = 1;this.canvasMove.stroke();// this.canvasMove.closePath();for (var i = 0; i < this.dotNumber; i++) {let _t =this.p - this.step * i * 2 >= 0? this.p - this.step * i * 2: 1 + (this.p - this.step * i * 2);const coord1 = this.calcCp([this.directionArr[index].x, this.directionArr[index].y],[this.center.x, this.center.y]);const x1 = this.calcRightNow(_t,this.center.x,coord1.x,this.directionArr[index].x);const y1 = this.calcRightNow(_t,this.center.y,coord1.y,this.directionArr[index].y);this.canvasMove.fillStyle ="rgba(" + this.dotColor + "," + (1 - (1 / this.dotNumber) * i) + ")";this.canvasMove.beginPath();this.canvasMove.arc(x1, y1, 1, 0, 2 * Math.PI);this.canvasMove.fill();this.canvasMove.closePath();}// 加个小飞机图标飞起来const xx = this.calcRightNow(this.p + this.step * 3,this.center.x,coord.x,this.directionArr[index].x);const yy = this.calcRightNow(this.p + this.step * 2,this.center.y,coord.y,this.directionArr[index].y);const img = this.createIcon(xx, yy, index);this.canvasMove.drawImage(img, xx - 8, yy - 8);this.end[index].x = x;this.end[index].y = y;},// 获取当前时间p时贝塞尔曲线的x, y点, 此方法不区分x ycalcRightNow(p, start, controlPoint, end) {return (Math.pow(1 - p, 2) * start +2 * p * (1 - p) * controlPoint +Math.pow(p, 2) * end);},getAngle(x, y) {var radian = Math.atan(y / x); // 弧度var angle = Math.floor(180 / (Math.PI / radian)); // 弧度转角度if (x < 0) {// x小于0的时候加上180°,即实际角度angle = angle + 180;}return angle;},createIcon(x, y, index) {const deg = this.getAngle(x - this.end[index].x, y - this.end[index].y);const c = document.createElement("canvas");c.width = 16;c.height = 16;const cCtx = c.getContext("2d");cCtx.translate(8, 8);if (y < this.end[index].y &&((Math.abs(deg) > 80 && Math.abs(deg) < 91) || (deg > 240 && deg < 270))) {cCtx.drawImage(this.plane, -8, -8);} else if (x >= this.end[index].x && y < this.end[index].y) {cCtx.rotate(((-deg + 20) * Math.PI) / 180);cCtx.drawImage(this.plane, -8, -8);cCtx.rotate(((deg - 20) * Math.PI) / 180);} else if (x < this.end[index].x && y < this.end[index].y) {cCtx.rotate(((-deg + 160) * Math.PI) / 180);cCtx.drawImage(this.plane, -8, -8);cCtx.rotate(((deg - 160) * Math.PI) / 180);} else if (x < this.end[index].x && y >= this.end[index].y) {cCtx.rotate(((-deg + 45) * Math.PI) / 180);cCtx.drawImage(this.plane, -8, -8);cCtx.rotate(((deg - 45) * Math.PI) / 180);} else {cCtx.rotate(((225 - deg) * Math.PI) / 180);cCtx.drawImage(this.plane, -8, -8);cCtx.rotate(((deg - 225) * Math.PI) / 180);}return c;},drawMove() {cancelAnimationFrame(this.requestAnimationFrameName);// 动态线的画布this.canvasMove.clearRect(0, 0, 10000, 10000);if (this.p >= 1) {this.p = this.step;this.end = JSON.parse(JSON.stringify(this.endKeep));}// 点的画布this.canvasPoint.clearRect(0, 0, 10000, 10000);this.drawPoint(this.center.x, this.center.y);this.directionArr.forEach((item, index) => {this.drawMivie(index);this.drawPoint(item.x, item.y);});this.p = this.accAdd(this.p, this.step);this.requestAnimationFrameName = requestAnimationFrame(this.drawMove);},/** num: 要被转换的数字* exnum: 当前中心坐标 不一定是x还是y*/calcCp(start, end) {let middleX = 0;let middleY = 0;if (start[0] > end[0] && start[1] > end[1]) {middleX = ((start[0] + end[0]) / 2) * this.rate;middleY = ((start[1] + end[1]) / 2) * (2 - this.rate);}if (start[0] > end[0] && start[1] < end[1]) {middleX = ((start[0] + end[0]) / 2) * this.rate;middleY = ((start[1] + end[1]) / 2) * this.rate;}if (start[0] < end[0] && start[1] > end[1]) {middleX = ((start[0] + end[0]) / 2) * (2 - this.rate);middleY = ((start[1] + end[1]) / 2) * (2 - this.rate);}if (start[0] < end[0] && start[1] < end[1]) {middleX = ((start[0] + end[0]) / 2) * (2 - this.rate);middleY = ((start[1] + end[1]) / 2) * this.rate;}return {x: middleX,y: middleY,};},accAdd(arg1, arg2) {let r1, r2, m;try {r1 = arg1.toString().split(".")[1].length;} catch (e) {r1 = 0;}try {r2 = arg2.toString().split(".")[1].length;} catch (e) {r2 = 0;}m = Math.pow(10, Math.max(r1, r2));return (arg1 * m + arg2 * m) / m;},},
};
</script><style lang="scss" scoped>
.box {// background-color: #333;// height: 100vh;
}
.mapBox {// margin: 100px;position: relative;display: flex;align-items: center;justify-content: center;.map {width: 892px;height: 656px;position: relative;img {width: 100%;height: 100%;}.dot {width: 200px;display: flex;flex-direction: column;align-items: center;position: absolute;z-index: 99;top: 399px;left: 489px;animation: jump 4s infinite ease-in;.title {width: 180px;line-height: 38px;background: rgba(0, 4, 7, 0.79);border-radius: 19px;color: #ffffff;font-size: 18px;text-align: center;}img {width: 56px;}@keyframes jump {0% {top: 396px;transform: translateY(0);}50% {transform: translateY(-20%);}100% {top: 396px;transform: translateY(0);}}}}.canvas {position: absolute;top: 0;left: 0;}.airport {width: 16px;height: 16px;z-index: -1;position: absolute;}
}
</style>