技术:微信小程序
 

概述

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

详细

概述

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

详细

一、前期准备工作

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

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

  • 实现图片编辑

  • 实现添加文字

  • 实现导出图片

2、案例目录结构

二、程序实现具体步骤

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

三、案例运行效果图

四、总结与备注

暂无

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

微信小程序--canvas画布实现图片的编辑的更多相关文章

  1. 微信小程序canvas把正方形图片绘制成圆形

    wxml代码: <view class="result-page"> <canvas canvas-id='firstCanvas' style='width:1 ...

  2. 微信小程序 canvas 字体自动换行(支持换行符)

    微信小程序 canvas 自动适配 自动换行,保存图片分享到朋友圈  https://github.com/richard1015/News 微信IDE演示代码https://developers.w ...

  3. 技术博客--微信小程序canvas实现图片编辑

    技术博客--微信小程序canvas实现图片编辑 我们的这个小程序不仅仅是想给用户提供一个保存和查找的平台,还希望能给用户一个展示自己创意的舞台,因此我们实现了图片的编辑部分.我们对对图片的编辑集成了很 ...

  4. 原创:WeZRender:微信小程序Canvas增强组件

    WeZRender是一个微信小程序Canvas增强组件,基于HTML5 Canvas类库ZRender. 使用 WXML: <canvas style="width: 375px; h ...

  5. 微信小程序canvas生成并保存图片

    ---恢复内容开始--- 微信小程序canvas生成并保存图片,具体实现效果如下图     实现效果需要做以下几步工作 一.先获取用户屏幕大小,然后才能根据屏幕大小来定义canvas的大小 二.获取图 ...

  6. 微信小程序-canvas绘制文字实现自动换行

    在使用微信小程序canvas绘制文字时,时常会遇到这样的问题:因为canvasContext.fillText参数为 我们只能设置文本的最大宽度,这就产生一定的了问题.如果我们绘制的文本长度不确定或者 ...

  7. 微信小程序:本地资源图片无法通过 WXSS 获取,可以使用网络图片或者 base64或者使用image标签

    微信小程序:本地资源图片无法通过 WXSS 获取,可以使用网络图片或者 base64或者使用image标签 一.问题 报错信息: VM696:2 pages/user/user.wxss 中的本地资源 ...

  8. 微信小程序-显示外链图片 bug

    微信小程序-显示外链图片 bug 显示外链图片 bug 403 bug 禁止外链,未授权 https://httpstatuses.com/403 image component 图片.支持 JPG. ...

  9. [技术博客]海报图片生成——小程序canvas画布

    目录 背景介绍 canvas简介 代码实现 难点讲解 圆角矩形裁剪失败之PS的妙用 编码不要过硬 对过长的文字进行截取 真机首次生成时字体不对 drawImage只能使用本地图片 背景介绍 目标:利用 ...

随机推荐

  1. Linux下C语言的socket网络编程

    关于详细的服务器建立的步骤以及相关的socket套接字的知识我已经在python socket编程的文章中提到过了,大家可以参看那一篇博客来历接socket套接字编程的内容,由于要是用C相关的API所 ...

  2. win 10 slmgr.vbs -xpr 无法运行,被豆麦笔记打开解决方法

    win 10 slmgr.vbs -xpr 无法运行,被豆麦笔记打开解决方法 删除这个豆麦笔记 如果之前已经在 控制面板 程序中卸载过,那么是找不到的,我们先运行 slmgr.vbs -xpr,这个时 ...

  3. POJ 3169 Layout 【差分约束】+【spfa】

    <题目链接> 题目大意: 一些母牛按序号排成一条直线.有两种要求,A和B距离不得超过X,还有一种是C和D距离不得少于Y,问可能的最大距离.如果没有最大距离输出-1,如果1.n之间距离任意就 ...

  4. HashMap 源码阅读

    前言 之前读过一些类的源码,近来发现都忘了,再读一遍整理记录一下.这次读的是 JDK 11 的代码,贴上来的源码会去掉大部分的注释, 也会加上一些自己的理解. Map 接口 这里提一下 Map 接口与 ...

  5. 通用图片加载组件UniversalImageLoader

    通用图片加载组件UniversalImageLoader   UniversalImageLoader是一款通用图片加载组件.该组件支持多种图片来源,如网络.SD卡.Assets文件夹等.在网络请求的 ...

  6. Redis自学笔记:3.5入门-集合类型

    3.5集合类型 3.5.1介绍 在集合中的每个元素都是不同的,且没有顺序 表3-4集合类型和列表类型的对比 - 集合类型 列表类型 存储内容 至多232-1个字符串 至多232-1个字符串 有序性 否 ...

  7. ES搜索社区

    好问题 1.比如我要索引的条目为“33分钟侦探”,我想在用户输入“3”.“33”.“三三”.“三十三”.“三十三分钟”等的情况下都命中该条目,请问有没有什么好的方式实现? PS:使用的是ansj分词器 ...

  8. 【DWM1000】 非官方开源定位代码bitcraze

    蓝点DWM1000 模块已经打样测试完毕,有兴趣的可以申请购买了,更多信息参见 蓝点论坛 正文: 最近关注DWM1000 定位,一方面在看DWM1000 官方提供的代码,也在四处网上找资料看资料. 其 ...

  9. php回话控制 cookie

    <?php class CustomCookie{ static private $_instance = null; private function __construct(array $o ...

  10. Eclipse 设置Web测试的浏览器

    Window->Preferences->General->Web Browser->选择Use external web browser->选择Default syst ...