给Meta2d 提交了一个 PR

问题引入

先附送上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

相关推荐
海带紫菜菠萝汤8 小时前
FFmpeg.wasm 实践:在浏览器中运行 FFmpeg 的能力边界与性能瓶颈
前端·javascript·ffmpeg·音视频·wasm
名字还没想好☜8 小时前
React 受控输入框光标跳到末尾:格式化输入时的 selection 丢失 bug 与修复
前端·javascript·react.js·bug·react·next.js
Access开发易登软件8 小时前
Access 怎么做前后端分离?用 Web API 读写 SQL Server
前端·数据库·人工智能·microsoft·excel·access
10share8 小时前
React 新一代样式隔离方案 —— 编译时、零运行时、原生写法
前端·react.js
Revolution619 小时前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
我要两颗404西柚9 小时前
Stage three:VUE工程化与实战工具
前端·javascript·vue.js
光影少年9 小时前
RN 的EventEmitter 双向通信
前端·react native·react.js
布鲁飞丝10 小时前
从零实现富文本编辑器#-浏览器选区与编辑器选区模型同步
java·前端·编辑器
写不来代码的草莓熊10 小时前
Cesium 地图交互绘制圆形、多边形
前端·javascript·地图
weixin_4277716110 小时前
前端版本检测
前端