优化蒙版
This commit is contained in:
@@ -57,7 +57,8 @@ function drawImageWithFit(
|
||||
}
|
||||
|
||||
export function isElementMaskActive(mask?: ElementMaskConfig): boolean {
|
||||
if (!mask?.enabled) return false;
|
||||
if (!mask) return false;
|
||||
if (mask.enabled === false) return false;
|
||||
if (mask.type === 'image') return !!mask.image?.trim();
|
||||
return true;
|
||||
}
|
||||
@@ -66,6 +67,7 @@ export function createDefaultElementMask(elem: TemplateElement): ElementMaskConf
|
||||
return {
|
||||
enabled: true,
|
||||
type: 'rect',
|
||||
mode: 'include',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: elem.width,
|
||||
@@ -110,40 +112,168 @@ export function resolveMaskCornerRadiiPx(
|
||||
};
|
||||
}
|
||||
|
||||
/** 使用 destination-in 将已绘制元素裁剪到蒙版区域 */
|
||||
export async function applyElementMask(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
function resolveMaskRegionPx(
|
||||
elem: TemplateElement,
|
||||
ex: number,
|
||||
ey: number,
|
||||
scale: number,
|
||||
mask: ElementMaskConfig
|
||||
mask: ElementMaskConfig,
|
||||
scale: number
|
||||
) {
|
||||
if (!isElementMaskActive(mask)) return;
|
||||
|
||||
const region = resolveMaskRegionMm(elem, mask);
|
||||
const mx = ex + Math.round(region.x * scale);
|
||||
const my = ey + Math.round(region.y * scale);
|
||||
const mw = Math.max(1, Math.round(region.width * scale));
|
||||
const mh = Math.max(1, Math.round(region.height * scale));
|
||||
return {
|
||||
x: Math.round(region.x * scale),
|
||||
y: Math.round(region.y * scale),
|
||||
w: Math.max(1, Math.round(region.width * scale)),
|
||||
h: Math.max(1, Math.round(region.height * scale)),
|
||||
};
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
function fillRectMaskAlpha(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
mx: number,
|
||||
my: number,
|
||||
mw: number,
|
||||
mh: number,
|
||||
mask: ElementMaskConfig,
|
||||
scale: number
|
||||
) {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
traceRoundedRect(ctx, mx, my, mw, mh, resolveMaskCornerRadiiPx(mask, scale, mw, mh));
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/** 将图片按亮度转为 alpha 蒙版(白=不透明,黑=透明) */
|
||||
function buildImageLuminanceMaskCanvas(
|
||||
img: HTMLImageElement,
|
||||
w: number,
|
||||
h: number,
|
||||
fit: 'contain' | 'cover' | 'fill',
|
||||
invert: boolean
|
||||
): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) return canvas;
|
||||
|
||||
drawImageWithFit(ctx, img, 0, 0, w, h, fit);
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const luminance = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const alpha = invert ? 255 - luminance : luminance;
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = alpha;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/** 生成与元素同尺寸的蒙版 alpha 图(不透明区域保留图层像素) */
|
||||
async function buildMaskAlphaCanvas(
|
||||
elem: TemplateElement,
|
||||
mask: ElementMaskConfig,
|
||||
ew: number,
|
||||
eh: number,
|
||||
scale: number
|
||||
): Promise<HTMLCanvasElement> {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, ew);
|
||||
canvas.height = Math.max(1, eh);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return canvas;
|
||||
|
||||
const { x: mx, y: my, w: mw, h: mh } = resolveMaskRegionPx(elem, mask, scale);
|
||||
const include = (mask.mode ?? 'include') === 'include';
|
||||
|
||||
if (mask.type === 'image' && mask.image) {
|
||||
const img = await loadImageOrNull(mask.image);
|
||||
if (img) {
|
||||
drawImageWithFit(ctx, img, mx, my, mw, mh, mask.imageFit ?? 'fill');
|
||||
} else {
|
||||
ctx.fillStyle = '#000';
|
||||
traceRoundedRect(ctx, mx, my, mw, mh, resolveMaskCornerRadiiPx(mask, scale, mw, mh));
|
||||
ctx.fill();
|
||||
const maskCanvas = buildImageLuminanceMaskCanvas(
|
||||
img,
|
||||
mw,
|
||||
mh,
|
||||
mask.imageFit ?? 'fill',
|
||||
!include
|
||||
);
|
||||
ctx.drawImage(maskCanvas, mx, my);
|
||||
} else if (include) {
|
||||
fillRectMaskAlpha(ctx, mx, my, mw, mh, mask, scale);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = '#000';
|
||||
traceRoundedRect(ctx, mx, my, mw, mh, resolveMaskCornerRadiiPx(mask, scale, mw, mh));
|
||||
ctx.fill();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
if (include) {
|
||||
fillRectMaskAlpha(ctx, mx, my, mw, mh, mask, scale);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, ew, eh);
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
fillRectMaskAlpha(ctx, mx, my, mw, mh, mask, scale);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/** 对已绘制的图层离屏画布应用蒙版(destination-in) */
|
||||
async function applyMaskToLayerCanvas(
|
||||
layerCtx: CanvasRenderingContext2D,
|
||||
elem: TemplateElement,
|
||||
mask: ElementMaskConfig,
|
||||
ew: number,
|
||||
eh: number,
|
||||
scale: number
|
||||
) {
|
||||
const alphaCanvas = await buildMaskAlphaCanvas(elem, mask, ew, eh, scale);
|
||||
layerCtx.save();
|
||||
layerCtx.globalCompositeOperation = 'destination-in';
|
||||
layerCtx.drawImage(alphaCanvas, 0, 0);
|
||||
layerCtx.restore();
|
||||
}
|
||||
|
||||
type DrawLayerFn = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层,应用蒙版后再合成到主画布。
|
||||
* 仅对当前元素的 layer 回调内容生效,边框等装饰应在合成后单独绘制。
|
||||
*/
|
||||
export async function composeElementLayer(
|
||||
mainCtx: CanvasRenderingContext2D,
|
||||
elem: TemplateElement,
|
||||
ex: number,
|
||||
ey: number,
|
||||
ew: number,
|
||||
eh: number,
|
||||
scale: number,
|
||||
drawLayer: DrawLayerFn
|
||||
) {
|
||||
const mask = isElementMaskActive(elem.mask) ? elem.mask : undefined;
|
||||
|
||||
if (!mask) {
|
||||
await drawLayer(mainCtx, ex, ey, ew, eh);
|
||||
return;
|
||||
}
|
||||
|
||||
const layerCanvas = document.createElement('canvas');
|
||||
layerCanvas.width = Math.max(1, ew);
|
||||
layerCanvas.height = Math.max(1, eh);
|
||||
const layerCtx = layerCanvas.getContext('2d');
|
||||
if (!layerCtx) {
|
||||
await drawLayer(mainCtx, ex, ey, ew, eh);
|
||||
return;
|
||||
}
|
||||
|
||||
await drawLayer(layerCtx, 0, 0, ew, eh);
|
||||
await applyMaskToLayerCanvas(layerCtx, elem, mask, ew, eh, scale);
|
||||
mainCtx.drawImage(layerCanvas, ex, ey);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 composeElementLayer */
|
||||
export const drawElementContentWithMask = composeElementLayer;
|
||||
|
||||
Reference in New Issue
Block a user