需求一:生成二维码名片海报
<!--
* @Author: mawenli
* @Date: 2021-12-03 14:01:42
* @LastEditors: lizhen
* @LastEditTime: 2021-12-16 16:10:49
* @Description:
-->
<template>
<div class="sharePannel-box">
<div
class="main-box"
:style="[
{
background: `url(${PUBLIC_IMG_PATH}/doctor-card.jpeg) no-repeat `,
backgroundSize: '100%',
},
]">
<image @tap="handelClickHidden" :src="`${PUBLIC_IMG_PATH}/cancelIcon.png`" alt="" class="cancel-img">
<div class="info-box">
<image :src="doctorInfo.docAvatar?doctorInfo.docAvatar:`${PUBLIC_IMG_PATH}/default-doctor.png`" mode="aspectFill" alt="" class="icon-img">
<div class="name">{{doctorInfo.docName.length > 10 ?doctorInfo.docName.slice(0,10)+'...':doctorInfo.docName}}</div>
<div class="text-box" v-if="doctorInfo.titleName" >
<div class="title">{{doctorInfo.titleName}}</div>
<div class="line"></div>
<div class="title1">{{doctorInfo.officeName}}</div>
</div>
<div :class="{'code-box':1,'mg-top':!doctorInfo.titleName}">
<image :src="doctorInfo.qrCode" class="code-img" />
</div>
<div class="btn-box">
<m-button
btnText="分享二维码给患者"
fontsize="28"
radius="60"
:customStyle="{
width: '334rpx',
height: '80rpx',
}"
@click="handleClickShare"
>
<template v-slot:btn-icon>
<image
class="share-icon"
:src="`${PUBLIC_IMG_PATH}/shareIcon.png`"
mode="aspectFill"
/>
</template>
</m-button>
</div>
</div>
</div>
<div class="canvas-wrapper" style="width: 590rpx; height: 916rpx;">
<canvas type="2d" ref="canvas" id="poster-canvas" class="poster-canvas" style="width: 100%; height: 100%;" />
</div>
</div>
</template>
<script>
import { WxCanvas2d } from '@/utils/wx-canvas-2d';
export default {
props:['PUBLIC_IMG_PATH','doctorInfo'],
data() {
return {
canvas: null | WxCanvas2d,
canDraw:false,
posterURL: '',
}
},
onReady() {
this.canvas = new WxCanvas2d();
this.createCanvas()
},
methods: {
handelClickHidden () {
this.$emit('hiddenFun')
},
toJSON(){},
sharePoster() {
const posterURL = this.posterURL;
if (posterURL) {
uni.hideLoading();
uni.showShareImageMenu({
path: posterURL,
});
} else {
this.drawCanvas();
}
},
createCanvas() {
return new Promise((resolve, reject) => {
// 创建
this.canvas.create({
query: '.poster-canvas',
rootWidth: 750, // 参考设备宽度 (即开发时UI设计稿的宽度,默认375,可改为750)
component: this, // 自定义组件内需要传 this
radius: 16,
})
.then(() => {
console.log('画布创建成功');
this.canDraw = true
resolve(null);
})
.catch((err) => {
console.log('----',err)
uni.hideLoading();
uni.showToast({ title: '生成失败,请稍后重试!' });
reject(err);
});
});
},
drawCanvas () {
let series = []
let docName = this.doctorInfo.docName.length > 10 ? this.doctorInfo.docName.slice(0,10)+'...' : this.doctorInfo.docName
if(this.doctorInfo.titleName) {
series = [
{
type: 'image', // 图片
url: `${this.PUBLIC_IMG_PATH}/doctor-card.jpeg`,
x: 0,
y: 0,
width: 590,
height: 916,
mode: 'aspectFill',
},
{
type: 'rect',
x: 235,
y: 48,
width: 120,
height: 120,
bgColor: '#ffffff',
radius: 60,
},
{
type: 'image',
url: this.doctorInfo.docAvatar ? this.doctorInfo.docAvatar :`${this.PUBLIC_IMG_PATH}/default-doctor.png`,
x: 237,
y: 50,
width: 116,
height: 116,
mode: 'aspectFill',
radius: 58,
},
{
type: 'text',
text: docName,
x: 295,
y: 184,
color: '#ffffff',
fontSize: 40,
fontWeight: 'bold',
lineHeight: 56,
align: 'center',
},
{
type: 'rect',
x: 294,
y: 253,
width: 2,
height: 24,
bgColor: 'rgba(255, 255, 255, 0.8)',
},
{
type: 'text',
text: this.doctorInfo.titleName,
x: 294 - this.doctorInfo.titleName.length * 24 - 8,
y: 248,
color: '#ffffff',
fontSize: 24,
lineHeight: 34,
align: 'left',
},
{
type: 'text',
text: this.doctorInfo.officeName,
x: 304,
y: 248,
color: '#ffffff',
fontSize: 24,
lineHeight: 34,
align: 'left',
},
{
type: 'rect',
x: 115,
y: 456,
width: 360,
height: 360,
bgColor: 'rgb(255, 255, 255)',
radius: 16,
},
{
type: 'image',
url: this.doctorInfo.qrCode,
x: 115,
y: 456,
width: 360,
height: 360,
mode: 'scaleToFill',
radius: 16,
},
]
} else {
series = [
{
type: 'image', // 图片
url: `${this.PUBLIC_IMG_PATH}/doctor-card.jpeg`,
x: 0,
y: 0,
width: 590,
height: 916,
mode: 'aspectFill',
},
{
type: 'rect',
x: 235,
y: 48,
width: 120,
height: 120,
bgColor: '#ffffff',
radius: 60,
},
{
type: 'image',
url: this.doctorInfo.docAvatar ? this.doctorInfo.docAvatar :`${this.PUBLIC_IMG_PATH}/default-doctor.png`,
x: 237,
y: 50,
width: 116,
height: 116,
mode: 'aspectFill',
radius: 58,
},
{
type: 'text',
text: docName,
x: 295,
y: 184,
color: '#ffffff',
fontSize: 40,
fontWeight: 'bold',
lineHeight: 56,
align: 'center',
},
{
type: 'rect',
x: 115,
y: 456,
width: 360,
height: 360,
bgColor: 'rgb(255, 255, 255)',
radius: 16,
},
{
type: 'image',
url: this.doctorInfo.qrCode,
x: 115,
y: 456,
width: 360,
height: 360,
mode: 'scaleToFill',
radius: 16,
},
]
}
this.canvas.draw({
series: series
})
.then(() => {
this.toDataURL();
})
.catch((err) => {
console.log('====',err)
uni.hideLoading();
uni.showToast({ title: '生成失败,请稍后重试!',icon: 'none' });
});
},
toDataURL() {
this.canvas
.toDataURL()
.then((res) => {
this.posterURL = res
this.sharePoster();
})
.catch(() => {
wx.hideLoading();
wx.showToast({ title: '生成失败,请稍后重试!3' });
});
},
handleClickShare() {
uni.showLoading({ title: '生成中' });
// this.drawCanvas()
if (this.canDraw) {
this.sharePoster();
} else {
this.createCanvas().then(() => {
this.sharePoster();
});
}
},
}
}
</script>
<style lang="scss" scoped>
.sharePannel-box{
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.7);
z-index: 1000;
.main-box {
width: 590rpx;
height: 916rpx;
position: absolute;
top: 332rpx;
left: 50%;
transform: translateX(-50%);
border-radius: 24rpx;
z-index: 1002;
}
.cancel-img {
width: 46rpx;
height: 46rpx;
display: block;
position: absolute;
right: -23rpx;
top: -70rpx;
z-index: 1001;
}
.info-box {
margin-top: 48rpx;
display: flex;
flex-direction: column;
align-items: center;
.icon-img {
width: 116rpx;
height: 116rpx;
border-radius: 50%;
border: 2px solid $color-white;
}
.name {
@include font-size(40,500);
width: 80%;
text-align: center;
color: $color-white;
margin-top: 16rpx;
}
.text {
margin-top: 8rpx;
color: #d5dbff;
@include font-size(24);
}
.text-box {
margin-top: 8rpx;
color: #d5dbff;
@include font-size(24);
display: flex;
align-items: center;
width: 590rpx;
.title {
width: 50%;
text-align: right;
}
.title1 {
width: 50%;
text-align: left;
}
.line {
width: 1rpx;
height: 28rpx;
background-color: #d5dbff;
margin: 0 14rpx;
}
}
.code-box {
width: 360rpx;
height: 360rpx;
background-color: $color-white;
border-radius: 16rpx;
margin-top: 114rpx;
display: flex;
align-items: center;
justify-content: center;
.code-img {
width: 320rpx;
height: 320rpx;
}
}
.mg-top {
margin-top: 156rpx;
}
.btn-box {
margin-top: 32rpx;
}
.share-icon {
width: 30rpx;
height: 30rpx;
margin-right: 16rpx;
}
}
.canvas-wrapper {
position: fixed;
left: -1000px;
top: -1000px;
}
}
</style>
需求二:分享截取特定区域的内容
<view class="blood-canvas">
<BloodCircle ref="bloodCircleRef" :chartData="chartData" canvasId="blood-circle"/>
</view>
<view class="canvas-wrapper" style="width: 686rpx; height: 600rpx">
<canvas type="2d" id="poster-canvas" class="poster-canvas" style="width: 100%; height: 100%" />
</view>
.canvas-wrapper {
position: fixed;
left: -1000px;
top: -1000px;
}
import { WxCanvas2d } from '@/utils/wx-canvas-2d'
data() {
return {
canvas: null | WxCanvas2d,
canDraw: false,
}
},
onReady() {
this.canvas = new WxCanvas2d()
this.$nextTick(() => {
this.createCanvas()
})
},
// 转发功能
onShareAppMessage(res) {
if (this.bloodData.bloodSugarNum) {
const promise = new Promise(async (resolve) => {
let res = await this.getTempImg()
let shareImgUrl = await this.uploadFile(res)
console.log(shareImgUrl, '######')
resolve({
title: '记录血糖、饮食,栗子助你控糖',
path: '/pages/home/home',
imageUrl: shareImgUrl,
})
})
return {
title: '记录血糖、饮食,栗子助你控糖',
path: '/pages/home/home',
promise,
}
} else {
return {
title: '记录血糖、饮食,栗子助你控糖',
path: '/pages/home/home',
}
}
},
getTempImg() {
return new Promise((resolve, reject) => {
if (this.$refs.bloodCircleRef) {
this.$refs.bloodCircleRef.getImage(async (path) => {
this.circleImg = path
// 画图
let canvasUrl = await this.drawCanvas()
resolve(canvasUrl)
})
} else {
reject('err')
}
})
},
createCanvas() {
return new Promise((resolve, reject) => {
// 创建
this.canvas
.create({
query: '.poster-canvas',
rootWidth: 750, // 参考设备宽度 (即开发时UI设计稿的宽度,默认375,可改为750)
radius: 16,
})
.then(() => {
console.log('画布创建成功')
this.canDraw = true
resolve(null)
})
.catch((err) => {
console.log('画布创建失败', err)
uni.hideLoading()
reject(err)
})
})
},
drawCanvas() {
let series = []
series = [
{
type: 'text',
text: `${this.$date.formatDate(this.bloodData.dateTime, 'YYYY/MM/DD hh:mm')}`,
x: 32,
y: 40,
color: '#101734',
fontSize: 32,
fontWeight: 'bold',
lineHeight: 45,
},
{
type: 'text',
text: '历史记录 >',
x: 487,
y: 41,
color: '#ACAEB4',
fontSize: 32,
fontWeight: 'normal',
lineHeight: 45,
},
{
type: 'image',
url: this.circleImg,
x: 146,
y: 116,
width: 408,
height: 400,
mode: 'aspectFill',
align: 'center',
},
{
type: 'text',
text: `${this.bloodData.dataType}`,
x: 346,
y: 221,
color: '#212222',
fontSize: 32,
fontWeight: 'normal',
lineHeight: 45,
align: 'center',
},
{
type: 'text',
text: `${this.bloodData.bloodSugarNum}`,
x: 346,
y: 293,
color: '#212222',
fontSize: 88,
fontWeight: 'normal',
lineHeight: 88,
align: 'center',
},
{
type: 'text',
text: 'mmol/L',
x: 346,
y: 386,
color: '#7F8383',
fontSize: 28,
fontWeight: 'normal',
align: 'center',
},
{
type: 'text',
text: `你的血糖数值属于:`,
x: 167,
y: 495,
color: '#606269',
fontSize: 32,
fontWeight: 'normal',
},
{
type: 'text',
text: `${this.bloodData.bloodSugarResult}`,
x: 455,
y: 495,
color: `${this.bloodData.bloodSugarResultCode == 11 ? '#23C0A4' : this.bloodData.bloodSugarResultCode == 12 ? '#329DFF' : '#FF9900'}`,
fontSize: 32,
fontWeight: 'normal',
},
]
return new Promise((resolve) => {
this.canvas
.draw({
series: series,
})
.then(async () => {
let res = await this.toDataURL()
resolve(res)
})
.catch((err) => {
console.log('生成图片失败', err)
uni.hideLoading()
})
})
},
toDataURL() {
return new Promise((resolve) => {
this.canvas
.toDataURL()
.then((res) => {
resolve(res)
})
.catch((err) => {
console.log('====', err)
wx.hideLoading()
})
})
},
async uploadFile(filePath) {
let _that = this
let token = await AuthProvider.getAccessToken()
let resArr = await uni.uploadFile({
url: _that.$config.UPLOAD_FILE_URL,
name: 'file',
filePath,
header: { Authorization: token },
})
if (resArr[0]) {
console.log('识别图片报错')
_that.showError(resArr[0].errMsg)
return false
} else {
const res = resArr[1]
const resData = JSON.parse(res.data)
if (resData.code == 100) {
return resData.data.url
}
return false
}
},
wx-canvas-2d.js
/**
* 错误码字典
* @var {Object}
*/
const ERR_CODE = {
100: '生成图片失败',
101: '获取设置信息失败',
102: '保存图片到相册失败',
103: '授权失败',
104: '用户拒绝授权',
105: '用户前往授权页',
106: '获取图片信息失败',
107: '加载图片失败',
}
let systemInfo
function getSystemInfoSync() {
if (!systemInfo) {
systemInfo = wx.getSystemInfoSync()
}
return systemInfo
}
const SYS_INFO = getSystemInfoSync()
/*
* 微信小程序 canvas-2d 绘图工具,轻量、便捷、容易维护。
* 基于 https://github.com/kiccer/wx-canvas-2d 修订
*/
class WxCanvas2d {
query = '' // canvas 的查询条件
bgColor = null // canvas 背景色
radius = null // canvas 背景色
component = null // 如果是在自定义组件中,需要获取到自定义组件的内部 this 变量 (即,传入 this)
canvas = null // canvas 节点
ctx = null // canvas 上下文
dpr = 1 // 像素比
rootWidth = 0 // UI设计稿宽度
fontFamily = 'sans-serif' // 默认字体,目前好像只有这个是可用的
startTime = Date.now()
debugger = false // 调试模式
constructor() {
// Object.defineProperty(this, 'extendDrawMethods', {
// value: {},
// writable: false,
// enumerable: false,
// configurable: true
// })
}
static extendDrawMethods = {}
static addSeries = function (type, handle) {
WxCanvas2d.extendDrawMethods[type] = handle
}
// 创建画布
create(opts) {
// console.log(opts)
return new Promise((resolve, reject) => {
const options = {
query: '',
rootWidth: 375,
...opts,
}
if (!options.query) reject(new Error("[WxCanvas2d] 'query' is empty."))
this.query = options.query
this.bgColor = options.bgColor
this.component = options.component
this.radius = options.radius
const query = this.component ? wx.createSelectorQuery().in(this.component) : wx.createSelectorQuery()
query
.select(this.query)
.fields({ node: true, size: true })
.exec((res) => {
// console.log(res)
if (!res[0]) return false
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = SYS_INFO.pixelRatio
this.canvas = canvas
this.ctx = ctx
this.dpr = dpr
this.rootWidth = options.rootWidth
this.canvas.width = res[0].width * this.dpr
this.canvas.height = res[0].height * this.dpr
// this.ctx.scale(this.dpr, this.dpr)
resolve(true)
return true
})
})
}
// 清空画布
clear() {
this.ctx.clearRect(0, 0, this.xDpr(this.canvas.width), this.xDpr(this.canvas.height))
if (this.radius) {
this.drawRectPath({
x: 0,
y: 0,
width: (this.canvas.width / SYS_INFO.screenWidth / this.dpr) * this.rootWidth,
height: (this.canvas.height / SYS_INFO.screenWidth / this.dpr) * this.rootWidth,
radius: this.radius,
})
this.ctx.clip()
}
if (this.bgColor) {
this.ctx.fillStyle = this.bgColor
this.ctx.fillRect(0, 0, this.xDpr(this.canvas.width), this.xDpr(this.canvas.height))
}
}
// canvas 大小适配
xDpr(val) {
return (val * this.dpr * SYS_INFO.screenWidth) / this.rootWidth
}
// 绘制画布 (重设画布大小)
draw(opts) {
// console.log(opts)
return new Promise((resolve, reject) => {
this.startTime = Date.now()
const { series } = opts
const query = this.component ? wx.createSelectorQuery().in(this.component) : wx.createSelectorQuery()
query
.select(this.query)
.fields({ node: true, size: true })
.exec((res) => {
// console.log(res)
if (!res[0]) return false
// 重设画布大小
this.canvas.width = res[0].width * this.dpr
this.canvas.height = res[0].height * this.dpr
// this.ctx.scale(this.dpr, this.dpr)
this.clear() // 画之前先清空一次画布
// 根据 zIndex 排序 (从小到大,先画小的,这样越大的显示在越上方)
const _series = series.map((n) => ({ ...n, zIndex: n.zIndex || 0 })).sort((n, m) => n.zIndex - m.zIndex)
// 绘制方法映射表
const drawFunc = {
rect: (cvs, opts) => this.drawRect(opts),
image: (cvs, opts) => this.drawImage(opts),
text: (cvs, opts) => this.drawText(opts),
line: (cvs, opts) => this.drawLine(opts),
arc: (cvs, opts) => this.drawArc(opts),
...WxCanvas2d.extendDrawMethods,
}
// 按顺序绘制图层方法
const next = (index = 0) => {
if (index < _series.length) {
const options = _series[index]
if (drawFunc[options.type]) {
// debugLogout(`正在绘制 [${options.type}] (${index + 1}/${_series.length})`)
this.styleClear() // 绘制新图层前,先还原一次样式设置
drawFunc[options.type](this, options)
.then(() => {
// debugLogout(`绘制成功 [${options.type}] (${index + 1}/${_series.length})`);
next(++index)
})
.catch((err) => {
// debugLogout('绘制失败');
reject(err) // 绘制失败抛错
})
} else {
// console.warn(`[WxCanvas2d] Unknown type: '${options.type}'`)
// debugLogout(`未知类型 type: '${options.type}'`, 'error');
next(++index)
}
} else {
// debugLogout(`绘制完成 (${Date.now() - this.startTime}ms)`);
resolve(1) // 所有图层绘制完毕
}
}
// debugLogout('开始绘制');
next() // 开始按顺序绘制图层
return true
})
})
}
// 清空 (初始化) 样式
styleClear() {
this.ctx.setTextAlign = 'left'
this.ctx.textBaseline = 'top'
this.ctx.fillStyle = '#000'
this.ctx.font = `${this.xDpr((12 * this.rootWidth) / SYS_INFO.screenWidth)}px ${this.fontFamily}`
this.ctx.lineCap = 'butt'
this.ctx.setLineDash([1, 0])
this.ctx.lineDashOffset = 0
this.ctx.lineJoin = 'bevel'
this.ctx.lineWidth = this.xDpr(1)
this.ctx.strokeStyle = '#000'
// this.setContainerRadius()
}
// 设置线的样式
setLineStyle(lineStyle = {}) {
const {
cap = 'butt', // butt | round | square
join = 'bevel', // bevel | round | miter
offset = 0,
dash = [1, 0],
color = '#000',
width = 2,
} = lineStyle
this.ctx.lineCap = cap
this.ctx.setLineDash(dash.map((n) => this.xDpr(n)))
this.ctx.lineDashOffset = this.xDpr(offset)
this.ctx.lineJoin = join
this.ctx.lineWidth = this.xDpr(width)
this.ctx.strokeStyle = color
}
// 绘制矩形
drawRect(opts) {
return new Promise((resolve) => {
const {
x = 0,
y = 0,
width = 0,
height = 0,
// color = '',
bgColor = '',
radius = 0,
lineStyle,
// blur = 0
} = opts
// 防止 radius 设置过大
const radiusMin = Math.min(radius, Math.min(width, height) / 2)
// this.ctx.strokeStyle = color
// 设置线段样式
this.setLineStyle(lineStyle)
// 设置填充色
this.ctx.fillStyle = bgColor
this.drawRectPath({
x: x,
y: y,
width: width,
height: height,
radius: radiusMin,
})
if (lineStyle?.color) {
this.ctx.stroke()
}
if (bgColor) {
this.ctx.fill()
}
resolve(1)
})
}
// 绘制矩形路径
drawRectPath(opts) {
const { x = 0, y = 0, width = 0, height = 0, radius = 0 } = opts
// console.log(_opts)
// 圆角起始/结束方向
const angle = {
top: Math.PI * 1.5,
right: 0,
bottom: Math.PI * 0.5,
left: Math.PI,
}
// 圆角方向
const angleArr = [
[angle.left, angle.top],
[angle.top, angle.right],
[angle.right, angle.bottom],
[angle.bottom, angle.left],
]
// 圆角中心点坐标
const arcPos = [
[x + radius, y + radius].map((n) => this.xDpr(n)), // left top
[x + width - radius, y + radius].map((n) => this.xDpr(n)), // top right
[x + width - radius, y + height - radius].map((n) => this.xDpr(n)), // right bottom
[x + radius, y + height - radius].map((n) => this.xDpr(n)), // bottom left
]
this.ctx.beginPath()
Array(4)
.fill(0)
.forEach((n, i) => {
this.ctx.arc(...arcPos[i], this.xDpr(radius), ...angleArr[i])
})
this.ctx.closePath()
}
// 绘制图片
drawImage(opts) {
// console.log(opts)
const { url = '', x = 0, y = 0, width = 0, height = 0, mode = 'scaleToFill', radius = 0 } = opts
// scaleToFill: 缩放: 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
// aspectFit: 缩放: 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
// aspectFill: 缩放: 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
// widthFix: 缩放: 宽度不变,高度自动变化,保持原图宽高比不变
// top: 裁剪: 不缩放图片,只显示图片的顶部区域
// bottom: 裁剪: 不缩放图片,只显示图片的底部区域
// center: 裁剪: 不缩放图片,只显示图片的中间区域
// left: 裁剪: 不缩放图片,只显示图片的左边区域
// right: 裁剪: 不缩放图片,只显示图片的右边区域
// top left: 裁剪: 不缩放图片,只显示图片的左上边区域
// top right: 裁剪: 不缩放图片,只显示图片的右上边区域
// bottom left: 裁剪: 不缩放图片,只显示图片的左下边区域
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: (res) => {
// console.log(res)
// console.log(wx.getFileSystemManager)
// console.log(wx.getFileSystemManager().readFileSync(res, 'base64'))
const imgWidth = res.width
const imgHeight = res.height
const aspectRatio = width / height
let widthRatio = 1
let heightRatio = 1
// 原图等比例缩放后截取范围的长宽比
if (mode === 'aspectFit') {
widthRatio = res.width / res.height < aspectRatio ? ((width / res.width) * res.height) / height : 1
heightRatio = res.width / res.height > aspectRatio ? ((height / res.height) * res.width) / width : 1
} else if (mode === 'aspectFill') {
widthRatio = res.width / res.height > aspectRatio ? ((width / res.width) * res.height) / height : 1
heightRatio = res.width / res.height < aspectRatio ? ((height / res.height) * res.width) / width : 1
}
const imgCut = {
scaleToFill: [0, 0, imgWidth, imgHeight], // 缩放: 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFit: [
(res.width - res.width * widthRatio) / 2,
(res.height - res.height * heightRatio) / 2,
res.width * widthRatio,
res.height * heightRatio,
], // 缩放: 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
aspectFill: [
(res.width - res.width * widthRatio) / 2,
(res.height - res.height * heightRatio) / 2,
res.width * widthRatio,
res.height * heightRatio,
], // 缩放: 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
widthFix: [], // 缩放: 宽度不变,高度自动变化,保持原图宽高比不变
top: [(imgWidth - width) / 2, 0, width, height], // 裁剪: 不缩放图片,只显示图片的顶部区域
bottom: [(imgWidth - width) / 2, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的底部区域
center: [(imgWidth - width) / 2, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的中间区域
left: [0, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的左边区域
right: [imgWidth - width, (imgHeight - height) / 2, width, height], // 裁剪: 不缩放图片,只显示图片的右边区域
'top left': [0, 0, width, height], // 裁剪: 不缩放图片,只显示图片的左上边区域
'top right': [imgWidth - width, 0, width, height], // 裁剪: 不缩放图片,只显示图片的右上边区域
'bottom left': [0, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的左下边区域
'bottom right': [imgWidth - width, imgHeight - height, width, height], // 裁剪: 不缩放图片,只显示图片的右下边区域
}
// console.log(mode)
const img = this.canvas.createImage()
img.src = res.path
img.onload = () => {
if (radius) {
this.ctx.save()
this.drawRectPath({ x, y, width, height, radius })
this.ctx.clip()
}
this.ctx.drawImage(
img,
// ...(imgCut[mode] || imgCut.scaleToFill).map((n, i) => i >= 4 ? this.xDpr(n) : n)
...(imgCut[mode] || []),
this.xDpr(x) || 0,
this.xDpr(y) || 0,
this.xDpr(width || res.width),
this.xDpr(height || res.height),
)
if (radius) {
this.ctx.restore()
}
resolve(1)
}
img.onerror = (err) => {
// debugLogout(err);
reject(errCode(107))
}
},
fail: (err) => {
// debugLogout(err);
reject(errCode(106))
},
})
})
}
// 绘制文本
drawText(opts) {
// console.log(opts)
return new Promise((resolve) => {
const _opts = {
// text: '',
x: 0,
y: 0,
color: '#000',
fontSize: 12,
fontWeight: '',
width: Infinity,
baseline: 'top', // top | hanging | middle | alphabetic | ideographic | bottom
align: 'left', // left | right | center | start | end
...opts,
text: String(opts.text) || '',
ellipsis: Math.max(+opts.ellipsis, 0), // 最多显示行数,超出显示...
lineHeight: opts.lineHeight || opts.fontSize || 12,
}
let start = 0 // 截取的起始下标
let index = 0 // 行数下标
let splitStr = [] // 拆分后的文本数组
this.ctx.textAlign = _opts.align
this.ctx.textBaseline = _opts.baseline
this.ctx.fillStyle = _opts.color
this.ctx.font = `${_opts.fontWeight} ${this.xDpr(_opts.fontSize)}px ${this.fontFamily}`
// 拆分文本
_opts.text.split('').forEach((n, i) => {
const str = _opts.text.slice(start, i + 1)
if (this.ctx.measureText(str).width < this.xDpr(_opts.width)) {
splitStr[index] = str
} else {
start = i
index++
}
})
// 最大显示行,超出显示省略号
if (_opts.ellipsis && splitStr.length > _opts.ellipsis) {
splitStr = splitStr.slice(0, _opts.ellipsis)
splitStr[_opts.ellipsis - 1] = splitStr[_opts.ellipsis - 1].slice(0, -1) + '...'
}
// 循环绘制文本
splitStr.forEach((n, i) => {
const y = _opts.y + _opts.lineHeight * i + (_opts.lineHeight - _opts.fontSize) / 2
this.ctx.fillText(n, this.xDpr(_opts.x), this.xDpr(y), 590)
})
resolve(1)
})
}
// 绘制线段
drawLine(opts) {
// console.log(opts)
return new Promise((resolve) => {
const { lineStyle, line = [] } = opts
// 设置线段样式
this.setLineStyle(lineStyle)
// 绘制线段
line.forEach((n, i) => {
if (!i) {
this.ctx.beginPath()
this.ctx.moveTo(...n.point.map((n) => this.xDpr(n)))
} else {
this.ctx.lineTo(...n.point.map((n) => this.xDpr(n)))
this.ctx.stroke()
}
})
resolve(1)
})
}
// 绘制弧形
drawArc(opts) {
return new Promise((resolve) => {
const { x = 0, y = 0, r = 0, start = 0, end = 0, reverse = false, lineStyle } = opts
// 设置线段样式
this.setLineStyle(lineStyle)
// 绘制线段
this.ctx.beginPath()
this.ctx.arc(this.xDpr(x), this.xDpr(y), this.xDpr(r), start, end, reverse)
this.ctx.stroke()
resolve(1)
})
}
// 保存画布内容到相册
saveToAlbum(opts) {
return new Promise((resolve, reject) => {
const _opts = {
x: 0,
y: 0,
width: 0,
height: 0,
destWidth: 0,
destHeight: 0,
modalOption: {},
...opts,
}
wx.canvasToTempFilePath({
x: _opts.x,
y: _opts.y,
width: _opts.width,
height: _opts.height,
destWidth: this.xDpr(_opts.destWidth),
destHeight: this.xDpr(_opts.destHeight),
canvas: this.canvas,
success: (res) => {
const tempFilePath = res.tempFilePath
getAuth('writePhotosAlbum')
.then((res) => {
if (res?.code === 1) {
wx.showModal({
title: _opts.modalOption.title || '获取权限',
content: _opts.modalOption.content || '请前往开启相册权限',
success:
_opts.modalOption.success ||
((res) => {
if (res.confirm) {
wx.openSetting()
// debugLogout(`${ERR_CODE[105]} (105)`, 'error');
reject(errCode(105))
} else if (res.cancel) {
// debugLogout(`${ERR_CODE[104]} (104)`, 'error');
reject(errCode(104))
}
}),
})
} else if ([2, 3].indexOf(res?.code) >= 0) {
saveImageToPhotosAlbum(tempFilePath)
.then(() => {
// debugLogout('保存图片到相册成功');
resolve(1)
})
.catch(() => {
// debugLogout(`${ERR_CODE[102]} (102)`, 'error');
reject(errCode(102))
})
}
})
.catch(() => {
// debugLogout(`${ERR_CODE[101]} (101)`, 'error');
reject(errCode(101))
})
},
fail: () => {
// debugLogout(`${ERR_CODE[100]} (100)`, 'error');
reject(errCode(100))
},
})
})
}
toDataURL(opts) {
return new Promise((resolve, reject) => {
const _opts = {
x: 0,
y: 0,
width: 0,
height: 0,
destWidth: 0,
destHeight: 0,
modalOption: {},
...opts,
}
wx.canvasToTempFilePath({
x: _opts.x,
y: _opts.y,
width: _opts.width,
height: _opts.height,
destWidth: this.xDpr(_opts.destWidth),
destHeight: this.xDpr(_opts.destHeight),
canvas: this.canvas,
success: (res) => {
const tempFilePath = res.tempFilePath
resolve(tempFilePath)
},
fail: () => {
// debugLogout(`${ERR_CODE[100]} (100)`, 'error');
reject(errCode(100))
},
})
})
}
// 增加新的绘制类型
// addSeries (type, handle) {
// this.extendDrawMethods[type] = (...opts) => {
// handle(this, ...opts)
// }
// }
}
/**
* 生成错误代码信息
* @param {Number} code 错误码
* @return {Object} 错误代码信息
*/
function errCode(code) {
return {
code,
msg: ERR_CODE[code],
}
}
/**
* 小程序获取权限
* @param {String} name 权限名称
* @return {Promise} Promise
*/
function getAuth(name) {
return new Promise((resolve, reject) => {
wx.getSetting({
success(res) {
const callback = (code) => ({ settings: res, code })
if (res.authSetting['scope.' + name] !== undefined && res.authSetting['scope.' + name] !== true) {
// 用户主动取消过
resolve(callback(1))
} else if (res.authSetting['scope.' + name] === undefined) {
// 第一次向用户获取
resolve(callback(2))
} else {
// 用户已授权
resolve(callback(3))
}
},
fail(err) {
reject(err)
},
})
})
}
/**
* 保存图片到相册
* @param {String} tempFilePath 临时文件路径
* @return {Promise} Promise
*/
function saveImageToPhotosAlbum(tempFilePath) {
return new Promise((resolve, reject) => {
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
resolve(1)
},
fail: (err) => {
reject(err)
},
})
})
}
export { WxCanvas2d }