@visactor/vgrammar-wordcloud
Version:
WordCloud layout transform for VGrammar
316 lines (306 loc) • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: !0
}), exports.CloudLayout = void 0;
const vrender_core_1 = require("@visactor/vrender-core"), vutils_1 = require("@visactor/vutils"), vgrammar_util_1 = require("@visactor/vgrammar-util"), base_1 = require("./base"), spirals_1 = require("./spirals"), util_1 = require("./util"), MAX_ARGUMENTS_LENGTH = 6e4;
class CloudLayout extends base_1.BaseLayout {
constructor(options) {
var _a;
super((0, vutils_1.merge)({}, CloudLayout.defaultOptions, options)), this.cw = 64,
this.ch = 2048, this._size = [ 256, 256 ], this._isBoardExpandCompleted = !1, this._placeStatus = 0,
this._tTemp = null, this._dtTemp = null, this._dy = 0, this.cacheMap = new Map,
this.options.minFontSize <= CloudLayout.defaultOptions.minFontSize && (this.options.minFontSize = CloudLayout.defaultOptions.minFontSize),
this.spiral = (0, vutils_1.isString)(this.options.spiral) ? null !== (_a = spirals_1.spirals[this.options.spiral]) && void 0 !== _a ? _a : spirals_1.spirals.archimedean : this.options.spiral,
this.random = this.options.random ? Math.random : () => 1, this.getTextPadding = (0,
util_1.functor)(this.options.padding);
}
zoomRatio() {
return this._originSize[0] / this._size[0];
}
dy() {
return this._dy;
}
layoutWord(index) {
const d = this.data[index];
if ("" === ("" + d.text).trim()) return !0;
const {maxRadius: maxRadius, center: center} = (0, vgrammar_util_1.getMaxRadiusAndCenter)(this.options.shape, this._size);
if (d.x = center[0], d.y = center[1], cloudSprite(this.contextAndRatio, d, this.data, index, this.cw, this.ch),
this._placeStatus = 0, d.hasText && this.place(this._board, d, this._bounds, maxRadius)) return this.result.push(d),
this._bounds ? cloudBounds(this._bounds, d) : this._bounds = [ {
x: d.x + d.x0,
y: d.y + d.y0
}, {
x: d.x + d.x1,
y: d.y + d.y1
} ], d.x -= this._size[0] >> 1, d.y -= this._size[1] >> 1, this._tTemp = null, this._dtTemp = null,
!0;
if (this.updateBoardExpandStatus(d.fontSize), d.hasText && this.shouldShrinkContinue()) {
if (1 === this._placeStatus) {
const maxSize0 = d.fontSize * this._originSize[0] / this.options.minFontSize, distSize0 = Math.max(d.width, d.height);
if (distSize0 <= maxSize0) this._board = this.expandBoard(this._board, this._bounds, distSize0 / this._size[0]); else {
if (!this.options.clip) return !0;
this._board = this.expandBoard(this._board, this._bounds, maxSize0 / this._size[0]);
}
} else this._placeStatus, this._board = this.expandBoard(this._board, this._bounds);
return this.updateBoardExpandStatus(d.fontSize), !1;
}
return this._tTemp = null, this._dtTemp = null, !0;
}
layout(words, config) {
this.initProgressive(), this.result = [], this._size = [ config.width, config.height ],
this.clearCache(), this._originSize = [ ...this._size ];
const contextAndRatio = this.getContext(vrender_core_1.vglobal.createCanvas({
width: 1,
height: 1
}));
this.contextAndRatio = contextAndRatio, this._board = new Uint32Array((this._size[0] >> 5) * this._size[1]).fill(0),
this._bounds = null;
words.length;
this.result = [];
const data = words.map(((d, i) => ({
text: this.getText(d),
fontFamily: this.getTextFontFamily(d),
fontStyle: this.getTextFontStyle(d),
fontWeight: this.getTextFontWeight(d),
angle: this.getTextRotate(d, i),
fontSize: ~~this.getTextFontSize(d),
padding: this.getTextPadding(d),
xoff: 0,
yoff: 0,
x1: 0,
y1: 0,
x0: 0,
y0: 0,
hasText: !1,
sprite: null,
datum: d,
x: 0,
y: 0,
width: 0,
height: 0
}))).sort((function(a, b) {
return b.fontSize - a.fontSize;
}));
if (this.originalData = data, this.data = data, this.progressiveRun(), !this.options.clip && this.options.enlarge && this._bounds && this.shrinkBoard(this._bounds),
this._bounds && [ "cardioid", "triangle", "triangle-upright" ].includes(this.options.shape)) {
const currentCenterY = (this._bounds[0].y + this._bounds[1].y) / 2;
this._dy = -(currentCenterY - this._originSize[1] / 2);
}
return this.result;
}
formatTagItem(words) {
const size = this._size, zoomRatio = this.zoomRatio(), globalDy = this.dy(), dx = size[0] >> 1, dy = size[1] >> 1, n = words.length, result = [];
let w, t;
for (let i = 0; i < n; ++i) w = words[i], t = {}, t.datum = w.datum, t.x = (w.x + dx) * zoomRatio,
t.y = (w.y + dy + globalDy) * zoomRatio, t.fontFamily = w.fontFamily, t.fontSize = w.fontSize * zoomRatio,
t.fontStyle = w.fontStyle, t.fontWeight = w.fontWeight, t.angle = w.angle, result.push(t);
return result;
}
output() {
return this.outputCallback ? this.outputCallback(this.formatTagItem(this.result)) : this.formatTagItem(this.result);
}
progressiveOutput() {
return this.outputCallback ? this.outputCallback(this.formatTagItem(this.progressiveResult)) : this.formatTagItem(this.progressiveResult);
}
updateBoardExpandStatus(fontSize) {
this._isBoardExpandCompleted = fontSize * (this._originSize[0] / this._size[0]) < this.options.minFontSize;
}
shouldShrinkContinue() {
return !this.options.clip && this.options.shrink && !this._isBoardExpandCompleted;
}
shrinkBoard(bounds) {
const leftTopPoint = bounds[0], rightBottomPoint = bounds[1];
if (rightBottomPoint.x >= this._size[0] || rightBottomPoint.y >= this._size[1]) return;
const minXValue = Math.min(leftTopPoint.x, this._size[0] - rightBottomPoint.x), minYValue = Math.min(leftTopPoint.y, this._size[1] - rightBottomPoint.y), minRatio = 2 * Math.min(minXValue / this._size[0], minYValue / this._size[1]);
this._size = this._size.map((v => v * (1 - minRatio)));
}
expandBoard(board, bounds, factor) {
const oldW = this._size[0], oldH = this._size[1], oldRowStride = oldW >> 5, expandedLeftWidth = oldW * (factor || 1.1) - oldW >> 5;
let diffWidth = 2 * expandedLeftWidth > 2 ? expandedLeftWidth : 2;
diffWidth % 2 != 0 && diffWidth++;
let diffHeight = Math.ceil(oldH * (diffWidth << 5) / oldW);
diffHeight % 2 != 0 && diffHeight++;
const newW = oldW + (diffWidth << 5), newH = oldH + diffHeight, newRowStride = newW >> 5, paddingLeft = diffWidth / 2, paddingTop = diffHeight / 2, newBoard = new Uint32Array(newH * newRowStride).fill(0);
for (let y = 0; y < oldH; y++) {
const sourceStartIndex = y * oldRowStride, sourceEndIndex = sourceStartIndex + oldRowStride, destStartIndex = (y + paddingTop) * newRowStride + paddingLeft, rowData = board.slice(sourceStartIndex, sourceEndIndex);
newBoard.set(rowData, destStartIndex);
}
if (this._size = [ newW, newH ], bounds) {
const offsetX = (diffWidth << 5) / 2, offsetY = diffHeight / 2;
bounds[0].x += offsetX, bounds[0].y += offsetY, bounds[1].x += offsetX, bounds[1].y += offsetY;
}
return newBoard;
}
insertZerosToArray(array, index, length) {
if (this.options.customInsertZerosToArray) return this.options.customInsertZerosToArray(array, index, length);
const len = Math.floor(length / 6e4), restLen = length % 6e4;
for (let i = 0; i < len; i++) array.splice(index + 6e4 * i, 0, ...new Array(6e4).fill(0));
array.splice(index + 6e4 * len, 0, ...new Array(restLen).fill(0));
}
getContext(canvas) {
canvas.width = 1, canvas.height = 1;
const imageData = canvas.getContext("2d", {
willReadFrequently: !0
}).getImageData(0, 0, 1, 1), ratio = Math.sqrt(imageData.data.length >> 2);
canvas.width = (this.cw << 5) / ratio, canvas.height = this.ch / ratio;
const context = canvas.getContext("2d", {
willReadFrequently: !0
});
return context.fillStyle = context.strokeStyle = "red", context.textAlign = "center",
{
context: context,
ratio: ratio,
canvas: canvas
};
}
place(board, tag, bounds, maxRadius) {
let isCollide = !1;
if (this.shouldShrinkContinue() && (tag.width > this._size[0] || tag.height > this._size[1])) return this._placeStatus = 1,
!1;
const dt = this.random() < .5 ? 1 : -1;
if (!this.shouldShrinkContinue() && this.isSizeLargerThanMax(tag, dt)) return null;
const startX = tag.x, startY = tag.y, maxDelta = Math.sqrt(this._size[0] * this._size[0] + this._size[1] * this._size[1]), s = this.spiral(this._size);
let dxdy, dx, dy, _tag, t = -dt;
for (this._tTemp = null, this._dtTemp = null; dxdy = s(t += dt); ) {
dx = dxdy[0], dy = dxdy[1];
const radius = Math.sqrt(dx ** 2 + dy ** 2);
let rad = Math.atan(dy / dx);
dx < 0 ? rad += Math.PI : dy < 0 && (rad = 2 * Math.PI + rad);
const rx = this.shape(rad);
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
if (radius >= maxRadius) isCollide && null === this._tTemp && (this._tTemp = t,
this._dtTemp = dt); else {
if (tag.x = startX + ~~(radius * rx * Math.cos(-rad)), tag.y = startY + ~~(radius * rx * Math.sin(-rad)),
_tag = tag, this.options.clip) if (this.shouldShrinkContinue()) {
if (isPartOutside(_tag, this._size)) {
isCollide && null === this._tTemp && (this._tTemp = t, this._dtTemp = dt);
continue;
}
} else {
if (isFullOutside(_tag, this._size)) {
isCollide && null === this._tTemp && (this._tTemp = t, this._dtTemp = dt);
continue;
}
isPartOutside(_tag, this._size) && (_tag = clipInnerTag(_tag, this._size));
} else if (isPartOutside(_tag, this._size)) {
isCollide && null === this._tTemp && (this._tTemp = t, this._dtTemp = dt);
continue;
}
if (isCollide = !0, (!bounds || collideRects(_tag, bounds)) && (!bounds || !cloudCollide(_tag, board, this._size))) {
const sprite = _tag.sprite, w = _tag.width >> 5, sw = this._size[0] >> 5, lx = _tag.x - (w << 4), sx = 127 & lx, msx = 32 - sx, h = _tag.y1 - _tag.y0;
let last, x = (_tag.y + _tag.y0) * sw + (lx >> 5);
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;
}
return tag.sprite = null, _tag.sprite = null, !0;
}
}
}
return null !== this._tTemp && (this._placeStatus = 3), !this.shouldShrinkContinue() && this.setCache(_tag, dt),
!1;
}
clearCache() {
this.cacheMap.clear();
}
setCache(tag, dt) {
const cacheKey = `${tag.angle}-${dt}`, w = tag.x1 - tag.x0, h = tag.y1 - tag.y0;
if (!this.cacheMap.has(cacheKey)) return void this.cacheMap.set(cacheKey, {
width: w,
height: h
});
const {width: width, height: height} = this.cacheMap.get(cacheKey);
(w < width && h < height || w <= width && h < height) && this.cacheMap.set(cacheKey, {
width: w,
height: h
});
}
isSizeLargerThanMax(tag, dt) {
const cacheKey = `${tag.angle}-${dt}`;
if (!this.cacheMap.has(cacheKey)) return !1;
const {width: width, height: height} = this.cacheMap.get(cacheKey), w = tag.x1 - tag.x0, h = tag.y1 - tag.y0;
return w >= width && h >= height;
}
}
function cloudSprite(contextAndRatio, d, data, di, cw, ch) {
if (d.sprite) return;
const c = contextAndRatio.context, ratio = contextAndRatio.ratio;
c.setTransform(ratio, 0, 0, ratio, 0, 0), c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
let x = 0, y = 0, maxh = 0;
const n = data.length;
let w, w32, h, i, j;
for (--di; ++di < n; ) {
if (d = data[di], c.save(), c.font = d.fontStyle + " " + d.fontWeight + " " + ~~((d.fontSize + 1) / ratio) + "px " + d.fontFamily,
w = c.measureText(d.text + "m").width * ratio, h = d.fontSize << 1, d.angle) {
const sr = Math.sin(d.angle), cr = Math.cos(d.angle), wcr = w * cr, wsr = w * sr, hcr = h * cr, hsr = h * sr;
w = Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 31 >> 5 << 5, h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
} else w = w + 31 >> 5 << 5;
if (h > maxh && (maxh = h), x + w >= cw << 5 && (x = 0, y += maxh, maxh = 0), y + h >= ch) break;
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio), d.angle && c.rotate(d.angle),
c.fillText(d.text, 0, 0), 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, d.x1 = w >> 1, d.y1 = h >> 1,
d.x0 = -d.x1, d.y0 = -d.y1, d.hasText = !0, x += w;
}
const pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, sprite = [];
for (;--di >= 0; ) {
if (!(d = data[di]).hasText) continue;
for (w = d.width, w32 = w >> 5, h = d.y1 - d.y0, i = 0; i < h * w32; i++) sprite[i] = 0;
if (x = d.xoff, null == x) return;
y = d.yoff;
let seen = 0, seenRow = -1;
for (j = 0; j < h; j++) {
for (i = 0; i < w; i++) {
const k = w32 * j + (i >> 5), m = pixels[(y + j) * (cw << 5) + (x + i) << 2] ? 1 << 31 - i % 32 : 0;
sprite[k] |= m, seen |= m;
}
seen ? seenRow = j : (d.y0++, h--, j--, y++);
}
d.y1 = d.y0 + seenRow, d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
}
}
function cloudCollide(tag, board, size) {
const sw = size[0] >> 5, sprite = tag.sprite, w = tag.width >> 5, lx = tag.x - (w << 4), sx = 127 & lx, msx = 32 - sx, h = tag.y1 - tag.y0;
let last, x = (tag.y + tag.y0) * sw + (lx >> 5);
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 cloudBounds(bounds, d) {
const b0 = bounds[0], b1 = bounds[1];
d.x + d.x0 < b0.x && (b0.x = d.x + d.x0), d.y + d.y0 < b0.y && (b0.y = d.y + d.y0),
d.x + d.x1 > b1.x && (b1.x = d.x + d.x1), d.y + d.y1 > b1.y && (b1.y = d.y + d.y1);
}
function collideRects(a, b) {
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
}
exports.CloudLayout = CloudLayout, CloudLayout.defaultOptions = {
enlarge: !1,
minFontSize: 2,
maxSingleWordTryCount: 2
};
const isFullOutside = (tag, size) => tag.x + tag.x0 > size[0] || tag.y + tag.y0 > size[0] || tag.x + tag.x1 < 0 || tag.y + tag.y1 < 0, isPartOutside = (tag, size) => tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1];
function clipInnerTag(tag, size) {
const sprite = tag.sprite, h = tag.y1 - tag.y0, w = tag.width >> 5;
let x = 0;
const _sprite = [], js = Math.max(-(tag.y0 + tag.y), 0), je = Math.min(h + (size[1] - (tag.y1 + tag.y)), h), is = Math.max(-(tag.x0 + tag.x), 0) >> 5, ie = Math.min(w + (size[0] - (tag.x1 + tag.x) >> 5) + 1, w);
for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) j < js || je <= j || i < is || ie <= i || _sprite.push(sprite[x + i]);
x += w;
}
const xl = is << 5, xr = w - ie << 5, yb = js, yt = h - je;
return Object.assign(Object.assign({}, tag), {
width: tag.width - xl - xr,
height: tag.height - yb - yt,
x0: tag.x0 + xl,
x1: tag.x1 - xr,
y0: tag.y0 + yb,
y1: tag.y1 - yt,
x: tag.x + xl / 2 - xr / 2,
sprite: _sprite
});
}
//# sourceMappingURL=cloud-layout.js.map