mind-map/apps/web/src/canvas-chrome.ts
oliver 4bffec1d81 fix: make glass cards a solid elevated surface without strip dividers
Remove header bands and inner hairlines that read as stacked strips; restore a lighter opaque fill and heavier shadow so cards stay readable on the dark canvas.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 23:46:48 +08:00

165 lines
4.3 KiB
TypeScript

/** Shared canvas typography + chrome helpers for bubble/card nodes. */
export const CANVAS_FONT =
'"Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", system-ui, sans-serif';
export function canvasFont(
sizePx: number,
weight: 400 | 500 | 600 | 700 = 400,
): string {
return `${weight === 400 ? "" : `${weight} `}${sizePx}px ${CANVAS_FONT}`;
}
/** Soft drop shadow under a node (world units). */
export function paintNodeShadow(
ctx: CanvasRenderingContext2D,
zoom: number,
opts?: { selected?: boolean; weight?: "default" | "heavy" },
) {
const heavy = opts?.weight === "heavy";
const z = Math.max(0.5, zoom);
if (heavy) {
ctx.shadowColor = opts?.selected
? "rgba(0, 0, 0, 0.62)"
: "rgba(0, 0, 0, 0.5)";
ctx.shadowBlur = (opts?.selected ? 28 : 22) / z;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 7 / z;
return;
}
ctx.shadowColor = opts?.selected
? "rgba(0, 0, 0, 0.45)"
: "rgba(0, 0, 0, 0.28)";
ctx.shadowBlur = (opts?.selected ? 18 : 12) / z;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 3 / z;
}
export function clearShadow(ctx: CanvasRenderingContext2D) {
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
/** Low-alpha accent wash clipped to current path. */
export function fillAccentWash(
ctx: CanvasRenderingContext2D,
accent: string,
alpha = 0.14,
) {
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = accent;
ctx.fill();
ctx.restore();
}
/** Left accent rail inside a rounded rect (not for diamonds/pills). */
export function paintAccentRail(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
h: number,
accent: string,
radius = 10,
) {
const railW = 4;
ctx.save();
ctx.beginPath();
const rr = Math.min(radius, h / 2);
ctx.moveTo(x + rr, y);
ctx.lineTo(x + railW, y);
ctx.lineTo(x + railW, y + h);
ctx.lineTo(x + rr, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y);
ctx.closePath();
ctx.fillStyle = accent;
ctx.globalAlpha = 0.95;
ctx.fill();
ctx.restore();
}
export function paintBadgeChip(
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
theme: {
labelSecondary: string;
border: string;
bgLayer1: string;
glassHeader?: string;
glassBorder?: string;
},
zoom: number,
): number {
const padX = 6;
const h = 16 / Math.max(0.75, Math.min(1.2, zoom));
ctx.font = canvasFont(10 / Math.max(0.75, Math.min(1.2, zoom)), 500);
const tw = ctx.measureText(text).width;
const w = tw + padX * 2;
ctx.beginPath();
const r = h / 2;
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fillStyle = theme.glassHeader ?? "rgba(255,255,255,0.06)";
ctx.fill();
ctx.strokeStyle = theme.glassBorder ?? theme.border;
ctx.lineWidth = 1 / Math.max(0.5, zoom);
ctx.stroke();
ctx.fillStyle = theme.labelSecondary;
ctx.textBaseline = "middle";
ctx.fillText(text, x + padX, y + h / 2);
return w + 4;
}
/** Soft outer glow in accent hue for glass cards (shadow only — do not change fill alpha). */
export function paintGlassGlow(
ctx: CanvasRenderingContext2D,
accent: string,
zoom: number,
selected?: boolean,
) {
ctx.shadowColor = accent;
ctx.shadowBlur = (selected ? 20 : 12) / Math.max(0.5, zoom);
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
/** Soft top sheen — keep short so it does not read as a header divider. */
export function paintGlassHighlight(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
radius: number,
highlight: string,
) {
ctx.save();
ctx.beginPath();
const rr = Math.min(radius, h / 2, w / 2);
const band = Math.min(12, h * 0.2);
ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
ctx.lineTo(x + w, y + band);
ctx.lineTo(x, y + band);
ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y);
ctx.closePath();
const g = ctx.createLinearGradient(x, y, x, y + band);
g.addColorStop(0, highlight);
g.addColorStop(1, "rgba(255,255,255,0)");
ctx.globalAlpha = 0.55;
ctx.fillStyle = g;
ctx.fill();
ctx.restore();
}