@poster-render/shared
Version:
taro海报组件,兼容企微、支付宝
447 lines • 14.3 kB
JavaScript
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
/**
* 绘制线
* @param ctx
* @param options
*/
export async function drawLine(common, options) {
var _a;
const { ctx } = common;
ctx.save();
ctx.setLineDash(options.lineDash || []);
ctx.lineWidth = options.lineWidth;
ctx.strokeStyle = options.color;
ctx.lineCap = options.lineCap || "butt";
ctx.lineDashOffset = (_a = options.lineDashOffset) !== null && _a !== void 0 ? _a : 0.0;
ctx.beginPath();
ctx.moveTo(options.x, options.y);
ctx.lineTo(options.destX, options.destY);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
/**
* 绘制矩形
* @param ctx
* @param options
*/
export async function drawRect(common, options) {
var _a, _b;
const { ctx } = common;
const radius = normalizeRadius(options.radius);
const { x, y, width, height, borderWidth, borderColor, backgroundColor } = options;
ctx.save();
ctx.setLineDash((_a = options.lineDash) !== null && _a !== void 0 ? _a : []);
ctx.lineDashOffset = (_b = options.lineDashOffset) !== null && _b !== void 0 ? _b : 0.0;
ctx.beginPath();
ctx.moveTo(x + radius.topLeft, y);
// 绘制上边
ctx.lineTo(x + width - radius.topRight, y);
// 绘制右上角圆弧
ctx.arcTo(x + width, y, x + width, y + radius.topRight, radius.topRight);
// 绘制右边
ctx.lineTo(x + width, y + height - radius.bottomRight);
// 绘制右下角圆弧
ctx.arcTo(x + width, y + height, x + width - radius.bottomRight, y + height, radius.bottomRight);
// 绘制下边
ctx.lineTo(x + radius.bottomLeft, y + height);
// 绘制左下角圆弧
ctx.arcTo(x, y + height, x, y + height - radius.bottomLeft, radius.bottomLeft);
// 绘制左边
ctx.lineTo(x, y + radius.topLeft);
// 绘制左上角圆弧
ctx.arcTo(x, y, x + radius.topLeft, y, radius.topLeft);
ctx.closePath();
if (backgroundColor) {
ctx.fillStyle = backgroundColor;
ctx.fill();
}
if (borderColor && borderWidth) {
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.stroke();
}
ctx.restore();
}
/**
* 解析圆角半径
* @param radius
* @desc 老版本canvas圆角半径小于2的话安卓会出问题,新版本待测
*/
function normalizeRadius(radius = 2) {
if (typeof radius === "number") {
return {
topLeft: Math.max(radius, 2),
topRight: Math.max(radius, 2),
bottomLeft: Math.max(radius, 2),
bottomRight: Math.max(radius, 2),
};
}
if (Array.isArray(radius)) {
// TODO: 验证安卓新版canvas圆角半径小于2的问题
return {
topLeft: Math.max(radius[0], 2),
topRight: Math.max(radius[1], 2),
bottomRight: Math.max(radius[2], 2),
bottomLeft: Math.max(radius[3], 2),
};
}
return {
topLeft: 2,
topRight: 2,
bottomLeft: 2,
bottomRight: 2,
};
}
/**
* 图片缓存
*/
const imageCache = new Map();
/**
* 下载图片
* @param options
*/
export async function downloadImage(options) {
return new Promise((resolve) => {
const isWeb = !!document && !options.canvas.createImage;
const image = isWeb ? new Image() : options.canvas.createImage();
if (isWeb) {
// 解决web端跨域问题
image.setAttribute("crossOrigin", "Anonymous");
}
image.onload = () => resolve(image);
image.onerror = () => {
// 真机下不要打印base64,会导致控制台卡死
if (!options.src.startsWith("data:image")) {
console.error("[poster-render]: downloadImage error", options.src);
}
// TODO base64下载失败时转成临时文件
resolve(undefined);
};
image.src = options.src;
if (options.src.startsWith("data:image") && !options.cacheKey) {
console.warn("[poster-render]: 使用base64图片时建议指定cacheKey");
}
});
}
/**
* 加载图片
* @param options
*/
export async function loadImage(options) {
var _a, _b;
if (!options.src) {
return Promise.resolve(undefined);
}
const cacheKey = (_a = options.cacheKey) !== null && _a !== void 0 ? _a : options.src;
if (imageCache.has(cacheKey)) {
return Promise.resolve(imageCache.get(cacheKey));
}
const img = await downloadImage(options);
if (img) {
imageCache.set((_b = options.cacheKey) !== null && _b !== void 0 ? _b : options.src, img);
}
return img;
}
/**
* 绘制图片
*/
export async function drawImage(common, options) {
const { ctx, canvas } = common;
let image = await loadImage({
ctx,
canvas,
src: options.src,
cacheKey: options.cacheKey,
});
if (!image && options.defaultSrc) {
image = await loadImage({
ctx,
canvas,
src: options.defaultSrc,
cacheKey: options.cacheKey,
});
}
if (!image) {
console.info(`图片下载失败,跳过渲染!`);
return;
}
ctx.save();
// 绘制区域
await drawRect(common, options);
// 裁剪
ctx.clip();
// 绘制图片
await fitImage(ctx, image, options);
ctx.restore();
}
/**
* 图片处理
* @see https://www.cnblogs.com/AIonTheRoad/p/14063041.html
* @param options
*/
async function fitImage(ctx, image, options) {
var _a, _b;
const mode = options.mode || "fill";
// 图片宽高比
const imageRatio = image.width / image.height;
// 绘制区域宽高比
const rectRatio = options.width / options.height;
let sw, sh, sx, sy, dx, dy, dw, dh;
if (mode === "contain") {
if (imageRatio <= rectRatio) {
dh = options.height;
dw = dh * imageRatio;
dx = options.x + (options.width - dw) / 2;
dy = options.y;
}
else {
dw = options.width;
dh = dw / imageRatio;
dx = options.x;
dy = options.y + (options.height - dh) / 2;
}
ctx.drawImage(image, dx, dy, dw, dh);
}
else if (mode === "cover") {
if (imageRatio <= rectRatio) {
sw = image.width;
sh = sw / rectRatio;
sx = 0;
sy = (image.height - sh) / 2;
}
else {
sh = image.height;
sw = sh * rectRatio;
sx = (image.width - sw) / 2;
sy = 0;
}
ctx.drawImage(image, (_a = options.sx) !== null && _a !== void 0 ? _a : sx, (_b = options.sy) !== null && _b !== void 0 ? _b : sy, sw, sh, options.x, options.y, options.width, options.height);
}
else {
ctx.drawImage(image, options.x, options.y, options.width, options.height);
}
}
/**
* 测量文字信息
* @param ctx
* @param text
* @param options
*/
function measureText(ctx, text, options) {
ctx.save();
if (options) {
const { fontStyle = "normal", fontFamily = "normal", fontWeight = "normal", fontSize, baseLine = "top", } = options;
ctx.textBaseline = baseLine;
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
}
const textMetrics = ctx.measureText(text);
ctx.restore();
return textMetrics;
}
/**
* 计算TextDecoration位置
* @param options
*/
function calcTextDecorationPosition(actualHeight, options) {
const { fontSize, textDecoration, baseLine = "top" } = options;
const height = actualHeight || fontSize;
let deltaY = 0;
if (baseLine === "top") {
if (textDecoration === "overline") {
deltaY = 0;
}
else if (textDecoration === "underline") {
deltaY = height;
}
else {
deltaY = height / 2;
}
}
else if (baseLine === "bottom") {
if (textDecoration === "overline") {
deltaY = -height;
}
else if (textDecoration === "underline") {
deltaY = 0;
}
else {
deltaY = -height / 2;
}
}
else {
if (textDecoration === "overline") {
deltaY = -height / 2;
}
else if (textDecoration === "underline") {
deltaY = height / 2;
}
else {
deltaY = 0;
}
}
return deltaY;
}
export async function drawText(common, options) {
const { ctx } = common;
const { textAlign = "left", opacity = 1, lineNum = 1, lineHeight = 0, baseLine = "top", fontWeight = "normal", fontStyle = "normal", fontFamily = "sans-serif", textDecoration, } = options;
ctx.save();
ctx.beginPath();
ctx.font = `${fontStyle} ${fontWeight} ${options.fontSize}px ${fontFamily}`;
ctx.globalAlpha = opacity;
ctx.fillStyle = options.color;
ctx.textAlign = textAlign;
ctx.textBaseline = baseLine;
let textWidth = measureText(ctx, options.text, {
fontSize: options.fontSize,
fontFamily,
fontStyle,
fontWeight,
baseLine,
}).width;
const width = typeof options.width === "number"
? options.width
: options.width(textWidth, (text, options) => measureText(ctx, text, options));
const x = typeof options.x === "number"
? options.x
: options.x(textWidth, (text, options) => measureText(ctx, text, options));
const textArr = [];
if (textWidth > width) {
// 文本宽度 大于 渲染宽度
let fillText = "";
let line = 1;
for (let i = 0; i <= options.text.length - 1; i++) {
// 将文字转为数组,一行文字一个元素
fillText = fillText + options.text[i];
if (measureText(ctx, fillText, {
fontSize: options.fontSize,
fontFamily,
fontStyle,
fontWeight,
baseLine,
}).width >= width) {
if (line === lineNum) {
if (i !== options.text.length - 1) {
fillText = fillText.substring(0, fillText.length - 1) + "...";
}
}
if (line <= lineNum) {
textArr.push(fillText);
}
fillText = "";
line++;
}
else {
if (line <= lineNum) {
if (i === options.text.length - 1) {
textArr.push(fillText);
}
}
}
}
textWidth = width;
}
else {
textArr.push(options.text);
}
textArr.forEach((item, index) => {
var _a;
const y = options.y + (lineHeight || options.fontSize) * index;
ctx.fillText(item, x, y);
const { width, actualBoundingBoxAscent = 0, actualBoundingBoxDescent = 0, } = measureText(ctx, item, {
fontSize: options.fontSize,
fontFamily,
fontStyle,
fontWeight,
baseLine,
});
const actualHeight = actualBoundingBoxAscent + actualBoundingBoxDescent;
if (textDecoration) {
const deltaY = calcTextDecorationPosition(actualHeight, options);
ctx.moveTo(x, y + deltaY);
ctx.lineTo(x + width, y + deltaY);
ctx.lineWidth = (_a = options.textDecorationWidth) !== null && _a !== void 0 ? _a : 2;
ctx.strokeStyle = options.color;
ctx.stroke();
}
});
ctx.closePath();
ctx.restore();
return textWidth;
}
/**
* 提前加载图片
* @param urls
*/
export async function preloadImage(ctx, canvas, images) {
const needLoadImages = Array.from(new Set(images.filter((item) => { var _a; return !imageCache.has((_a = item.cacheKey) !== null && _a !== void 0 ? _a : item.src); })));
const loadedImages = await Promise.all(needLoadImages.map((item) => loadImage({
ctx,
canvas,
src: item.src,
cacheKey: item.cacheKey,
})));
return !loadedImages.includes(undefined);
}
/**
* 清除画布
* @param common
*/
export function clearCanvas(common) {
common.ctx.clearRect(0, 0, common.canvas.width, common.canvas.height);
}
/**
*
* @param common
* @param config
* @returns
*/
export async function renderItem(common, config) {
switch (config.type) {
case "image":
return await drawImage(common, config);
case "text":
return await drawText(common, config);
case "line":
return await drawLine(common, config);
case "rect":
return await drawRect(common, config);
default:
throw new Error("[poster-render]: Unknown item type");
}
}
/**
* 渲染一组数据
* @param common
* @param list
* @returns
*/
export async function render(common, list) {
var e_1, _a;
try {
try {
for (var list_1 = __asyncValues(list), list_1_1; list_1_1 = await list_1.next(), !list_1_1.done;) {
const item = list_1_1.value;
await renderItem(common, item);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (list_1_1 && !list_1_1.done && (_a = list_1.return)) await _a.call(list_1);
}
finally { if (e_1) throw e_1.error; }
}
return true;
}
catch (error) {
return false;
}
}
//# sourceMappingURL=canvas.js.map