Skip to content

Commit 0707051

Browse files
authored
Merge pull request #676 from VisActor/feat/optimaztion-wordcloud-layout
Feat/optimaztion wordcloud layout
2 parents 1e32338 + 0e8db66 commit 0707051

2 files changed

Lines changed: 73 additions & 28 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "fix: fix bug of wordcloud layout\n\n",
5+
"type": "none",
6+
"packageName": "@visactor/vgrammar-core"
7+
}
8+
],
9+
"packageName": "@visactor/vgrammar-core",
10+
"email": "lixuef1313@163.com"
11+
}

packages/vgrammar-wordcloud/src/cloud-layout.ts

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> implements IPro
9090
_dy: number = 0;
9191

9292
contextAndRatio?: { context: CanvasRenderingContext2D; ratio: number; canvas: HTMLCanvasElement };
93-
_board: number[];
93+
_board: Uint32Array;
9494
/** 已经绘制文字的最小包围盒 */
9595
_bounds: Bounds;
9696

@@ -189,20 +189,20 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> implements IPro
189189
const distSize0 = Math.max(d.width, d.height);
190190
if (distSize0 <= maxSize0) {
191191
// 扩大尺寸满足最小字体要求 =》 按照要求扩大board
192-
this.expandBoard(this._board, this._bounds, distSize0 / this._size[0]);
192+
this._board = this.expandBoard(this._board, this._bounds, distSize0 / this._size[0]);
193193
} else if (this.options.clip) {
194194
// 扩大尺寸不满足最小字体要求,但支持裁剪 =》 按最大尺寸扩大,裁剪词语
195-
this.expandBoard(this._board, this._bounds, maxSize0 / this._size[0]);
195+
this._board = this.expandBoard(this._board, this._bounds, maxSize0 / this._size[0]);
196196
} else {
197197
// 扩大尺寸不满足最小字体要求,且不支持裁剪 =》 丢弃词语
198198
return true;
199199
}
200200
} else if (this._placeStatus === 3) {
201201
// 扩大画布
202-
this.expandBoard(this._board, this._bounds);
202+
this._board = this.expandBoard(this._board, this._bounds);
203203
} else {
204204
// 扩大画布
205-
this.expandBoard(this._board, this._bounds);
205+
this._board = this.expandBoard(this._board, this._bounds);
206206
}
207207
// 更新一次状态,下次大尺寸词语进入裁剪
208208
this.updateBoardExpandStatus(d.fontSize);
@@ -223,7 +223,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> implements IPro
223223
this._originSize = [...this._size];
224224
const contextAndRatio = this.getContext(vglobal.createCanvas({ width: 1, height: 1 }));
225225
this.contextAndRatio = contextAndRatio;
226-
this._board = new Array((this._size[0] >> 5) * this._size[1]).fill(0);
226+
this._board = new Uint32Array((this._size[0] >> 5) * this._size[1]).fill(0);
227227
// 已经绘制文字的最小包围盒
228228
this._bounds = null;
229229

@@ -340,34 +340,68 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> implements IPro
340340
this._size = this._size.map(v => v * (1 - minRatio)) as any;
341341
}
342342

343-
// 扩充 bitmap
344-
private expandBoard(board: number[], bounds: Bounds, factor?: any) {
345-
const expandedLeftWidth = (this._size[0] * (factor || 1.1) - this._size[0]) >> 5;
343+
/**
344+
* [已优化] 通过重建法高效扩展画板,添加边框。
345+
*
346+
* @returns {number[]} 返回一个全新的、尺寸更大的画板数组。
347+
* @notice 这是一个重大变更:此函数不再原地修改 board,而是返回一个新数组。
348+
* 调用方需要相应地更新其引用,例如:this.board = this.expandBoard(...);
349+
*/
350+
private expandBoard(board: Uint32Array, bounds: Bounds, factor?: any): Uint32Array {
351+
// --- 1. 计算所有尺寸和偏移量 ---
352+
const oldW = this._size[0];
353+
const oldH = this._size[1];
354+
const oldRowStride = oldW >> 5; // 每行的“块”数
355+
356+
// 计算水平和垂直方向需要增加的“块”数
357+
const expandedLeftWidth = (oldW * (factor || 1.1) - oldW) >> 5;
346358
let diffWidth = expandedLeftWidth * 2 > 2 ? expandedLeftWidth : 2;
347359
if (diffWidth % 2 !== 0) {
348-
diffWidth++;
360+
diffWidth++; // 确保为偶数,以便左右对称
349361
}
350-
let diffHeight = Math.ceil((this._size[1] * (diffWidth << 5)) / this._size[0]);
362+
363+
let diffHeight = Math.ceil((oldH * (diffWidth << 5)) / oldW);
351364
if (diffHeight % 2 !== 0) {
352-
diffHeight++;
365+
diffHeight++; // 确保为偶数,以便上下对称
353366
}
354-
const w = this._size[0];
355-
const h = this._size[1];
356-
const widthArr = new Array(diffWidth).fill(0);
357-
358-
const heightArr = new Array((diffHeight / 2) * (diffWidth + (w >> 5))).fill(0);
359-
this.insertZerosToArray(board, h * (w >> 5), heightArr.length + diffWidth / 2);
360-
for (let i = h - 1; i > 0; i--) {
361-
this.insertZerosToArray(board, i * (w >> 5), widthArr.length);
367+
368+
const newW = oldW + (diffWidth << 5);
369+
const newH = oldH + diffHeight;
370+
const newRowStride = newW >> 5;
371+
372+
const paddingLeft = diffWidth / 2;
373+
const paddingTop = diffHeight / 2;
374+
375+
// --- 2. 创建并填充新画板 ---
376+
const newBoard = new Uint32Array(newH * newRowStride).fill(0);
377+
378+
// --- 3. 一次性将旧数据复制到新画板中心 ---
379+
for (let y = 0; y < oldH; y++) {
380+
// 计算旧画板中当前行的读取位置
381+
const sourceStartIndex = y * oldRowStride;
382+
const sourceEndIndex = sourceStartIndex + oldRowStride;
383+
384+
// 计算新画板中当前行的写入位置(考虑顶部和左侧边框)
385+
const destStartIndex = (y + paddingTop) * newRowStride + paddingLeft;
386+
387+
// 使用 slice 提取行数据(高效),用 set 写入新位置(最高效)
388+
const rowData = board.slice(sourceStartIndex, sourceEndIndex);
389+
newBoard.set(rowData, destStartIndex);
362390
}
363-
this.insertZerosToArray(board, 0, heightArr.length + diffWidth / 2);
364-
this._size = [w + (diffWidth << 5), h + diffHeight];
391+
392+
// --- 4. 更新尺寸和边界信息 ---
393+
this._size = [newW, newH];
365394
if (bounds) {
366-
bounds[0].x += (diffWidth << 5) / 2;
367-
bounds[0].y += diffHeight / 2;
368-
bounds[1].x += (diffWidth << 5) / 2;
369-
bounds[1].y += diffHeight / 2;
395+
const offsetX = (diffWidth << 5) / 2;
396+
const offsetY = diffHeight / 2;
397+
bounds[0].x += offsetX;
398+
bounds[0].y += offsetY;
399+
bounds[1].x += offsetX;
400+
bounds[1].y += offsetY;
370401
}
402+
403+
// --- 5. 返回新创建的画板 ---
404+
return newBoard;
371405
}
372406

373407
// 分组扩充填充数组, 一次填充超过大概126000+会报stack overflow,worker环境下大概6w,这边取个比较小的
@@ -402,7 +436,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> implements IPro
402436
return { context: context, ratio: ratio, canvas };
403437
}
404438

405-
private place(board: number[], tag: TagItem, bounds: Bounds, maxRadius: number) {
439+
private place(board: Uint32Array, tag: TagItem, bounds: Bounds, maxRadius: number) {
406440
let isCollide = false;
407441
// 情况1,超长词语
408442
if (this.shouldShrinkContinue() && (tag.width > this._size[0] || tag.height > this._size[1])) {
@@ -725,7 +759,7 @@ function cloudSprite(contextAndRatio: any, d: TagItem, data: TagItem[], di: numb
725759
}
726760

727761
// Use mask-based collision detection.
728-
function cloudCollide(tag: TagItem, board: number[], size: [number, number]) {
762+
function cloudCollide(tag: TagItem, board: Uint32Array, size: [number, number]) {
729763
const sw = size[0] >> 5;
730764
const sprite = tag.sprite;
731765
const w = tag.width >> 5;

0 commit comments

Comments
 (0)