微信小程序--canvas画布实现图片的编辑

文章来源:CSDN 发布日期:2024-04-18 分类:小程序博客 阅读( )

概述

上传图片,编辑图片大小,添加文字,改变文字颜色等

详细

概述

微信小程序--canvas画布实现图片的编辑

详细

一、前期准备工作

软件环境:微信开发者工具
官方下载地址:微信开发者工具下载地址与更新日志 | 微信开放文档

1、基本需求。
  • 实现上传图片

  • 实现图片编辑

  • 实现添加文字

  • 实现导出图片

2、案例目录结构

image.png

二、程序实现具体步骤
1.index.js代码(canvas-drag)
// components/canvas-drag/index.jsconst dragGraph = function ({ x, y, w, h, type, text, fontSize = 20, color = 'red', url }, canvas, factor) {if (type === 'text') {canvas.setFontSize(fontSize);const textWidth = canvas.measureText(this.text).width;const textHeight = fontSize + 10;const halfWidth = textWidth / 2;const halfHeight = textHeight / 2;this.x = x + halfWidth;this.y = y + halfHeight;} else {this.x = x;this.y = y;}this.w = w;this.h = h;this.fileUrl = url;this.text = text;this.fontSize = fontSize;this.color = color;this.ctx = canvas;this.rotate = 0;this.type = type;this.selected = true;this.factor = factor;this.MIN_WIDTH = 50;this.MIN_FONTSIZE = 10;}dragGraph.prototype = {/**     * 绘制元素     */paint() {this.ctx.save();// TODO 剪切// this._drawRadiusRect(0, 0, 700, 750, 300);// this.ctx.clip();// 由于measureText获取文字宽度依赖于样式,所以如果是文字元素需要先设置样式if (this.type === 'text') {this.ctx.setFontSize(this.fontSize);this.ctx.setTextBaseline('middle');this.ctx.setTextAlign('center');this.ctx.setFillStyle(this.color);}// 选择区域的中心点this.centerX = this.type === 'text' ? this.x : this.x + (this.w / 2);this.centerY = this.type === 'text' ? this.y : this.y + (this.h / 2);// 旋转元素this.ctx.translate(this.centerX, this.centerY);this.ctx.rotate(this.rotate * Math.PI / 180);this.ctx.translate(-this.centerX, -this.centerY);// 渲染元素if (this.type === 'text') {this.ctx.fillText(this.text, this.x, this.y);} else if (this.type === 'image') {this.ctx.drawImage(this.fileUrl, this.x, this.y, this.w, this.h);}// 如果是选中状态,绘制选择虚线框,和缩放图标、删除图标if (this.selected) {this.ctx.setLineDash([10, 10]);this.ctx.setLineWidth(2);this.ctx.setStrokeStyle('red');this.ctx.lineDashOffset = 10;if (this.type === 'text') {const textWidth = this.ctx.measureText(this.text).width;const textHeight = this.fontSize + 10const halfWidth = textWidth / 2;const halfHeight = textHeight / 2;const textX = this.x - halfWidth;const textY = this.y - halfHeight;this.ctx.strokeRect(textX, textY, textWidth, textHeight);this.ctx.drawImage('./icon/close.png', textX - 15, textY - 15, 30, 30);this.ctx.drawImage('./icon/scale.png', textX + textWidth - 15, textY + textHeight - 15, 30, 30);} else {this.ctx.strokeRect(this.x, this.y, this.w, this.h);this.ctx.drawImage('./icon/close.png', this.x - 15, this.y - 15, 30, 30);this.ctx.drawImage('./icon/scale.png', this.x + this.w - 15, this.y + this.h - 15, 30, 30);}}this.ctx.restore();},/**     * 判断点击的坐标落在哪个区域     * @param {*} x 点击的坐标     * @param {*} y 点击的坐标     */isInGraph(x, y) {const selectW = this.type === 'text' ? this.ctx.measureText(this.text).width : this.w;const selectH = this.type === 'text' ? this.fontSize + 10 : this.h;// 删除区域左上角的坐标和区域的高度宽度const delW = 30;const delH = 30;const delX = this.type === 'text' ? this.x - (selectW / 2) : this.x;const delY = this.type === 'text' ? this.y - (selectH / 2) : this.y;// 旋转后的删除区域坐标const transformDelX = this._getTransform(delX, delY, this.rotate - this._getAngle(this.centerX, this.centerY, delX, delY)).x - (delW / 2);const transformDelY = this._getTransform(delX, delY, this.rotate - this._getAngle(this.centerX, this.centerY, delX, delY)).y - (delH / 2);// 变换区域左上角的坐标和区域的高度宽度const scaleW = 30;const scaleH = 30;const scaleX = this.type === 'text' ? this.x + (selectW / 2) : this.x + selectW;const scaleY = this.type === 'text' ? this.y + (selectH / 2) : this.y + selectH;// 旋转后的变换区域坐标const transformScaleX = this._getTransform(scaleX, scaleY, this.rotate + this._getAngle(this.centerX, this.centerY, scaleX, scaleY)).x - (scaleW / 2);const transformScaleY = this._getTransform(scaleX, scaleY, this.rotate + this._getAngle(this.centerX, this.centerY, scaleX, scaleY)).y - (scaleH / 2);const moveX = this.type === 'text' ? this.x - (selectW / 2) : this.x;const moveY = this.type === 'text' ? this.y - (selectH / 2) : this.y;// 测试使用// this.ctx.setLineWidth(1);// this.ctx.setStrokeStyle('red');// this.ctx.strokeRect(transformDelX, transformDelY, delW, delH);// this.ctx.setLineWidth(1);// this.ctx.setStrokeStyle('black');// this.ctx.strokeRect(transformScaleX, transformScaleY, scaleW, scaleH);if (x - transformScaleX >= 0 && y - transformScaleY >= 0 &&transformScaleX + scaleW - x >= 0 && transformScaleY + scaleH - y >= 0) {// 缩放区域return 'transform';} else if (x - transformDelX >= 0 && y - transformDelY >= 0 &&transformDelX + delW - x >= 0 && transformDelY + delH - y >= 0) {// 删除区域return 'del';} else if (x - moveX >= 0 && y - moveY >= 0 &&moveX + selectW - x >= 0 && moveY + selectH - y >= 0) {// 移动区域return 'move';}// 不在选择区域里面return false;},/**     * 两点求角度     * @param {*} px1      * @param {*} py1      * @param {*} px2      * @param {*} py2      */_getAngle(px1, py1, px2, py2) {const x = px2 - px1;const y = py2 - py1;const hypotenuse = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));//斜边长度const cos = x / hypotenuse;const radian = Math.acos(cos);const angle = 180 / (Math.PI / radian);return angle;},/**     * 点选择一定角度之后的坐标     * @param {*} x      * @param {*} y      * @param {*} rotate 旋转的角度     */_getTransform(x, y, rotate) {const angle = (Math.PI / 180) * (rotate);const r = Math.sqrt(Math.pow((x - this.centerX), 2) + Math.pow((y - this.centerY), 2));const a = Math.sin(angle) * r;const b = Math.cos(angle) * r;return {x: this.centerX + b,y: this.centerY + a,};},/**     *      * @param {*} px 手指按下去的坐标     * @param {*} py 手指按下去的坐标     * @param {*} x 手指移动到的坐标     * @param {*} y 手指移动到的坐标     * @param {*} currentGraph 当前图层的信息     */transform(px, py, x, y, currentGraph) {// 获取选择区域的宽度高度if (this.type === 'text') {this.ctx.setFontSize(this.fontSize);}const centerX = this.type === 'text' ? this.x : this.x + (this.w / 2);const centerY = this.type === 'text' ? this.y : this.y + (this.h / 2);const diffXBefore = px - centerX;const diffYBefore = py - centerY;const diffXAfter = x - centerX;const diffYAfter = y - centerY;const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180;// 旋转的角度this.rotate = currentGraph.rotate + angleAfter - angleBefore;const lineA = Math.sqrt(Math.pow((centerX - px), 2) + Math.pow((centerY - py), 2));const lineB = Math.sqrt(Math.pow((centerX - x), 2) + Math.pow((centerY - y), 2));if (this.type === 'image') {const w = currentGraph.w + (lineB - lineA);const h = currentGraph.h + (lineB - lineA);this.w = w <= this.MIN_WIDTH ? this.MIN_WIDTH : w;this.h = h <= this.MIN_WIDTH ? this.MIN_WIDTH : h;if (w > this.MIN_WIDTH && h > this.MIN_WIDTH) {// 放大 或 缩小this.x = currentGraph.x - (lineB - lineA) / 2;this.y = currentGraph.y - (lineB - lineA) / 2;}} else if (this.type === 'text') {const fontSize = currentGraph.fontSize * ((lineB - lineA) / lineA + 1);this.fontSize = fontSize <= this.MIN_FONTSIZE ? this.MIN_FONTSIZE : fontSize;}},/**     * 画圆角矩形     */_drawRadiusRect(x, y, w, h, r) {const br = r / 2;this.ctx.beginPath();this.ctx.moveTo(this.toPx(x + br), this.toPx(y));    // 移动到左上角的点this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y));this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br));this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br));this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br));this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h));this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br));this.ctx.lineTo(this.toPx(x), this.toPx(y + br));this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br));},toPx(rpx) {return rpx * this.factor;},}Component({/**     * 组件的属性列表     */properties: {graph: {type: Object,value: {},observer: 'onGraphChange',},bgColor: {type: String,value: '',},bgImage: {type: String,value: '',},width: {type: Number,value: 750,},height: {type: Number,value: 750,},},/**     * 组件的初始数据     */data: {},attached() {const sysInfo = wx.getSystemInfoSync();const screenWidth = sysInfo.screenWidth;this.factor = screenWidth / 750;if (typeof this.drawArr === 'undefined') {this.drawArr = [];}this.ctx = wx.createCanvasContext('canvas-label', this);this.draw();},/**     * 组件的方法列表     */methods: {toPx(rpx) {return rpx * this.factor;},onGraphChange(n, o) {if (JSON.stringify(n) === '{}') return;this.drawArr.push(new dragGraph(Object.assign({x: 30,y: 30,}, n), this.ctx, this.factor));this.draw();},draw() {if (this.data.bgImage !== '') {this.ctx.drawImage(this.data.bgImage, 0, 0, this.toPx(this.data.width), this.toPx(this.data.height));}if (this.data.bgColor !== '') {this.ctx.save();this.ctx.setFillStyle(this.data.bgColor);this.ctx.fillRect(0, 0, this.toPx(this.data.width), this.toPx(this.data.height));this.ctx.restore();}this.drawArr.forEach((item) => {item.paint();});return new Promise((resolve) => {this.ctx.draw(false, () => {resolve();});});},start(e) {const { x, y } = e.touches[0];this.tempGraphArr = [];this.drawArr && this.drawArr.forEach((item, index) => {item.selected = false;const action = item.isInGraph(x, y);if (action) {if (action === 'del') {this.drawArr.splice(index, 1);this.ctx.clearRect(0, 0, this.toPx(this.data.width), this.toPx(this.data.height));this.ctx.draw();} else if (action === 'transform' || action === 'move') {item.action = action;this.tempGraphArr.push(item);// 保存点击时的坐标this.currentTouch = { x, y };}}});// 保存点击时元素的信息if (this.tempGraphArr.length > 0) {const lastIndex = this.tempGraphArr.length - 1;this.tempGraphArr[lastIndex].selected = true;this.currentGraph = Object.assign({}, this.tempGraphArr[lastIndex]);}this.draw();},move(e) {const { x, y } = e.touches[0];if (this.tempGraphArr && this.tempGraphArr.length > 0) {const currentGraph = this.tempGraphArr[this.tempGraphArr.length - 1];if (currentGraph.action === 'move') {currentGraph.x = this.currentGraph.x + (x - this.currentTouch.x);currentGraph.y = this.currentGraph.y + (y - this.currentTouch.y);} else if (currentGraph.action === 'transform') {currentGraph.transform(this.currentTouch.x, this.currentTouch.y, x, y, this.currentGraph);}this.draw();}},end(e) {this.tempGraphArr = [];},export() {return new Promise((resolve, reject) => {this.drawArr = this.drawArr.map((item) => {item.selected = false;return item;});this.draw().then(() => {wx.canvasToTempFilePath({canvasId: 'canvas-label',success: (res) => { resolve(res.tempFilePath); },fail: (e) => { reject(e); },}, this);});})},changColor(color) {const selected = this.drawArr.filter((item) => item.selected);if (selected.length > 0) {selected[0].color = color;}this.draw();},changeBgColor(color) {this.data.bgImage = '';this.data.bgColor = color;this.draw();},changeBgImage(url) {this.data.bgColor = '';this.data.bgImage = url;this.draw();}}})
2.index.wxss代码(canvas-drag)
/* components/canvas-drag/index.wxss */.movable-label {margin-top: 300rpx;width: 750rpx;    height: 400rpx;background: #eee;}.movable-block {width: 120rpx;height: 120rpx;background: #ccc;}.movable-block .image-con {width: 100%;height: 100%;}
3.index.wxml代码(canvas-drag)
<!--components/canvas-drag/index.wxml--><canvas canvas-id='canvas-label' disable-scroll="true"bindtouchstart="start"bindtouchmove="move"bindtouchend="end"style='width: {{width}}rpx; height: {{height}}rpx;'></canvas>
4.index.js逻辑代码(index)

a.部分的功能实现

import CanvasDrag from '../../components/canvas-drag/canvas-drag';Page({data: {graph: {},},/**     * 添加测试图片     */onAddTest() {this.setData({graph: {w: 120,h: 120,type: 'image',url: '../../assets/images/test.jpg',}});},/**     * 添加图片     */onAddImage() {wx.chooseImage({success: (res) => {this.setData({graph: {w: 200,h: 200,type: 'image',url: res.tempFilePaths[0],}});}})},/**     * 添加文本     */onAddText() {this.setData({graph: {type: 'text',text: 'helloworld',}});},/**     * 导出图片     */onExport() {CanvasDrag.export().then((filePath) => {console.log(filePath);wx.previewImage({urls: [filePath]})}).catch((e) => {console.error(e);})},/**     * 改变文字颜色     */onChangeColor() {CanvasDrag.changFontColor('blue');},/**     * 改变背景颜色     */onChangeBgColor() {CanvasDrag.changeBgColor('yellow');},/**     * 改变背景照片     */onChangeBgImage() {CanvasDrag.changeBgImage('../../assets/images/test.jpg');},})
三、案例运行效果图

1.gif

四、总结与备注

暂无

最新文章:

二维码