小程序电子签名组件

在这里插入图片描述

sign.js

const env = require("../../env");
Component({
  data: {
    FILE_SERVER: env.FILE_SERVER,
    canvasName: 'handWriting',
    canvas: null,
    ctx: null,
    canvasWidth: 0,
    canvasHeight: 0,
    transparent: 1, // 透明度
    selectColor: 'black',
    lineColor: '#1A1A1A', // 颜色
    lineSize: 1.5, // 笔记倍数
    lineMin: 0.5, // 最小笔画半径
    lineMax: 4, // 最大笔画半径
    pressure: 1, // 默认压力
    smoothness: 60, // 顺滑度,用60的距离来计算速度
    currentPoint: {},
    currentLine: [], // 当前线条
    firstTouch: true, // 第一次触发
    radius: 1, // 画圆的半径
    cutArea: {
      top: 0,
      right: 0,
      bottom: 0,
      left: 0
    }, // 裁剪区域
    bethelPoint: [], // 保存所有线条生成的贝塞尔点;
    lastPoint: 0,
    chirography: [], // 笔迹
    currentChirography: {}, // 当前笔迹
    linePrack: [] // 划线轨迹 , 生成线条的实际点
  },

  methods: {
    selectColorEvent(e) {
      const color = e.currentTarget.dataset.color;
      const hex = e.currentTarget.dataset.hex;
      this.setData({
        selectColor: color,
        lineColor: hex
      });
    },

    //画笔开始
    uploadScaleStart(e) {
      if (e.type !== 'touchstart') return false;
      const {
        ctx,
        lineColor,
        transparent,
        currentLine,
        firstTouch
      } = this.data;
      //初始线条设置颜色
      ctx.fillStyle = lineColor
      // 设置半透明
      ctx.globalAlpha = transparent
      const currentPoint = {
        x: e.touches[0].x,
        y: e.touches[0].y
      };
      currentLine.unshift({
        time: new Date().getTime(),
        dis: 0,
        x: currentPoint.x,
        y: currentPoint.y
      });
      this.setData({
        currentPoint
      });

      if (firstTouch) {
        this.setData({
          cutArea: {
            top: currentPoint.y,
            right: currentPoint.x,
            bottom: currentPoint.y,
            left: currentPoint.x
          },
          firstTouch: false
        });
      }
      //点划线
      this.pointToLine(currentLine);
    },

    //画笔移动
    uploadScaleMove(e) {
      if (e.type !== 'touchmove') return false;
      if (e.cancelable && !e.defaultPrevented) {
        e.preventDefault();
      }
      const point = {
        x: e.touches[0].x,
        y: e.touches[0].y
      };

      // 更新裁剪区域
      const cutArea = this.data.cutArea;
      if (point.y < cutArea.top) cutArea.top = point.y;
      if (point.x > cutArea.right) cutArea.right = point.x;
      if (point.y > cutArea.bottom) cutArea.bottom = point.y;
      if (point.x < cutArea.left) cutArea.left = point.x;

      this.setData({
        cutArea,
        lastPoint: this.data.currentPoint,
        currentPoint: point
      });

      const currentLine = this.data.currentLine;
      currentLine.unshift({
        time: new Date().getTime(),
        dis: this.distance(this.data.currentPoint, this.data.lastPoint),
        x: point.x,
        y: point.y
      });

      this.pointToLine(currentLine);
    },

    //画笔结束
    uploadScaleEnd(e) {
      if (e.type !== 'touchend') return;
      const point = {
        x: e.changedTouches[0].x,
        y: e.changedTouches[0].y
      };
      this.setData({
        lastPoint: this.data.currentPoint,
        currentPoint: point
      });

      const currentLine = this.data.currentLine;
      currentLine.unshift({
        time: new Date().getTime(),
        dis: this.distance(this.data.currentPoint, this.data.lastPoint),
        x: point.x,
        y: point.y
      });

      this.pointToLine(currentLine);

      const currentChirography = {
        lineSize: this.data.lineSize,
        lineColor: this.data.lineColor
      };
      const chirography = this.data.chirography;
      chirography.unshift(currentChirography);
      this.setData({
        chirography,
        linePrack: [currentLine, ...this.data.linePrack],
        currentLine: []
      });
    },


    //有点画线
    pointToLine(line) {
      this.calcBethelLine(line);
    },

    // 计算贝塞尔曲线
    calcBethelLine(line) {
      if (line.length <= 1) {
        line[0].r = this.data.radius;
        return;
      }

      let x0, x1, x2, y0, y1, y2, r0, r1, r2, len, lastRadius, dis = 0,
        time = 0,
        curveValue = 0.5;

      if (line.length <= 2) {
        x0 = line[1].x;
        y0 = line[1].y;
        x2 = line[1].x + (line[0].x - line[1].x) * curveValue;
        y2 = line[1].y + (line[0].y - line[1].y) * curveValue;
        x1 = x0 + (x2 - x0) * curveValue;
        y1 = y0 + (y2 - y0) * curveValue;
      } else {
        x0 = line[2].x + (line[1].x - line[2].x) * curveValue;
        y0 = line[2].y + (line[1].y - line[2].y) * curveValue;
        x1 = line[1].x;
        y1 = line[1].y;
        x2 = x1 + (line[0].x - x1) * curveValue;
        y2 = y1 + (line[0].y - y1) * curveValue;
      }

      len = this.distance({
        x: x2,
        y: y2
      }, {
        x: x0,
        y: y0
      });
      lastRadius = this.data.radius;
      for (let n = 0; n < line.length - 1; n++) {
        dis += line[n].dis;
        time += line[n].time - line[n + 1].time;
        if (dis > this.data.smoothness) break;
      }

      this.data.radius = Math.min((time / len) * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize;
      line[0].r = this.data.radius;

      if (line.length <= 2) {
        r0 = (lastRadius + this.data.radius) / 2;
        r1 = r0;
        r2 = r1;
      } else {
        r0 = (line[2].r + line[1].r) / 2;
        r1 = line[1].r;
        r2 = (line[1].r + line[0].r) / 2;
      }

      let n = 5;
      let point = [];
      for (let i = 0; i < n; i++) {
        let t = i / (n - 1);
        let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2;
        let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2;
        let r = lastRadius + ((this.data.radius - lastRadius) / n) * i;
        point.push({
          x,
          y,
          r
        });
        if (point.length == 3) {
          let a = this.ctaCalc(point[0].x, point[0].y, point[0].r, point[1].x, point[1].y, point[1].r, point[2].x, point[2].y, point[2].r);
          a[0].color = this.data.lineColor;
          this.bethelDraw(a, 1);
          point = [{
            x,
            y,
            r
          }];
        }
      }
      this.setData({
        currentLine: line
      });
    },

    // 求两点之间距离
    distance(a, b) {
      let x = b.x - a.x;
      let y = b.y - a.y;
      return Math.sqrt(x * x + y * y);
    },

    // 计算贝塞尔曲线
    ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) {
      let a = [],
        vx01, vy01, norm, n_x0, n_y0, vx21, vy21, n_x2, n_y2;
      vx01 = x1 - x0;
      vy01 = y1 - y0;
      norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2;
      vx01 = (vx01 / norm) * r0;
      vy01 = (vy01 / norm) * r0;
      n_x0 = vy01;
      n_y0 = -vx01;
      vx21 = x1 - x2;
      vy21 = y1 - y2;
      norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2;
      vx21 = (vx21 / norm) * r2;
      vy21 = (vy21 / norm) * r2;
      n_x2 = -vy21;
      n_y2 = vx21;

      a.push({
        mx: x0 + n_x0,
        my: y0 + n_y0,
        color: '#1A1A1A'
      });
      a.push({
        c1x: x1 + n_x0,
        c1y: y1 + n_y0,
        c2x: x1 + n_x2,
        c2y: y1 + n_y2,
        ex: x2 + n_x2,
        ey: y2 + n_y2
      });
      a.push({
        c1x: x2 + n_x2 - vx21,
        c1y: y2 + n_y2 - vy21,
        c2x: x2 - n_x2 - vx21,
        c2y: y2 - n_y2 - vy21,
        ex: x2 - n_x2,
        ey: y2 - n_y2
      });
      a.push({
        c1x: x1 - n_x2,
        c1y: y1 - n_y2,
        c2x: x1 - n_x0,
        c2y: y1 - n_y0,
        ex: x0 - n_x0,
        ey: y0 - n_y0
      });
      a.push({
        c1x: x0 - n_x0 - vx01,
        c1y: y0 - n_y0 - vy01,
        c2x: x0 + n_x0 - vx01,
        c2y: y0 + n_y0 - vy01,
        ex: x0 + n_x0,
        ey: y0 + n_y0
      });

      a[0].mx = parseFloat(a[0].mx.toFixed(1));
      a[0].my = parseFloat(a[0].my.toFixed(1));
      for (let i = 1; i < a.length; i++) {
        a[i].c1x = parseFloat(a[i].c1x.toFixed(1));
        a[i].c1y = parseFloat(a[i].c1y.toFixed(1));
        a[i].c2x = parseFloat(a[i].c2x.toFixed(1));
        a[i].c2y = parseFloat(a[i].c2y.toFixed(1));
        a[i].ex = parseFloat(a[i].ex.toFixed(1));
        a[i].ey = parseFloat(a[i].ey.toFixed(1));
      }
      return a;
    },

    // 绘制贝塞尔曲线
    bethelDraw(point, is_fill, color) {
      let ctx = this.data.ctx;
      ctx.beginPath();
      ctx.moveTo(point[0].mx, point[0].my);
      ctx.fillStyle = color || point[0].color
      ctx.strokeStyle = color || point[0].color
      for (let i = 1; i < point.length; i++) {
        ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey);
      }
      ctx.stroke();
      if (is_fill) {
        ctx.fill(); // 填充图形
      }
    },


    //设置canvas背景色默认导出的canvas的背景为透明
    setCanvasBg(color) {
      const {
        ctx,
        canvasWidth,
        canvasHeight
      } = this.data;
      ctx.rect(0, 0, canvasWidth, canvasHeight);
      ctx.fillStyle = color
      ctx.fill();
    },

    //画布重置-重画
    retDraw() {
      const ctx = this.data?.ctx;
      ctx.clearRect(0, 0, 700, 730);
      ctx.draw();
      // 设置 canvas 背景
      this.setCanvasBg('#fff');
    },

    //完成
    subCanvas() {
      const _this = this;
      wx.canvasToTempFilePath({
        canvas: this.data.canvas,
        fileType: 'png',
        quality: 1, //图片质量
        success(res) {
          // 旋转图片并重新生成图片地址
          _this.rotateAndSaveImage(res.tempFilePath);
        },
        complete(res) {
          console.log(res, 'rse')
        }
      });
    },

    // 旋转图片并保存
    rotateAndSaveImage(tempFilePath) {
      const _this = this;
      const query = wx.createSelectorQuery().in(this);
      // 获取canvas节点
      query.select('#myCanvas_roreate')
        .fields({
          node: true,
          size: true
        })
        .exec((res) => {
          const canvas = res[0].node;
          const ctx = canvas.getContext('2d');
          // 设置画布大小为旋转后的宽高
          const canvasWidth = res[0].height;
          const canvasHeight = res[0].width;
          canvas.width = canvasWidth;
          canvas.height = canvasHeight;
          // 旋转 90 度
          ctx.translate(canvasWidth / 2, canvasHeight / 2); // 移动到中心点
          ctx.rotate(-Math.PI / 2); // 旋转 90 度
          ctx.translate(-canvasHeight / 2, -canvasWidth / 2); // 移动回去
          ctx.fillStyle = '#fff'
          // 将原始图片绘制到旋转后的 canvas
          const img = canvas.createImage();
          img.src = tempFilePath;
          img.onload = () => {
            ctx.drawImage(img, 0, 0, canvasHeight, canvasWidth);
            // 生成新的图片地址
            wx.canvasToTempFilePath({
              canvas: canvas,
              fileType: 'png',
              quality: 1,
              success(res) {
                _this.triggerEvent('signComplate',res.tempFilePath)
              },
              fail(err) {
                console.error('生成旋转图片失败:', err);
              }
            });
          };
        });
    },
  },


  lifetimes: {
    attached: function () {
      const _this = this;
      const query = wx.createSelectorQuery().in(this)
      query.select('#myCanvas')
        .fields({
          node: true,
          size: true
        })
        .exec((res) => {
          const resDataFirstObj = res[0] || {};
          const canvas = resDataFirstObj.node;
          const ctx = canvas.getContext('2d');
          const dpr = wx.getSystemInfoSync().pixelRatio;
          canvas.width = resDataFirstObj.width * dpr;
          canvas.height = resDataFirstObj.height * dpr;
          ctx.scale(dpr, dpr);
          _this.setData({
            ctx,
            canvas,
            canvasWidth: canvas.width,
            canvasHeight: canvas.height
          });
          _this.setCanvasBg('#fff');
        })
    },
  }
});

index.scss

.wrapper {
  display: flex;
  align-content: center;
  flex-direction: row;
  justify-content: center;
  overflow: hidden;
  box-sizing: border-box;
  width: 100%;
  height: 100vh;
  padding: 30rpx 0;
  font-size: 28rpx;

  //左侧模块
  .handLeft {
    height: 95vh;
    display: inline-flex;
    flex-direction: column;
    justify-content: space-between;
    align-content: space-between;
    flex: 1;
    // 画笔颜色选择
    .penColor_box {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      .color_select {
        width: 60rpx;
        height: 60rpx;
      }

      .black-select.color_select {
        width: 90rpx;
        height: 90rpx;
      }

      .red-select.color_select {
        width: 90rpx;
        height: 90rpx;
      }
    }
    // 功能按钮
    .button_box {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    
      .baseBtn {
        color: #666;
        font-size: 28rpx;
        padding: 60rpx 0rpx;
        margin: 10px 0;

        .text{
          transform: rotate(90deg);
          letter-spacing: 8rpx;
        }
      }

      .delBtn {
        color: #666;
      }
      
      .subBtn {
        background: #008ef6;
        color: #fff;
      }
    }
  }

  // canvas 中间模块
  .handCenter {
    border: 4rpx dashed #e9e9e9;
    flex: 5;
    overflow: hidden;
    box-sizing: border-box;
    .handWriting {
      background: #fff;
      width: 100%;
      height: 100%;
    }
  }

  //右侧模块
  .handRight {
    display: inline-flex;
    align-items: center;
    .handTitle {
      transform: rotate(90deg);
      flex: 1;
      color: #666;
    }
  }
}

index.wxml

<view class="wrapper">
  <!-- 左侧模块 -->
  <view class="handLeft">
    <!-- 画笔颜色选择 -->
    <view class="penColor_box">
      <image bindtap="selectColorEvent" data-color="black" data-hex="#1A1A1A" src="{{FILE_SERVER}}/common/{{ selectColor === 'black' ?'color_black_selected':'color_black' }}.png" class="{{selectColor === 'black' ? 'black-select' : ''}} color_select"></image>
      <image bindtap="selectColorEvent" style="margin-top: 10rpx;" data-color="red" data-hex="#ca262a" src="{{FILE_SERVER}}/common/{{ selectColor === 'red' ? 'color_red_selected':'color_red' }}.png" class="{{selectColor === 'red' ? 'red-select' : ''}} color_select"></image>
    </view>

    <!-- 功能按钮 -->
    <view class="button_box">
      <button bindtap="retDraw" class="baseBtn delBtn"><view class="text">重写</view></button>

      <button bindtap="subCanvas" class="baseBtn subBtn"><view class="text">完成</view></button>
    </view>
  </view>

  <!-- 中间模块 -->
  <view class="handCenter">
    <canvas class="handWriting" disable-scroll="{{true}}" bindtouchstart="uploadScaleStart" bindtouchmove="uploadScaleMove" bindtouchend="uploadScaleEnd" type="2d" id="myCanvas"></canvas>

    <canvas class="handWriting" disable-scroll="{{true}}"  type="2d" id="myCanvas_roreate"></canvas>
  </view>

  <!-- 右侧模块 -->
  <view class="handRight">
    <view class="handTitle">请签名</view>
  </view>
</view>
以下是Android图片裁剪的示例代码: ```java // 引用[1] // 在AndroidManifest.xml文件中添加权限 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> // 在Activity中调用图片裁剪功能 private static final int REQUEST_CODE_PICK_IMAGE = 1; private static final int REQUEST_CODE_CROP_IMAGE = 2; private Uri mImageUri; // 启动图片选择器 private void pickImage() { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE); } // 处理图片选择结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK) { mImageUri = data.getData(); cropImage(); } else if (requestCode == REQUEST_CODE_CROP_IMAGE && resultCode == RESULT_OK) { // 处理裁剪后的图片 Bitmap bitmap = BitmapFactory.decodeFile(mImageUri.getPath()); // 显示裁剪后的图片 imageView.setImageBitmap(bitmap); } } // 启动图片裁剪 private void cropImage() { Intent intent = new Intent("com.android.camera.action.CROP"); intent.setDataAndType(mImageUri, "image/*"); intent.putExtra("crop", "true"); intent.putExtra("aspectX", 1); intent.putExtra("aspectY", 1); intent.putExtra("outputX", 200); intent.putExtra("outputY", 200); intent.putExtra("return-data", false); intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri); startActivityForResult(intent, REQUEST_CODE_CROP_IMAGE); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值