如何实现一个词云 (4)

实现代码如下:

/** * 根据数组中存储的像素信息绘制canvas * @param {*} board * @param {*} paintSize */ export const paint = (board, paintSize) => { const curSize = paintSize const imageData = new ImageData(curSize[0], curSize[1]); let array = imageData.data for (let i = 0; i < curSize[1]; i++) { for (let j = 0; j < (curSize[0] >> 5); j++) { let value = board[i * (curSize[0] >> 5) + j] for (let k = 0; k < 32; k++) { // 遮罩,获取对应位置bit值 const msk = 0b1 << (32 - k) if (value & msk) { // 占用像素, 填充白色 for(let l = 0; l < 4; l++) { array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + l] = 255; } } else { // 未占用像素, 填充黑色 for(let l = 0; l < 3; l++) { array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + l] = 0; } array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + 3] = 255; } // 数组元素分割线, 填充红色, 间隔32px if (k === 0) { array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + 0] = 255; array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + 1] = 0; array[i * curSize[0] * 4 + j * 32 * 4 + k * 4 + 2] = 0; } } } } const canvas = document.createElement('canvas') canvas.width = curSize[0] canvas.height = curSize[1] const ctx = canvas.getContext('2d') ctx.putImageData(imageData, 0, 0) canvas.style.marginRight = '10px' document.body.appendChild(canvas) } const word = data[0] // 绘制螺蛳粉的像素信息 paint(word.sprite, [word.w, word.h])

绘制效果如图:

如何实现一个词云

其中“已占用”像素以白色绘制,“未占用”像素使用黑色绘制,红色竖线为数组中每个元素的分割线,即两条红色竖线之间为一个整数所保存的32个像素的占用信息。

初始化像素信息

在全局初始化变量board来存储整个画布的像素信息,board的长度为要绘制的画布的宽 * 高,初始全部填充为0(画布上没有放置任何单词)。

const size = [500, 500] // [宽,高] const board = new Array(size[0], size[1]).fill(0)

为了获取单词的像素信息,需要计算单词宽高,将单词绘制到画布上,然后使用ctx.getImageData(sx, sy, sw, sh)方法来获取像素信息。它的四个参数分别是起始点x坐标,起始点y坐标,截取宽度,截取高度。

ImageData的data中使用四个整数来表示一个像素点的颜色,没有被绘制到的部分默认值为0, 0, 0, 0。我们只需要知道当前像素是否被占用,所以只要取alpha的值即可,1为占用,0为为占用。

通过ctx.measureText方法获取文字的宽度,为了避免文字被截断,使用字号 * 2作为单词高度,文字的宽高决定了sprite数组的大小。

为了尽量少的操作canvas节省性能,获取像素信息的方案采取类似css精灵图的方案。首先初始化一个大的画布,然后一次尽可能多的在一个大画布上绘制文字,使用ctx.getImageData(0, 0, 画布宽度, 画布高度)获取整个画布的像素信息数组,然后根据文字的绘制坐标及宽高信息,在整个画布数组中截取文字对应的像素占用信息并保存到词汇的sprite数组中。

注意,词汇的sprite不是一次全部获取完成的。在尝试放置词汇时,会尝试获取该词汇对应的sprite,如果发现sprite还未初始化,则以当前词汇为起始索引开始一轮词汇sprite初始化。初始的canvas大小为2048 * 2048,当绘制不下时停止绘制,更新已绘制的词汇sprite,随后进行放置尝试。直到放置单词的sprite不存在时,再进行第下一次的批量sprite获取。

获取单词像素占用信息(sprite数组)流程图:

无法复制加载中的内容

代码实现:

/** * 获取单词sprite数组 * @param {*} contextAndRatio canvas上下文和画布比例 * @param {*} d 单词信息 * @param {*} data 所有单词 * @param {*} di 当前单词index */ function cloudSprite(contextAndRatio, d, data, di) { // 如果当前单词已经拥有sprite信息,跳过 if (d.sprite) return; // 精灵图画布大小为2048 * 2048 var c = contextAndRatio.context, ratio = contextAndRatio.ratio; c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); var x = 0, y = 0, maxh = 0, n = data.length, w, // 单词长度(px) w32, // 画布长度(数组中一行的元素个数) h, // 单词高(px) i, j; --di; while (++di < n) { d = data[di]; c.save(); c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font; // 设置文字属性 w = c.measureText(d.text + "m").width * ratio; // 获取文字宽度 h = d.size << 1; // 因为没有获取文字高度的api,为了保证截取像素完整,默认高度为单词fontSize * 2 // 如果单词有旋转属性,计算旋转后的宽高 if (d.rotate) { var sr = Math.sin(d.rotate * cloudRadians), cr = Math.cos(d.rotate * cloudRadians), wcr = w * cr, wsr = w * sr, hcr = h * cr, hsr = h * sr; w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); } else { w = (w + 0x1f) >> 5 << 5; } // w, h为旋转后,词语所占区域的宽高 if (h > maxh) maxh = h; // 记录当前行最大高度 // 如果当前行放不下,就另起一行,y方向向下移动当前行的最大高度 if (x + w >= (cw << 5)) { x = 0; y += maxh; maxh = 0; } if (y + h >= ch) break; // 绘制区域的高度为2048px,超过长度下次绘制 c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); if (d.rotate) c.rotate(d.rotate * cloudRadians); c.fillText(d.text, 0, 0); if (d.padding) { c.lineWidth = 2 * d.padding; c.strokeText(d.text, 0, 0); } c.restore(); // 词语绘制完成,记录其在画布上的相对位置和范围 d.width = w; d.height = h; d.xoff = x; d.yoff = y; // x0, x1, y0, y1是四角相对于中心点的相对坐标 d.x1 = w >> 1; d.y1 = h >> 1; d.x0 = -d.x1; d.y0 = -d.y1; d.hasText = true; // x位置右移,等待下一个词语绘制 x += w; } // 获取整个精灵图画布的像素信息 var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, sprite = []; // 根据单词的位置和长宽信息从pixels中截取并保存单词部分的像素信息 while (--di >= 0) { d = data[di]; if (!d.hasText) continue; w = d.width; w32 = w >> 5; h = d.y1 - d.y0; // Zero the buffer for (i = 0; i < h * w32; i++) sprite[i] = 0; x = d.xoff; if (x == null) return; y = d.yoff; var seen = 0, seenRow = -1; // 遍历像素,根据单词的绘制坐标与宽高信息,在画布中获取对应像素信息,保存至sprite for (j = 0; j < h; j++) { for (i = 0; i < w; i++) { // 在sprite数组中,每一个Uint32的数字记录了32个像素的绘制情况 // 在pixels中,只取alpha通道的值,因此需要每个像素需要 << 2 得到alpha通道 var k = w32 * j + (i >> 5), m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; sprite[k] |= m; // 更新sprite对应像素信息 seen |= m; // 记录当前行是否有着色信息 } // 如果当前行发现着色,开始记录行号 if (seen) seenRow = j; else { // 如果当前行未发现着色,则在结果中省去该行(高度--,y坐标++,左上角相对坐标++) d.y0++; h--; j--; y++; } } d.y1 = d.y0 + seenRow; // 更新右下角相对坐标 d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); // 舍弃数组中冗余部分 } } // 获取单词的宽高、左右边界坐标、像素占用等信息。 data.forEach((word, index) => cloudSprite(contextAndRatio, word, data, index))

将绘制后的canvas显示出来如下:

如何实现一个词云

下图是使用paint函数绘制的各单词的sprite数组:

如何实现一个词云

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgjjpy.html