问题引入
先附送上PR链接:github.com/le5le-com/m...
今日在开发公司项目需求的时候发现一个问题,在Mac m1下使用Meta2d
内置的toPng
方法绘制png图片,在高清屏下会导致图片模糊。具体糊成啥样,请看VCR:
毕竟项目是需要面向客户的,拿出这样的图,作为一个有强迫症的coder,肯定是不能容忍的!!!于是就带着问题去翻看了@meta2d/core
的源码。
产生原因分析
Meta2d
源码对于toPng
源码实现如下(仅截取主函数):
ini
toPng(padding: Padding = 2, callback?: BlobCallback, containBkImg = false) {
const rect = getRect(this.store.data.pens);
if (!isFinite(rect.width)) {
throw new Error('can not to png, because width is not finite');
}
const oldRect = deepClone(rect);
const storeData = this.store.data;
// TODO: 目前背景颜色优先级更高
const isDrawBkImg =
containBkImg && !storeData.background && this.store.bkImg;
// 主体在背景的右侧,下侧
let isRight = false,
isBottom = false;
if (isDrawBkImg) {
rect.x += storeData.x;
rect.y += storeData.y;
calcRightBottom(rect);
if (rectInRect(rect, this.canvasRect, true)) {
// 全部在区域内,那么 rect 就是 canvasRect
Object.assign(rect, this.canvasRect);
} else {
// 合并区域
const mergeArea = getRectOfPoints([
...rectToPoints(rect),
...rectToPoints(this.canvasRect),
]);
Object.assign(rect, mergeArea);
}
isRight = rect.x === 0;
isBottom = rect.y === 0;
}
// 有背景图,也添加 padding
const p = formatPadding(padding);
rect.x -= p[3];
rect.y -= p[0];
rect.width += p[3] + p[1];
rect.height += p[0] + p[2];
calcRightBottom(rect);
const canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
if (
canvas.width > 32767 ||
canvas.height > 32767 ||
(!navigator.userAgent.includes('Firefox') &&
canvas.height * canvas.width > 268435456) ||
(navigator.userAgent.includes('Firefox') &&
canvas.height * canvas.width > 472907776)
) {
throw new Error(
'can not to png, because the size exceeds the browser limit'
);
}
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'middle'; // 默认垂直居中
if (isDrawBkImg) {
const x = rect.x < 0 ? -rect.x : 0;
const y = rect.y < 0 ? -rect.y : 0;
ctx.drawImage(
this.store.bkImg,
x,
y,
this.canvasRect.width,
this.canvasRect.height
);
}
const background =
this.store.data.background || this.store.options.background;
if (background) {
// 绘制背景颜色
ctx.save();
ctx.fillStyle = background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
if (!isDrawBkImg) {
ctx.translate(-rect.x, -rect.y);
} else {
// 平移画布,画笔的 worldRect 不变化
ctx.translate(
isRight ? storeData.x : -oldRect.x,
isBottom ? storeData.y : -oldRect.y
);
}
for (const pen of this.store.data.pens) {
// 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
if (!isShowChild(pen, this.store) || pen.visible == false) {
continue;
}
// TODO: hover 待考虑,若出现再补上
const { active } = pen.calculative;
pen.calculative.active = false;
if (pen.calculative.img) {
renderPenRaw(ctx, pen);
} else {
renderPen(ctx, pen);
}
pen.calculative.active = active;
}
if (callback) {
canvas.toBlob(callback);
return;
}
return canvas.toDataURL();
}
通过阅读源码,发现toPng
实现的核心是canvas
,猛然间发现了问题出现的原因!canvas
绘制图片,必然得跟dpi联系,就跟绿茶配青梅,天生是一对。
使用 canvas
绘制图片或者是文字在 Retina
屏中会非常模糊。究其原因,canvas
不是矢量图,而是像图片一样是位图模式的。高 dpi 显示设备意味着每平方英寸有更多的像素。也就是说二倍屏,浏览器就会以 2 个像素点的宽度来渲染一个像素,该 canvas
在 Retina
屏幕下相当于占据了2倍的空间,相当于图片被放大了一倍,因此绘制出来的图片文字等会变模糊。
因此,要做 Retina
屏适配,关键是知道当前屏幕的设备像素比,然后将 canvas
放大到该设备像素比来绘制,然后将 canvas
压缩到一倍来展示。
解决办法
既然知道了问题产生的原因,解决起来也就很好办了。在浏览器的 window
对象中有一个 devicePixelRatio
的属性,该属性表示了屏幕的设备像素比,即用几个(通常是 2 个)像素点宽度来渲染 1 个像素。所以我们只需要根据实际渲染倍率来缩放canvas
,即可解决如上问题。
注意基础知识点:
要设置 canvas
的画布大小,使用的是 canvas.width
和 canvas.height
;
要设置画布的实际渲染大小,使用的 style
属性或 CSS
设置的 width
和 height
,只是简单的对画布进行缩放。
如上所诉,针对meta2d源码,笔者做了如下改动:
修改了toPng函数之后,在本地测试,效果如下:
肉眼可见的变清晰了不少!
改动完成之后,顺带给Meta2d
官网提交了PR,期待官方的merge
!