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

概述

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

详细

一、前期准备工作

软件环境:微信开发者工具
官方下载地址:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html

1、基本需求。
  • 实现上传图片
  • 实现图片编辑
  • 实现添加文字
  • 实现导出图片
2、案例目录结构

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

二、程序实现具体步骤

1.index.js代码(canvas-drag)
// components/canvas-drag/index.js
const 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 + 10
const 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 && 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  {
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)

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');
},

})

三、案例运行效果图

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

四、总结与备注

暂无

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

Original: https://www.cnblogs.com/demodashi/p/10503400.html
Author: demo例子集
Title: 微信小程序–canvas画布实现图片的编辑

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/551583/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • oclint 是编译器缺省语法检查功能的增强

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年5月30日
    099
  • SpringBoot-shiro

    SpringBoot-shiro 12.1 快速入门 1、导入依赖 org.apache.shiro shiro-core 1.8.0 org.slf4j jcl-over-slf…

    技术杂谈 2023年6月21日
    093
  • Python数据分析–Numpy常用函数介绍(1)–工具安装及Numpy介绍

    Anaconda 是一个跨平台的版本,通过命令行来管理安装包。进行大规模数据处理、预测分析和科学计算。它包括近 200 个工具包,大数据处理需要用到的常见包有 NumPy 、 Sc…

    技术杂谈 2023年7月25日
    073
  • Rust:axum学习笔记(2) response

    上一篇的hello world里,示例过于简单,仅仅只是返回了一个字符串,实际上axum的response能返回各种格式,包括: plain_texthtmljsonhttp St…

    技术杂谈 2023年5月31日
    082
  • 自写一个生成ID的工具类

    平时项目中只要涉及表,那么一定能接触到众多各式各样的ID编号,博主整理一些常用的ID格式,整合一个ID生成工具类,供大家参考,如果有什么不足指出,烦请留言批评指正,尽量改正,感激不…

    技术杂谈 2023年7月25日
    0233
  • JetBrains新产品Aqua——自动化测试开发工具(抢鲜体验)

    转载请注明出处❤️ 作者:测试蔡坨坨 原文链接:caituotuo.top/9a093c88.html 你好,我是测试蔡坨坨。 随着行业内卷越来越严重,自动化测试已成为测试工程师的…

    技术杂谈 2023年7月11日
    0101
  • 【数据结构】11.java源码关于TreeMap

    目录 1.TreehMap的内部结构2.TreehMap构造函数3.元素新增策略4.元素删除5.元素修改和查找6.特殊操作7.扩容8.总结 1.TreeMap的内部结构 首先确认一…

    技术杂谈 2023年7月24日
    097
  • 2-ESP8266转CAN总线和RS232通讯模块-CAN总线通信测试Arduino

    说明 这里测试其中一块板子和另一块板子进行CAN总线通信(用户可以接其它CAN总线设备) 测试 1.解压.rar文件 2.把下面三个文件放到安装的ESP8266的库文件夹里面 3….

    技术杂谈 2023年6月1日
    097
  • 从URL到页面显示

    从URL到页面显示 1.解析 URL 浏览器第一步要做的就是解析 URL,从而生成发送给 Web 服务器的请求信息。 URL 元素组成 http: + // Web服务器 + [/…

    技术杂谈 2023年7月25日
    071
  • 使用easyExcel注意事项

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年7月11日
    095
  • Echarts 实现飞线图效果

    Echarts 实现飞线图效果 实现的基本效果如下所示: 实现echarts飞线图的灵感是来自网上的demo,比如 https://github.com/guohaining/ec…

    技术杂谈 2023年6月1日
    0110
  • 关于实现

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年5月31日
    088
  • 编写CentOS的System V init启动脚本

    所有System V init脚本都命名为/etc/rc.d/init.d/必须没有”.init”后缀。 示例脚本: 注意:重启和重载功能可以(通常)组合成…

    技术杂谈 2023年5月31日
    077
  • 武林头条-建站小能手争霸赛

    好话说在前头 如果你 想在自己的主机上 搭建一个属于自己的、独一无二的博客 想学会自主安装WordPress 掌握CentOS常用命令 可以在WordPress上自主安装主题和插件…

    技术杂谈 2023年7月11日
    071
  • 为什么人们都讨厌开会?

    原创不易,求分享、求一键三连 会议是工作中最重要的组成部分,因为他需要解决两个核心难题: 信息传递; 达成一致; 有效的信息传递是战略落地的前提条件,更多的认知统一更是可以加速成功…

    技术杂谈 2023年6月1日
    0112
  • 【全网最全的博客美化系列教程】19.旋转立方体的实现

    【全网最全的博客美化系列教程】09.添加”扩大/缩小浏览区域大小” 按钮 【全网最全的博客美化系列教程】10.小火箭置顶特效的实现 【全网最全的博客美化系列…

    技术杂谈 2023年5月31日
    084
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球