jtwang7 / JavaScript-Note

JavaScript学习笔记
10 stars 2 forks source link

canvas 绘制 #25

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

canvas 绘制

参考:

canvas 画布

html 通过标签 <canvas /> 创建一个画布,浏览器可以在该 <canvas /> 画布元素上绘图。

注:创建 <canvas /> 元素时必须要设置其 width 和 height 属性,告诉浏览器所能绘制的画布范围。除了在 <canvas width="..." height="..." /> 上初始化 width 和 height 属性外,同其他元素一样,可以通过 js 在 DOM 节点上设置宽高属性。

// 创建画布元素
<canvas id="drawing" width="200" height="200">

id 属性一般是必要的,因为后续我们会通过 id 来获取该 DOM 节点。

获取绘图上下文

要在画布上绘制图形,第一步先获取绘图上下文: 通过调用 canvas DOM 元素的 getContext() 方法,我们可以获取 canvas 元素的绘图上下文引用。对于平面图形,需要给方法传入参数 "2d",此时我们获取的就是 2D 的绘图上下文。

let drawing = document.getElementById("drawing"); // 获取 DOM 节点

if (drawing.getContext) { // 确保浏览器支持 <canvas />
  let ctx = drawing.getContext("2d"); // 获取绘图上下文
}

使用 canvas 元素时,最好检查 getContext 方法是否存在,确保浏览器支持 <canvas />

描边 & 填充

描边:为图形边界着色,对应属性 strokeStyle,属性值可以时字符串,渐变对象或者图案对象。

字符串表示颜色值,可以是 CSS 支持的任意格式:名称,十六进制代码,rgb,rgba,hsl,hsla

填充:为形状自动填充指定样式,对应 fillStyle 属性,样式可以是颜色、渐变或图像。

let drawing = document.getElementById("drawing"); // 获取 DOM 节点

if (drawing.getContext) { // 确保浏览器支持 <canvas />
  let ctx = drawing.getContext("2d"); // 获取绘图上下文

  ctx.strokeStyle = 'red';
  ctx.fillStyle = '#fff';
}

strokeStyle 和 fillStyle 直属于 ctx 上下文对象的属性,因此后续所有在该画布上下文中绘制的图形,都会使用这两种描边和填充样式,除非再次修改。

绘制矩形

矩形是 canvas 唯一一个直接提供的形状绘制方式。canvas 绘图上下文提供了相关的三个方法:

  1. context.fillRect(x, y, w, h):在画布上绘制并自动填充矩形,填充颜色用 fillStyle 属性指定。
  2. strokeRect(x, y, w, h):在画布上绘制矩形轮廓,边界颜色用 strokeStyle 属性指定。
  3. clearRect(x, y, w, h):擦除画布中某个区域

    (x, y) 为绘制的坐标起点(矩形左上角),(w, h) 为矩形的宽高。

let drawing = document.getElementById("drawing"); // 获取 DOM 节点

if (drawing.getContext) { // 确保浏览器支持 <canvas />
  let ctx = drawing.getContext("2d"); // 获取绘图上下文

  // 坐标 (30, 30) 处绘制一个宽高 50 ,半透明蓝色矩形
  ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
  ctx.fillRect(30, 30, 50, 50); 
}

clearRect()

context.clearRect() 用于清除画布某个区域,这个方法在某些时候特别重要。 首先,我们要知道:在 canvas 画布中绘制的图形是永久保留的,如此强调这一点是因为,若要实现一个物体的移动效果,单纯改变绘制的坐标是行不通的,如下:

let drawing = document.getElementById("drawing"); // 获取 DOM 节点

if (drawing.getContext) { // 确保浏览器支持 <canvas />
  let ctx = drawing.getContext("2d"); // 获取绘图上下文

  let x = 0, y = 0;
  ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
  setInterval(()=>{
    ctx.fillRect(x, y, 50, 50); 
    x += 1;
    y += 2;
  }, 1000)
}

上述效果会保留所有绘制的矩形,因此最终会展示出来一连串的矩形。我们需要在下一次绘制矩形时,清除上一次的绘制,最简单的方法就是在下一次绘制时,清空整个画布区域(前提时该画布中没有其他需要保留的图形)。

let drawing = document.getElementById("drawing"); // 获取 DOM 节点

if (drawing.getContext) { // 确保浏览器支持 <canvas />
  let ctx = drawing.getContext("2d"); // 获取绘图上下文

  let x = 0, y = 0;
  ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
  setInterval(()=>{
    ctx.clearRect(0, 0, 200, 200); // 清除 (0, 0) 坐标起的 200 * 200 区域画布,针对本例是当前画布所有区域。
    ctx.fillRect(x, y, 50, 50); 
    x += 1;
    y += 2;
  }, 1000)
}

绘制路径

canvas 只提供了绘制矩形的方法,若想要绘制更加复杂的图形或形状,就需要调用 2D 绘图上下文的路径绘制。 路径绘制方法:

  1. context.beginPath():表示开始绘制新的路径。
  2. context.arc(x, y, radius, startAngle, endAngle, counterclockwise):以坐标(x, y) 为圆心,radius 为半径画一条弧线,起始角度 startAngle,结束角度 endAngle (单位均为弧度),counterclockwise 表示是否逆时针计算起始角度和结束角度(默认顺时针)
  3. context.arcTo(x1, y1, x2, y2, radius):以给定半径 radius,从上一点开始,绘制一条经过 (x1, y1) 到 (x2, y2) 的弧线。
  4. context.quadraticCurveTo(cx, cy, x, y):以 (cx, cy) 为控制点,绘制一条从上一点到 (x, y) 的弧线(二次贝塞尔曲线)
  5. context.bezierCurveTo(c1x, c1y, c2x, c2y, x, y):以 (c1x, c1y) 和 (c2x, c2y) 为控制点,绘制一条从上一点到 (x, y) 的弧线 (三次贝塞尔曲线)
  6. context.lineTo(x, y):绘制一条从上一点到 (x, y) 的直线
  7. context.moveTo(x, y):不绘制线条,只把绘制光标移动到 (x, y),即可以调整“上一点”的位置
  8. context.rect(x, y, width, height):以给定宽度和高度在 (x, y) 绘制一个矩形。该方法与矩形绘制相比,区别在于该方法创建的是一条路径,不是独立的图形。

路径绘制完成后,canvas 还提供了额外的操作路径方法:

  1. context.closePath():绘制一条返回起点的线,即闭合路径。
  2. context.fill():填充(闭合)路径,填充颜色通过 fillStyle 属性指定。
  3. context.stroke():描画路径,轮廓颜色通过 strokeStyle 属性指定。
  4. context.clip():基于已有路径创建一个剪切区域。
// 绘制一个圆形
let drawing = document.getElementById("drawing");
if (drawing.getContext) {
  let ctx = drawing.getContext("2d");
  // 创建路径
  ctx.beginPath();
  // 绘制圆形路径 (圆心 (0,0),半径 100)
  ctx.arc(0, 0, 100, 0, 2 * Math.PI, false)
  // 指定填充颜色
  ctx.fillStyle = '#fff';
  // 填充路径
  ctx.fill();
}

注意:填充路径或描画路径是必要的,否则展示的路径填充或描画轮廓颜色均为 '#000000'。

jtwang7 commented 3 years ago

canvas 图片绘制:drawImage()

参考文章:

ctx.drawImage()

canvas 将图片绘制到画布上,就是依靠 ctx 上下文对象的 drawImage() 方法实现的。 参考:HTML5 canvas drawImage() 方法 可以知道它的一些基本用法

定义和用法

  1. JavaScript 语法 1 在画布上定位图像: context.drawImage(img,x,y);

  2. JavaScript 语法 2 在画布上定位图像,并规定图像的宽度和高度: context.drawImage(img,x,y,width,height);

  3. JavaScript 语法 3 剪切图像,并在画布上定位被剪切的部分: context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

参数值

参数 描述
img 规定要使用的图像、画布或视频。
sx 可选。开始剪切的 x 坐标位置。
sy 可选。开始剪切的 y 坐标位置。
swidth 可选。被剪切图像的宽度。
sheight 可选。被剪切图像的高度。
x 在画布上放置图像的 x 坐标位置。
y 在画布上放置图像的 y 坐标位置。
width 可选。要使用的图像的宽度。(伸展或缩小图像)
height 可选。要使用的图像的高度。(伸展或缩小图像)

drawImage 方法绘制无效问题

在 html 里图片的加载时是异步的。(设置src本身是同步的,但是浏览器下载和显示图片是异步的。) 因此,在资源还没有加载完成的时候就执行了 drawImage,所以无法成功加载到画布当中。 为了解决图片异步加载的问题,我们通过添加一个 onload 事件,并将 drawImage 作为它的回调函数,等待图片加载完成后自动执行回调。

onload 事件会在页面或图像加载完成后立即发生。

正确代码示例

<canvas id='mycanvas' width="500" height="500"></canvas>
<script type="text/javascript">
  window.onload = function(){
    // 创建画布
    let mycanvas = document.getElementById('mycanvas')
    let ctx = mycanvas.getContext('2d')

    // 创建 Image 实例
    let img = new Image();
    img.onload = function(){
      // 等待图片异步加载完成后,执行绘制操作
      ctx.drawImage(img,100,100);    
    }
    // 加载图片源
    img.src = "https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1537549551&di=3f8d4d76679adcae225387f7d6b199aa&src=http://gss0.baidu.com/-4o3dSag_xI4khGko9WTAnF6hhy/lvpics/h=800/sign=b49dc48f8718367ab28972dd1e728b68/9922720e0cf3d7ca7f0736d0f31fbe096a63a9a6.jpg";
  } 
</script>
jtwang7 commented 2 years ago

Canvas to Image

参考:HTMLCanvasElement.toDataURL()

方法集合

Canvas可以导出为图片的方法有以下几种: ✅ toDataURL():该方法可以将Canvas内容导出为一张Base64编码格式的图片。示例代码如下:

var canvas = document.getElementById('myCanvas');
var image = canvas.toDataURL("image/png");

✅ toBlob():该方法可以将Canvas内容导出为Blob对象。示例代码如下:

var canvas = document.getElementById('myCanvas');
canvas.toBlob(function(blob) {
  // 将blob对象转换成URL
  var url = URL.createObjectURL(blob);
  // 创建一个img标签显示图片
  var img = document.createElement('img');
  img.src = url;
  document.body.appendChild(img);
});

✅ drawImage():该方法可以将Canvas内容绘制到一个img标签或者另一个Canvas中,实现导出为图片的效果。示例代码如下:

var canvas = document.getElementById('myCanvas');
var img = new Image();
img.src = canvas.toDataURL("image/png");
// 将Canvas内容绘制到一个img标签中
document.body.appendChild(img);

✅ toDataURLHD():该方法可以将Canvas内容导出为高清晰度的图片,适用于高分辨率屏幕。示例代码如下:

var canvas = document.getElementById('myCanvas');
var image = canvas.toDataURLHD("image/png");

toDataURL() 简介

HTMLCanvasElement.toDataURL() 方法返回一个包含图片展示的 data URI。可以使用 type 参数指定其类型,默认为 PNG 格式。图片的分辨率为96dpi。

语法

canvas.toDataURL(type, encoderOptions);

参数:

返回值: 包含 data URI 的DOMString

toBlob() 简介

canvas.toBlob() 方法是在 HTML5 中提供的 Canvas API 的一部分,它允许将 Canvas 元素的内容保存为一个 Blob 对象。Blob 对象是二进制数据的容器,它通常用于将数据发送到服务器或在客户端进行本地存储。

相较于 toDataURL() 方法,blob 对象的存储体积更小,可以有效避免图片较大时 toDataURL 生成的 base64 文件路径过长,超出浏览器限制,导致无法下载的问题。

以下是使用 canvas.toBlob() 方法的示例代码:

var canvas = document.getElementById("myCanvas");
canvas.toBlob(function(blob) {
  // 处理 Blob 对象
  var url = URL.createObjectURL(blob);
  // 将 URL 分配给图像元素
  var img = document.createElement("img");
  img.src = url;
  document.body.appendChild(img);
}, "image/jpeg", 0.95);

填坑记录

关于 WebGL Canvas 通过 toDataUrl 导出图片,结果显示空白的问题

为了将 canvas 保存为图片地址,我们需要获取到 canvas 实例对象,通常我们只需要通过 document.querySelector() 等 DOM 元素获取方法获取 canvas 对象即可,但是 webgl 类型的 canvas 需要做一步特殊的处理:

let canvas = document.querySelector('xxx');  // 获取 canvas 实例对象
let webgl = canvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});  // 修改 canvas 对象的上下文
let imgUrl = webgl.canvas.toDataURL('image/webgl');  // 调用修改后的 canvas.toDataURL 方法

这么做的原因是因为: webgl canvas 的 preserveDrawingBuffer 默认值为 false (WebGLRenderer),它的 bool 值决定了是否保留绘图缓冲区,默认是不保留,这就导致完成 webgl canvas 绘图后,我们获取不到画布信息,转出来的 image url 自然就变成了一个空白图片。因此我们需要在上下文中修改该属性值,然后再重新获取 canvas 实例对象转为 url 形式。

关于 webgl preserveDrawingBuffer 属性的详细解答

I know this has been answered elsewhere but I can't find it so ....

preserveDrawingBuffer: false

means WebGL can swap buffers instead of copy buffers.

WebGL canvases have 2 buffers. The one you're drawing to and the one being displayed. When it comes time to draw the webpage WebGL has 2 options

Copy the drawing buffer to the display buffer.

This operation is slower obviously as copying thousands or millions pixels is not a free operation

Swap the two buffers.

This operation is effectively instant as nothing really needs to happen except to swap the contents of 2 variables.

Whether WebGL swaps or copies is up to the browser and various other settings but if preserveDrawingBuffer is false WebGL can swap, if it's true it can't.

If you'd like to see a perf difference I'd suggested trying your app on mobile phone. Make sure antialiasing is off too since antialiasing requires a resolve step which is effectively copy operation.

空白图片导出原因汇总

当使用 canvas 元素的 toDataURL() 方法导出图片时,可能会出现导出的图片是空白的情况。这通常是由于以下原因之一导致的:

  1. 图片未完全渲染:toDataURL() 方法只能导出已经完全渲染的内容。如果您的 canvas 元素包含了一些异步操作或延迟加载的内容,可能需要等待它们完全加载后再导出图片。
  2. 跨域问题:如果您的 canvas 元素中包含了来自其他域名的图像或视频,由于浏览器的同源策略,您可能无法导出包含这些内容的图片。解决方法是确保这些资源与您的网站在同一个域名下,或者使用跨域资源共享(CORS)解决跨域问题。
  3. canvas 元素尺寸问题:toDataURL() 方法只会导出 canvas 元素的内容,如果 canvas 元素的尺寸为0或者非常小,那么导出的图片也会是空白的。您可以通过检查 canvas 元素的尺寸是否正确来解决这个问题。
  4. 渲染顺序问题:如果您的 canvas 元素包含多个图层,可能需要在绘制到 canvas 上之前将它们按正确的顺序绘制,否则导出的图片可能会是空白的。您可以检查您的绘制代码,确保正确的绘制顺序。

如果您无法解决这个问题,您可以尝试使用其他工具来导出 canvas 元素的内容,例如使用 html2canvas 库,这个库可以将整个 DOM 元素渲染为图片,并导出为 data URL 或者 blob 格式的图片。

jtwang7 commented 1 year ago

WebGL Canvas 对象保存为 Image URL

问题场景

浏览器对于 WebGL Context 上下文存在一定的数量限制,当一个页面中 WebGL Context 上下文数量过多时,浏览器会按先进先出的顺序丢失上下文信息,我们可以将 WebGL 上下文信息导出为图片展示,同时销毁较早的 WebGL Context 避免意外丢失的情况。

示例

本示例以 deck.gl + mapbox 第三方地图库为例,生成 WebGL Context 模拟问题场景。

export default function Map({ ods }: { ods: any[] }) {

  // ......

  const mapboxRef = useRef<MapRef>(null!);

  // 保存 deckgl 的 canvas 对象
  const deckglCanvas = useRef<HTMLCanvasElement>(null!);

  // 将不同 canvas 对象绘制到同一 canvas 对象上,并导出 url
  function canvasClone(canvasElements: HTMLCanvasElement[]) {
    const canvas = document.createElement("canvas");
    canvas.width = _.max(canvasElements.map((c) => c.width))!;
    canvas.height = _.max(canvasElements.map((c) => c.height))!;
    const ctx = canvas.getContext("2d");
    if (ctx) {
      for (const cloneTraget of canvasElements) {
        ctx.globalAlpha = 1;
        ctx.drawImage(cloneTraget, 0, 0);
      }
    }
    const url = canvas.toDataURL("image/png");
    return url;
  }

  const onMapboxLoad = () => {
    // mapbox 实例
    const instance = mapboxRef.current.getMap();

    // webgl 保存为 image 展示并销毁 webgl context
    setTimeout(() => {
      const url = canvasClone([
        mapboxRef.current.getCanvas(), // 🔥
        deckglCanvas.current,
      ]);
      setURL(url);
    }, 500);
  };

  // layers
  const arcUid = useId();
  const arclayer = useMemo(() => {
    return new ArcLayer({
      id: arcUid,
      data: ods,
      pickable: true,
      getWidth: 1,
      greatCircle: true,
      getSourcePosition: (d) => d.from.coordinates,
      getTargetPosition: (d) => d.to.coordinates,
      getSourceColor: (d) => [244, 164, 158],
      getTargetColor: (d) => [255, 101, 0],
    });
  }, [arcUid, ods]);

  const deckglUid = useId();

  // image-url lists
  const [url, setURL] = useState<string | undefined>(undefined);

  return !url ? (
    <DeckGL
      id={deckglUid}
      glOptions={{ preserveDrawingBuffer: true }} // 🔥
      initialViewState={INITIAL_VIEW_STATE}
      viewState={viewState}
      layers={[scatterplotlayer, arclayer]}
      width={width}
      height={height}
      style={{ position: "relative" }}
      controller={true}
      onAfterRender={({ gl }) => {
        deckglCanvas.current = gl.canvas as HTMLCanvasElement; // 🔥
      }}
    >
      <StaticMap
        ref={mapboxRef}
        preserveDrawingBuffer={true}  // 🔥 
        mapboxAccessToken={mapboxAccessToken}
        mapStyle="mapbox://styles/mapbox/light-v11"
        onLoad={onMapboxLoad}
      />
    </DeckGL>
  ) : (
    <img
      alt="OD弧线图"
      src={url}
      style={{ position: "relative", width, height }}
    />
  );
}

🔥 关键点

  1. 确保 WebGL Context 上下文中 preserveDrawingBuffer 属性开启

    具体原因见 关于 webgl preserveDrawingBuffer 属性的详细解答

    // webgl context - options
    xxx.getContext( 'webgl' , { preserveDrawingBuffer: true })

    在本例中,deckglmapboxpreserveDrawingBuffer 属性进行了不同方式的封装,因此具体配置需根据官网进行调整。

  2. 尽量基于第三方库正确的获取对应的 canvas 对象 本例中 deckglmapbox 均有对应的 canvas 对象获取方法,若第三方库暴露了相应的获取方法,则应尽量使用这些方法,避免直接使用 getElementById 等方式获取 canvas 对象,因为该方法获取的 canvas 对象可能并不完全或者过时。

  3. canvas 对象合并

    function canvasClone(canvasElements: HTMLCanvasElement[]) {
    const canvas = document.createElement("canvas");
    canvas.width = _.max(canvasElements.map((c) => c.width))!;
    canvas.height = _.max(canvasElements.map((c) => c.height))!;
    const ctx = canvas.getContext("2d");
    if (ctx) {
      for (const cloneTraget of canvasElements) {
        ctx.globalAlpha = 1;
        ctx.drawImage(cloneTraget, 0, 0);
      }
    }
    const url = canvas.toDataURL("image/png");
    return url;
    }
jtwang7 commented 1 year ago

Canvas 模糊问题填坑记录

当你遇到 Canvas 绘制模糊的问题时,或许你可以看看这篇文章~ 🥳

原因解析

在高清显示屏出现之前,比如屏幕宽度为1000px 那么其宽度上的物理像素也是1000px,而在高清屏出现之后,屏幕宽度为 1000px 时,物理像素有可能达到 2000px 或者更高。为了方便查询物理像素和屏幕像素的比值,在 window 对象上增加了一个属性 devicePixelRatio 来表示这个比值。 比如在 devicePixelRatio 为2的设备上,当我们使用 CSS 绘制一条 1px 的线时,为了保证绘制的大小,物理像素实际上会使用 2px 来绘制这一条线,而这样的转换是浏览器自动处理的,所以对开发者来说并没有太大的困扰。 那么上面既然说了浏览器在渲染的时候会自动处理像素比的问题,CSS 绘制的图像经过转换之后不会出现模糊的问题,但 canvas 上怎么就出现了呢? 因为在 canvas 标签上定义的 width 和 height 的值并不会被转换,100px 就会被渲染成 100px 的物理像素,但是设备要求的是 200px,这时浏览器只能智能地填充像素之间的空白,以适应要求的大小。这就是 canvas 绘制的图片出现模糊的原因。

解决方案

function createHDCanvas (canvas, target, w, h) {
  const ratio = window.devicePixelRatio || 1;
  canvas.width = w * ratio; // 实际渲染像素
  canvas.height = h * ratio; // 实际渲染像素
  canvas.style.width = `${w}px`; // 控制显示大小
  canvas.style.height = `${h}px`; // 控制显示大小
  const ctx = canvas.getContext('2d');
  // 绘制 canvas: 注意此时画布宽高是 w * ratio, h * ratio。
  // 其中 target 是外部 canvas 或 image,将其覆盖到当前 canvas 上。
  // canvas 通常是一个空容器,用于承载 target 这个实际目标。
  ctx.drawImage(target, 0, 0, w * ratio, h * ratio)
  // 缩放尺寸
  ctx.scale(ratio, ratio)
  // canvas 绘制
  return canvas;
}

🌈 CanvasRenderingContext2D.scale() 是 Canvas 2D API 根据 x 水平方向和 y 垂直方向,为 canvas 单位添加缩放变换的方法: 默认的,在 canvas 中一个单位实际上就是一个像素。例如,如果我们将 0.5 作为缩放因子,最终的单位会变成 0.5 像素,并且形状的尺寸会变成原来的一半。相似的方式,我们将 2.0 作为缩放因子,将会增大单位尺寸变成两个像素。形状的尺寸将会变成原来的两倍。 对应示例中 ctx.scale(ratio, ratio) 的含义就是:将 canvas 画布的像素尺寸扩大 ratio 倍。

【canvas 画布尺寸和样式尺寸是一致的,不存在显示像素与物理像素不一致的情况,在 canvas 中一个单位实际上就是一个像素,所以 scale 缩放同时影响这两个尺寸不难理解】

由于我们将 canvas 初始画布尺寸设置为了元素尺寸设置 ratio 倍,因此将 canvas 放大后,确保了元素尺寸和画布尺寸一致,画布中的内容完全展示。若不调用 scale 缩放方法,canvas 只能展示部分的绘图内容。

🔥 上述方法实际绘制的元素尺寸被扩大了 ratio 倍,图像展示仍是模糊的,我们还需要用原来的尺寸去加载当前图像,相当于变相对图像进行了压缩,此时图像大小为初定元素尺寸(物理像素),显示像素则为原来的两倍(2 * 元素尺寸)。

Pixi.js 解决方案

const app = new PIXI.Application({
  // ......
  resolution: 2, // 👉 设置像素比
  autoDensity: true, // 👉 自适应尺寸大小
});

总结

Canvas导出图片模糊的问题通常是由于导出的图片分辨率低于实际画布分辨率的情况。Canvas实际上是基于像素的画布,如果导出的图片分辨率低于实际画布分辨率,那么就会导致图片模糊。

解决方法包括:


2023-04-25 补充:canvas 绘制时的像素比处理

假设我们现在 canvas 和 css 的大小都是 10 * 10,那么 canvas 画完的照片中就会有 100 个(像素)点,也就是只有 100 个点的信息;但是到了高清屏中(如 dpr = 2),我们需要 400 个点的信息,原来的点不够用怎么办?于是就会有一套算法来自动生成这些点的信息,从而造成了模糊。那应该怎么办呢🤔?我们需要更多的点,所以可以这样子搞,把画布放大 dpr 倍,也就是把 canvas 变大 dpr 倍,而 css 的大小保持不变,虽然 canvas 变大了,但是最终在页面上绘制的时候放到 css 中又会因为等比缩放(上面第一点说到的原因)变回来了原来大小,这样一折腾,点就变多了。但是要注意什么呢,画布变大了,相应的绘制操作(画圆、画矩形等)也需要相应放大,一般有两种方法:

✅ 方法一

// 放大画布之后,直接用 scale 放大整个坐标系
// 但是你要知道我们一直是在放大的坐标系上绘制的,可能不知道什么时候(比如重新设置画布宽高),scale 可能就会被重置成 1 了,从而造成画面错乱
adaptDPR() { // 在初始化 canvas 的时候就要调用该方法
    const dpr = window.devicePixelRatio;
    const { width, height } = this.canvas;
    // 重新设置 canvas 自身宽高大小和 css 大小。放大 canvas;css 保持不变,因为我们需要那么多的点
    this.canvas.width = Math.round(width * dpr);
    this.canvas.height = Math.round(height * dpr);
    this.canvas.style.width = width + 'px';
    this.canvas.style.height = height + 'px';
    // 直接用 scale 放大整个坐标系,相对来说就是放大了每个绘制操作
    this.ctx2d.scale(dpr, dpr);
    // 接下来的绘制操作和往常一样,比如画个矩形 ctx2d.strokeRect(x, y, w, h);原来该怎么算就怎么算,该怎么调用还是怎么调用
}

✅ 方法二

// 放大画布之后,需要把每一个绘制的 api 都乘以 dpr
// * 这样一来使用的时候就会很麻烦,所以我们需要把所有的绘制操作进行统一封装
// 可以参考这个库:https://github.com/jondavidjohn/hidpi-canvas-polyfill,不过这个库也不是所有 api 都覆盖
adaptDPR() { // 在初始化 canvas 的时候就要调用该方法
    const dpr = window.devicePixelRatio;
    const { width, height } = this.canvas;
    // 重新设置 canvas 自身宽高大小和 css 大小。放大 canvas;css 保持不变,因为我们需要那么多的点
    this.canvas.width = Math.round(width * dpr);
    this.canvas.height = Math.round(height * dpr);
    this.canvas.style.width = width + 'px';
    this.canvas.style.height = height + 'px';
    // 注意这里没有用 scale
}
// 接下来在每个涉及绘制的 api 时都乘以 dpr,比如绘制矩形的时候
strokeRect(x: number, y: number, w: number, h: number) {
    const { ctx2d, dpr } = this;
    if (!ctx2d) return;
    x = x * dpr;
    y = y * dpr;
    w = w * dpr;
    h = h * dpr;
    ctx2d.strokeRect(x, y, w, h);
}
jtwang7 commented 1 year ago

Image Download (base on Canvas)

基于 Canvas 的浏览器图片下载方法

base64

利用 Canvas 将图片转为 base64 地址,创建 a 标签下载图片地址。

存在的问题:图片体积过大时,转成的 base64 url 地址字符串过长,可能会超出浏览器限制的字符串最大长度而被截断,从而下载失败。 解决方法:转用 toBlob() 生成 blob 对象

function getImageDataURL(image) {
    // 创建画布
    const canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    const ctx = canvas.getContext('2d');
    // 以图片为背景剪裁画布
    ctx.drawImage(image, 0, 0, image.width, image.height);

    return canvas.toDataURL('image/png', 1);
}

function downLoad(downloadName, url) {
    const tag = document.createElement('a');
    tag.download = downloadName // download 属性值是最终下载得到的图片文件的文件名

    const image = new Image();
    image.src = url;
    // 🔥 设置 crossOrigin 属性,否则图片跨域会报错
    image.setAttribute('crossOrigin', 'Anonymous');
    // 等待图片加载完成再下载资源
    image.addEventListner('load', () => {
        tag.href = getImageDataURL(image); // 关联图片链接
        tag.click(); // 触发点击
    })
}

blob

function getImageDataURL(image) {

    return canvas.toDataURL('image/png', 1);
}

function downLoad(downloadName, url) {
    const tag = document.createElement('a');
    tag.download = downloadName // download 属性值是最终下载得到的图片文件的文件名

    const image = new Image();
    image.src = url;
    // 🔥 设置 crossOrigin 属性,否则图片跨域会报错
    image.setAttribute('crossOrigin', 'Anonymous');
    // 等待图片加载完成再下载资源
    image.addEventListner('load', () => {
        // 创建画布
        const canvas = document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;
        const ctx = canvas.getContext('2d');
        // 以图片为背景剪裁画布
        ctx.drawImage(image, 0, 0, image.width, image.height);
        // 生成 Blob 对象实例
        canvas.toBlob((blob) => {
          tag.href = URL.createObjectURL(blob!);
          tag.click();
        });
    })
}