UNPKG

@visactor/vgrammar-wordcloud-shape

Version:

Layout WordCloud in specified shape, this is a transform for VGrammar.

151 lines (148 loc) 8.37 kB
export function segmentation(segmentationInput) { const {size: size, maskCanvas: maskCanvas} = segmentationInput, imageData = maskCanvas.getContext("2d", { willReadFrequently: !0 }).getImageData(0, 0, maskCanvas.width, maskCanvas.height), labels = new Array(size[0] * size[1]).fill(0); let curLabel = 1; const offset = [ [ 0, 1 ], [ 1, 0 ], [ -1, 0 ], [ 0, -1 ] ]; let queue = []; for (let i = 0; i < size[1]; i++) for (let j = 0; j < size[0]; j++) if (0 === labels[i * size[0] + j] && !segmentationInput.isEmptyPixel(imageData, i, j)) { labels[i * size[0] + j] = curLabel, queue.push([ i, j ]); for (let k = 0; k < queue.length; k++) for (let m = 0; m < 4; m++) { let row = queue[k][0] + offset[m][0], col = queue[k][1] + offset[m][1]; row = row < 0 ? 0 : row >= size[1] ? size[1] - 1 : row, col = col < 0 ? 0 : col >= size[0] ? size[0] - 1 : col, 0 !== labels[row * size[0] + col] || segmentationInput.isEmptyPixel(imageData, row, col) || (labels[row * size[0] + col] = curLabel, queue.push([ row, col ])); } curLabel++, queue = []; } const boundaries = {}, areas = {}, centers = {}, maxPoints = {}, maxR = {}, ratios = {}, shapeBounds = { x1: 1 / 0, x2: -1 / 0, y1: 1 / 0, y2: -1 / 0, width: 0, height: 0 }; for (let i = 0; i < size[1]; i++) for (let j = 0; j < size[0]; j++) { const label = labels[i * size[0] + j]; 0 !== label && (isBoundaryPixel(i, j) && (boundaries[label] = boundaries[label] || [], boundaries[label].push([ j, i ]), maxPoints[label] || (maxPoints[label] = [ 1 / 0, -1 / 0, 1 / 0, -1 / 0 ]), i < maxPoints[label][0] && (maxPoints[label][0] = i), i > maxPoints[label][1] && (maxPoints[label][1] = i), j < maxPoints[label][2] && (maxPoints[label][2] = j), j > maxPoints[label][3] && (maxPoints[label][3] = j), j < shapeBounds.x1 && (shapeBounds.x1 = j), j > shapeBounds.x2 && (shapeBounds.x2 = j), i < shapeBounds.y1 && (shapeBounds.y1 = i), i > shapeBounds.y2 && (shapeBounds.y2 = i)), areas[label] = areas[label] || 0, areas[label]++); } const allBoundaries = []; for (const label in boundaries) { const boundary = boundaries[label], x = ~~(boundary.reduce(((acc, cur) => acc + cur[0]), 0) / boundary.length), y = ~~(boundary.reduce(((acc, cur) => acc + cur[1]), 0) / boundary.length); centers[label] = [ x, y ], allBoundaries.push(...boundary); const [yMin, yMax, xMin, xMax] = maxPoints[label]; maxR[label] = ~~Math.max(Math.sqrt((x - xMin) ** 2 + (y - yMin) ** 2), Math.sqrt((x - xMax) ** 2 + (y - yMax) ** 2), Math.sqrt((x - xMin) ** 2 + (y - yMax) ** 2), Math.sqrt((x - xMax) ** 2 + (y - yMin) ** 2)), ratios[label] = (xMax - xMin) / (yMax - yMin); } const regions = Object.keys(centers).map((key => ({ label: key - 1, boundary: boundaries[key], area: areas[key], center: centers[key], maxPoint: maxPoints[key], maxR: maxR[key], ratio: ratios[key] }))); shapeBounds.width = shapeBounds.x2 - shapeBounds.x1 + 1, shapeBounds.height = shapeBounds.y2 - shapeBounds.y1 + 1; const x = ~~(allBoundaries.reduce(((acc, cur) => acc + cur[0]), 0) / allBoundaries.length), y = ~~(allBoundaries.reduce(((acc, cur) => acc + cur[1]), 0) / allBoundaries.length), shapeMaxR = ~~Math.max(Math.sqrt((x - shapeBounds.x1) ** 2 + (y - shapeBounds.y1) ** 2), Math.sqrt((x - shapeBounds.x2) ** 2 + (y - shapeBounds.y2) ** 2), Math.sqrt((x - shapeBounds.x1) ** 2 + (y - shapeBounds.y2) ** 2), Math.sqrt((x - shapeBounds.x2) ** 2 + (y - shapeBounds.y1) ** 2)), shapeRatio = shapeBounds.width / shapeBounds.height, shapeArea = Object.keys(areas).reduce(((acc, key) => acc + areas[key]), 0), segmentation = { regions: regions, labels: labels, labelNumber: curLabel - 1 }; return Object.assign(segmentationInput, { segmentation: segmentation, shapeBounds: shapeBounds, shapeMaxR: shapeMaxR, shapeRatio: shapeRatio, shapeCenter: [ x, y ], shapeArea: shapeArea }); function isBoundaryPixel(i, j) { const offset = [ [ 0, 1 ], [ 1, 0 ], [ -1, 0 ], [ 0, -1 ] ]; if (0 === i || 0 === j || i === size[1] - 1 || j === size[0] - 1) return !0; for (let k = 0; k < 4; k++) { let row = i + offset[k][0], col = j + offset[k][1]; if (row = row < 0 ? 0 : row >= size[1] ? size[1] - 1 : row, col = col < 0 ? 0 : col >= size[0] ? size[0] - 1 : col, 0 === labels[row * size[0] + col]) return !0; } return !1; } } export function removeBorder(image, canvas, isEmptyPixel) { canvas.width = image.width, canvas.height = image.height; const ctx = canvas.getContext("2d", { willReadFrequently: !0 }); ctx.clearRect(0, 0, canvas.width, canvas.height), ctx.drawImage(image, 0, 0); const width = canvas.width, imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); let top = 0, bottom = imageData.height, left = 0, right = imageData.width; const rowBlank = (width, y) => { for (let x = 0; x < width; ++x) if (!isEmptyPixel(imageData, y, x)) return !1; return !0; }, columnBlank = (x, y0, y1) => { for (let y = y0; y < y1; ++y) if (!isEmptyPixel(imageData, y, x)) return !1; return !0; }; for (;top < bottom && rowBlank(width, top); ) ++top; for (;bottom - 1 > top && rowBlank(width, bottom - 1); ) --bottom; for (;left < right && columnBlank(left, top, bottom); ) ++left; for (;right - 1 > left && columnBlank(right - 1, top, bottom); ) --right; const trimmed = ctx.getImageData(left, top, right - left, bottom - top); return canvas.width = trimmed.width, canvas.height = trimmed.height, ctx.clearRect(0, 0, canvas.width, canvas.height), ctx.putImageData(trimmed, 0, 0), canvas; } export function scaleAndMiddleShape(image, size) { const width = image.width, height = image.height; let scale = size[0] / width; height * scale > size[1] && (scale = size[1] / height); const newWidth = Math.floor(scale * width), newHeight = Math.floor(scale * height); return { x: (size[0] - newWidth) / 2, y: (size[1] - newHeight) / 2, width: newWidth, height: newHeight, scale: scale }; } export function allocateWords(words, segmentationOutput) { const {segmentation: {regions: regions}} = segmentationOutput; let areaMax = -1 / 0, totalArea = 0, areaMaxIndex = 0; regions.forEach(((region, index) => { const area = region.area; area > areaMax && (areaMax = area, areaMaxIndex = index), totalArea += area; })); let wordsSum = 0; regions.forEach((region => { const area = region.area, regionNum = Math.ceil(area / totalArea * words.length), regionWeight = area / areaMax; region.words = [], region.regionNum = regionNum, region.regionWeight = regionWeight, wordsSum += regionNum; })), wordsSum < words.length && (regions[areaMaxIndex].wordsNum += words.length - wordsSum); let currIndex = areaMaxIndex; const regionNums = regions.map((region => region.regionNum)); words.forEach((word => { let failCounter = 0, updateCounter = 0; word.regionIndex = -1; do { if (regionNums[currIndex] > 0 && word.weight <= regions[currIndex].regionWeight) { word.regionIndex = currIndex, regions[currIndex].words.push(word), regionNums[currIndex]--, currIndex = (currIndex + 1) % regions.length; break; } currIndex = (currIndex + 1) % regions.length, failCounter++, updateCounter++, updateCounter > regions.length + 1 && (regions.forEach((region => { region.regionWeight += .15; })), updateCounter = 0); } while (-1 === word.regionIndex && failCounter < 3 * regions.length); -1 === word.regionIndex && (word.regionIndex = areaMaxIndex, regions[areaMaxIndex].words.push(word), regionNums[areaMaxIndex]--); })), regions.forEach((region => { region.words.sort(((a, b) => b.weight - a.weight)); })); } //# sourceMappingURL=segmentation.js.map