UNPKG

@visactor/vgrammar-wordcloud-shape

Version:

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

277 lines (268 loc) 15.3 kB
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