给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

相关推荐
LaughingZhu5 小时前
Product Hunt 每日热榜 | 2026-05-21
前端·人工智能·经验分享·chatgpt·html
怕浪猫5 小时前
Electron 开发实战(一):从零入门核心基础与环境搭建
前端·electron·ai编程
小鹏linux6 小时前
Ubuntu 22.04 部署开源免费具有精美现代web页面的Casdoor账号管理系统
linux·前端·ubuntu·开源·堡垒机
前端若水7 小时前
会话管理:创建、切换、删除对话历史
前端·人工智能·python·react.js
Bigger7 小时前
mini-cc:一个轻量级 AI 编程助手的诞生
前端·ai编程·claude
涵涵(互关)7 小时前
Naive-ui树型选择器只显示根节点
前端·ui·vue
BY组态7 小时前
Ricon组态系统最佳实践:从零开始构建物联网监控平台
前端·物联网·iot·web组态·组态
BY组态7 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·物联网·iot·web组态·组态
SoaringHeart7 小时前
Flutter进阶:OverlayEntry 插入图层管理器 NOverlayZIndexManager
前端·flutter
放下华子我只抽RuiKe57 小时前
React 从入门到生产(四):自定义 Hook
前端·javascript·人工智能·深度学习·react.js·自然语言处理·前端框架