使用canvas在图片上画圆圈,并可点击圆圈在圆圈下方画垂直线(适配移动端)

一、UI

二、实现

<div class="carBrightSpot">
      <!-- 轮播图 -->
      <div class="block-swiper">
        <div class="wrapper">
          <swiper :options="swiperOption" id="swiper">
            <swiper-slide v-for="(item,index) of carImageResult" :key="item.id">
                    <!-- <img class="swiper-img" :src="item.image"> -->
                  <canvas  width="800" height="600" :ref="index" @click="toLineBottom(index)"></canvas>
            </swiper-slide>
          </swiper>
        </div>
        <!-- 分页器 -->
        <div class="myPagination">
          <div class="swiper-pagination" slot="pagination"></div>
        </div>
      </div>
      <!-- 热区的亮点 -->
      <div class="brightSpot">
        <div class="oneBrightSpot" v-if="curCarHotspotPositionResultList.length==0">请点击</div>
        <div class="oneBrightSpot" v-else @click="goDetail(item.id)" v-for="item in curCarHotspotPositionResultList">{{item.postName}}</div>
      </div>
    </div>
 // 车亮点
      carImageResult:[{
        carHotspotResultList:[]
      }],
      // 当前点击圆点的亮点数组
      curCarHotspotPositionResultList:[],
      pageNumber:1,
      pageSize:100,
      // 背景图片缓存
      imgs: [new Image(),new Image(),new Image()], 
      // canvas对象数组
      contexts: [], 
      // 保存位置数组
      allPositionArr:[],
      // 保存未点击状态的canvas
      canvasHistory:[]
// 轮播图选项
      swiperOption: {
        pagination: ".swiper-pagination", //分页器
        paginationClickable: true, //可点击原点切换
        autoplayDisableOnInteraction: false, //用户操作swiper之后,是否禁止autoplay。默认为true:停止。如果设置为false,用户操作swiper之后自动切换不会停止,每次都会重新启动autoplay。
        // loop: true,//可循环(到达最后一张继续切换从第一张开始滑动)
        // autoplay: 1500,//可选选项,自动切换的时间间隔(单位ms),不设定该参数slide不会自动切换。
        //用于切换时,去掉其他轮播图的垂直线(若存在)
        onSlideChangeEnd:(swiper)=>{
          //用于切换右上角显示的该底图的全部热点区域
          let swiperActiveIndex = swiper.activeIndex;
          this.activeIndex = swiperActiveIndex;
          let vm = this;
          // 切换时回归状态
          // 清空当前点击圆点亮点数组
          vm.curCarHotspotPositionResultList = [];
          // console.log("ss",this.activeIndex);
          // alert(swiper.activeIndex);
          // 重绘无线画面
          let canvasWidth = vm.getChangeFit(690,'w');
          let canvasHeight = vm.getChangeFit(630,'h');
          let canvasHistory = vm.canvasHistory;
          canvasHistory.map((item,index)=>{
            if(index==swiperActiveIndex){
              return;
            }
            let img = new Image();
            // 获取保存的无线画布状态
            img.src = item;
            img.onload = function(){
                // 清除画布
                vm.contexts[index].clearRect(0, 0, canvasWidth, canvasHeight);
                // 重绘画布
                vm.contexts[index].drawImage(img,0,0);
            }
          })
        }
      },
// 获取数据
    getCatImageList(){
          const vm =this;
                let params={
                    pageNumber:vm.pageNumber,
                    pageSize:vm.pageSize,
                    moduleId:vm.moduleId,
                    carId:vm.carId,
                }
                this.api(vm, '/api/app/car/getCatImageList.json', params, function (res) {
                  let data = res;
                  vm.carImageResult = data;
                  vm.$nextTick(()=>{
                        data.map((item,index)=>{
                        vm.imgs[index].src = item.image;
                        // vm.img.src = res[1].image;
                      const canvas = vm.$refs[index][0];
                      // 设置画布大小
                      // UI图轮播图宽度
                      let canvasWidth = vm.getChangeFit(690,'w');
                      // 这个距离为UI图轮播图上边框到下面文本框上边框的距离(为了画垂直线到文本框上面)
                      let canvasHeight = vm.getChangeFit(630,'h');
                      // 动态设置canvas画布的宽高
                      canvas.setAttribute('width',canvasWidth+'px');
                      canvas.setAttribute('height',canvasHeight+'px');
                      // 获取2D上下文
                      vm.contexts.push(canvas.getContext('2d'));
                      // 获取图片大小(UI轮播图大小)
                      let width = vm.getChangeFit(690,'w');
                      let height = vm.getChangeFit(516,'h');
                      // 绘制图片
                      vm.imgs[index].onload = function(){
                            let orginWidth = vm.imgs[index].width;
                            let orginHeight = vm.imgs[index].height;
                            // console.log("sdfsd",orginWidth,orginHeight);
                            vm.contexts[index].drawImage(vm.imgs[index],0,0,width,height);
                            // // 绘制圆点
                            vm.drawCircle(index,width,height,orginWidth,orginHeight);
                      }
                      })
                      })
                },function(err){
                    // vm.fallBack = true;
                })
    },
    // 适配单位
    getChangeFit(uiNumber,type){
      let clientWidth = document.documentElement.clientWidth;
      let clientHeight = document.documentElement.clientHeight;
      let changeNumber = '';
      if(type=='w'){
        changeNumber = Math.floor(clientWidth*uiNumber/750);
      }else{
        changeNumber = Math.floor(clientHeight*uiNumber/1200);
      }
      return changeNumber;
    },
    // 适配圆点坐标
    getImgFit(ox,oy,originImgWidth,originImgHeight,curImgWidth,curImgHeight){                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
      let x = Math.floor(curImgWidth*ox/originImgWidth);
      let y = Math.floor(curImgHeight*oy/originImgHeight);
      return {x:x,y:y};
    },
// 画一张底图全部圆
    drawCircle(index,curImgWidth,curImgHeight,originImgWidth,originImgHeight){
      const vm = this;
      // 该底图中的全部圆点的坐标
      let positionArr = vm.carImageResult[index].carHotspotResultList;
      // 绘制圆点
      positionArr.map(item=>{
          //开始绘制新路径(画圆)
          // 计算坐标换算
          let point = vm.getImgFit(item.x,item.y,originImgWidth,originImgHeight,curImgWidth,curImgHeight);
          let x = point.x;
          let y = point.y;
          // 将换算后的点放入保存所有坐标点的数据
          // console.log("vm.allPositionArr.length",vm.allPositionArr.length,index);
          
          if(vm.allPositionArr.length<index+1){
            // 第一次创建数组
            vm.allPositionArr[index] = [];
            vm.allPositionArr[index].push({x,y});
          }else{
            // 第一次之后
            vm.allPositionArr[index].push({x,y});
          }
          // 绘制圆
          vm.contexts[index].beginPath();
          vm.contexts[index].arc(x, y, 7, 0, 2*Math.PI, false);
          vm.contexts[index].closePath(); //关闭路径
          vm.contexts[index].strokeStyle = "rgb(20, 193, 253)"; // 描边颜色为蓝色
          vm.contexts[index].lineWidth = 2; //指定描边线的宽度
          //以填充方式绘制圆
          vm.contexts[index].stroke();
      })
      const canvas = vm.$refs[index][0];
      // 保存无线状态的canvas画布上的图形
      vm.canvasHistory.push(canvas.toDataURL())
    },
    // 画线
    drawLine(sx,sy,ex,ey,index,pindex){
      const vm = this;
      vm.curCarHotspotPositionResultList = vm.carImageResult[index].carHotspotResultList[pindex].carHotspotPositionResultList;
      // 重绘无线画面
      let canvasWidth = vm.getChangeFit(690,'w');
      let canvasHeight = vm.getChangeFit(630,'h');
      let img = new Image();
      // 获取保存的无线画布状态
      img.src = vm.canvasHistory[index];
      img.onload = function(){
          // 清除画布
          vm.contexts[index].clearRect(0, 0, canvasWidth, canvasHeight);
          // 重绘画布
          vm.contexts[index].drawImage(img,0,0);
          // 绘制直线路径
          vm.contexts[index].beginPath();
          // console.log("contexts",vm.contexts[index]);
          vm.contexts[index].moveTo(sx, sy); //画线的起点
          vm.contexts[index].lineTo(ex, ey); //终点
          vm.contexts[index].closePath();
          vm.contexts[index].strokeStyle = "rgb(20, 193, 253)";
          vm.contexts[index].stroke();
       }
      
    },
    // 点击圆点画线
    toLineBottom(cindex){
    // 点击的底图当前所有圆点坐标
    let curPosition = this.allPositionArr[cindex];
    // 获取鼠标点击的坐标
    let position = this.getEventPosition(event,cindex);
    // 计算鼠标点击的坐标和每个圆点的距离来判断是否点击到该圆点
    curPosition.map((item,index)=>{
      // 两点距离小于半径则是点击该圆点
      if(this.calculateDistance(item.x,item.y,position.x,position.y)){
        // 画垂直线
        // 这里1000足够大即可,超出部分画布不会显示的,y+6是得再圆点坐标下面即圆边开始画线
        this.drawLine(item.x,item.y+6,item.x,1000,cindex,index)
      }
    })
    },
    // 获取鼠标点击位置
    getEventPosition(ev,index){
      // 轮播图所需(一张图的宽度)
      let canvasWidth = this.getChangeFit(690,'w');
      var x, y;
      if (ev.layerX || ev.layerX == 0) {
        // 轮播图第二、三。。张图距离左边的距离需要加上前面相应张数轮播图的宽度大小之和
        x = ev.layerX +canvasWidth*index;
        y = ev.layerY;
      } else if (ev.offsetX || ev.offsetX == 0) { // Opera
        x = ev.offsetX+canvasWidth*index;
        y = ev.offsetY;
      }
      return {x: x, y: y};
    },
    // 计算点和点距离(计算是否点击到圆)
    calculateDistance(sx,sy,ex,ey){
      var dis = Math.sqrt((sx - ex) * (sx - ex) + (sy - ey) * (sy - ey));//Math.sqrt()求平方跟
      if(dis <= 7){
        return true;
      }else{
        return false
      }
    }

三、总结

步骤

(1)首先在每个轮播图里放一个canvas画布。用于每个画布上绘制背景图和圆圈和垂直线。

(2)在每个画布上绘制轮播图,轮播图的大小因为移动端要适配宽高,所有需要使用一个函数去计算,实际宽高=UI中宽高比例*实际屏幕宽高。

(3)画布的大小也需要计算,宽度与轮播图宽度一致,高度需要从轮播图上边框的距离到提示框上边框的的距离进行比例计算。(用于后面限制绘制的垂直线的长度)

(4)绘制圆圈,需要坐标X,Y,这里后台给的是相对于原图的X,Y故需要获取原图的宽高,再进行比例计算。当前宽高=轮播图宽高*x/y坐标相对于原图宽高的比例。同时使用数组将转化后的X,Y坐标保存在一个数组中,用来后面计算距离。

(5)同时因为有3个轮播图,每个轮播图中都有一个画布,故需要用一个数组去存储创建的canvas的2D上下文用于绘制。这样也保证每个canvas的绘制不会互相干扰。

(6)点击圆点并画垂直线。

1)获取鼠标点击时相对于点击容器的X,Y坐标。

2)将当前底图中所有点和鼠标点击的坐标进行计算,计算两点的距离,若两点距离小于半径,则视为点击了该圆圈。

3)以该点击的圆的X,Y坐标为基准,Y坐标增加一些后以该为起点绘制一条垂直线,终点只要保证X与起点一致,Y可无限大,因为画布就那么大超出不会显示。

(7)由于还有轮播图切换的原因,需要在每次切换时让所有轮播图片的画面回归初始画面,即没有垂直线。故需要在绘制每张轮播图及其圆点时保存当前绘制的状态,即使用canvas.toDataURL()将当前状态保存为一个URL,并存入数组,在轮播图切换的时候再取出来使用图片去绘制画布即可回归最迟没有垂直线的状态。

四、新需求:圆点闪烁,添加子节点

圆点闪烁原理:

(1)使用两个数组去保存三个canvas不断重绘时的状态(快照),两个数组只有绘制的颜色不一,其他都一样。同时点击绘制线的时候去刷新这两个数组对应canvas保存的快照。

(2)使用定时器去不断使用这两个数组去重绘即可实现圆点的闪烁,其实就是不断的重绘。

(3)同时还需要用2个数组去保存最初的canvas状态,用于绘制线和切换轮播图的时候使用。

添加子节点原理:

使用负外边距去连接线

同时由于canvas绘图清晰度问题将画布变为原本的两倍,相应绘制其他的也要相应变成2倍,如点图片的宽高。

// 背景图片缓存
      imgs: [],
      // 背景图片缓存
      imgs2: [],
      // canvas对象数组
      contexts: [],
      // 保存位置数组
      allPositionArr: [],
      // 保存未点击状态的canvas
      canvasHistory: [],
      // 保存两种颜色的最终画布状态
      canvasFirstColor:[],
      canvasSecondColor:[],
      // 保存两种颜色的无线画布状态
      canvasFirstColorNoline:[],
      canvasSecondColorNoline:[],
      // 两种颜色的圆圈
      firstColor:'rgb(20, 193, 253)',
      secondColor:'rgb(8, 166, 219)',
      // 切换两个颜色数组
      type:1,
// 获取数据
    getCatImageList() {
      const vm = this;
      let params = {
        pageNumber: vm.pageNumber,
        pageSize: vm.pageSize,
        moduleId: vm.moduleId,
        carId: vm.carId
      };
      this.api(
        vm,
        "/api/app/car/getCatImageList.json",
        params,
        function(res) {
          let data = res;
          // 初始化显示隐藏二级
          data.map(item=>{
            item.carHotspotResultList.map(item2=>{
                item2.carHotspotPositionResultList.map(item3=>{
                  item3.show = false;
                })
            })
          })
          // console.log("data",data);
          data.map((item,index)=>{
            // 初始化图片缓存和数组
              vm.imgs.push(new Image());
              vm.imgs2.push(new Image());
              vm.allPositionArr[index] = [];
          })
          vm.carImageResult = data;
          if(data){
              vm.$nextTick(() => {
                // vm.getRatio();
                // // 画第一个张颜色圆
                vm.draw(vm.firstColor,1);
                // // // 画第二张颜色圆
                setTimeout(()=>{
                  vm.draw(vm.secondColor,2);
                  vm.toShy();
                },500)
              });
          }
        },
        function(err) {
          // vm.fallBack = true;
        }
      );
    },
    // 闪烁
    toShy(){
      const vm = this;
      setInterval(()=>{
        if(vm.type==1){
          vm.type=2
        }else{
          vm.type=1;
        }
        vm.reDraw(vm.type);
      },500)
    },
 // 重绘
    reDraw(type){
      let vm = this;
          // 清除画布
          let canvasWidth = vm.getChangeFit(vm.canvasWidth, "w");
          let canvasHeight = vm.getChangeFit(vm.canvasHeight, "h");
          let canvasFirstColor = null;
          if(type==1){
            canvasFirstColor = vm.canvasFirstColor;
          }else{
             canvasFirstColor = vm.canvasSecondColor;
          }
          canvasFirstColor.map((item, index) => {
            let img = new Image();
            // 获取保存的无线画布状态
            img.src = item;
            img.onload = function() {
              // 清除画布
              vm.contexts[index].clearRect(0, 0, canvasWidth*2, canvasHeight*2);
              // 重绘画布
              vm.contexts[index].drawImage(img, 0, 0);
            };
          });
    },
    draw(color,type) {
      const vm = this;
      let data = vm.carImageResult;
      data.map((item, index) => {
        if(type==1){
          vm.imgs[index].src = item.image;
        }else{
          vm.imgs2[index].src = item.image;
        }
        // vm.img.src = res[1].image;
        if(type==1){
          const canvas = vm.$refs[index][0];
          // 设置画布大小
          // UI图轮播图宽度
          let canvasWidth = vm.getChangeFit(vm.canvasWidth, "w") * 2;
          // 这个距离为UI图轮播图上边框到下面文本框上边框的距离(为了画垂直线到文本框上面)
          let canvasHeight = vm.getChangeFit(vm.canvasHeight, "h") * 2;
          // 动态设置canvas画布的宽高
          canvas.setAttribute("width", canvasWidth + "px");
          canvas.setAttribute("height", canvasHeight + "px");
          canvas.style.width = vm.getChangeFit(vm.canvasWidth, "w")+ 'px';
          canvas.style.height =  vm.getChangeFit(vm.canvasHeight, "h")+ 'px';
          // 获取2D上下文
          const context = canvas.getContext("2d");
          vm.contexts.push(context);
        }
        // 获取图片大小(UI轮播图大小)
        let width = vm.getChangeFit(vm.imgWidth, "w")*2;
        let height = vm.getChangeFit(vm.imgHeight, "h")*2;
        if(type==1){
          // 绘制图片
          vm.imgs[index].onload = function() {
            let orginWidth = vm.imgs[index].width;
            let orginHeight = vm.imgs[index].height;
            vm.contexts[index].drawImage(vm.imgs[index], 0, 0, width, height);
            // // 绘制圆点
            vm.drawCircle(index, width, height, orginWidth, orginHeight,color,type);
          };
        }else{
          // 绘制图片
          vm.imgs2[index].onload = function() {
            let orginWidth = vm.imgs[index].width;
            let orginHeight = vm.imgs[index].height;
            vm.contexts[index].drawImage(vm.imgs[index], 0, 0, width, height);
            // // 绘制圆点
            vm.drawCircle(index, width, height, orginWidth, orginHeight,color,type);
          };
        }
      });
    },
// 画一张底图全部圆
    drawCircle(
      index,
      curImgWidth,
      curImgHeight,
      originImgWidth,
      originImgHeight,
      color,type
    ) {
      // alert("type",type);
      const vm = this;
      // 该底图中的全部圆点的坐标
      let positionArr = vm.carImageResult[index].carHotspotResultList;
      // 绘制圆点
      if(positionArr){
      positionArr.map(item => {
        let carHotspotPositionResultList =
          item.carHotspotPositionResultList.length;
        if (carHotspotPositionResultList == 0) {
          return;
        }
        //开始绘制新路径(画圆)
        // 计算坐标换算
        let point = vm.getImgFit(
          item.x,
          item.y,
          originImgWidth,
          originImgHeight,
          curImgWidth,
          curImgHeight
        );
        let x = point.x;
        let y = point.y;
        // 将换算后的点放入保存所有坐标点的数据
        // console.log("vm.allPositionArr.length",vm.allPositionArr.length,index);
        //只存一次
        if(type==1){
            vm.allPositionArr[index].push({ x, y });
        }
        // 绘制圆
        vm.contexts[index].beginPath();
        vm.contexts[index].arc(x, y, vm.radius, 0, 2 * Math.PI, false);
        vm.contexts[index].closePath(); //关闭路径
        vm.contexts[index].strokeStyle = color; // 描边颜色为蓝色     
        vm.contexts[index].lineWidth = 7; //指定描边线的宽度
        //以填充方式绘制圆
        vm.contexts[index].stroke();
      });
      }
      const canvas = vm.$refs[index][0];
      // 保存无线状态的canvas画布上的图形
      
      if(type==1){
        vm.canvasFirstColor[index] = canvas.toDataURL();
        vm.canvasFirstColorNoline[index] = canvas.toDataURL();
        // alert("canvasFirstColor",vm.canvasFirstColor.length);
      }else{
        // alert("canvasSecondColor in",vm.canvasSecondColor.length);
        vm.canvasSecondColor[index] = canvas.toDataURL();
        vm.canvasSecondColorNoline[index] = canvas.toDataURL();
        // alert("canvasSecondColor out",vm.canvasSecondColor.length);
      }
    },
// 画线222
    drawLine2(sx, sy, ex, ey, index, pindex) {
      // console.log("ssdrawLine2");
      const vm = this;
      vm.curCarHotspotPositionResultList =
       vm.carImageResult[index].carHotspotResultList[pindex].carHotspotPositionResultList;
      // 重绘第一种颜色无线画面
      let canvasWidth = vm.getChangeFit(vm.canvasWidth, "w");
      let canvasHeight = vm.getChangeFit(vm.canvasHeight, "h");
      let img = new Image();
      let img2 = new Image();
      // 第一张
      // 获取保存的无线画布状态
      img.src = vm.canvasFirstColorNoline[index];
      img.onload = function() {
        // 清除画布
        vm.contexts[index].clearRect(0, 0, canvasWidth*2, canvasHeight*2);
        // 重绘画布
        vm.contexts[index].drawImage(img, 0, 0);
        // 绘制直线路径
        vm.contexts[index].beginPath();
        vm.contexts[index].moveTo(sx, sy); //画线的起点
        vm.contexts[index].lineTo(ex, ey); //终点
        vm.contexts[index].closePath();
        vm.contexts[index].strokeStyle = "rgb(20, 193, 253)";
        vm.contexts[index].stroke();
        const canvas = vm.$refs[index][0];
        vm.canvasFirstColor[index] = canvas.toDataURL();
      };
      // 重绘第二种颜色无线画面
      // 第二张
      // 获取保存的无线画布状态
      img2.src = vm.canvasSecondColorNoline[index];
      img2.onload = function() {
        // 清除画布
        vm.contexts[index].clearRect(0, 0, canvasWidth, canvasHeight);
        // 重绘画布
        vm.contexts[index].drawImage(img2, 0, 0);
        // 绘制直线路径
        vm.contexts[index].beginPath();
        // console.log("contexts",vm.contexts[index]);
        vm.contexts[index].moveTo(sx, sy); //画线的起点
        vm.contexts[index].lineTo(ex, ey); //终点
        vm.contexts[index].closePath();
        vm.contexts[index].strokeStyle = "rgb(20, 193, 253)";
        vm.contexts[index].stroke();
        const canvas = vm.$refs[index][0];
        vm.canvasSecondColor[index] = canvas.toDataURL();
      };
    },
    // 点击圆点画线
    toLineBottom(cindex) {
      // 点击的底图当前所有圆点坐标
      let curPosition = this.allPositionArr[cindex];
      // 获取鼠标点击的坐标
      let position = this.getEventPosition(event, cindex);
      // 计算鼠标点击的坐标和每个圆点的距离来判断是否点击到该圆点
      curPosition.map((item, index) => {
        // 两点距离小于半径则是点击该圆点
        if (this.calculateDistance(item.x, item.y, position.x*2, position.y*2)) {
          // 画垂直线
          // 这里1000足够大即可,超出部分画布不会显示的,y+6是得再圆点坐标下面即圆边开始画线
          // this.drawLine(item.x, item.y + 6, item.x, 1000, cindex, index);
          this.drawLine2(item.x, item.y + 20, item.x, 1000, cindex, index);
        }
      });
    },

 

<!-- 热区的亮点 -->
      <div class="brightSpot">
        <div class="oneBrightSpot" v-if="curCarHotspotPositionResultList.length==0">点击蓝圈获取资讯</div>
        <div class="outerBrightSpot" v-else>
          <div class="allBrightSpot" v-for="(item,index) in curCarHotspotPositionResultList">
              <div class="oneBrightSpot"  v-if="item.childPosition.length==0" @click="goDetail(item.id)" >
                {{item.postName}}
              </div>
              <div class="oneBrightSpot"  v-else  @click="item.show = !item.show">
                {{item.postName}}
              </div>
              <!-- 子节点 -->
              <transition name="slide-fade" :duration="{ enter: 3000, leave: 800 }">
              <div class="allChildBrightSpot" v-if="item.show">
                <div class="oneChildBrightSpot" v-if="item.childPosition"  v-for="(child,index) in item.childPosition"  >
                  <div class="childBrightSpot" @click="goDetail(child.id,null,null,$event)">{{child.postName}}</div>
                </div>
              </div>
              </transition>
          </div>
        </div>
      </div>

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值