草庐IT

【uniapp】微信小程序canvas签名旋转生成图片

rrrrroy_Ha 2023-04-15 原文

用uniapp开发微信小程序过程中,遇到有审批签名的需求,签名的代码网上还是比较多的,插件市场上大神们比比皆是不再赘述,但遇到一个小细节导致同事有被难倒,无奈咨询于我,细节就是如何把绘制的canvas旋转个角度再生成图片。

如果只为结论,下面都可以不用看了:旋转图片让后端做,是最快的最方便的(毕竟java有现成工具类),如果真有这种实际业务就交给他们吧不用不好意思,而且后端能做的更多,比如通过色差工具不允许保存空面板等

但是,套用钱老——“后端能搞的,前端为啥不能搞,前端难道比人家矮一截?”的精神(某些方面实际不止矮一截【狗头】),我们还是要通过研究和实现来增加我们自身的职业自信的,前端已经不是以前的“切图仔”啦。

那么如何解决呢?且看下面的思路。

先描述下具体的背景:

        1.审批签名需要用户在一张空白画布上通过手指触碰屏幕并滑动的方式“写字”,完成后保存成一张图片,达到电子签名的目的。

        2.在页面中放置一个canvas标签,设置滑动事件,点击保存时调用uni.canvasToTempFilePath生成临时图片地址,再通过uni.uploadFile上传到后台,就此即可将用户手写的内容生成图片并保存在服务器了。

但是!一般的签名是需要用户横屏手写的,但对手机来说实际是竖着写的,保存的图片也是按手机(竖长型)的样子来的,在pc端展示的话需要用css旋转才行,但若有后台生成pdf此类需求的话,那就不好去旋转图片了,为了一劳永逸还得从生成的源头控制才行。

解决思路1:在保存的事件中去旋转canvas

canvas的上下文是提供旋转函数的canvasContext.rotate,但在实际过程中旋转没效果,而且假若成功,对用户体验也不好,放弃。

解决思路2:再添加一个“隐藏”的canvas,当画好后,在保存事件中动态将签名canvas的内容旋转复制到“隐藏”的canvas中,再将后者的内容生成图片上传,便能曲线实现我们的需求。

这个思路也是官方提供的,详见 canvas绘制好怎么旋转? | 微信开放社区

用代码说话

<template>
  <view>
    <!-- 自定义导航栏 -->
    <NaviBar title="签署" :autoBack="true" />
    <view class="wrapper">
      <view class="handBtn">
        <button @click="retDraw" class="delBtn">清除</button>
        <button @click="saveCanvasAsImg" class="saveBtn">取消</button>
        <button @click="subCanvas" class="subBtn">确认</button>
      </view>
      <view class="handCenter">
        <canvas class="handWriting" :disable-scroll="true" @touchstart="uploadScaleStart" @touchmove="uploadScaleMove" canvas-id="handWriting" />
        <!--用于旋转图片的canvas容器-->
        <canvas style="position: absolute" :style="{ width: cavWidth, height: cavWidth1 }" canvas-id="handWriting2"></canvas>
      </view>
    </view>
  </view>
</template>

<script>
import { BASE_URL } from '@/common/config.js'
import { getUToken } from '@/store'
import { storage } from '@/api/api'
import { replaceCurrentSignApi } from '@/api/flow.js'
export default {
  name: 'Signature',
  data() {
    return {
      canvasName: 'handWriting',
      ctx: '',
      startX: null,
      startY: null,
      canvasWidth: 0,
      canvasHeight: 0,
      selectColor: 'black',
      lineColor: '#1A1A1A', // 颜色
      canvas: null,
      cavWidth: 2000,
      cavWidth1: 2000,
      lineSize: 5 // 笔记倍数
    }
  },
  onLoad() {
    this.ctx = uni.createCanvasContext('handWriting', this)
    this.$nextTick(() => {
      uni
        .createSelectorQuery()
        .select('.handCenter')
        .boundingClientRect((rect) => {
          this.canvasWidth = rect.width
          this.canvasHeight = rect.height
          /* 将canvas背景设置为 白底,不设置  导出的canvas的背景为透明 */
          this.setCanvasBg('#fff')
        })
        .exec()
    })
  },
  methods: {
    // 笔迹开始
    uploadScaleStart(e) {
      this.startX = e.changedTouches[0].x
      this.startY = e.changedTouches[0].y
      //设置画笔参数
      //画笔颜色
      this.ctx.setStrokeStyle(this.lineColor)
      //设置线条粗细
      this.ctx.setLineWidth(this.lineSize)
      //设置线条的结束端点样式
      this.ctx.setLineCap('round') //'butt'、'round'、'square'
      //开始画笔
      this.ctx.beginPath()
    },
    // 笔迹移动
    uploadScaleMove(e) {
      //取点
      let temX = e.changedTouches[0].x
      let temY = e.changedTouches[0].y
      //画线条
      this.ctx.moveTo(this.startX, this.startY)
      this.ctx.lineTo(temX, temY)
      this.ctx.stroke()
      this.startX = temX
      this.startY = temY
      this.ctx.draw(true)
    },
    /**
     * 重写
     */
    retDraw() {
      this.ctx.clearRect(0, 0, 700, 730)
      this.ctx.draw()
      //设置canvas背景
      this.setCanvasBg('#fff')
    },
    /**
     * @param {Object} str
     * @param {Object} color
     * 选择颜色
     */
    selectColorEvent(str, color) {
      this.selectColor = str
      this.lineColor = color
    },
    // 确认
    subCanvas() {
      // uni.canvasToTempFilePath({
      //   canvasId: 'handWriting',
      //   fileType: 'png',
      //   quality: 1, //图片质量
      //   success(res) {
      //     // console.log(res.tempFilePath, 'canvas生成图片地址');
      //     const { uToken } = getUToken()
      //     uni.uploadFile({
      //       url: `${BASE_URL}${storage}/upload`,
      //       header: {
      //         'staff-token': uToken
      //       },
      //       filePath: res.tempFilePath,
      //       name: 'file',
      //       formData: {
      //         plateform: '',
      //         fileType: 'image'
      //       },
      //       success: ({ statusCode, data, errMsg }) => {
      //         const parseData = JSON.parse(data)
      //         if (statusCode == 200 && parseData.code == 100) {
      //           replaceCurrentSignApi({
      //             fileId: parseData.data.fileId
      //           }).then(([err, res]) => {
      //             if (err) {
      //               uni.showToast('保存签名失败')
      //               return
      //             }
      //             // 返回上一层并传参
      //             let pages = getCurrentPages() // 获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。
      //             let prevPage = pages[pages.length - 2] //上一页页面实例
      //             prevPage.$vm.otherFun(parseData) // 给上一页绑定方法otherFun,传参地址id
      //             uni.navigateBack()
      //           })
      //         } else {
      //           uni.showToast(errMsg)
      //         }
      //       }
      //     })
      //   }
      // })
      const _this = this

      uni.canvasToTempFilePath({
        canvasId: 'handWriting',
        fileType: 'png',
        quality: 1, //图片质量
        success(res) {
          console.log(res.tempFilePath, 'canvas生成图片地址')
          wx.getImageInfo({
            // 获取图片的信息
            src: res.tempFilePath,
            success: (res1) => {
              console.log(res1)
              // 将canvas1的内容复制到canvas2中
              let canvasContext = wx.createCanvasContext('handWriting2')
              let rate = res1.height / res1.width
              let width = 300 / rate
              let height = 300
              _this.cavWidth = 300 / rate
              _this.cavWidth1 = 300
              canvasContext.translate(height / 2, width / 2)
              canvasContext.rotate((270 * Math.PI) / 180)
              canvasContext.drawImage(res.tempFilePath, -width / 2, -height / 2, width, height)
              canvasContext.draw(false, () => {
                // 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
                wx.canvasToTempFilePath({
                  // 把当前画布指定区域的内容导出生成指定大小的图片。在 draw() 回调里调用该方法才能保证图片导出成功。
                  canvasId: 'handWriting2',
                  fileType: 'png',
                  quality: 1, //图片质量
                  success(res2) {
                    // 调用uni.uploadFile上传图片即可
                    console.log(res2)
                  }
                })
              })
            }
          })
        }
      })
      // wx.createSelectorQuery()
      //   .select('#handWriting')
      //   .fields({ node: true, size: true })
      //   .exec((res) => {
      //     console.log(res);
      //     _this.canvas = res[0].node
      //     // const rotateCtx = rotateCanvas.getContext('2d')
      //   })
    },
    //旋转图片,生成新canvas实例
    rotate(cb) {
      const that = this
      wx.createSelectorQuery()
        .select('#handWriting2')
        .fields({ node: true, size: true })
        .exec((res) => {
          const rotateCanvas = res[0].node
          const rotateCtx = rotateCanvas.getContext('2d')
          //this.ctxW-->所绘制canvas的width
          //this.ctxH -->所绘制canvas的height
          rotateCanvas.width = this.ctxH
          rotateCanvas.height = this.ctxW
          wx.canvasToTempFilePath({
            canvas: that.canvas,
            success(res) {
              const img = rotateCanvas.createImage()
              img.src = res.tempFilePath
              img.onload = function () {
                rotateCtx.translate(rotateCanvas.width / 2, rotateCanvas.height / 2)
                rotateCtx.rotate((270 * Math.PI) / 180)
                rotateCtx.drawImage(img, -rotateCanvas.height / 2, -rotateCanvas.width / 2)
                rotateCtx.scale(that.pixelRatio, that.pixelRatio)
                cb(rotateCanvas)
              }
            },
            fail(err) {
              console.log(err)
            }
          })
        })
    },
    //取消
    saveCanvasAsImg() {
      this.retDraw()
      uni.navigateBack()
    },
    //设置canvas背景色  不设置  导出的canvas的背景为透明
    //@params:字符串  color
    setCanvasBg(color) {
      /* 将canvas背景设置为 白底,不设置  导出的canvas的背景为透明 */
      //rect() 参数说明  矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
      //这里是 canvasHeight - 4 是因为下边盖住边框了,所以手动减了写
      this.ctx.rect(0, 0, this.canvasWidth, this.canvasHeight - 4)
      // ctx.setFillStyle('red')
      this.ctx.setFillStyle(color)
      this.ctx.fill() //设置填充
      this.ctx.draw() //开画
    },
    toJSON() {}
  }
}
</script>

<style>
page {
  background: #fbfbfb;
  height: auto;
  overflow: hidden;
}

.wrapper {
  position: relative;
  width: 100%;
  height: 85vh;
  margin: 20rpx 0;
  overflow: auto;
  display: flex;
  align-content: center;
  flex-direction: row;
  justify-content: center;
  font-size: 28rpx;
}

.handWriting {
  background: #fff;
  width: 100%;
  height: 85vh;
}

.handCenter {
  border-left: 2rpx solid #e9e9e9;
  flex: 5;
  overflow: hidden;
  box-sizing: border-box;
}

.handBtn button {
  font-size: 28rpx;
}

.handBtn {
  height: 85vh;
  display: inline-flex;
  flex-direction: column;
  justify-content: space-between;
  align-content: space-between;
  flex: 1;
}

.delBtn {
  width: 200rpx;
  position: absolute;
  bottom: 350rpx;
  left: -35rpx;
  transform: rotate(90deg);
  color: #666;
}

.subBtn {
  width: 200rpx;
  position: absolute;
  bottom: 52rpx;
  left: -35rpx;
  display: inline-flex;
  transform: rotate(90deg);
  background: #29cea0;
  color: #fff;
  margin-bottom: 60rpx;
  text-align: center;
  justify-content: center;
}

/*Peach - 新增 - 保存*/

.saveBtn {
  width: 200rpx;
  position: absolute;
  bottom: 590rpx;
  left: -35rpx;
  transform: rotate(90deg);
  color: #666;
}
</style>

代码重点:

 

 看看具体效果吧:

随便画一个,只有点击保存,再去同步第二个canvas,此时是没点确定的状态。

 

 点击确认,可以看到内容就生成好啦。

 这样,就能前端实现canvas的旋转上传啦。

后续:

实际测试过程中,会有画布大小的兼容性问题,具体表现为,在代码里设置的画布背景是白色,在某些屏宽(宽高比比iphone7还大)的iphone上,生成的图片可能会存在右侧一竖列区域(如果没做旋转处理实际上是图片的底部区域)是透明的情况,用图片指示一下。

 可能是由于ctx.clearRect,ctx.rect填充区域同事给写死,或是填充高度和画布高度不一致?,或canvas自己问题,建议在这里就不要动态的获取canvas宽高去动态填充指定范围的颜色了,暴力一点,直接填充2000大小区域,类似于this.ctx.clearRect(0, 0, 2000, 2000),this.ctx.rect(0, 0, 2000, 2000),省时省力哈哈。

参考资料(资料2仅提供思路,没实现出来):

canvas绘制好怎么旋转? | 微信开放社区

微信小程序 图片旋转后上传_big_badwolf的博客-CSDN博客_微信小程序旋转图片后上传

有关【uniapp】微信小程序canvas签名旋转生成图片的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  3. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  4. ruby-on-rails - Ruby on Rails - 为文本区域和图片生成列 - 2

    我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数

  5. ruby-on-rails - 如何生成传递一些自定义参数的 `link_to` URL? - 2

    我正在使用RubyonRails3.0.9,我想生成一个传递一些自定义参数的link_toURL。也就是说,有一个articles_path(www.my_web_site_name.com/articles)我想生成如下内容:link_to'Samplelinktitle',...#HereIshouldimplementthecode#=>'http://www.my_web_site_name.com/articles?param1=value1¶m2=value2&...我如何编写link_to语句“alàRubyonRailsWay”以实现该目的?如果我想通过传递一些

  6. ruby-on-rails - 如何在 Rails 3 中创建自定义脚手架生成器? - 2

    有这些railscast。http://railscasts.com/episodes/218-making-generators-in-rails-3有了这个,你就会知道如何创建样式表和脚手架生成器。http://railscasts.com/episodes/216-generators-in-rails-3通过这个,您可以了解如何添加一些文件来修改脚手架View。我想把两者结合起来。我想创建一个生成器,它也可以创建脚手架View。有点像RyanBates漂亮的生成器或web_app_themegem(https://github.com/pilu/web-app-theme)。我

  7. 报告回顾丨模型进化狂飙,DetectGPT能否识别最新模型生成结果? - 2

    导读语言模型给我们的生产生活带来了极大便利,但同时不少人也利用他们从事作弊工作。如何规避这些难辨真伪的文字所产生的负面影响也成为一大难题。在3月9日智源Live第33期活动「DetectGPT:判断文本是否为机器生成的工具」中,主讲人Eric为我们讲解了DetectGPT工作背后的思路——一种基于概率曲率检测的用于检测模型生成文本的工具,它可以帮助我们更好地分辨文章的来源和可信度,对保护信息真实、防止欺诈等方面具有重要意义。本次报告主要围绕其功能,实现和效果等展开。(文末点击“阅读原文”,查看活动回放。)Ericmitchell斯坦福大学计算机系四年级博士生,由ChelseaFinn和Chri

  8. 旋转矩阵的几何意义 - 2

    点向量坐标矩阵的几何意义介绍旋转矩阵的几何含义之前,先介绍一下点向量坐标矩阵的几何含义点:在一维空间下就是一个标量,如同一条直线上,以任意某一个位置为0点,以一定的尺度间隔为1,2,3...,相反方向为-1,-2,-3...;如此就形成了一维坐标系,这时候任何一个点都可以用一个数值表示,如点p1=5,即即从原点出发沿着x轴正方向移动5个尺度;点p2=-3,负方向移动3个尺度;     在一维坐标系上过原点做垂直于一维坐标系的直线,则形成了二维坐标系,此时描述一个点需要两个数值来表示点p3=(3,2),即从原点出发沿着x轴正方向移动3个尺度,在此基础上沿着y轴正方向移动两个尺度的位置就是点p3。

  9. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  10. 微信小程序通过字典表匹配对应数据 - 2

    前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立

随机推荐