在实际进行单词放置时,单词坐标是以像素为单位,这就会造成进行碰撞检测时,board的整数与sprite的整数是错位的,无法直接进行"与"运算来获取碰撞结果。
这时就需要将对应位置的像素信息提取出来,组成一个新的整数来与board中的数值进行运算。
上图中,实际需要比较黄色透明矩形(画布)与绿色矩形(单词)内的像素。对于这种情况,需要分别对1、2、3列内的像素进行比较,因为单词的像素矩形与画布的矩形存在错位的情况,以第一个1列为例,需要将单词B区域的像素取出,在左侧补零,组成一个新的整数,然后在于画布对应位置的整数进行运算。对于第二列来说,需要取单词第一个整数的右边部分像素,与第二个单元格的左边部分像素来组成一个整数计算。
对于一个32位的二进制数,我们可以方便的用>>或<<实现保留左侧部分信息或保留右侧部分信息,分别计算后再进行一次或操作即可得到一个新的32位数。
获取红色透明矩形部分第一行像素占用的伪代码:
// 设wordSpriteLeft为第一行第一个数值 // wordSpriteRight为第一行第二个数值 // 设偏移量为x // 获取第一个数值右侧部分 const leftPartInBoard = wordSpriteLeft << (32 - x) // 获取第二个数值左侧部分 const rightPartInBoard = wordSpriteRight >> x // 合并组成新数值 const newValue = leftPartInBoard | rightPartInBoard // 碰撞检测 const isCollide = newValue & board[i] 碰撞检测代码实现 /** * 检测单词是否重叠 * @param {*} tag 单词 * @param {*} board 画布像素占用信息 * @param {*} sw 画布长度 */ function cloudCollide(tag, board, sw) { sw >>= 5; // 获取画布长度在数组中对应的宽度 var sprite = tag.sprite, w = tag.width >> 5, // 单词在数组中的宽 lx = tag.x - (w << 4), // 单词左边界x坐标(px) sx = lx & 0x7f, // 单词偏移(px), 当前元素右侧移除数量 msx = 32 - sx, // 需要从sprite上一个元素中移除的数量 h = tag.y1 - tag.y0, // 单词高度 x = (tag.y + tag.y0) * sw + (lx >> 5), // 数组中的起始位置 last; // 逐行遍历单词sprite,判断与已绘制内容重叠 for (var j = 0; j < h; j++) { last = 0; for (var i = 0; i <= w; i++) { // last << msx 获取sprite前一个元素超出board左侧边界的部分 // (last = sprite[j * w + i]) >>> sx 获取sprite超出board右侧边界的部分,并将值赋给last,便于下一个元素的计算 // 将以上两部分进行"或"操作,合并成完整的32位像素信息 // 将新合并的数字与board对应数组进行"与"操作,值为0则不重叠,返回true,否则返回false if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) & board[x + i]) return true; } x += sw; } return false; }放置单词函数代码实现
// 遍历螺线线上的点,检测单词是否可以放置 export const place = (board, word, bounds, size, getPosition) => { const startX = word.x; // 初始值为画布 x 中点 const startY = word.y; // 初始值为画布 y 中点 const maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]) const s = getPosition; // 阿基米德螺线函数 const dt = Math.random() < .5 ? 1 : -1; let t = -dt; let dxdy, dx, dy; while (dxdy = s(t += dt)) { dx = ~~dxdy[0]; // x 偏移量 dy = ~~dxdy[1]; // y 偏移量 // 超出最大范围,单词无法放置 if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; word.x = startX + dx; // 获取单词在画布中的 x 坐标 word.y = startY + dy; // 获取单词在画布中的 y 坐标 // 文字超出画布范围时跳过 if (word.x + word.x0 < 0 || word.y + word.y0 < 0 || word.x + word.x1 > size[0] || word.y + word.y1 > size[1]) continue; // 碰撞检测 if (!bounds || !cloudCollide(word, board, size[0])) { // 与board进行像素对比 if (!bounds || collideRects(word, bounds)) { // 将单词的像素占用信息更新到board let sprite = word.sprite, w = word.width >> 5, sw = size[0] >> 5, lx = word.x - (w << 4), sx = lx & 0x7f, // sprite数组左侧偏移 msx = 32 - sx, // 位移遮罩 h = word.y1 - word.y0, x = (word.y + word.y0) * sw + (lx >> 5), last; // 逐行遍历 for (let j = 0; j < h; j++) { last = 0; for (let i = 0; i <= w; i++) { board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); } x += sw; } word.sprite = null; // 可以放置该单词 return true; } } } // 该单词无法放置 return false; } 绘制词云 渲染函数 /** * 渲染词云 * @param {*} size * @param {*} data */ const renderWordCloud = (size, data) => { const center = [size[0] / 2, size[1] / 2] const results = [] const board = new Array((size[0] >> 5) * size[1]).fill(0); const getPosition = archimedeanSpiral(size); // const getPosition = archimedeanSpiral(size, {step: 1, b: 10}); let bounds = null let i = 0 // data = data.map((data, i) => {data.text = `${i}${data.text}`;return data}) while (i < data.length) { var d = data[i]; d.x = center[0] d.y = center[1] // 收集词汇像素占用情况 cloudSprite(cloudSpriteCanvasInfo, d, data, i, size[0] >> 5, size[1]); if (d.hasText && place(board, d, bounds, [...size], getPosition)) { results.push(d); if (bounds) cloudBounds(bounds, d); else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}]; // Temporary hack d.x -= size[0] >> 1; d.y -= size[1] >> 1; } i++ } const resultCanvasInfo = createCanvas(size[0], size[1]) results.map(word => { word.x = word.x + center[0]; word.y = word.y + center[1]; return word }).forEach(word => drawText(resultCanvasInfo.context, word)) paint(board, size) document.body.appendChild(resultCanvasInfo.canvas) }一个简单的词云就完成了~