@visactor/vgrammar-wordcloud-shape
Version:
Layout WordCloud in specified shape, this is a transform for VGrammar.
277 lines (268 loc) • 15.3 kB
JavaScript
export function layout(words, layoutConfig, segmentationOutput) {
const {size: size, stepFactor: stepFactor} = layoutConfig, {segmentation: {regions: regions}, tempCanvas: canvas, boardSize: boardSize, shapeCenter: shapeCenter, shapeMaxR: shapeMaxR, shapeRatio: shapeRatio} = segmentationOutput, board = initBoardWithShape(segmentationOutput), ctx = canvas.getContext("2d", {
willReadFrequently: !0
});
for (const region of regions) {
const {words: regionWords, center: center, maxR: maxR, ratio: ratio} = region;
for (let i = 0; i < regionWords.length; i++) {
measureSprite(canvas, ctx, words, i);
const word = regionWords[i];
word.x = center[0], word.y = center[1], word.hasText && word.sprite && place(board, word, maxR, ratio, size, boardSize, stepFactor) && (word.hasPlaced = !0);
}
}
for (let _ = 0; _ < layoutConfig.textLayoutTimes; _++) {
const failedWords = words.filter((word => (word.hasPlaced || (word.hasText = !1,
word.sprite = null, word.fontSize = Math.max(~~(word.fontSize * layoutConfig.fontSizeShrinkFactor), layoutConfig.minFontSize)),
!word.hasPlaced)));
if (0 === failedWords.length) break;
for (let i = 0; i < failedWords.length; i++) {
const word = failedWords[i];
measureSprite(canvas, ctx, failedWords, i), word.x = shapeCenter[0], word.y = shapeCenter[1],
word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor) && (word.hasPlaced = !0);
}
}
layoutConfig.board = board;
}
export function layoutSelfShrink(words, layoutConfig, segmentationOutput) {
const {size: size, stepFactor: stepFactor} = layoutConfig, {segmentation: {regions: regions}, tempCanvas: canvas, boardSize: boardSize} = segmentationOutput, board = initBoardWithShape(segmentationOutput), ctx = canvas.getContext("2d", {
willReadFrequently: !0
});
for (const region of regions) {
const {words: regionWords, center: center, maxR: maxR, ratio: ratio} = region;
let fontFactor = 1;
for (let i = 0; i < regionWords.length; i++) {
measureSprite(canvas, ctx, words, i);
const word = regionWords[i];
if (word.x = center[0], word.y = center[1], word.hasText && word.sprite && place(board, word, maxR, ratio, size, boardSize, stepFactor)) word.hasPlaced = !0; else {
fontFactor *= layoutConfig.fontSizeShrinkFactor;
for (let j = i; j < regionWords.length; j++) word.hasText = !1, word.sprite = null,
word.fontSize = Math.max(~~(word.fontSize * fontFactor), layoutConfig.minFontSize);
i--;
}
}
}
layoutConfig.board = board;
}
export function layoutGlobalShrink(words, layoutConfig, segmentationOutput) {
const {stepFactor: stepFactor, importantWordCount: importantWordCount, globalShinkLimit: globalShinkLimit} = layoutConfig, {size: size, segmentation: {regions: regions}, tempCanvas: canvas, boardSize: boardSize, shapeCenter: shapeCenter, shapeMaxR: shapeMaxR, shapeRatio: shapeRatio} = segmentationOutput, ctx = canvas.getContext("2d", {
willReadFrequently: !0
}), boardOrigin = initBoardWithShape(segmentationOutput);
let board = boardOrigin.slice(0);
const fontFactor = layoutConfig.fontSizeShrinkFactor;
let id = null, idIntialFactor = 1, globalShinkFactor = 1;
const importantCount = importantWordCount;
let weightStd = 0;
if (words.length > importantCount) {
weightStd = words.sort(((word0, word1) => word1.weight - word0.weight))[importantCount].weight;
}
for (let k = 0; k < regions.length; k++) {
const region = regions[k], {words: regionWords, center: center, maxR: maxR, ratio: ratio} = region;
let restartTag = !1;
for (let i = 0; i < regionWords.length; i++) {
measureSprite(canvas, ctx, words, i);
const word = regionWords[i];
if (word.x = center[0], word.y = center[1], !word.skip && word.hasText && word.sprite && place(board, word, maxR, ratio, size, boardSize, stepFactor)) word.hasPlaced = !0; else {
if (!word.skip && word.weight > weightStd && globalShinkFactor > globalShinkLimit) {
const wordId = word.datum[Symbol.for("vGrammar_id")];
wordId !== id && (id = wordId, idIntialFactor = globalShinkFactor), globalShinkFactor *= fontFactor,
words.forEach((word => {
word.hasText = !1, word.sprite = null, word.fontSize = word.fontSize * fontFactor;
})), board = boardOrigin.slice(0), restartTag = !0;
break;
}
if (!word.skip && word.datum[Symbol.for("vGrammar_id")] === id) {
words.forEach((word => {
word.hasText = !1, word.sprite = null, word.fontSize = word.fontSize / globalShinkFactor;
})), word.skip = !0, globalShinkFactor = idIntialFactor, id = null, board = boardOrigin.slice(0),
restartTag = !0;
break;
}
}
}
restartTag && (k = -1);
}
for (let _ = 0; _ < layoutConfig.textLayoutTimes; _++) {
const failedWords = words.filter((word => (word.hasPlaced || (word.hasText = !1,
word.sprite = null, word.fontSize = Math.max(~~(word.fontSize * layoutConfig.fontSizeShrinkFactor), layoutConfig.minFontSize)),
!word.hasPlaced)));
if (0 === failedWords.length) break;
for (let i = 0; i < failedWords.length; i++) {
const word = failedWords[i];
measureSprite(canvas, ctx, failedWords, i), word.x = shapeCenter[0], word.y = shapeCenter[1],
word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor) && (word.hasPlaced = !0);
}
}
layoutConfig.board = board;
}
export function layoutSelfEnlarge(words, layoutConfig, segmentationOutput) {
const {size: size, stepFactor: stepFactor, importantWordCount: importantWordCount} = layoutConfig, {segmentation: {regions: regions}, tempCanvas: canvas, boardSize: boardSize, shapeCenter: shapeCenter, shapeMaxR: shapeMaxR, shapeRatio: shapeRatio} = segmentationOutput, ctx = canvas.getContext("2d", {
willReadFrequently: !0
}), boardOrigin = initBoardWithShape(segmentationOutput);
let board = boardOrigin.slice(0);
const fontFactor = layoutConfig.fontSizeEnlargeFactor, importantCount = Math.min(importantWordCount, words.length);
let weightStd = 0;
if (words.length > importantCount) {
weightStd = words.sort(((word0, word1) => word1.weight - word0.weight))[importantCount - 1].weight;
}
let importantWordSuccessedNum = 0, globalEnlargeFactor = 1, layoutFinish = !1;
for (let k = 0; k < regions.length; k++) {
const region = regions[k], {words: regionWords, center: center, maxR: maxR, ratio: ratio} = region;
let restartTag = !1;
for (let i = 0; i < regionWords.length; i++) {
measureSprite(canvas, ctx, words, i);
const word = regionWords[i];
if (word.x = center[0], word.y = center[1], word.hasText && word.sprite && place(board, word, maxR, ratio, size, boardSize, stepFactor)) {
if (word.hasPlaced = !0, word.weight >= weightStd && importantWordSuccessedNum++,
importantWordSuccessedNum >= importantCount && !layoutFinish) {
globalEnlargeFactor *= fontFactor, words.forEach((word => {
word.hasText = !1, word.sprite = null, word.fontSize = word.fontSize * fontFactor;
})), board = boardOrigin.slice(0), restartTag = !0, importantWordSuccessedNum = 0;
break;
}
} else {
if (word.weight >= weightStd && globalEnlargeFactor > 1) {
words.forEach((word => {
word.hasText = !1, word.sprite = null, word.fontSize = word.fontSize / fontFactor;
})), globalEnlargeFactor /= fontFactor, layoutFinish = !0, board = boardOrigin.slice(0),
restartTag = !0;
break;
}
if (word.weight >= weightStd) return layoutGlobalShrink(words, layoutConfig, segmentationOutput);
}
}
restartTag && (k = -1);
}
for (let _ = 0; _ < layoutConfig.textLayoutTimes; _++) {
const failedWords = words.filter((word => (word.hasPlaced || (word.hasText = !1,
word.sprite = null, word.fontSize = Math.max(~~(word.fontSize * layoutConfig.fontSizeShrinkFactor), layoutConfig.minFontSize)),
!word.hasPlaced)));
if (0 === failedWords.length) break;
for (let i = 0; i < failedWords.length; i++) {
const word = failedWords[i];
measureSprite(canvas, ctx, failedWords, i), word.x = shapeCenter[0], word.y = shapeCenter[1],
word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor) && (word.hasPlaced = !0);
}
}
layoutConfig.board = board;
}
function place(board, word, maxR, ratio, size, boardSize, stepFactor) {
const startX = word.x, startY = word.y, spiral = archimedeanSpiral(ratio), dt = 1 * stepFactor;
let dxdy, dx, dy, t = -dt;
for (;dxdy = spiral(t += dt); ) {
const {wordSize: wordSize, bounds: {dTop: dTop, dBottom: dBottom, dLeft: dLeft, dRight: dRight}} = word;
if (dx = dxdy[0], dy = dxdy[1], Math.min(Math.abs(dx), Math.abs(dy)) >= maxR) break;
if (word.x = ~~(startX + dx), word.y = ~~(startY + dy), !(word.x - dLeft < 0 || word.x + dRight > size[0] || word.y - dTop < 0 || word.y + dBottom > size[1] || isCollideWithBoard(word, board, boardSize))) return placeWordOnBoard(word, board, boardSize),
!0;
}
return !1;
}
export function placeWordOnBoard(word, board, boardSize) {
const {wordSize: wordSize} = word, sprite = word.sprite, w = wordSize[0] >> 5, sw = boardSize[0] >> 5, lx = word.x - (w << 4), sx = lx % 32, msx = 32 - sx, h = wordSize[1];
let last, x = (word.y - (wordSize[1] >> 1)) * sw + (lx >> 5);
if (0 === sx) for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) board[x + i] |= sprite[j * w + i];
x += sw;
} else 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;
}
}
export function isCollideWithBoard(word, board, boardSize) {
const {sprite: sprite, wordSize: wordSize} = word, sw = boardSize[0] >> 5, w = wordSize[0] >> 5, lx = word.x - (w << 4), sx = lx % 32, msx = 32 - sx, h = wordSize[1];
let last, x = (word.y - (wordSize[1] >> 1)) * sw + (lx >> 5);
if (0 === sx) for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) if (board[x + i] & sprite[j * w + i]) return !0;
x += sw;
} else for (let j = 0; j < h; j++) {
last = 0;
for (let i = 0; i <= w; i++) if ((last << msx | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) & board[x + i]) return !0;
x += sw;
}
return !1;
}
function archimedeanSpiral(ratio) {
return function(t) {
return [ ratio * (t *= .1) * Math.cos(t), t * Math.sin(t) ];
};
}
export function measureSprite(canvas, ctx, words, wi) {
if (words[wi].sprite || 0 === words[wi].fontSize) return;
const cw = 2048, radians = Math.PI / 180, n = words.length;
canvas.width = cw, canvas.height = 2048, ctx.clearRect(0, 0, cw, 2048), ctx.textAlign = "center";
let wordW, wordH, x = 0, y = 0, maxHeight = 0, yMax = 0;
const wiDist = wi;
for (--wi; ++wi < n; ) {
const word = words[wi], fontSize = Math.max(word.fontSize, 2);
if (ctx.save(), ctx.font = word.fontStyle + " " + word.fontWeight + " " + fontSize + "px " + word.fontFamily,
wordW = ctx.measureText(word.text + "m").width + 2 * word.padding, wordH = 2 * fontSize + 2 * word.padding,
0 !== word.rotate) {
const sr = Math.sin(word.rotate * radians), cr = Math.cos(word.rotate * radians), wcr = wordW * cr, wsr = wordW * sr, hcr = wordH * cr, hsr = wordH * sr;
wordW = Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)), wordH = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
}
if (wordW = wordW + 31 >> 5 << 5, wordH = Math.ceil(wordH), wordH > maxHeight && (maxHeight = wordH),
x + wordW >= cw && (x = 0, y += maxHeight, maxHeight = wordH), y + wordH >= 2048) {
if (0 === y) {
word.hasText = !1;
continue;
}
break;
}
y + wordH >= yMax && (yMax = y + wordH), ctx.translate(x + (wordW >> 1), y + (wordH >> 1)),
0 !== word.rotate && ctx.rotate(word.rotate * radians), ctx.fillText(word.text, 0, 0),
word.padding && (ctx.lineWidth = 2 * word.padding, ctx.strokeText(word.text, 0, 0)),
ctx.restore(), word.LT = [ x, y ], word.wordSize = [ wordW, wordH ], word.hasText = !0,
x += wordW;
}
if (0 === yMax) return;
const pixels = ctx.getImageData(0, 0, cw, yMax).data;
let i, j;
for (;--wi >= wiDist; ) {
const word = words[wi];
if (!word.hasText) {
word.bounds = {
dTop: 1 / 0,
dBottom: -1 / 0,
dLeft: 1 / 0,
dRight: -1 / 0
};
break;
}
const {LT: LT = [ 0, 0 ], wordSize: wordSize} = word;
[x, y] = LT;
const w32 = wordSize[0] >> 5, sprite = new Array(w32 * wordSize[1]).fill(0);
let [dTop, dBottom, dLeft, dRight] = [ 1 / 0, -1 / 0, 1 / 0, -1 / 0 ];
for (j = 0; j < wordSize[1]; j++) {
let seen;
for (i = 0; i < wordSize[0]; i++) if (pixels[4 * ((y + j) * cw + (x + i)) + 3] > 0) {
const m = 1 << 31 - i % 32;
sprite[w32 * j + (i >> 5)] |= m, i < dLeft && (dLeft = i), i > dRight && (dRight = i),
seen |= m;
}
seen && (j < dTop && (dTop = j), j > dBottom && (dBottom = j));
}
word.bounds = {
dTop: (wordSize[1] >> 1) - dTop,
dBottom: dBottom - (wordSize[1] >> 1),
dLeft: (wordSize[0] >> 1) - dLeft,
dRight: dRight - (wordSize[0] >> 1)
}, word.sprite = sprite, delete word.LT;
}
}
function initBoardWithShape(segmentationOutput) {
const {segmentation: {labels: labels}, boardSize: boardSize, size: size} = segmentationOutput, w32 = boardSize[0] >> 5, board = new Array(w32 * size[1]).fill(0);
for (let i = 0; i < size[1]; i++) for (let j = 0; j < size[0]; j++) {
if (0 === labels[i * size[0] + j]) {
const m = 1 << 31 - j % 32;
board[w32 * i + (j >> 5)] |= m;
}
}
if (boardSize[0] > size[0]) {
const m = (1 << boardSize[0] - size[0]) - 1;
for (let y = 0; y < size[1]; y++) {
board[w32 * y + w32 - 1] |= m;
}
}
return board;
}
//# sourceMappingURL=wordle.js.map