先上个图
我的小程序 完美工具 前前后后搞了2年多了,期间搞了很多乱七八糟的功能,各种拼凑。什么时钟啦,头像制作啦、图片水印啦、昵称装扮啦、红眼等等一大堆。
中间也不知道啥原因,有几天访问量暴增,导致调用的腾讯AI人脸五官识别用量暴增,支出费用蹭蹭上涨,后来云开发调整收费策略,就把所有与腾讯云相关的应用全部下了。
前几天重新捣鼓了一下表情包制作工具--斗图高手,优化了界面,优化了一下代码,现在终于重新发布了。
主要用了小程序的canvas接口,canvas本来有很多不错的封装框架,比如fabricjs、konvasjs,可惜因为小程序无DOM操作,不能直接使用,只好自己参考别人的自己简单写了一下
核心类
const dragLayer = function ({
centered = true,
w,
h,
scale = 1,
x = 30,
y = 30,
type,
rotate = 0,
locked = false,
selected = false,
text,
fontSize = 30,
fontWeight = "bold",
fontStyle = 'normal',
color = 'rgba(255,255,255,1)',
bgcolor = '',
strokeTextColor = "",
shadowColor = "",
url = null,
img,
loaded = false,
}) {
// w = w * factor
// h = h * factor
if (type === 'text') {
ctx.font = `${fontSize}px ${this.fontWeight} Arial`;
w = ctx.measureText(text).width + 30;
h = fontSize + 30;
}
if (centered) {
x = (canvasWidth - w) / 2
y = (canvasHeight - h) / 2
}
this.centerX = x + w / 2;
this.centerY = y + h / 2;
this.w = w;
this.h = h;
this.scale = scale;
this.x = x;
this.y = y;
// 4个顶点坐标
this.square = [
[this.x, this.y],
[this.x + this.w, this.y],
[this.x + this.w, this.y + this.h],
[this.x, this.y + this.h]
];
this.type = type;
this.centered = centered
this.rotate = rotate;
this.locked = locked;
this.selected = selected;
this.url = url;
this.img = img
this.loaded = loaded;
this.text = text;
this.fontSize = fontSize;
this.fontWeight = fontWeight;
this.fontStyle = fontStyle;
this.color = color;
this.bgcolor = bgcolor;
this.shadowColor = shadowColor;
this.strokeTextColor = strokeTextColor;
};
dragLayer.prototype = {
/**
* 绘制元素
*/
paint() {
// 由于measureText获取文字宽度依赖于样式,所以如果是文字元素需要先设置样式
if (this.type === 'text') {
ctx.font = `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px Arial, sans-serif`;
ctx.fillStyle = this.color;
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// ctx.shadowColor = this.shadowColor; //设定阴影颜色效果
let lines = this.text.split('\n')
let temp = lines.sort(function (a, b) {
return a.length - b.length;
});
this.w = ctx.measureText(temp[temp.length - 1]).width + 30;
this.h = this.fontSize * temp.length + 30;
// 字体区域中心点不变,左上角位移
this.x = this.centerX - this.w / 2;
this.y = this.centerY - this.h / 2;
}
// 旋转元素
ctx.translate(this.centerX, this.centerY);
ctx.rotate(this.rotate * Math.PI / 180);
ctx.translate(-this.centerX, -this.centerY);
// 渲染元素
switch (this.type) {
case 'text':
ctx.save()
ctx.shadowBlur = this.fontSize * 0.2; //设定阴影的模糊程度 默认0
let lines = this.text.split('\n')
// console.log(lines)
ctx.translate(0, -(this.h - 30 - this.fontSize) / 2);
lines.forEach((item, index) => {
ctx.fillText(item, this.centerX, this.centerY + this.fontSize * (index));
})
ctx.restore()
this._drawSelect()
break;
case 'bg':
ctx.fillStyle = this.color;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
break
default:
if (this.loaded) {
ctx.drawImage(this.img, this.x, this.y, this.w, this.h);
this._drawSelect()
} else {
loadImage(this.url).then(res => {
this.img = res
this.loaded = true
ctx.drawImage(this.img, this.x, this.y, this.w, this.h);
this._drawSelect()
})
}
break
}
},
_drawSelect() {
// 如果是选中状态,绘制选择虚线框,和缩放图标、删除图标
const handleRadius = HANDLE_RADIUS
if (this.selected) {
ctx.lineWidth = STROKE_WIDTH;
if (this.locked) {
ctx.strokeStyle = '#ccc';
ctx.setLineDash([3, 3]);
//
ctx.beginPath();
ctx.arc(this.x + this.w, this.y, handleRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
ctx.drawImage(imgObj.LOCK_ICON, this.x + this.w - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
} else {
ctx.strokeStyle = STROKE_COLOR;
}
ctx.strokeRect(this.x, this.y, this.w, this.h);
ctx.fillStyle = '#fff';
if (!this.locked) {
//画del
ctx.drawImage(imgObj.DELETE_ICON, this.x + this.w - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
ctx.beginPath();
ctx.arc(this.x + this.w, this.y, handleRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
//缩放
ctx.drawImage(imgObj.SCALE_ICON, this.x - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
ctx.beginPath();
ctx.arc(this.x, this.y, handleRadius, 0, Math.PI * 2);
// ctx.fill()
ctx.stroke();
ctx.closePath();
//缩放
ctx.drawImage(imgObj.SCALE_ICON, this.x + this.w - handleRadius, this.y + this.h - handleRadius, handleRadius * 2, handleRadius * 2);
ctx.beginPath();
ctx.arc(this.x + this.w, this.y + this.h, handleRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
//旋转
ctx.drawImage(imgObj.ROTATE_ICON, this.x - handleRadius, this.y + this.h - handleRadius, handleRadius * 2, handleRadius * 2);
ctx.beginPath();
ctx.arc(this.x, this.y + this.h, handleRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
}
}
},
/**
* 判断点击的坐标落在哪个区域
* @param {*} x 点击的坐标
* @param {*} y 点击的坐标
*/
isInLayer(x, y) {
//leftTop
const leftTopCenter = this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate);
const leftTopX = leftTopCenter[0] - BUTTON_WIDTH / 2;
const leftTopY = leftTopCenter[1] - BUTTON_HEIGHT / 2;
const leftTopArea = [
[leftTopX, leftTopY],
[leftTopX + BUTTON_WIDTH, leftTopY],
[leftTopX + BUTTON_WIDTH, leftTopY + BUTTON_HEIGHT],
[leftTopX, leftTopY + BUTTON_HEIGHT]
]
//rightTop
const rightTopCenter = this._rotatePoint(this.x + this.w, this.y, this.centerX, this.centerY, this.rotate);
const rightTopX = rightTopCenter[0] - BUTTON_WIDTH / 2;
const rightTopY = rightTopCenter[1] - BUTTON_HEIGHT / 2;
const rightTopArea = [
[rightTopX, rightTopY],
[rightTopX + BUTTON_WIDTH, rightTopY],
[rightTopX + BUTTON_WIDTH, rightTopY + BUTTON_HEIGHT],
[rightTopX, rightTopY + BUTTON_HEIGHT]
]
//rightBottom
const rightBottomCenter = this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this.centerY, this.rotate);
const rightBottomX = rightBottomCenter[0] - BUTTON_WIDTH / 2;
const rightBottomY = rightBottomCenter[1] - BUTTON_HEIGHT / 2;
const rightBottomArea = [
[rightBottomX, rightBottomY],
[rightBottomX + BUTTON_WIDTH, rightBottomY],
[rightBottomX + BUTTON_WIDTH, rightBottomY + BUTTON_HEIGHT],
[rightBottomX, rightBottomY + BUTTON_HEIGHT]
]
//leftBottom
const leftBottomCenter = this._rotatePoint(this.x, this.y + this.h, this.centerX, this.centerY, this.rotate);
const leftBottomX = leftBottomCenter[0] - BUTTON_WIDTH / 2;
const leftBottomY = leftBottomCenter[1] - BUTTON_HEIGHT / 2;
const leftBottomArea = [
[leftBottomX, leftBottomY],
[leftBottomX + BUTTON_WIDTH, leftBottomY],
[leftBottomX + BUTTON_WIDTH, leftBottomY + BUTTON_HEIGHT],
[leftBottomX, leftBottomY + BUTTON_HEIGHT]
]
//判断位置区域
if (this.insidePolygon(leftBottomArea, [x, y]))
return 'rotate'
else if (this.insidePolygon(rightTopArea, [x, y]))
return 'del'
else if (this.insidePolygon(leftTopArea, [x, y]) || this.insidePolygon(rightBottomArea, [x, y]))
return 'scale';
else if (this.insidePolygon(this.square, [x, y]))
return 'move';
// 不在选择区域里面
return false;
},
/**
* 判断一个点是否在多边形内部
* @param points 多边形坐标集合
* @param testPoint 测试点坐标
* 返回true为真,false为假
* */
insidePolygon(points, testPoint) {
let x = testPoint[0] * factor,
y = testPoint[1] * factor;
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
let xi = points[i][0],
yi = points[i][1];
let xj = points[j][0],
yj = points[j][1];
let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
},
/**
* 计算旋转后矩形四个顶点的坐标(相对于画布)
* @private
*/
_rotateSquare() {
this.square = [
this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate),
this._rotatePoint(this.x + this.w, this.y, this.centerX, this.centerY, this.rotate),
this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this.centerY, this.rotate),
this._rotatePoint(this.x, this.y + this.h, this.centerX, this.centerY, this.rotate),
];
},
/**
* 计算旋转后的新坐标(相对于画布)
* @param x
* @param y
* @param centerX
* @param centerY
* @param degrees
* @returns {*[]}
* @private
*/
_rotatePoint(x, y, centerX, centerY, degrees) {
let newX = (x - centerX) * Math.cos(degrees * Math.PI / 180) - (y - centerY) * Math.sin(degrees * Math.PI / 180) + centerX;
let newY = (x - centerX) * Math.sin(degrees * Math.PI / 180) + (y - centerY) * Math.cos(degrees * Math.PI / 180) + centerY;
return [newX, newY];
},
/**
*
* @param {*} px 手指按下去的坐标
* @param {*} py 手指按下去的坐标
* @param {*} x 手指移动到的坐标
* @param {*} y 手指移动到的坐标
* @param {*} currentLayer 当前图层的信息
*/
transform(px, py, x, y, currentLayer, action) {
// 获取选择区域的宽度高度
const diffXBefore = px - this.centerX;
const diffYBefore = py - this.centerY;
const diffXAfter = x - this.centerX;
const diffYAfter = y - this.centerY;
const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;
const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180;
// 旋转的角度
if (action == 'rotate' && ROTATE_ENABLED) {
this.rotate = currentLayer.rotate + angleAfter - angleBefore;
}
const lineA = Math.sqrt(Math.pow((this.centerX - px), 2) + Math.pow((this.centerY - py), 2));
const lineB = Math.sqrt(Math.pow((this.centerX - x), 2) + Math.pow((this.centerY - y), 2));
if (this.type === 'text') {
const fontSize = currentLayer.fontSize * ((lineB - lineA) / lineA + 1);
this.fontSize = fontSize <= MIN_FONTSIZE ? MIN_FONTSIZE : fontSize;
// 旋转位移后重新计算坐标
ctx.font = `${this.fontSize}px Arial`;
// this.w = ctx.measureText(this.text).width + 30;
// this.h = this.fontSize + 30;
let lines = this.text.split('\n')
let temp = lines.sort(function (a, b) {
return a.length - b.length;
});
this.w = ctx.measureText(temp[temp.length - 1]).width + 30;
this.h = this.fontSize * temp.length + 30;
// 字体区域中心点不变,左上角位移
this.x = this.centerX - this.w / 2;
this.y = this.centerY - this.h / 2;
} else //if (this.type === 'image')
{
let resize_rito = lineB / lineA;
let new_w = currentLayer.w * resize_rito;
let new_h = currentLayer.h * resize_rito;
this.scale = resize_rito
if (currentLayer.w < currentLayer.h && new_w < MIN_WIDTH) {
new_w = MIN_WIDTH;
new_h = MIN_WIDTH * currentLayer.h / currentLayer.w;
} else if (currentLayer.h <= currentLayer.w && new_h <= MIN_WIDTH) {
new_h = MIN_WIDTH;
new_w = MIN_WIDTH * currentLayer.w / currentLayer.h;
}
this.w = new_w;
this.h = new_h;
this.x = currentLayer.x - (new_w - currentLayer.w) / 2;
this.y = currentLayer.y - (new_h - currentLayer.h) / 2;
}
},
};
欢迎体验