Skip to content

Commit 416e435

Browse files
authored
Merge pull request #249 from VisActor/fix/fix-bug-of-wordcloud-layout
fix: fix bug of wordcloud layout
2 parents 94672bc + 6aa264e commit 416e435

File tree

2 files changed

+88
-28
lines changed

2 files changed

+88
-28
lines changed
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/vlayouts"
7+
}
8+
],
9+
"packageName": "@visactor/vlayouts",
10+
"email": "[email protected]"
11+
}

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

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> {
8888
_dy: number = 0;
8989

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

@@ -187,20 +187,20 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> {
187187
const distSize0 = Math.max(d.width, d.height);
188188
if (distSize0 <= maxSize0) {
189189
// 扩大尺寸满足最小字体要求 =》 按照要求扩大board
190-
this.expandBoard(this._board, this._bounds, distSize0 / this._size[0]);
190+
this._board = this.expandBoard(this._board, this._bounds, distSize0 / this._size[0]);
191191
} else if (this.options.clip) {
192192
// 扩大尺寸不满足最小字体要求,但支持裁剪 =》 按最大尺寸扩大,裁剪词语
193-
this.expandBoard(this._board, this._bounds, maxSize0 / this._size[0]);
193+
this._board = this.expandBoard(this._board, this._bounds, maxSize0 / this._size[0]);
194194
} else {
195195
// 扩大尺寸不满足最小字体要求,且不支持裁剪 =》 丢弃词语
196196
return true;
197197
}
198198
} else if (this._placeStatus === 3) {
199199
// 扩大画布
200-
this.expandBoard(this._board, this._bounds);
200+
this._board = this.expandBoard(this._board, this._bounds);
201201
} else {
202202
// 扩大画布
203-
this.expandBoard(this._board, this._bounds);
203+
this._board = this.expandBoard(this._board, this._bounds);
204204
}
205205
// 更新一次状态,下次大尺寸词语进入裁剪
206206
this.updateBoardExpandStatus(d.fontSize);
@@ -221,7 +221,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> {
221221
this._originSize = [...this._size];
222222
const contextAndRatio = this.getContext(this.options.createCanvas({ width: 1, height: 1 }));
223223
this.contextAndRatio = contextAndRatio;
224-
this._board = new Array((this._size[0] >> 5) * this._size[1]).fill(0);
224+
this._board = new Uint32Array((this._size[0] >> 5) * this._size[1]).fill(0);
225225
// 已经绘制文字的最小包围盒
226226
this._bounds = null;
227227

@@ -338,34 +338,83 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> {
338338
this._size = this._size.map(v => v * (1 - minRatio)) as any;
339339
}
340340

341-
// 扩充 bitmap
342-
private expandBoard(board: number[], bounds: Bounds, factor?: any) {
343-
const expandedLeftWidth = (this._size[0] * (factor || 1.1) - this._size[0]) >> 5;
341+
// /**
342+
// * [已优化] 插入指定数量的零到数组中。
343+
// * 针对 length 较小的场景,这是最高效的实现。
344+
// * 在新的 expandBoard 实现中,此函数不再被需要,但为保持完整性而提供。
345+
// */
346+
// private insertZerosToArray(array: any[], index: number, length: number): void {
347+
// if (length <= 0) {
348+
// return;
349+
// }
350+
// // 对于 length 较小的场景,创建临时数组的开销极小,
351+
// // 而单次 splice() 调用可以利用 V8 的 C++ 底层优化,性能最好。
352+
// const zerosToInsert = new Array(length).fill(0);
353+
// array.splice(index, 0, ...zerosToInsert);
354+
// }
355+
356+
/**
357+
* [已优化] 通过重建法高效扩展画板,添加边框。
358+
*
359+
* @returns {number[]} 返回一个全新的、尺寸更大的画板数组。
360+
* @notice 这是一个重大变更:此函数不再原地修改 board,而是返回一个新数组。
361+
* 调用方需要相应地更新其引用,例如:this.board = this.expandBoard(...);
362+
*/
363+
private expandBoard(board: Uint32Array, bounds: Bounds, factor?: any): Uint32Array {
364+
// --- 1. 计算所有尺寸和偏移量 ---
365+
const oldW = this._size[0];
366+
const oldH = this._size[1];
367+
const oldRowStride = oldW >> 5; // 每行的“块”数
368+
369+
// 计算水平和垂直方向需要增加的“块”数
370+
const expandedLeftWidth = (oldW * (factor || 1.1) - oldW) >> 5;
344371
let diffWidth = expandedLeftWidth * 2 > 2 ? expandedLeftWidth : 2;
345372
if (diffWidth % 2 !== 0) {
346-
diffWidth++;
373+
diffWidth++; // 确保为偶数,以便左右对称
347374
}
348-
let diffHeight = Math.ceil((this._size[1] * (diffWidth << 5)) / this._size[0]);
375+
376+
let diffHeight = Math.ceil((oldH * (diffWidth << 5)) / oldW);
349377
if (diffHeight % 2 !== 0) {
350-
diffHeight++;
378+
diffHeight++; // 确保为偶数,以便上下对称
351379
}
352-
const w = this._size[0];
353-
const h = this._size[1];
354-
const widthArr = new Array(diffWidth).fill(0);
355-
356-
const heightArr = new Array((diffHeight / 2) * (diffWidth + (w >> 5))).fill(0);
357-
this.insertZerosToArray(board, h * (w >> 5), heightArr.length + diffWidth / 2);
358-
for (let i = h - 1; i > 0; i--) {
359-
this.insertZerosToArray(board, i * (w >> 5), widthArr.length);
380+
381+
const newW = oldW + (diffWidth << 5);
382+
const newH = oldH + diffHeight;
383+
const newRowStride = newW >> 5;
384+
385+
const paddingLeft = diffWidth / 2;
386+
const paddingTop = diffHeight / 2;
387+
388+
// --- 2. 创建并填充新画板 ---
389+
const newBoard = new Uint32Array(newH * newRowStride).fill(0);
390+
391+
// --- 3. 一次性将旧数据复制到新画板中心 ---
392+
for (let y = 0; y < oldH; y++) {
393+
// 计算旧画板中当前行的读取位置
394+
const sourceStartIndex = y * oldRowStride;
395+
const sourceEndIndex = sourceStartIndex + oldRowStride;
396+
397+
// 计算新画板中当前行的写入位置(考虑顶部和左侧边框)
398+
const destStartIndex = (y + paddingTop) * newRowStride + paddingLeft;
399+
400+
// 使用 slice 提取行数据(高效),用 set 写入新位置(最高效)
401+
const rowData = board.slice(sourceStartIndex, sourceEndIndex);
402+
newBoard.set(rowData, destStartIndex);
360403
}
361-
this.insertZerosToArray(board, 0, heightArr.length + diffWidth / 2);
362-
this._size = [w + (diffWidth << 5), h + diffHeight];
404+
405+
// --- 4. 更新尺寸和边界信息 ---
406+
this._size = [newW, newH];
363407
if (bounds) {
364-
bounds[0].x += (diffWidth << 5) / 2;
365-
bounds[0].y += diffHeight / 2;
366-
bounds[1].x += (diffWidth << 5) / 2;
367-
bounds[1].y += diffHeight / 2;
408+
const offsetX = (diffWidth << 5) / 2;
409+
const offsetY = diffHeight / 2;
410+
bounds[0].x += offsetX;
411+
bounds[0].y += offsetY;
412+
bounds[1].x += offsetX;
413+
bounds[1].y += offsetY;
368414
}
415+
416+
// --- 5. 返回新创建的画板 ---
417+
return newBoard;
369418
}
370419

371420
// 分组扩充填充数组, 一次填充超过大概126000+会报stack overflow,worker环境下大概6w,这边取个比较小的
@@ -400,7 +449,7 @@ export class CloudLayout extends BaseLayout<ICloudLayoutOptions> {
400449
return { context: context, ratio: ratio, canvas };
401450
}
402451

403-
private place(board: number[], tag: TagItem, bounds: Bounds, maxRadius: number) {
452+
private place(board: Uint32Array, tag: TagItem, bounds: Bounds, maxRadius: number) {
404453
let isCollide = false;
405454
// 情况1,超长词语
406455
if (this.shouldShrinkContinue() && (tag.width > this._size[0] || tag.height > this._size[1])) {
@@ -723,7 +772,7 @@ function cloudSprite(contextAndRatio: any, d: TagItem, data: TagItem[], di: numb
723772
}
724773

725774
// Use mask-based collision detection.
726-
function cloudCollide(tag: TagItem, board: number[], size: [number, number]) {
775+
function cloudCollide(tag: TagItem, board: Uint32Array, size: [number, number]) {
727776
const sw = size[0] >> 5;
728777
const sprite = tag.sprite;
729778
const w = tag.width >> 5;

0 commit comments

Comments
 (0)