Skip to content

canvas 像素操作

到目前为止,我们尚未深入了解 Canvas 画布真实像素的原理,事实上,你可以直接通过 ImageData 对象操纵像素数据,直接读取或将数据数组写入该对象中。稍后我们也将深入了解如何控制图像使其平滑(反锯齿)以及如何从 Canvas 画布中保存图像。

ImageData 对象

ImageData 对象中存储着 canvas 对象真实的像素数据,它包含以下几个只读属性:

  • width:图片宽度,单位是像素

  • height:图片高度,单位是像素

  • data:Uint8ClampedArray 类型的一维数组,包含着 RGBA 格式的整型数据,范围在 0 至 255 之间(包括 255)。 data 属性返回一个 Uint8ClampedArray,它可以被使用作为查看初始像素数据。每个像素用 4 个 1bytes 值 (按照红,绿,蓝和透明值的顺序; 这就是"RGBA"格式) 来代表。每个颜色值部份用 0 至 255 来代表。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引 0 位置。像素从左到右被处理,然后往下,遍历整个数组。

    Uint8ClampedArray 包含 height × width × 4 字节数据,索引值从 0 到 (height× width × 4)-1

    例如,要读取图片中位于第 2 行,第 200 列的像素的蓝色部份,你会写以下代码:

    js
    blueComponent = imageData.data[((1 * (imageData.width * 4)) + (200 * 4)) + 2];

    根据行、列读取某像素点的 R/G/B/A 值的公式:

    imageData.data[(((row - 1) * (imageData.width * 4)) + (column * 4)) + 0/1/2/3];

    你可能用会使用 Uint8ClampedArray.length 属性来读取像素数组的大小(以字节为单位):

    js
    var numBytes = imageData.data.length;

创建一个 ImageData 对象

去创建一个新的,空白的 ImageData 对象,你应该会使用 createImageData() 方法。有 2 个版本的 createImageData() 方法。

js
var myImageData = ctx.createImageData(width, height);

上面代码创建了一个新的具体特定尺寸的 ImageData 对象。所有像素被预设为透明黑。

你也可以创建一个被 anotherImageData 对象指定的相同像素的 ImageData 对象。这个新的对象像素全部被预设为透明黑。这个并非复制了图片数据。

js
var myImageData = ctx.createImageData(anotherImageData);

得到场景像素数据

为了获得一个包含画布场景像素数据的 ImageData 对象,你可以用 getImageData() 方法:

js
var myImageData = ctx.getImageData(left, top, width, height);

这个方法会返回一个 ImageData 对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为 (left, top), (left + width, top), (left, top + height), 以及 (left + width, top + height) 四个点。这些坐标点被设定为画布坐标空间元素。

备注:

任何在画布以外的元素都会被返回成一个透明黑的 ImageData 对象。

颜色选择器

在这个例子里面,我们会使用 getImageData() 去展示鼠标光标下的颜色。为此,我们要当前鼠标的位置,记为 layerX 和 layerY,然后我们去查询 getImageData() 给我们提供的在那个位置的像数数组里面的像素数据。最后我们使用数组数据去设置背景颜色和 <div> 的文字去展示颜色值。

vue
<template>
  <canvas id="online-image-scale" width="200" height="200"></canvas>
  <div id="hovered-color" class="w-100px h-100px"></div>
  <div id="selected-color" class="w-100px h-100px"></div>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
import { withBase } from 'vitepress'
onMounted(() => {
  const img = new Image()
  img.crossOrigin = 'anonymous';
  img.src = withBase('https://lukecheng2.oss-cn-chengdu.aliyuncs.com/public/img/88e011e13fc534253be9b867b157a124.jpg')
  var canvas = document.getElementById('online-image-scale') as HTMLCanvasElement;
  var ctx = canvas.getContext('2d')!;
  img.onload = function() {
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
  };

  var hoveredColor = document.getElementById('hovered-color');
  var selectedColor = document.getElementById('selected-color');

  function pick(event, destination) {
    console.log(event,'xx')
    var x = event.offsetX;
    var y = event.offsetY;
    var pixel = ctx.getImageData(x, y, 1, 1);
    var data = pixel.data;

    const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
    destination.style.background = rgba;
    destination.textContent = rgba;

    return rgba;
  }

  canvas.addEventListener('mousemove', function(event) {
      pick(event, hoveredColor);
  });
  canvas.addEventListener('click', function(event) {
      pick(event, selectedColor);
  });
})
</script>

在场景中写入像素数据

你可以用 putImageData() 方法去对场景进行像素数据的写入。

dx,dy 表示偏移量。

js
ctx.putImageData(myImageData, dx, dy);

比如你想从左上角开始写入数据:

js
ctx.putImageData(myImageData, 0, 0);

图片灰度和反相颜色

在这个例子里,我们遍历所有像素以改变他们的数值。然后我们将被修改的像素数组通过 putImageData() 放回到画布中去。invert 函数仅仅是去减掉颜色的最大色值 255。grayscale 函数仅仅是用红绿和蓝的平均值。你也可以用加权平均,例如 x = 0.299r + 0.587g + 0.114b 这个公式。更多资料请参考维基百科的 Grayscale。

vue
<template>
  <canvas id="gray-and-reverse-picture-color" width="200" height="200"></canvas>
  <div>
    <label>
      inverted<input value="inverted" name="color" id="inverted" type="radio"/>
    </label>
    <label>
      grayscale<input value="grayscale" name="color" id="n" type="radio"/>
    </label>
    <label>
      original<input value="original" name="color" id="n" type="radio"/>
    </label>
  </div>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
import { withBase } from 'vitepress'
onMounted(() => {
  var img = new Image();
  img.crossOrigin = 'anonymous';
  img.src = withBase('https://lukecheng2.oss-cn-chengdu.aliyuncs.com/public/img/88e011e13fc534253be9b867b157a124.jpg');

  var canvas = document.getElementById('gray-and-reverse-picture-color') as HTMLCanvasElement;
  var ctx = canvas.getContext('2d')!;

  img.onload = function() {
      ctx.drawImage(img, 0, 0);
  };

  var original = function() {
      ctx.drawImage(img, 0, 0);
  };

  var invert = function() {
      ctx.drawImage(img, 0, 0);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const data = imageData.data;
      for (var i = 0; i < data.length; i += 4) {
          data[i]     = 255 - data[i];     // red
          data[i + 1] = 255 - data[i + 1]; // green
          data[i + 2] = 255 - data[i + 2]; // blue
      }
      ctx.putImageData(imageData, 0, 0);
  };

  var grayscale = function() {
      ctx.drawImage(img, 0, 0);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const data = imageData.data;
      for (var i = 0; i < data.length; i += 4) {
          var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
          data[i]     = avg; // red
          data[i + 1] = avg; // green
          data[i + 2] = avg; // blue
      }
      ctx.putImageData(imageData, 0, 0);
  };

  const inputs = document.querySelectorAll('[name=color]');
  for (const input of inputs) {
      input.addEventListener("change", function(evt: any) {
          switch (evt!.target!.value) {
              case "inverted":
                  return invert();
              case "grayscale":
                  return grayscale();
              default:
                  return original();
          }
      });
  }
})
</script>

缩放与反锯齿

可以看到,开启反锯齿后,放大查看画布并感觉没有图像是由一些模糊小方格组成了,而是感觉图像本来就很模糊。

vue
<template>
  <canvas id="anti-aliasing-canvas" width="300" height="227"></canvas>
  <canvas id="anti-aliasing-zoom" width="300" height="227"></canvas>
  <div>
  <label for="smoothbtn">
    <input type="checkbox" name="smoothbtn" checked id="smoothbtn">
    Enable image smoothing
  </label>
  </div>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
import { withBase } from 'vitepress'
onMounted(() => {
  var img = new Image();
  img.src = withBase('https://lukecheng2.oss-cn-chengdu.aliyuncs.com/public/img/88e011e13fc534253be9b867b157a124.jpg')
  img.onload = function() {
    draw(img);
  };

  function draw(img) {
    var canvas = document.getElementById('anti-aliasing-canvas') as HTMLCanvasElement;
    var ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    img.style.display = 'none';
    var zoomctx = (document.getElementById('anti-aliasing-zoom') as HTMLCanvasElement).getContext('2d');

    var smoothbtn = document.getElementById('smoothbtn');
    var toggleSmoothing = function(event) {
      zoomctx.imageSmoothingEnabled = this.checked;
    };
    smoothbtn.addEventListener('change', toggleSmoothing);

    var zoom = function(event) {
      // 话说为什么老喜欢用 layerX 呀,坐标位置根本不在画布里
      // var x = event.layerX;
      // var y = event.layerY;
      var x = event.offsetX;
      var y = event.offsetY;
      zoomctx.drawImage(canvas,
        Math.abs(x - 5),
        Math.abs(y - 5),
        10, 10,
        0, 0,
        200, 200);
    };

    canvas.addEventListener('mousemove', zoom);
  }
})
</script>

保存图片

HTMLCanvasElement 提供一个 toDataURL() 方法,此方法在保存图片的时候非常有用。它返回一个包含被类型参数规定的图像表现格式的数据链接。返回的图片分辨率是 96 dpi。

当你从画布中生成了一个数据链接,例如,你可以将它用于任何<image>元素,或者将它放在一个有 download 属性的超链接里用于保存到本地。

  • 你也可以从画布中创建一个 Blob 对像。

    这个创建了一个在画布中的代表图片的 Blob 对像。

Released under the MIT License.