first commit
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user