给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

相关推荐
专业抄代码选手10 分钟前
01|从手写 DOM 到虚拟 DOM:把 UI 从“操作”变成“描述”
前端·javascript·react.js
喜欢睡觉11 分钟前
单词管理系统
前端·后端
YHL19 分钟前
向量数据库入门(二):Embedding → 语义搜索 → RAG 日记助手
前端·架构
码视野20 分钟前
基于 Vue3 + Element Plus 的【青少年心理健康智能测评与 AI 情绪树洞陪护干预系统】设计与实现(含PRD/三端源码/大屏)
前端·vue.js·人工智能·vue3
专业抄代码选手21 分钟前
03|React 为什么需要协调:比较两棵树,而不是重建页面
前端·javascript·react.js
灯澜忆梦23 分钟前
【基于GO的Web开发11】gin获取URL‑Path 路径参数
前端·后端·golang·html·gin
王霸天24 分钟前
GLB 文件太大怎么办?我把它从 47MB 压到 8MB,全程没碰命令行
前端·javascript·程序员
半个落月24 分钟前
Next.js 16 笔记应用实战:Redis 数据链路与组件两版拆分详解
前端·redis·next.js
YIAN25 分钟前
告别等后端!React+Vite 前端独立开发全流程:接口封装 + Mock 方案 + 状态管理
前端·react.js·vite
PedroQue9929 分钟前
@meng-xi/vite-plugin v1.3.0:generateUni 一键流水线
前端·vite