图谱手绘风格切换
这个示例保持一棵小型居中层级图不变,并在运行时在普通风格与手绘风格之间切换。它结合了自定义节点插槽包裹层、可选边框与纹理变体、对内置图层表面的 SVG 滤镜样式、全图节点与连线更新、小地图,以及共享画布导出控制。
将 Relation Graph 切换为手绘呈现模式
这个示例构建了什么
这个示例构建了一个居中层级查看器,可以在运行时在普通卡片呈现和类似草图的手绘皮肤之间切换。画布中展示了七个 KPI 风格节点、六条带标签的连线、一个浮动控制窗口,以及一个内置 minimap。
用户可以开启或关闭手绘模式、修改节点边框样式,并选择纸张、斜线、网格或点状等纹理叠加效果。图结构本身不会变化。相反,这个示例保持同一份已加载的层级数据不变,并原地重新设定其节点、连线、标签和箭头的主题样式。
这个示例最值得关注的一点在于,草图效果来自多层 relation-graph 功能的组合,而不是依赖单独的渲染器。自定义节点插槽、SVG filters、SCSS 覆盖以及图实例更新 API 协同工作,使同一张已有图能够呈现出截然不同的插画风格外观。
数据是如何组织的
数据来自 getJsonData(),返回一个 RGJsonData 对象,其中包含 rootId: "a"、一个扁平的 nodes 数组,以及一个扁平的 lines 数组。每条节点记录都带有 data.name 和 data.myicon,而六条连线最初只定义了 from 和 to。
在调用 setJsonData() 之前有一个预处理步骤。初始化期间,代码会遍历每一条连线,并注入统一的标签(Line Text)、随机的连接点偏移、一个随机的 junctionOffset,以及自定义的 my-hand-drawn-arrow-end marker。数据加载完成后,后续的样式变化不会重建 JSON,而是通过 updateNodeData() 和 updateLine() 更新已经渲染出来的节点与连线。
在真实产品中,这种结构同样可以表示小型组织架构图、团队仪表盘、能力地图或功能层级。data.name 可以映射为人员、部门、产品或服务,而运行时写入的这些样式字段则可以代表主题预设、展示模式,或品牌定制的插画风格变体。
relation-graph 是如何使用的
index.tsx 通过 RGProvider 包裹整个示例,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 使用共享的图上下文。图被配置为 center 布局,设置了明确的 levelGaps,在两个轴向上都使用居中对齐,并设置 defaultLineWidth: 2。同时,通过 defaultNodeColor: 'transparent' 和 defaultNodeBorderWidth: 0 有意弱化 relation-graph 内置的节点填充和边框渲染,让可见的卡片主体改由自定义插槽内容来定义。
主要扩展点是 RGSlotOnNode。每个节点都会渲染一个大图标、一个名称以及三行固定 KPI,然后根据 enableHandDrawn,将这些内容包裹在 MyNodeContentBox 或 HandDrawnBox 中。HandDrawnBox 会应用不对称的 border-radius 数值和可选的基于 SVG 的纹理,而 IconSwitcher 会将 node.data.myicon 映射到 Lucide 图标,若未匹配则回退到 HelpCircle。
这个示例还使用了 RGSlotOnView 来挂载 RGMiniView,因此查看器无需编写自定义视口代码就获得了一个内置总览面板。运行时行为由图实例 API 驱动:setJsonData() 负责加载图,getNodes() 配合 updateNodeData() 传播边框和纹理状态,getLines() 配合 updateLine() 切换线条形状,而 moveToCenter()、zoomToFit() 和 zoomToFitWithAnimation() 则在视觉变化后保持视口对齐。
手绘效果并不止于节点插槽。MySvgFilters 注入了 static-hand-drawn SVG filter 和自定义箭头 marker,而 my-relation-graph.scss 会在 .node-filter-hand-drawn 包裹层下定向作用于 relation-graph 内置的节点、连线和标签层。该 SCSS 添加了滤镜扭曲效果、便签风格的虚线标签,以及围绕自定义内容的选中态覆盖,而不是使用默认图阴影。
浮动控制面板来自共享组件 DraggableWindow。它在本示例中直接承载样式选择器,并且其设置面板使用 RGHooks.useGraphStore() 配合 setOptions() 来调整滚轮与拖拽行为。同一个共享辅助组件还通过 prepareForImageGeneration()、domToImageByModernScreenshot() 和 restoreAfterImageGeneration() 提供截图导出能力。
关键交互
核心交互是手绘模式开关。切换它会更改根包裹层 class,在 RGLineShape.Curve8 与 RGLineShape.StandardStraight 之间重写所有连线,并重新适配视口。
当手绘模式启用后,会额外出现两个选择器,用于设置边框样式和背景纹理。任意一个发生变化,都会触发一次全图遍历,把 nodeBorderStyle 和 nodeBackgroundStyle 写入每个节点的数据中,从而让整个层级一起改变外观。
浮动窗口本身也是可交互的。用户可以通过标题栏拖动它、将其最小化、打开设置面板,并在浏览当前展示时始终让它悬浮在图上方。
设置面板增加的是查看器级别的控制,而不是内容编辑能力。它可以切换滚轮行为、切换画布拖拽行为,并将当前图 DOM 导出为图片。
关键代码片段
下面这个 options 配置块说明,该示例保留了 relation-graph 原生的中心布局,同时将默认节点主体尽量简化,从而让插槽内容来定义可见的卡片样式。
const graphOptions: RGOptions = {
debug: true,
defaultJunctionPoint: RGJunctionPoint.border,
defaultNodeColor: 'transparent',
defaultNodeShape: RGNodeShape.rect,
defaultNodeBorderWidth: 0,
defaultLineWidth: 2,
layout: {
layoutName: 'center',
levelGaps: [500, 400, 400],
alignItemsX: 'center',
alignItemsY: 'center'
}
};
这个初始化步骤说明,在图加载之前就已经注入了连线标签、偏移量以及自定义草图箭头 marker。
const myJsonData: RGJsonData = await getJsonData();
myJsonData.lines.forEach(line => {
line.text = 'Line Text';
line.fromJunctionPointOffsetX = Math.random() * 10;
line.fromJunctionPointOffsetY = Math.random() * 10;
line.toJunctionPointOffsetX = Math.random() * 10;
line.toJunctionPointOffsetY = Math.random() * 10;
line.junctionOffset = Math.random() * 20 - 10;
line.endMarkerId = 'my-hand-drawn-arrow-end';
});
await graphInstance.setJsonData(myJsonData);
这个运行时更新函数是该演示的核心:它把选定的节点样式和线条几何形态传播到已经渲染出来的图中。
graphInstance.getNodes().forEach(node => {
graphInstance.updateNodeData(node, {
nodeBorderStyle,
nodeBackgroundStyle
});
});
graphInstance.getLines().forEach(line => {
graphInstance.updateLine(line, {
lineShape: enableHandDrawn ? RGLineShape.Curve8 : RGLineShape.StandardStraight,
});
});
这个控制片段展示了手绘开关是入口点,并且只有在草图模式启用后,边框和纹理选择器才会出现。
<SimpleUISelect data={[
{ value: false, text: 'None' },
{ value: true, text: 'Hand Drawn Style' }
]} currentValue={enableHandDrawn} onChange={setEnableHandDrawn} />
{
enableHandDrawn && <>
<SimpleUISelect data={[
{ value: 'rough', text: 'Rough' },
{ value: 'pencil', text: 'Pencil' },
{ value: 'thick', text: 'Thick' }
]} currentValue={nodeBorderStyle} onChange={setNodeBorderStyle} />
</>
}
这个节点插槽片段展示了同一个 KPI 卡片模板如何在不改变图数据结构的前提下,被包裹进普通盒子或手绘盒子中。
<RGSlotOnNode>
{({ node }) => {
const nodeContent = <div>{/* icon, name, KPI rows */}</div>;
if (enableHandDrawn) {
return <HandDrawnBox variant={node.data.nodeBorderStyle} texture={node.data.nodeBackgroundStyle}>
{nodeContent}
</HandDrawnBox>
} else {
return <MyNodeContentBox>{nodeContent}</MyNodeContentBox>
}
}}
</RGSlotOnNode>
这个包裹组件说明,手绘卡片并不只是颜色主题变化。边框几何形态和描边粗细都会随变体而改变。
const getWobbleStyle = () => {
switch (variant) {
case "pencil":
return {
borderRadius: "255px 15px 225px 15px/15px 225px 15px 255px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: color,
};
case "thick":
return {
borderRadius: "4px 6px 4px 10px / 8px 4px 10px 5px",
borderWidth: "4px",
borderStyle: "solid",
borderColor: color,
};
这个 SCSS 片段展示了该示例如何把手绘模式作用到 relation-graph 内置的节点、连线和标签 DOM 层上。
.node-filter-hand-drawn {
.relation-graph {
.rg-node-peel {
.rg-node {
filter: url(#static-hand-drawn);
}
}
.rg-line-peel {
.rg-line {
filter: url(#static-hand-drawn);
}
.rg-line-label {
filter: url(#static-hand-drawn);
background: #fff;
border: 2px dashed #666;
}
}
}
}
这段共享辅助代码说明,截图导出是通过图实例的准备和恢复流程完成的,而不是直接截取任意页面 DOM。
const downloadImage = async () => {
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
这个示例的独特之处
对比数据表明,这个示例与 custom-line-animation、custom-line-style、node 和 deep-each 较为接近,但它的关注重点与这些相邻示例都不同。最突出的区别在于,它把一个固定的居中层级图变成了一个完整的图外观切换器。通过同一个控制窗口,可以统一切换节点包裹层、边框变体、纹理叠加、线条形状、连线抖动、标签处理方式以及箭头样式。
与 custom-line-animation 相比,这个示例的重点不在于维护一整套动画或线条预设,而在于构建一种覆盖节点、连线、标签和包裹层的统一草图展示模式。与 custom-line-style 相比,它不只是切换 CSS 连线类名,还会通过 updateNodeData() 进一步传播节点级别的边框和纹理状态。
与主要并排比较不同节点渲染技术的 node 示例相比,这个示例保持同一个卡片模板不变,而是在运行时对整张图进行重设主题。与 deep-each 相比,它的图实例更新重点是整场景的展示切换,而不是子树强调或聚焦行为。
稀有性数据也支持一个更具体的判断:HandDrawnBox、注入式 SVG filters、逐节点纹理选择器、自定义箭头 marker,以及 Curve8 与直线之间的切换,这一组合对于一个偏样式导向的演示来说相当丰富。对于那些需要插画式或品牌化展示层、但又不想替换 relation-graph 渲染器的团队来说,它是一个更强的起点。
这种模式还适用于哪里
这种模式适用于图数据保持稳定、但视觉语言需要切换的仪表盘和展示视图。典型场景包括投资人叙事页面、产品战略图、教育内容、工作坊看板、白标租户主题,以及面向营销场景的组织图或能力地图。
它也适用于团队只需要一个临时展示模式,而不是永久性的自定义渲染器的情况。产品可以保留正常的 relation-graph 数据模型和查看器行为,然后在导出、现场演示、干系人评审或特殊报告模式下叠加一层风格化主题。