主题
复合图元与连线
这个示例用 @ggn/draw 画出一张完整的业务图。角色、卡片、胶囊和容器都由示例自己注册, draw 只负责矩形、椭圆、路径、文字、连线、渐变、场景和 SVG 输出。
实现:只用 @ggn/draw
页面里的图不是静态图片。文档、明暗背景、形状、端口、连线和箭头都会实时变成 draw 的 DrawDocument / 场景叶子,再由六段管线导出:
ts
import {
bindDocument, buildScene, layoutDocument, parseDocument, renderToSvg, resolveTheme,
} from "@ggn/draw";
import {
USE_CASE_TOKENS, useCaseDesignDocument,
useCaseDesignRegistry, useCaseDesignRouter,
} from "./usecase-draw";
const registry = useCaseDesignRegistry();
const theme = resolveTheme({
tokens: USE_CASE_TOKENS,
typography: { fontSize: 16 },
});
const document = parseDocument(useCaseDesignDocument("dark"));
if (!theme.ok || !document.ok) throw new Error("输入不合法");
const bound = bindDocument(document.value, registry);
if (!bound.ok) throw new Error("绑定失败");
const layout = layoutDocument(bound.value, {
typography: theme.value.typography,
router: useCaseDesignRouter,
});
if (!layout.ok) throw new Error("布局失败");
const scene = buildScene(bound.value, layout.value, {
registry,
theme: theme.value,
});
if (!scene.ok) throw new Error("场景构建失败");
const svg = renderToSvg(scene.value);浅色只替换两处:文档的浅色背景,以及 USE_CASE_TOKENS_LIGHT。节点、端口、pins、路由都不动。
设计稿的手画连线也不是一张独立 SVG,而是在 draw 的边注册项里逐片输出:
ts
import { polylineD } from "@ggn/draw";
registry = registry.with({
edges: {
link: {
markers: { end: "design-open-arrow" },
render(ctx) {
ctx.out.node({
kind: "path",
key: ctx.out.key(""),
// 路径表完整保存在 showcase.ts;未覆盖的边仍走通用折线。
d: DESIGN_EDGE_PATHS[ctx.edgeId] ?? polylineD(ctx.route),
style: { ...ctx.style, fill: "none" },
});
},
},
},
}, { override: true });完整实现住在 packages/draw/docs/.vitepress/theme/components/usecase-draw/showcase.ts; 这些形状与配色属于示例本身,不会被写进绘制库。
示例里的四类图元
| 形状 | 用到的词 | 对照目标稿的哪一条 |
|---|---|---|
| 独立人物 | 椭圆(圆盘)+ 椭圆(头)+ 路径(肩)+ 文字 | 头肩分色;名字在下方,两个端口高低分开 |
| 成员卡片 | 矩形 + 圆盘头像 + 文字 + 徽标胶囊 | 头像在上、名字靠底,右上角可放状态标签 |
| 胶囊节点 | 矩形(rx = 半高)+ 24×24 字形路径 | 玻璃面、专属图标;设计稿使用无图标槽的裸字形 |
| 容器 | 路径(42px 页签)+ 矩形(主体)+ 徽标 + 文字 | 上方圆角、下沿平直;页签宽度可按标题与徽标计算 |
| 连线 | 手画 path(圆角折线)+ 渐变描边 + 箭头 | 线色从冷走到暖;Chef 是灰蓝虚线,Food Critic 是完整暖色实线 |
设计稿的连线是手画的,因此示例用 useCaseDesignRouter 把圆弧折点原样写进路由; 其他图仍然走通用的避障或中点走廊路由。路由只决定折点,端口锚点一个像素都不动。 连线渐变使用 userSpaceOnUse:水平直线的包围盒高度为 0,用 objectBoundingBox 会让整条线在浏览器里消失。
另外,open-arrow 这类端点装饰是库内置的 marker:注册表要 createRegistry({ base: builtinRegistry }) 才拿得到。漏了 base 时不会抛异常,但 build 会产生 UNKNOWN_MARKER 和 FALLBACK_USED, 图上也会少一枚箭头。注册表的基础规则见节点注册。
样式如何复用
形状只拿得到 ctx.style(已解析成具体值的涂法),拿不到主题 token——但这不代表颜色写死: 文档的 styles 段里可以写 { token } 色标,resolveTheme 一换,渐变、图标、端口、投影跟着换。 上面两张图的节点、端口、位置与路由完全一样;只有 tokens 与浅色背景配方不同。
顺带一个「同一份几何怎么吃两套主题」的例子:头像几何只有一份(person.ts 里 44×44 的盘 + 头 + 肩),盘吃 ctx.style 的面与描边,头/肩吃 detailColor / detailAccent。所以换 tokens 只换颜色,形状一个数字都不用动;文档站示例也从这里取几何,两处不会各画各的。
这张图的背景也是文档的一部分:dark 是径向暗角 + 两团环境光 + 20px 点阵; light 是 #F8FAFC → #F1F5F9 斜向渐变 + 更淡的环境光 + 点阵。build 把它们展成铺满画布的矩形。 文档同时声明 canvas: 0 0 920 560,所以导出的 SVG 保持设计稿的原始 viewBox。
想自己改一版:把 <UseCaseCompare /> 里的文档换成你的 DrawDocument(谁连谁 + pins), 注册表换成你的形状——@ggn/draw 这边一行都不用动。