128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
/**
|
||
* Utility drawing functions.
|
||
* Adapted from agent-flow's draw-misc.ts (Apache 2.0).
|
||
*/
|
||
|
||
import { measureTextCached } from './render-cache';
|
||
|
||
/**
|
||
* Truncate text to fit within maxWidth, appending "..." if needed.
|
||
* Uses binary search for efficiency.
|
||
*/
|
||
export function truncateText(
|
||
ctx: CanvasRenderingContext2D,
|
||
text: string,
|
||
maxWidth: number,
|
||
font: string,
|
||
): string {
|
||
if (measureTextCached(ctx, font, text) <= maxWidth) return text;
|
||
|
||
let lo = 0;
|
||
let hi = text.length;
|
||
while (lo < hi) {
|
||
const mid = (lo + hi + 1) >> 1;
|
||
if (measureTextCached(ctx, font, text.slice(0, mid) + '...') <= maxWidth) {
|
||
lo = mid;
|
||
} else {
|
||
hi = mid - 1;
|
||
}
|
||
}
|
||
return lo > 0 ? text.slice(0, lo) + '...' : '...';
|
||
}
|
||
|
||
function fitTextPrefix(
|
||
ctx: CanvasRenderingContext2D,
|
||
text: string,
|
||
maxWidth: number,
|
||
font: string,
|
||
): string {
|
||
let lo = 0;
|
||
let hi = text.length;
|
||
while (lo < hi) {
|
||
const mid = (lo + hi + 1) >> 1;
|
||
if (measureTextCached(ctx, font, text.slice(0, mid)) <= maxWidth) {
|
||
lo = mid;
|
||
} else {
|
||
hi = mid - 1;
|
||
}
|
||
}
|
||
return text.slice(0, lo);
|
||
}
|
||
|
||
/**
|
||
* Wrap text into a small fixed number of canvas lines.
|
||
* The final line is truncated with "..." when the remaining text still overflows.
|
||
*/
|
||
export function wrapTextLines(
|
||
ctx: CanvasRenderingContext2D,
|
||
text: string,
|
||
maxWidth: number,
|
||
font: string,
|
||
maxLines: number,
|
||
): string[] {
|
||
const normalized = text.trim().replace(/\s+/g, ' ');
|
||
if (!normalized || maxLines <= 0) return [];
|
||
if (maxLines === 1) return [truncateText(ctx, normalized, maxWidth, font)];
|
||
|
||
const lines: string[] = [];
|
||
let remaining = normalized;
|
||
|
||
while (remaining && lines.length < maxLines) {
|
||
if (measureTextCached(ctx, font, remaining) <= maxWidth) {
|
||
lines.push(remaining);
|
||
break;
|
||
}
|
||
|
||
if (lines.length === maxLines - 1) {
|
||
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||
break;
|
||
}
|
||
|
||
const prefix = fitTextPrefix(ctx, remaining, maxWidth, font).trimEnd();
|
||
if (!prefix) {
|
||
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||
break;
|
||
}
|
||
|
||
const breakIndex = prefix.lastIndexOf(' ');
|
||
const line = breakIndex > 0 ? prefix.slice(0, breakIndex) : prefix;
|
||
lines.push(line);
|
||
remaining = remaining.slice(line.length).trimStart();
|
||
}
|
||
|
||
return lines;
|
||
}
|
||
|
||
// Pre-computed hex vertex unit offsets (avoids cos/sin per call)
|
||
const HEX_COS: number[] = [];
|
||
const HEX_SIN: number[] = [];
|
||
for (let i = 0; i < 6; i++) {
|
||
const angle = (Math.PI / 3) * i - Math.PI / 6;
|
||
HEX_COS.push(Math.cos(angle));
|
||
HEX_SIN.push(Math.sin(angle));
|
||
}
|
||
|
||
/**
|
||
* Draw a regular hexagon path centered at (x, y) with given radius.
|
||
*/
|
||
export function drawHexagon(
|
||
ctx: CanvasRenderingContext2D,
|
||
x: number,
|
||
y: number,
|
||
radius: number,
|
||
): void {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + radius * HEX_COS[0], y + radius * HEX_SIN[0]);
|
||
ctx.lineTo(x + radius * HEX_COS[1], y + radius * HEX_SIN[1]);
|
||
ctx.lineTo(x + radius * HEX_COS[2], y + radius * HEX_SIN[2]);
|
||
ctx.lineTo(x + radius * HEX_COS[3], y + radius * HEX_SIN[3]);
|
||
ctx.lineTo(x + radius * HEX_COS[4], y + radius * HEX_SIN[4]);
|
||
ctx.lineTo(x + radius * HEX_COS[5], y + radius * HEX_SIN[5]);
|
||
ctx.closePath();
|
||
}
|
||
|
||
/**
|
||
* SVG path data for the Claude spark logo (256×256 viewbox).
|
||
*/
|
||
export const CLAUDE_SPARK_D =
|
||
'M128,8C60.6,8,8,60.6,8,128s52.6,120,120,120s120-52.6,120-120S195.4,8,128,8z M161.6,169.6 c-4.8,8-16,10.8-24,6l-9.6-5.6l-9.6,5.6c-8,4.8-19.2,1.6-24-6c-4.8-8-1.6-19.2,6-24l9.6-5.6v-11.2l-9.6-5.6 c-8-4.8-10.8-16-6-24c4.8-8,16-10.8,24-6l9.6,5.6l9.6-5.6c8-4.8,19.2-1.6,24,6c4.8,8,1.6,19.2-6,24l-9.6,5.6v11.2l9.6,5.6 C163.2,150.4,166.4,161.6,161.6,169.6z';
|