(ctx: CanvasContext, words: WordItem[])
| 262 | } |
| 263 | |
| 264 | function layoutWords(ctx: CanvasContext, words: WordItem[]): WordItem[] { |
| 265 | const placed: WordItem[] = []; |
| 266 | const centerX = WIDTH / 2; |
| 267 | const centerY = HEIGHT / 2 - 28; |
| 268 | for (const original of words) { |
| 269 | const item = { ...original }; |
| 270 | ctx.font = `700 ${item.size}px ${CJK_FONT_STACK}`; |
| 271 | const metrics = ctx.measureText(item.word); |
| 272 | item.width = metrics.width; |
| 273 | item.height = item.size; |
| 274 | if (item.width > WIDTH - MARGIN * 2) continue; |
| 275 | |
| 276 | for (let attempt = 0; attempt < 4; attempt++) { |
| 277 | item.size = Math.max(10, original.size - attempt * 4); |
| 278 | ctx.font = `700 ${item.size}px ${CJK_FONT_STACK}`; |
| 279 | const nextMetrics = ctx.measureText(item.word); |
| 280 | item.width = nextMetrics.width; |
| 281 | item.height = item.size; |
| 282 | let placedItem = false; |
| 283 | for (let t = 0; t < 3600; t++) { |
| 284 | const angle = t * 0.38; |
| 285 | const radius = 5.2 * Math.sqrt(t); |
| 286 | item.x = centerX + Math.cos(angle) * radius - item.width / 2; |
| 287 | item.y = centerY + Math.sin(angle) * radius + item.height / 2; |
| 288 | if (item.x < MARGIN || item.y < MARGIN + item.height || item.x + item.width > WIDTH - MARGIN || item.y > HEIGHT - 78) continue; |
| 289 | if (overlaps(item, placed)) continue; |
| 290 | placed.push({ ...item }); |
| 291 | placedItem = true; |
| 292 | break; |
| 293 | } |
| 294 | if (placedItem) break; |
| 295 | } |
| 296 | } |
| 297 | return placed; |
| 298 | } |
| 299 | |
| 300 | function renderWordCloud(words: WordItem[], limit: number, validMessages: number): Buffer { |
| 301 | ensureCjkFont(); |
no test coverage detected