Files
lingxiao865 c5af079d8c first commit
2026-02-10 08:05:03 +08:00

361 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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