WebGL Tutorial
and more

getImageData

撰写时间:2025-04-03

修订时间:2025-04-03

设定目标

现在,我们准备取出一个Canvas所绘制的内容,然后居中绘制到另一个Canvas上面。

function drawImg() { let pathData = 'M 120 20 L 50 150 h 140 z M 90, 78 h 60 L 120 150 z'; let path = new Path2D(pathData); ctx.strokeStyle = 'aquamarine'; ctx.stroke(path); } drawImg();
canvas { border: 1px solid gray; width: 200px; height: 200px; }

对此并非长方形的图形,由于它是使用内部数据来绘制的,因此较难直接居中。但我们可以通过ctxgetImageData方法来获取其内部图像像素数据,找到其4个顶点的位置,再精准定位。

getImageData初窥

function drawImg() { let pathData = 'M 120 20 L 50 150 h 140 z M 90, 78 h 60 L 120 150 z'; let path = new Path2D(pathData); ctx.strokeStyle = 'aquamarine'; ctx.stroke(path); } function aboutImageData() { let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); pc.log(imgData); pc.log(imgData.width, imgData.height); pc.log(imgData.colorSpace); pc.log(imgData.data.length); } drawImg(); aboutImageData(); pc.openTopGroup();
body { display: flex; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

getImageData方法返回一个ImageData的实例imgData。该对象有4个重要属性,分别是width, height, colorSpacedata

widthheight属性值分别对应于getImageData方法最后2个参数widthheight,上面以canvaswidthheight属性值传入。

由于CanvasEditor默认应用了devicePixelRatio(在笔者电脑上默认为2.0),因此,canvas的绘图环境ctx的大小分别为400px400px。这两个值,即是canvaswidthheight属性值。

colorSpace值为srgb,表示使用了sRGB的颜色空间,故此每个像素均分为rgba4个通道,因此,像素的总数量及imgData.data的数组元素数量的关系为:

pixelsNum = imgData.width * imgData.height = 400 * 400 = 160000 arrayLength = pixelsNum * CORLOR_CHANEL_COMPS = 160000 * 4 = 640000

getImageData详解

从上节看出,由于需要使用4个数组元素来表示一个像素的rgba通道成分,因此imgData.data这个数组的元素数量往往太大了。为简化问题,本节将只在一个尺寸为10px × 10pxCanvas上绘制一条水平直线,以专注考查imgData.data的存储结构,以及其与像素数量、像素索引值、像素内容、Canvas宽高之间的内在关系。

此外,也是出于简化问题之故,这里暂时不应用devicePixelRatio

function getData() { return ctx.getImageData(0, 0, canvas.width, canvas.height); } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getAllPixels() { for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; pc.log(`%d: rgba(%d, %d, %d, %d)`, pixelIndex, r, g, b, a); } } function getFirstHilightPixel() { for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return pixelIndex; } } } function getLastHilightPixel() { for (let pixelIndex = pixelsNum - 1; pixelIndex >= 0; pixelIndex--) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return pixelIndex; } } } function getPixelIndexFromXY(x, y) { return y * canvas.width + x; } function getXYFromPixelIndex(imgData, pixelIndex) { let colsPerRow = imgData.width; let y = parseInt(pixelIndex / colsPerRow); let x = pixelIndex % colsPerRow; return [x, y]; } initCanvas(); pc.group(`1、Canvs Size`); pc.log(`width: %d, height: %d`, canvas.width, canvas.height); pc.groupEnd(); drawImg(); let imgData = getData(); let data = imgData.data; pc.group(`2、ImageData`); pc.log(`width: %d, height: %d`, imgData.width, imgData.height); pc.group(`data`); pc.log(`%o`, data); pc.log(`array length: %d`, data.length); pc.log(imgData); pc.groupEnd(); pc.groupEnd(); let pixelsNum = data.length / CORLOR_CHANEL_COMPS; pc.group(`3、Pixcels`); pc.log(`num: %d`,pixelsNum); pc.group(`Formulas`); pc.log('imgData.width * imgData.length'); pc.log('data.length / CORLOR_CHANEL_COMPS'); pc.groupEnd(); pc.groupCollapsed(`Contents`); getAllPixels(); pc.groupEnd(); pc.group(`HighLighted Pixels`); pc.group(`First Pixel`); var pixelIndex = getFirstHilightPixel(); pc.log(`Index: %d`, pixelIndex); var coOrds = getXYFromPixelIndex(imgData, pixelIndex); pc.log(`Coordinates: (%d, %d)`, coOrds[0], coOrds[1]); pc.groupEnd(); pc.group(`Last Pixel`); var pixelIndex = getLastHilightPixel(); pc.log(`Index: %d`, pixelIndex); var coOrds = getXYFromPixelIndex(imgData, pixelIndex); pc.log(`Coordinates: (%d, %d)`, coOrds[0], coOrds[1]);
const CORLOR_CHANEL_COMPS = 4; let canvas, ctx; function initCanvas() { canvas = document.querySelector('canvas'); canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); } function drawImg() { ctx.fillStyle = 'rgb(35, 162, 138)'; ctx.fillRect(0, 0, 10, 1); } pc.openTopGroup();
body { display: flex; } canvas { border: 1px solid gray; width: 10px; height: 10px; } div { flex: 1; }

Canvas尺寸

Canvas的默认尺寸为300px × 150px,我们通过CSS将其尺寸改为100px × 100px,这将影响到canvasclientWidthclientHeight属性值。

而在initCanvas函数中,将影响绘图环境的canvaswidthheight属性值分别取自于其clientWidthclientHeight属性值。这可以避免图像在画布中可能产生偏移或模糊的问题。

因此,canvas的尺寸大小最终确定为100px × 100px。具体详见解决Canvas图像模糊的问题

data属性

imgDatadata属性的类型是Uint8ClampedArray,这是每个元素字长为1字节的类型化数组,每个元素的值域在[0, 255]的范围之内,正好对应于rgba的每个通道值。

上节谈到,对于每个像素的每个rgba通道值,需使用一个数组元素来存储其值,因此data这个数组的元素数量为:

data.length = pixelsNum × 4 = imgData.width × imgData.height × 4 = 10 × 10 × 4 = 400

data属性中的像素数量

在不应用devicePixelRatio的情况下,canvas中所有像素的数量值等于:

canvasPixelsNum = canvas.width × canvs.height = 10 × 10 = 100

在调用getImageData方法时,可以只取出ctx的一部分数据,但上面的getData函数:

function getData() { return ctx.getImageData(0, 0, canvas.width, canvas.height); }

取出整个canvas的所有像素,则存储在data中所有像素的数量为:

dataPiexlesNum = imgData.width × imgData.height = 10 * 10 = 100

而由于datalength是确定的,因此,用其除以rgba通道值,也可以得出存储在data中所有像素的数量:

dataPiexlesNum = data.length ÷ 4 = 400 ÷ 4 = 100

data数组结构

data是一个一维数组,以线性的方式按顺序存储了每个像素的信息,且每4个相邻的元素存储了一个像素的rgba通道值。因此,根据特定像素索引值取出该像素数据的函数为:

function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); }

getAllPixels函数用以取出所有像素:

for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; pc.log(`%d: rgba(%d, %d, %d, %d)`, pixelIndex, r, g, b, a); }

所取出的每个像素的信息,均为一个数量为4的数组,分别对应于该像素的rgba通道值。因我们只使用一种颜色来绘图,因此,每个被点亮的像素,其值均为:

[35, 162, 138, 255]

与我们在drawImg函数中所指定的颜色值完全一致:

function drawImg() { ctx.fillStyle = 'rgb(35, 162, 138)'; ctx.fillRect(0, 0, 10, 1); }

从上面代码也可看出,我们只在Canvas的第一行绘制了一条直线,因此,在所返回的data数组中,只有前面10个像素有具体的颜色通道值,而后面其余的90个像素,则是完全透明的颜色:

rgba(0, 0, 0, 0)

取出有图像内容的像素索引值

函数getFirstHilightPixel用于取出第一个有图像内容的像素索引值:

function getFirstHilightPixel() { for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return pixelIndex; } } }

从第0个像素开始遍历,如果各个rgba通道值中,只要有一个成分的值不为0,则表示该像素已被点亮,则返回其索引值。

同理,函数getLastHilightPixel用于取出最后一个有图像内容的像素索引值:

function getLastHilightPixel() { for (let pixelIndex = pixelsNum - 1; pixelIndex >= 0; pixelIndex--) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return pixelIndex; } } }

这次是从后面开始往前遍历。

取出第一个及最后一个有内容的索引值后,下面将用这两个值来确定有图像的内容在Canvas上的X轴与Y轴的坐标值。

一维数组与二维坐标值的相互转换

如前所述data是一个一维的线性数组,而要求出各个像素所对应的(x, y)坐标值,则是从一维数组转换为二维坐标值的过程。

在不应用devicePixelRatio的情况下,Canvas上每个坐标,均对应于每个像素。对于任意一个坐标(x, y),可使用函数getPixelIndexFromXY求出像素索引值:

function getPixelIndexFromXY(x, y) { return y * canvas.width + x; }

此函数可应用在根据鼠标悬停的位置取出像素索引值的应用上面。在本站的Canvas ImageData Slider应用中,拉动Col (x)Row (y)拉杆,可自动求出pixelIndex的值。

有时可根据特定像素索引值,将所在的像素转换为二维平面上的坐标值。getXYFromPixelIndex可用于此。

function getXYFromPixelIndex(imgData, pixelIndex) { let colsPerRow = imgData.width; let y = parseInt(pixelIndex / colsPerRow); let x = pixelIndex % colsPerRow; return [x, y]; }

使用直线扫描法找出有图像内容的部分

现在,回到第一节的目标,在新的Canvas上居中绘制任意Canvas所绘制的内容。

对于任意的图形,我们可以为其绘制一个正好包裹其内容的矩形边界框,则该边界框内的所有内容即Canvas中有图像内容的部分。

注意,此边界框的确定,不能依靠在imgeData.data中的数据。它决定了内部图像数据的先后存储顺序,但与图像的四个端点无关。

而要确定任意图形的边界框,可使用直线扫描法来达成目标。

function getData() { return ctx.getImageData(0, 0, canvas.width, canvas.height); } function getPixelIndexFromXY(x, y) { return y * canvas.width + x; } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getMinY(imgData) { for(let rowIndex = 0; rowIndex < imgData.height; rowIndex++) { for (let colIndex = 0; colIndex < imgData.width; colIndex++) { let pixelIndex = getPixelIndexFromXY(colIndex, rowIndex); let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return rowIndex; } } } } function getMaxY(imgData) { for(let rowIndex = imgData.height - 1; rowIndex >= 0 ; rowIndex--) { for (let colIndex = 0; colIndex < imgData.width; colIndex++) { let pixelIndex = getPixelIndexFromXY(colIndex, rowIndex); let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return rowIndex; } } } } function getMinX(imgData) { for(let colIndex = 0; colIndex < imgData.width; colIndex++) { for (let rowIndex = 0; rowIndex < imgData.height; rowIndex++) { let pixelIndex = getPixelIndexFromXY(colIndex, rowIndex); let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return colIndex; } } } } function getMaxX(imgData) { for(let colIndex = imgData.width - 1; colIndex >= 0; colIndex--) { for (let rowIndex = 0; rowIndex < imgData.height; rowIndex++) { let pixelIndex = getPixelIndexFromXY(colIndex, rowIndex); let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return colIndex; } } } } function drawBoundingRect(x, y, width, height) { ctx.save(); ctx.beginPath(); ctx.strokeStyle = '#666'; ctx.strokeRect(x, y, width, height); ctx.restore(); } function centerImg(x, y, width, height) { let hlImgData = ctx.getImageData(x, y, width, height); ctx2.putImageData(hlImgData, (canvas2.width - width) / 2, (canvas2.height - height) / 2); } initCanvas(); initCanvas2(); drawImg(); let imgData = getData(); let minY = getMinY(imgData); let maxY = getMaxY(imgData); let minX = getMinX(imgData); let maxX = getMaxX(imgData); pc.log(`minY: %d, maxY: %d`, minY, maxY); pc.log(`minX: %d, maxX: %d`, minX, maxX); let x = minX; let y = minY; let width = maxX - minX; let height = maxY - minY; drawBoundingRect(x, y, width, height); centerImg(x, y, width, height);
const CORLOR_CHANEL_COMPS = 4; let canvas, ctx; let canvas2, ctx2; function initCanvas() { canvas = document.querySelector('canvas'); canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); } function initCanvas2() { canvas2 = document.querySelector('canvas:nth-of-type(2)'); canvas2.width = canvas2.clientWidth; canvas2.height = canvas2.clientHeight; ctx2 = canvas2.getContext('2d'); ctx2.clearRect(0, 0, canvas2.width, canvas2.height); } function drawImg() { ctx.beginPath(); ctx.moveTo(130, 20); ctx.lineTo(20, 120); ctx.lineTo(150, 150); ctx.closePath(); ctx.strokeStyle = 'lightgreen'; ctx.stroke(); } pc.openTopGroup();
body { display: flex; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

确定边界框的顶端直线的方法是,从Canvas的顶部开始,取一直线往其底部扫描。在扫描每行时,从左往右扫,从而确定各个扫描点。根据每个扫描点的位置,取出其像素,如果该位置的像素被点亮,则立即返回该位置作为Y轴上的最小坐标值。getMinY函数用于此目的。其逻辑是,对于每次的行扫描,一旦发现被点亮的像素,必定是Y轴上第一次发现的被点亮的像素,因为其上面若有被点亮的点,则函数早已经返回。

这种算法,只能确定所找到的像素必定是Y轴上的最小坐标值,但不能确定是否X轴上的最小坐标值。因此该函数只返回一个数值。

其余类似的3个函数的原理皆与此相同。

上面找到边界框的4个端点后,调用drawBoundingRect函数将其绘制出来。

确定边界框后,将其传给centerImg函数,该函数根据此边界框再次调用getImageData方法,但这次仅取出有图像内容的区域,然后再调用ctx2putImageData方法,即可在新的Canvas居中整个图像。

适当修改drawImg的内容,再运行,观察居中效果。

通过这种方式,可居中任意形状、任意内容的图像。

应用devicePixelRatio

getImageData方法的参数

let canvas2, ctx2; let imgData; function init() { canvas2 = document.querySelector('#canvas-2'); canvas2.style.width = canvas2.clientWidth + 'px'; canvas2.style.height = canvas2.clientHeight + 'px'; canvas2.width = canvas2.clientWidth * devicePixelRatio; canvas2.height = canvas2.clientHeight * devicePixelRatio; ctx2 = canvas2.getContext('2d'); ctx2.scale(devicePixelRatio, devicePixelRatio); ctx2.clearRect(0, 0, canvas2.clientWidth, canvas2.clientHeight); } function drawImg() { let path2DObj1 = new Path2D(); path2DObj1.rect(0, 0, canvas.clientWidth/2, canvas.clientHeight/2); ctx.fillStyle = 'hsl(0, 50%, 50%)'; ctx.fill(path2DObj1); let path2DObj2 = new Path2D(); path2DObj2.rect(canvas.clientWidth/2, canvas.clientHeight/2, canvas.clientWidth/2, canvas.clientHeight/2); ctx.fillStyle = 'hsl(180, 50%, 50%)'; ctx.fill(path2DObj2); } function duplicateImage() { imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); ctx2.putImageData(imgData, 0, 0); } function showInfo() { showCanvasInfo(); showImageDataInfo(); showPixelsInfo(); } function showCanvasInfo() { pc.groupCollapsed('Canvas'); pc.log(`clientWidth: %d, clientHeight: %d`, canvas.clientWidth, canvas.clientHeight); pc.log(`devicePixelRatio: %d`, devicePixelRatio); pc.log(`width: %d, height: %d`, canvas.width, canvas.height); pc.groupEnd(); } function showImageDataInfo() { pc.group('ImageData'); pc.log(`width: %d, height: %d`, imgData.width, imgData.height); pc.log(`data.length: %d`, imgData.data.length); pc.groupEnd(); } function showPixelsInfo() { pc.group('Pixels'); pc.log(`num: %d`, imgData.data.length / 4); pc.groupEnd(); } init(); drawImg(); duplicateImage(); showInfo(); pc.openTopGroup();
body { display: flex; gap: 1em; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

当应用了devicePixelRatio后,getImageData中的参数需使用:

let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

才能取出所有的图像范围。可见,getImageData方法不像其它方法,它不会自动应用devicePixelRatio

图像内部数据存储结构

设若当devicePixelRatio2时,当应用了devicePixelRatio后,像素数量是原来的多少倍?2倍,答案显而易见。我们会脱口而出。

现有一个canvasCSS设置其宽高值为200px × 200px,在应用了devicePixelRatio后,canvaswidthheight的值变为400px × 400px,则:

let before = 200 * 200; let after = 400 * 400; let ratio = after / before; pc.log(ratio);

像素数量变成了原来的4倍。这也意味着imgData.data的数组元素总数也随之增长为原来的4倍。

下面,我们使用LiveEditor,先在一个不应用devicePixelRatiocanvas中,只绘制一个红色的点。然后取出所有点亮的像素,查看它们的情况。

let imgData; let data; let pixelsNum; const CORLOR_CHANEL_COMPS = 4; function init() { let canvas = document.querySelector('canvas'); let ctx = canvas.getContext('2d'); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 1, 1); imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); data = imgData.data; pixelsNum = data.length / CORLOR_CHANEL_COMPS; } function getData() { imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); data = imgData.data; pixelsNum = data.length / CORLOR_CHANEL_COMPS; } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getAllHilightPixels() { let hlPixelIndices = []; for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { hlPixelIndices.push(pixelIndex); } } return hlPixelIndices; } function showInfo() { let hlPixelIndices = getAllHilightPixels(); pc.group('[' + hlPixelIndices.join(', ') + ']'); hlPixelIndices.forEach(pixelIndex => { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; pc.log(`%d: rgba(%d, %d, %d, %d)`, pixelIndex, r, g, b, a); }); } init(); showInfo(); pc.openTopGroup();
body { display: flex; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

只有一个像素被点亮。下面改为使用CanvasEditor,其默认应用了devicePixelRatio,使用与上面相同的绘图代码,只绘制一个红点。

let imgData; let data; let pixelsNum; const CORLOR_CHANEL_COMPS = 4; function drawImg() { ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 1, 1); } function getData() { imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); data = imgData.data; pixelsNum = data.length / CORLOR_CHANEL_COMPS; } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getAllHilightPixels() { let hlPixelIndices = []; for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { hlPixelIndices.push(pixelIndex); } } return hlPixelIndices; } function showInfo() { showCanvasInfo(); showImageDataInfo(); showPixelsInfo(); } function showCanvasInfo() { pc.groupCollapsed('Canvas'); pc.log(`clientWidth: %d, clientHeight: %d`, canvas.clientWidth, canvas.clientHeight); pc.log(`devicePixelRatio: %d`, devicePixelRatio); pc.log(`width: %d, height: %d`, canvas.width, canvas.height); pc.groupEnd(); } function showImageDataInfo() { pc.group('ImageData'); pc.log(`width: %d, height: %d`, imgData.width, imgData.height); pc.log(`data.length: %d`, imgData.data.length); pc.groupEnd(); } function showPixelsInfo() { pc.group('Pixels'); pc.log(`num: %d`, pixelsNum); pc.group('Highlight Pixels'); let hlPixelIndices = getAllHilightPixels(); pc.group('[' + hlPixelIndices.join(', ') + ']'); hlPixelIndices.forEach(pixelIndex => { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; pc.log(`%d: rgba(%d, %d, %d, %d)`, pixelIndex, r, g, b, a); }); pc.groupEnd(); pc.groupEnd(); pc.groupEnd(); } drawImg(); getData(); showInfo(); pc.openTopGroup();
body { display: flex; gap: 1em; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

从结果可以看出,对于原来的1个像素,当应用了值为2devicePixelRatio后,将使用在视图上、上下左右相邻的4个像素来绘制它。这也是为何应用devicePixelRatio之后,图像将非常清晰的内在原因。

这同时也带来一个新的问题,即如果我们根据特定像素的索引值来查询其像素的颜色值时,索引值所在的像素可能是某个像素的复本。例如,上面索引值为1400401所在的像素,其实都是索引值为0所在像素的复本。

Canvas坐标与像素对应关系

现在,对于Canvas上每一个坐标,其与相应的像素数量不再是1:1的关系,而是1 : (devicePixelRatio * devicePixelRatio)的关系。

let imgData; let data; let pixelsNum; const CORLOR_CHANEL_COMPS = 4; function drawImg() { ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 2, 1); } function getData() { imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); data = imgData.data; pixelsNum = data.length / CORLOR_CHANEL_COMPS; } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getAllHilightPixels() { let hlPixelIndices = []; for (let pixelIndex = 0; pixelIndex < pixelsNum; pixelIndex++) { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { hlPixelIndices.push(pixelIndex); } } return hlPixelIndices; } function getPixelIndicesFromXY(x, y) { let indices = []; let startIndex = y * imgData.width * devicePixelRatio + x * devicePixelRatio; indices.push(startIndex); for (let i = 1; i < devicePixelRatio; i++) { indices.push(startIndex + i); } for (let i = 1; i < devicePixelRatio; i++) { let nextRowIndex = startIndex + imgData.width * i; indices.push(nextRowIndex); for (let j = 1; j < devicePixelRatio; j++) { indices.push(nextRowIndex + j); } } return indices; } function getXYFromPixelIndex(imgData, pixelIndex) { let colsPerRow = imgData.width; let y = parseInt(pixelIndex / colsPerRow / devicePixelRatio); let x = parseInt((pixelIndex % colsPerRow) / devicePixelRatio); return [x, y]; } function showInfo() { showCanvasInfo(); showImageDataInfo(); showPixelsInfo(); } function showCanvasInfo() { pc.groupCollapsed('Canvas'); pc.log(`clientWidth: %d, clientHeight: %d`, canvas.clientWidth, canvas.clientHeight); pc.log(`devicePixelRatio: %d`, devicePixelRatio); pc.log(`width: %d, height: %d`, canvas.width, canvas.height); pc.groupEnd(); } function showImageDataInfo() { pc.groupCollapsed('ImageData'); pc.log(`width: %d, height: %d`, imgData.width, imgData.height); pc.log(`data.length: %d`, imgData.data.length); pc.groupEnd(); } function showPixelsInfo() { pc.group('Pixels'); pc.log(`num: %d`, pixelsNum); pc.group('Highlight Pixels'); pc.group('All'); let allIndices = getAllHilightPixels(); pc.log(allIndices); pc.groupEnd(); pc.group('Group into Coordinates (x, y)'); const theSet = new Set(); allIndices.forEach(pixelIndex => { let coords = getXYFromPixelIndex(imgData, pixelIndex); theSet.add('(' + coords.join(', ') + ')'); }); for (const item of theSet) { pc.group('Coords: ' + item); const [x, y] = item.match(/\d/g).map(str => parseInt(str)); let hlPixelIndices = getPixelIndicesFromXY(x, y); pc.group('Pixel Indices: [' + hlPixelIndices.join(', ') + ']'); hlPixelIndices.forEach(pixelIndex => { let pixel = getPixel(data, pixelIndex); const [r, g, b, a] = pixel; pc.log(`%d: rgba(%d, %d, %d, %d)`, pixelIndex, r, g, b, a); }); pc.groupEnd(); pc.groupEnd(); } pc.groupEnd(); pc.groupEnd(); pc.groupEnd(); pc.groupEnd(); } drawImg(); getData(); showInfo(); pc.openTopGroup();
body { display: grid; grid-template-columns: auto 1fr; gap: 1em; } canvas { border: 1px solid gray; width: 200px; height: 200px; }

计算像素数量的公式不变,还是:

pixelsNum = data.length / CORLOR_CHANEL_COMPS

getPixelIndicesFromXY函数改为:

function getPixelIndicesFromXY(x, y) { let indices = []; let startIndex = y * imgData.width * devicePixelRatio + x * devicePixelRatio; indices.push(startIndex); for (let i = 1; i < devicePixelRatio; i++) { indices.push(startIndex + i); } for (let i = 1; i < devicePixelRatio; i++) { let nextRowIndex = startIndex + imgData.width * i; indices.push(nextRowIndex); for (let j = 1; j < devicePixelRatio; j++) { indices.push(nextRowIndex + j); } } return indices; }

getXYFromPixelIndex函数改为:

function getXYFromPixelIndex(imgData, pixelIndex) { let colsPerRow = imgData.width; let y = parseInt(pixelIndex / colsPerRow / devicePixelRatio); let x = parseInt((pixelIndex % colsPerRow) / devicePixelRatio); return [x, y]; }

注,上面这两个函数的坐标值,均是CanvasCSS坐标值,而不是ctx绘图环境的坐标值。

应用devicePixelRatio后,因该值可能变化,且又是一对多的关系,因此上面这两个函数变为较为复杂了。

上面代码,先取出所有加亮的像素索引值,然后根据它们所对应的坐标值来分组,再列出它们的颜色值。

改变drawImg绘图函数的内容再运行,则可看出运行结果自动跟踪、并按坐标值来分类列出它们的像素索引值及其颜色值。但由于我们是跟底层像素数据在打交道,所绘制的图形不能过于复杂。绘制一个3px × 3px的矩形,则会产生9 × 4 = 36个像素被加亮,UI界面将被拉得比较长。

居中

let canvas2, ctx2; let imgData; const CORLOR_CHANEL_COMPS = 4; let boundingRect; function init() { canvas2 = document.querySelector('#canvas-2'); canvas2.style.width = canvas2.clientWidth + 'px'; canvas2.style.height = canvas2.clientHeight + 'px'; canvas2.width = canvas2.clientWidth * devicePixelRatio; canvas2.height = canvas2.clientHeight * devicePixelRatio; ctx2 = canvas2.getContext('2d'); ctx2.scale(devicePixelRatio, devicePixelRatio); ctx2.clearRect(0, 0, canvas2.clientWidth, canvas2.clientHeight); } function drawImg() { let pathData = 'M 120 20 L 50 150 h 140 z M 90, 78 h 60 L 120 150 z'; let path = new Path2D(pathData); ctx.strokeStyle = 'aquamarine'; ctx.stroke(path); } function getData() { imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); } function getPixel(data, pixelIndex) { let startIndex = pixelIndex * CORLOR_CHANEL_COMPS; return data.slice(startIndex, startIndex + CORLOR_CHANEL_COMPS); } function getPixelIndicesFromXY(x, y) { let indices = []; let startIndex = y * imgData.width * devicePixelRatio + x * devicePixelRatio; indices.push(startIndex); for (let i = 1; i < devicePixelRatio; i++) { indices.push(startIndex + i); } for (let i = 1; i < devicePixelRatio; i++) { let nextRowIndex = startIndex + imgData.width * i; indices.push(nextRowIndex); for (let j = 1; j < devicePixelRatio; j++) { indices.push(nextRowIndex + j); } } return indices; } function getMinY() { for(let rowIndex = 0; rowIndex < canvas.clientHeight; rowIndex++) { for (let colIndex = 0; colIndex < canvas.clientWidth; colIndex++) { let pixelIndex = getPixelIndicesFromXY(colIndex, rowIndex)[0]; let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return rowIndex; } } } } function getMaxY() { for(let rowIndex = canvas.clientHeight - 1; rowIndex >= 0 ; rowIndex--) { for (let colIndex = 0; colIndex < canvas.clientWidth; colIndex++) { let pixelIndex = getPixelIndicesFromXY(colIndex, rowIndex)[0]; let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return rowIndex; } } } } function getMinX() { for(let colIndex = 0; colIndex < canvas.clientWidth; colIndex++) { for (let rowIndex = 0; rowIndex < canvas.clientHeight; rowIndex++) { let pixelIndex = getPixelIndicesFromXY(colIndex, rowIndex)[0]; let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return colIndex; } } } } function getMaxX() { for(let colIndex = canvas.clientWidth - 1; colIndex >= 0; colIndex--) { for (let rowIndex = 0; rowIndex < canvas.clientHeight; rowIndex++) { let pixelIndex = getPixelIndicesFromXY(colIndex, rowIndex)[0]; let pixel = getPixel(imgData.data, pixelIndex); const [r, g, b, a] = pixel; if (r + g + b + a > 0.0) { return colIndex; } } } } function drawBoundingRect() { ctx.save(); ctx.beginPath(); ctx.strokeStyle = '#666'; const {x, y, width, height} = boundingRect; ctx.strokeRect(x, y, width, height); ctx.restore(); } function centerImg() { const {x, y, width, height} = boundingRect; let hlImgData = ctx.getImageData(x * devicePixelRatio, y * devicePixelRatio, width * devicePixelRatio, height * devicePixelRatio); ctx2.putImageData(hlImgData, (canvas2.width - width * devicePixelRatio) / 2, (canvas2.height - height * devicePixelRatio) / 2); } function showInfo() { showCanvasInfo(); showImageDataInfo(); showPixelsInfo(); showBoundingRectInfo(); } function showCanvasInfo() { pc.groupCollapsed('Canvas'); pc.log(`clientWidth: %d, clientHeight: %d`, canvas.clientWidth, canvas.clientHeight); pc.log(`devicePixelRatio: %d`, devicePixelRatio); pc.log(`width: %d, height: %d`, canvas.width, canvas.height); pc.groupEnd(); } function showImageDataInfo() { pc.groupCollapsed('ImageData'); pc.log(`width: %d, height: %d`, imgData.width, imgData.height); pc.log(`data.length: %d`, imgData.data.length); pc.groupEnd(); } function showPixelsInfo() { pc.groupCollapsed('Pixels'); pc.log(`num: %d`, imgData.data.length / 4); pc.groupEnd(); } function showBoundingRectInfo() { pc.group('BoundingRect'); const {x, y, width, height} = boundingRect; pc.log(`x: %d`, x); pc.log(`y: %d`, y); pc.log(`width: %d`, width); pc.log(`height: %d`, height); pc.groupEnd(); } function Rect(x, y, width, height) { return {x, y, width, height} } function createBoundingRect() { let minY = getMinY(); let maxY = getMaxY(); let minX = getMinX(); let maxX = getMaxX(); boundingRect = Rect(minX, minY, maxX - minX, maxY - minY); } init(); drawImg(); getData(); createBoundingRect(); drawBoundingRect(); centerImg(); showInfo(); pc.openTopGroup();
body { display: flex; gap: 1em; } canvas { border: 1px solid gray; width: 200px; height: 200px; } div { flex: 1; }

当应用了devicePixelRatio后,在涉及到坐标值时,共有2套坐标系。第一套坐标系是Canvas的基于CSS的坐标系,宽高均为200pxgetPixelIndicesFromXY, getMinX, getMaxX, getMinY, getMaxY等函数均使用这一套坐标系。

第二套坐标系是ctx的坐标系,宽高均为400px。在centerImg函数中,需将基于CSS的坐标系转换为此坐标系:

function centerImg() { const {x, y, width, height} = boundingRect; let hlImgData = ctx.getImageData(x * devicePixelRatio, y * devicePixelRatio, width * devicePixelRatio, height * devicePixelRatio); ctx2.putImageData(hlImgData, (canvas2.width - width * devicePixelRatio) / 2, (canvas2.height - height * devicePixelRatio) / 2); }

这是因为getImageDataputImageData方法均须使用第二套的坐标系。

参考资源

  1. HTML5 Canvas Element