uniapp を使用して WeChat アプレットを開発する過程で、承認と署名の必要性に遭遇しました. インターネットにはまだ多くの署名付きコードがあります. プラグイン市場には多くのマスターがいます. , どうしようもなく私に相談しました, 詳細はどのように描画されたキャンバスをある角度に回転させてから、画像を生成します。
結論だけなら、以下は見なくてもいいです:画像を回転させて、バックエンドに任せるのが一番手っ取り早くて便利です (結局、Java には既製のツールがあります)。バックエンドは、色差ツールを使用して空のパネルを保存できないようにするなど、さらに多くのことを行うことができます。
しかし、チェン・ラオの「バックエンドでできることはなぜフロントエンドができないのか? フロントエンドは他の人より短いのか?」という精神を適用し、私たち自身のプロとしての自信を高めることを実現するために、フロント最後は以前の「切り絵少年」ではなくなりました。
それで、それを解決する方法は?そして、次のアイデアを参照してください。
最初に具体的な背景を説明します。
1.署名を承認するには、ユーザーは空白のキャンバスに指で画面をタッチしてスライドすることで「書き込み」、完了後に画像として保存して、電子署名の目的を達成する必要があります。
2. ページにキャンバス タグを配置し、スライド イベントを設定し、[保存] をクリックしたときに uni.canvasToTempFilePath を呼び出して一時的な画像アドレスを生成し、uni.uploadFile を介してバックグラウンドにアップロードし、ユーザーの画像を生成します。手書きのコンテンツをサーバーに保存します。
しかし!一般的な署名は、横画面でユーザーが手書きする必要がありますが、携帯電話の場合、実際には縦書きで、保存された写真も携帯電話の形(縦長タイプ)で表示されている場合。 PC側ではcssで回転させる必要がありますが、バックグラウンドでpdfを生成する必要がある場合、画像を回転させるのは簡単ではありません。生成元。
解決策 1: 保存されたイベントでキャンバスを回転する
キャンバスのコンテキストは、回転機能を提供する canvasContext.rotate ですが、回転は実際のプロセスでは効果がなく、成功した場合はユーザー エクスペリエンスに良くないため、断念します。
解決策 2:別の「非表示」キャンバスを追加する 描画が完了したら、保存イベントで署名キャンバスのコンテンツを動的に回転して「非表示」キャンバスにコピーし、後者のコンテンツの画像を生成してアップロードします。曲線は私たちのニーズを満たします。
このアイデアも公式から提供されたもので、詳しくは「 描いたキャンバスを回転させるには?」を参照してください。| WeChat オープン コミュニティ
コードで話す
<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>
コードのハイライト:
具体的な効果を見てみましょう:
1 つだけ描画し、クリックして保存し、次に 2 つ目のキャンバスを同期します。この時点では、確認はありません。
OKをクリックすると、コンテンツが生成されていることがわかります。
このようにして、フロントエンドはキャンバスの回転アップロードを実現できます。
ファローアップ:
実際のテストプロセスでは、キャンバスのサイズとの互換性の問題が発生します. 具体的なパフォーマンスは、コードで設定されたキャンバスの背景が白であるということです. 右側の縦の列領域 (実際には回転されていない場合は画像の下の領域) が透明であることを示すために画像を使用してください。
ctx.clearRect、ctx.rect 塗りつぶし領域がハードコードされているか、塗りつぶしの高さがキャンバスの高さと一致していない可能性があります。、またはキャンバス自体の問題ですが、指定された範囲の色を動的に塗りつぶすためにキャンバスの幅と高さを動的に取得するのではなく、this.ctxのように、より暴力的で2000サイズの領域を直接塗りつぶすことをお勧めします.clearRect(0, 0, 2000, 2000 )、this.ctx.rect(0, 0, 2000, 2000)、時間と労力を節約できます。
参考資料 (資料 2 はアイデアを提供するだけで、実現されていません):