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;
|
||
}
|