web仿微信截图

229 阅读5分钟

简介

20241229155152_rec_-convert.gif

仿微信截图

实现:vue2+html2canvas+canvas,其实用什么写都差不多,只是几个状态控制好。

使用场景:审批时,对提交的资料指出错误项,如图片,这里其实还可以截视频的,指出视频中某项不合规或错误的地方。

功能

截图功能,大部分功能实现,画正方形,圆,箭头,画笔,文字,图片下载,颜色选择,以及回退。 以及可以移动绘制区域和放大缩小绘制区域

实现思路

总体组成:首先使用html2canvas 截一张需要绘制的底图。中间共使用三层。最下面一层(图片/canvas)用于这是,中间canvas层是绘制层,最上一层(canvas)用作蒙版。这样做的好处是将静态部分,相当静态部分归于一层,那么就不会每次绘制的时候绘制一次静态内容

绘制:每次绘制,将绘制的内容保存在数组中(包括颜色,粗细等状态),根据主要几个状态:c_draw_area_f(表示 框选完绘制区域),screenshot_f(代表按下截图完成),editImage(表示正在编辑),is_move_draw_area(鼠标表示正在移动可视区域)配合实现

回退:将绘制的内容放入到一个栈中,这里还可以去使用双栈实现回退和重写

移动绘制区域:根据鼠标在canvas上移动的距离,修改蒙版的坐标,重绘绘制蒙版层,以及框框上的8个点的坐标,框框上的8个点使用的是div。

改变绘制区域大小:同理修改蒙版的坐标和框框上的8个点的坐标,重绘绘制蒙版层

screenshot_f:表示按下截图完成

image.png c_draw_area_f:表示框选完 待绘制,因为做了可以重新框选的功能(类微信)

image.png

绘制完成后下载对应图片:由于绘制的内容是在编辑层,图片在最下层层,所以下载时,新建一个canvas,将cancas1(图片),canvas3(编辑层) 一起绘制到新的画布上,然后导出

代码部分

//html
 <div id="app">
    <el-image
      @click.native="preview=true"
      style="width: 100px; height: 100px"
      :src="url"
      ref="img0"
      :preview-src-list="[url]"
    >
    </el-image>
    <el-image
      v-if="imgUrl_c"
      style="width: 100px; height: 100px"
      :src="imgUrl_c"
      :preview-src-list="[imgUrl_c]"
    >
    </el-image>
    <el-image
      v-if="imgUrl_f"
      style="width: 100px; height: 100px"
      :src="imgUrl_f"
      :preview-src-list="[imgUrl_f]"
    >
    </el-image>
    <!-- <el-button class="btn" @click="getImg"> 裁剪 </el-button> -->
    <el-button class="btn2" @click="getImg" v-show="preview"> 编辑 </el-button>
    <el-button class="btn" @click="closeAll"  v-show="preview"> 关闭 </el-button>
    <!-- <el-button class="btn3" @click="draw_overlay"  v-show="preview"> test </el-button> -->
    <div class="canvas_box" v-if="editImage">
        <div v-show="c_draw_area_f" class="point" @mousedown="(e)=>point_down(e,i)" @mouseup="(e)=>point_up(e,i)" :style="show_positon(i)" v-for="(item,i) in 8" :key="i" ></div>
      <!-- //图片层 -->
      <canvas
        id="canvas"
        class="canvas"
        :height="c_h"
        :width="c_w"
        :style="{ zIndex: canvasZ }"
      >
      </canvas>
        <!-- 编辑层 -->
      <canvas
        id="canvas3"
        class="canvas3"
        :height="c_h"
        :width="c_w"
        :style="{ zIndex: canvasZ + 10 }"
      >
      </canvas>
       <!-- 蒙层 -->
       <canvas
        id="canvas2"
        class="canvas2"
        :height="c_h"
        :width="c_w"
        @mouseup="c_mouseup"
        @mousedown="c_mousedown"
        @mousemove="c_mousemove"
        :style="{ zIndex: canvasZ + 20,cursor: is_in_rect ? 'move' : 'crosshair' }"
      >
      </canvas>
      <div
        class="tool"
        :style="{ top: Math.max(edit_rect.y1,edit_rect.y0 )+5+'px', left:Math.min(edit_rect.x0,edit_rect.x1 )+5+'px' }"
        v-show="c_draw_area_f && screenshot_f&&move_point_i==-1"
      >
      <div style="background: #fff;padding: 5px 10px;">
        <i
          :style="{ backgroundColor: tool.type == edit_type ? '#d3e3fd' : '' }"
          v-for="tool in tools"
          :key="tool.icon"
          @click.stop="tool_click(tool.type)"
          :class="['iconfont', tool.icon]"
        >
        </i>
      </div>
        <div class="ctx_style" v-show="isToolEdit">
          <div @click="choose_line_w(index,line.lineWidth)" class="line_box"  :style="{background: line_index==index? '#d3e3fd':''}" v-for="(line,index) in lines" :key="line.class">
          <div  :class="['line_w',line.class]" :style="{background: line_index==index? ctx_color:''}" > </div>
          </div>
        <input type="color" class=" fill_color" @change="change_ctx_color" v-model="ctx_color">
          <div clas="to_up_arrow"></div>
        </div>
      </div>
      <div
        class="input_div"
        :style="{
          zIndex: canvasZ + 30,
          top: start_point.pageY + 'px',
          left: start_point.pageX + 'px',
        }"
        v-show="show_input_div"
      >
        <input ref="remark" :style="{color:ctx_color}" class="input_in_canvas" v-model="remark" />
      </div>
    </div>
    <div class="mask" v-show="isloading">loading...</div>
  </div>

数据部分data

       // line 的粗细选择
      lines:[
        {
        class:'w1',
        lineWidth:1
      },
      {
        class:'w3',
        lineWidth:3
      },
      {
        class:'w5',
        lineWidth:5
      },
      
    ],
    // 工具列表
    tools: [
        {
          icon: "icon-kuangxuan",
          type: "0",
        },
        {
          icon: "icon-huayuan",
          type: "1",
        },
        {
          icon: "icon-jiantou",
          type: "2",
        },
        {
          icon: "icon-huabi",
          type: "3",
        },
        {
          icon: "icon-text",
          type: "4",
        },
        {
          icon: "icon-xiazai",
          type: "5",
        },
        {
          icon: "icon-yulanxuanzhuan",
          type: "6",
        },
        {
          icon: "icon-chacha",
          type: "7",
        },
        {
          icon: "icon-gou",
          type: "8",
        },
      ],
        
      c_draw_area_f: false,  //表示 框选完绘制区域
        
      screenshot_f: false,  // 代表截图完成
      editImage: false, //表示编辑
      is_move_draw_area:false, //表示正在移动可视区域

      move_point_i:-1, //可视区域移动的8个点
      is_in_rect:false,

      isloading: false, 
      preview:false,  //显示编辑按钮
      line_index:1,
      ctx_lineWidth:3,
      ctx_color:'#d62424',
      canvasZ: 3000,
      url: require("./assets/test.jpg"),
      imgUrl: "",
      full_img_src: "",
      imgUrl_c: "",
      imgUrl_f: "",

      caik_w: 0,

      remark: "",
      start_point: {
        x: "",
        y: "",
        pageX: "",
        pageY: "",
      },

      edit_rect: {}, //可编辑区域
      start_point_store: { x: "", y: "" },

      edit_type: "-1",

      show_input_div: false,
      drawing_obj: null,
      stack: [],
computed:{
        isToolEdit() {  // 选中一种工具
      return this.edit_type != "-1";
    },
}

关键代码

鼠标按下事件

    // 蒙层事件监听
   c_mousedown(e) {
        //完成拍照
      if (this.screenshot_f) {
        // 如果选中工具,则进入绘制阶段
        if (this.isToolEdit) {
          if(this.move_point_i>-1)return
            // 鼠标在绘制范围内
          if (this.isInRect( e.offsetX,e.offsetY )) {
            // if(this.edit_type==4){
            //   this.show_input_div=true
            // }else{
            // 对文字方式特殊处理
            if (this.edit_type == 4) {
                 this.start_point_store.x = this.start_point.x;
                  this.start_point_store.y = this.start_point.y;
              if (this.show_input_div && this.start_point_store.x!=='' &&  this.start_point_store.y!=='') {
                this.draw_text(
                  this.start_point_store.x,
                  this.start_point_store.y
                );
              }
              this.show_input_div = !this.show_input_div;
              if (this.show_input_div) {
                setTimeout(() => {
                  this.$refs.remark.focus();
                });
              }
            }else{ 
              // this.mouse_down_start_time=Date.now();
              if (this.edit_type == 2) {
              var canvas3 = document.getElementById("canvas3");
              const ctx3 = canvas3.getContext("2d");
              ctx3.beginPath();
              // ctx3.moveTo(e.offsetX, e.offsetY);
              this.drawing_obj = {
                type: "fillArrow",
                // id:'line_'+this.stack.length,
                moveTo: [e.offsetX, e.offsetY],
                endTo: [],
              };
            } else if (this.edit_type == 3) {
              const canvas3 = document.getElementById("canvas3");
              const ctx3 = canvas3.getContext("2d");
              ctx3.beginPath();
              ctx3.moveTo(e.offsetX, e.offsetY);
              this.drawing_obj = {
                type: "fillLine",
                // id:'line_'+this.stack.length,
                moveTo: [e.offsetX, e.offsetY],
                lineTo: [],
              };
            } else if (this.edit_type == 0) {
              this.drawing_obj = {
                type: "strokeRect",
                // id:'line_'+this.stack.length,
                x: e.offsetX,
                y: e.offsetY,
                w: 0,
                h: 0,
              };
            } else if (this.edit_type == 1) {
              this.drawing_obj = {
                type: "ellipse",
                // id:'line_'+this.stack.length,
                x0: e.offsetX,
                y0: e.offsetY,
                xn: 0,
                yn: 0,
              };
            }
          } 
            this.start_point.x = e.offsetX;
            this.start_point.y = e.offsetY;
            this.start_point.pageX = e.pageX;
            this.start_point.pageY = e.pageY;
          }
        } else {
          this.start_point.x = e.offsetX;
          this.start_point.y = e.offsetY;
          this.start_point.pageX = e.pageX;
          this.start_point.pageY = e.pageY;
        // 已经绘制可视区域
        if(this.c_draw_area_f){
        // 表示将进行移动可视区域
          if(this.isInRect(e.offsetX,e.offsetY)){
             this.is_move_draw_area=true
             this.start_point.x = '';
          }else{
          this.c_draw_area_f = false; // 重新绘制可视需求
          }
        }
        }
      }
      return false
    },

鼠标移动事件

  c_mousemove(e) {
      if ( !this.screenshot_f ) {
        return;
      }
        // 移动可视区域
      if(this.is_move_draw_area){
        let canvas = document.getElementById("canvas2");
        const ctx = canvas.getContext("2d");
        const {movementX,movementY}=e
        this.draw_overlay() // 重新绘制蒙层
        this.edit_rect.x0+=movementX
        this.edit_rect.y0+=movementY
        this.edit_rect.x1+=movementX
        this.edit_rect.y1+=movementY
        // 可视区域
        ctx.clearRect(this.edit_rect.x0,  this.edit_rect.y0, this.edit_rect.w, this.edit_rect.h);

        ctx.strokeStyle='#549cd6'
        ctx.strokeRect(this.edit_rect.x0,  this.edit_rect.y0, this.edit_rect.w, this.edit_rect.h); 
        return 
      }
        // 移动的是 可视区域上的8个点,重新绘制可视区域
      if(this.move_point_i>-1){
        this.point_move(e)
        return
      }
      if ( this.edit_type == "4" ) {
        return;
      }
      if(!this.isToolEdit&&this.c_draw_area_f){
       this.is_in_rect= this.isInRect(e.offsetX,e.offsetY )  //todo
      }
      if(this.start_point.x == "" ){
        return
      }
      let canvas = document.getElementById("canvas2");
      const ctx = canvas.getContext("2d");
      //设置画笔
      let xn = e.offsetX;
      let yn = e.offsetY;
      let w = xn - this.start_point.x;
      let h = yn - this.start_point.y;
      ctx.strokeStyle = "red";
      if (this.isToolEdit) {
        this.draw_3_canvas(w, h, xn, yn); // 进入绘制
      } else {
         // 第一次在蒙版上绘制可视区域
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
        // ctx.strokeStyle = "red";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        this.caik_w = w;
        this.edit_rect = {
          x0: this.start_point.x,
          y0: this.start_point.y,
          x1: xn,
          y1: yn,
          w: w,
          h: h,
        };
        ctx.clearRect(this.start_point.x, this.start_point.y, w, h);
        ctx.strokeStyle='#549cd6'
        ctx.strokeRect(this.start_point.x, this.start_point.y, w, h);
      }
    },

下载图片

  getMImg() {
      var canvas = document.getElementById("canvas"); // 原图
      var canvas3 = document.getElementById("canvas3");// 编辑层
      var mergedCanvas = document.createElement("canvas");
   
      var mergedContext = mergedCanvas.getContext("2d");
      //this.edit_rect; 截图的范围 左上角,右下角,宽高
      const { x0, x1, y0, y1, w, h } = this.edit_rect;
      let x_s = Math.min(x0, x1); //只需要截出来那一部分
      let y_s = Math.min(y0, y1); //只需要截出来那一部分
      mergedCanvas.width = w;
      mergedCanvas.height = h;
      mergedContext.drawImage(canvas, x_s, y_s, w, h, 0, 0, w, h);
      mergedContext.drawImage(canvas3, x_s, y_s, w, h, 0, 0, w, h);
      return mergedCanvas;
    },