361 lines
7.5 KiB
JavaScript
361 lines
7.5 KiB
JavaScript
|
|
import { loadImage } from '../../common/canvas/index';
|
|||
|
|
import { getWindowInfo } from '../../common/utils';
|
|||
|
|
|
|||
|
|
const ratio = getWindowInfo().pixelRatio || 1;
|
|||
|
|
|
|||
|
|
// 元素中心为旋转点执行旋转
|
|||
|
|
const drawRotate = (
|
|||
|
|
ctx,
|
|||
|
|
x,
|
|||
|
|
y,
|
|||
|
|
rotate,
|
|||
|
|
) => {
|
|||
|
|
ctx.translate(x, y);
|
|||
|
|
ctx.rotate((Math.PI / 180) * Number(rotate));
|
|||
|
|
ctx.translate(-x, -y);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 绘制文字
|
|||
|
|
const drawText = (
|
|||
|
|
ctx,
|
|||
|
|
x,
|
|||
|
|
y,
|
|||
|
|
markHeight,
|
|||
|
|
text,
|
|||
|
|
fontWeight,
|
|||
|
|
fontSize,
|
|||
|
|
fontFamily,
|
|||
|
|
fillStyle,
|
|||
|
|
) => {
|
|||
|
|
ctx.font = `normal normal ${fontWeight} ${
|
|||
|
|
fontSize * ratio
|
|||
|
|
}px/${markHeight}px ${fontFamily}`;
|
|||
|
|
|
|||
|
|
ctx.fillStyle = fillStyle;
|
|||
|
|
ctx.textAlign = 'start';
|
|||
|
|
ctx.textBaseline = 'top';
|
|||
|
|
|
|||
|
|
ctx.fillText(text, x, y);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default async function generateBase64Url(
|
|||
|
|
canvas,
|
|||
|
|
canvasId,
|
|||
|
|
{
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
gapX,
|
|||
|
|
gapY,
|
|||
|
|
offsetLeft,
|
|||
|
|
offsetTop,
|
|||
|
|
rotate,
|
|||
|
|
alpha,
|
|||
|
|
watermarkContent,
|
|||
|
|
lineSpace,
|
|||
|
|
watermarkColor,
|
|||
|
|
layout,
|
|||
|
|
},
|
|||
|
|
onFinish,
|
|||
|
|
onFinally,
|
|||
|
|
) {
|
|||
|
|
const isHexagonal = layout === 'hexagonal';
|
|||
|
|
|
|||
|
|
let ctx;
|
|||
|
|
|
|||
|
|
// #ifdef MP || H5
|
|||
|
|
ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
if (!ctx) {
|
|||
|
|
ctx = uni.createCanvasContext(canvasId, this);
|
|||
|
|
}
|
|||
|
|
if (!ctx) {
|
|||
|
|
console.warn('当前环境不支持Canvas, 无法绘制水印');
|
|||
|
|
onFinish('');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let actualBackgroundSize = {
|
|||
|
|
width: gapX + width,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const canvasWidth = (gapX + width) * ratio;
|
|||
|
|
const canvasHeight = (gapY + height) * ratio;
|
|||
|
|
|
|||
|
|
const markWidth = width * ratio;
|
|||
|
|
const markHeight = height * ratio;
|
|||
|
|
|
|||
|
|
const dislocationRotateX = canvasWidth;
|
|||
|
|
const dislocationRotateY = canvasHeight;
|
|||
|
|
const dislocationDrawX = (gapX + width) * ratio;
|
|||
|
|
const dislocationDrawY = (gapY + height) * ratio;
|
|||
|
|
|
|||
|
|
canvas.width = canvasWidth;
|
|||
|
|
canvas.height = canvasHeight;
|
|||
|
|
|
|||
|
|
if (isHexagonal) {
|
|||
|
|
canvas.width = canvasWidth * 2;
|
|||
|
|
canvas.height = canvasHeight * 2;
|
|||
|
|
|
|||
|
|
// 两倍宽度+间距
|
|||
|
|
actualBackgroundSize = {
|
|||
|
|
width: gapX + width * 2 + width / 2,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.globalAlpha = alpha;
|
|||
|
|
|
|||
|
|
// h5需要全局缩放
|
|||
|
|
// #ifdef H5 || APP-PLUS
|
|||
|
|
ctx.scale(1 / ratio, 1 / ratio);
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
ctx.fillStyle = 'transparent';
|
|||
|
|
ctx.fillRect(0, 0, markWidth, markHeight);
|
|||
|
|
|
|||
|
|
ctx.translate(offsetLeft * ratio, offsetTop * ratio);
|
|||
|
|
|
|||
|
|
const contents = Array.isArray(watermarkContent)
|
|||
|
|
? watermarkContent
|
|||
|
|
: [{ ...watermarkContent }];
|
|||
|
|
|
|||
|
|
let top = 0;
|
|||
|
|
|
|||
|
|
// 预处理
|
|||
|
|
contents.forEach((item) => {
|
|||
|
|
item.top = top;
|
|||
|
|
if (item.url) {
|
|||
|
|
top += height;
|
|||
|
|
} else if (item.text) {
|
|||
|
|
top += lineSpace;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 绘制水印内容
|
|||
|
|
const renderWatermarkItem = async (
|
|||
|
|
item,
|
|||
|
|
offsetX = 0,
|
|||
|
|
offsetY = 0,
|
|||
|
|
rotateX = 0,
|
|||
|
|
rotateY = 0,
|
|||
|
|
) => {
|
|||
|
|
if (item.url) {
|
|||
|
|
const { url, isGrayscale = false } = item;
|
|||
|
|
const img = await loadImage({
|
|||
|
|
canvas,
|
|||
|
|
src: url,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ctx.save?.();
|
|||
|
|
drawRotate(ctx, rotateX, rotateY, rotate);
|
|||
|
|
|
|||
|
|
// TODO:其他技术栈修复了「灰度效果只影响图片,不影响文字」的bug,因为小程序不能创建临时canvas,暂时没有想到比较优雅的解决方案
|
|||
|
|
if (isGrayscale) {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
ctx.drawImage(
|
|||
|
|
img,
|
|||
|
|
offsetX,
|
|||
|
|
offsetY + item.top * ratio,
|
|||
|
|
width * ratio / 2,
|
|||
|
|
height * ratio / 2,
|
|||
|
|
);
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
ctx.drawImage(
|
|||
|
|
img,
|
|||
|
|
offsetX,
|
|||
|
|
offsetY + item.top * ratio,
|
|||
|
|
width * ratio,
|
|||
|
|
height * ratio,
|
|||
|
|
);
|
|||
|
|
// #endif
|
|||
|
|
const imgData = await getImageData(ctx, {
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
});
|
|||
|
|
const pixels = imgData.data;
|
|||
|
|
for (let i = 0; i < pixels.length; i += 4) {
|
|||
|
|
const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
|
|||
|
|
pixels[i] = lightness;
|
|||
|
|
pixels[i + 1] = lightness;
|
|||
|
|
pixels[i + 2] = lightness;
|
|||
|
|
}
|
|||
|
|
await putImageData(ctx, imgData, {
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
ctx.drawImage(
|
|||
|
|
img,
|
|||
|
|
offsetX,
|
|||
|
|
(offsetY + item.top * ratio),
|
|||
|
|
width * ratio / 2,
|
|||
|
|
height * ratio / 2,
|
|||
|
|
);
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
ctx.drawImage(
|
|||
|
|
img,
|
|||
|
|
offsetX,
|
|||
|
|
offsetY + item.top * ratio,
|
|||
|
|
width * ratio,
|
|||
|
|
height * ratio,
|
|||
|
|
);
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.restore?.();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (item.text) {
|
|||
|
|
const {
|
|||
|
|
text,
|
|||
|
|
fontSize = 16,
|
|||
|
|
fontFamily = 'normal',
|
|||
|
|
fontWeight = 'normal',
|
|||
|
|
} = item;
|
|||
|
|
const fillStyle = item?.fontColor || watermarkColor;
|
|||
|
|
|
|||
|
|
ctx.save?.();
|
|||
|
|
drawRotate(ctx, rotateX, rotateY, rotate);
|
|||
|
|
drawText(
|
|||
|
|
ctx,
|
|||
|
|
offsetX,
|
|||
|
|
offsetY + item.top * ratio,
|
|||
|
|
markHeight,
|
|||
|
|
text,
|
|||
|
|
fontWeight,
|
|||
|
|
fontSize,
|
|||
|
|
fontFamily,
|
|||
|
|
fillStyle,
|
|||
|
|
);
|
|||
|
|
ctx.restore?.();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 矩形水印
|
|||
|
|
for (const item of contents) {
|
|||
|
|
await renderWatermarkItem(item, 0, 0, 0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 六边形水印
|
|||
|
|
if (isHexagonal) {
|
|||
|
|
for (const item of contents) {
|
|||
|
|
await renderWatermarkItem(
|
|||
|
|
item,
|
|||
|
|
dislocationDrawX,
|
|||
|
|
dislocationDrawY,
|
|||
|
|
dislocationRotateX,
|
|||
|
|
dislocationRotateY,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
ctx.draw();
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 没有图片
|
|||
|
|
const canvasUrl = await exportCanvasImage.call(this, canvas, canvasId);
|
|||
|
|
onFinish(canvasUrl, actualBackgroundSize, ratio);
|
|||
|
|
|
|||
|
|
onFinally?.();
|
|||
|
|
}
|
|||
|
|
// 跨平台 Canvas 导出方法
|
|||
|
|
export function exportCanvasImage(canvas, canvasId) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
let parsed = false;
|
|||
|
|
// #ifdef H5 || MP
|
|||
|
|
resolve(canvas.toDataURL('image/png'));
|
|||
|
|
parsed = true;
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
if (parsed) return;
|
|||
|
|
const query = uni.createSelectorQuery().in(this);
|
|||
|
|
|
|||
|
|
query
|
|||
|
|
.select(`#${canvasId}`)
|
|||
|
|
.fields({ node: true, size: true })
|
|||
|
|
.exec(async (res) => {
|
|||
|
|
if (!res[0]?.node) {
|
|||
|
|
console.error('Canvas node not found');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.canvasToTempFilePath({
|
|||
|
|
// #ifdef MP
|
|||
|
|
canvas: res[0].node,
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef MP
|
|||
|
|
canvasId,
|
|||
|
|
// #endif
|
|||
|
|
success: res => resolve(res.tempFilePath),
|
|||
|
|
fail: reject,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
async function getImageData(ctx, options) {
|
|||
|
|
let result;
|
|||
|
|
// #ifdef H5 || MP
|
|||
|
|
result = ctx.getImageData(options.x, options.y, options.width, options.height);
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
if (!result) {
|
|||
|
|
result = new Promise((resolve) => {
|
|||
|
|
uni.canvasGetImageData({
|
|||
|
|
canvasId: options.canvasId,
|
|||
|
|
x: options.x,
|
|||
|
|
y: options.y,
|
|||
|
|
width: options.width,
|
|||
|
|
height: options.height,
|
|||
|
|
success: (res) => {
|
|||
|
|
// 小程序/App 返回的数据结构需要转换
|
|||
|
|
resolve({
|
|||
|
|
data: new Uint8ClampedArray(res.data),
|
|||
|
|
width: res.width,
|
|||
|
|
height: res.height,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
}, this);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
async function putImageData(ctx, imageData, options) {
|
|||
|
|
let result;
|
|||
|
|
// #ifdef H5 || MP
|
|||
|
|
ctx.putImageData(imageData, options.x, options.y);
|
|||
|
|
result = Promise.resolve();
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
if (!result) {
|
|||
|
|
result = new Promise((resolve, reject) => {
|
|||
|
|
uni.canvasPutImageData({
|
|||
|
|
canvasId: options.canvasId,
|
|||
|
|
x: options.x,
|
|||
|
|
y: options.y,
|
|||
|
|
width: imageData.width,
|
|||
|
|
height: imageData.height,
|
|||
|
|
data: imageData.data,
|
|||
|
|
success: resolve,
|
|||
|
|
fail: reject,
|
|||
|
|
}, this);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|