
tldraw 自定义笔画宽度用 StyleProp 与 DrawShapeUtil.configure 将内置 4 档画笔大小替换为 12 档数值选择器【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldrawtldraw SDK 默认的 draw画笔/涂鸦工具只有s、m、l、xl四档预设粗细在很多标注、演示、手写板场景中不够用。本文以 tldraw 官方示例库中的 stroke-size-picker 示例位于 apps/examples/src/examples/ui/stroke-size-picker为蓝本完整讲解如何通过StyleProp.define定义数值型样式、借助DrawShapeUtil.configure让渲染/命中测试/图片导出统一使用自定义笔画宽度并组装一个只对 draw 工具生效的十二档自定义样式面板。读完你可以在自己的 tldraw 应用中实现任意粒度的数值型画笔宽度并学会一套自定义 style 样式 定制样式面板的可复用套路。一、示例目标与背景为什么默认的画笔粗细不够用在阅读实现之前先理解 tldraw 内置大小样式的约束tldraw 内置的DefaultSizeStyle是一个枚举型样式enum style可用值只有[s, m, l, xl]四个定义位于 packages/tlschema/src/styles/TLSizeStyle.tsexport const DefaultSizeStyle StyleProp.defineEnum(tldraw:size, { defaultValue: m, values: [s, m, l, xl], })draw 形状的默认笔画宽度正是通过这张枚举表换算出来的。内置常量表定义在 packages/tldraw/src/lib/shapes/shared/default-shape-constants.tsexport const STROKE_SIZES: RecordTLDefaultSizeStyle, number { s: 1, m: 1.75, l: 2.5, xl: 5, }该换算在 DrawShapeUtil.tsx 的options.getDefaultDisplayValues中完成strokeWidth: theme.strokeWidth * STROKE_SIZES[size]。本示例的目标正是打破这种四档枚举 → 换算系数的间接模型直接为 draw 形状赋予一个数值型笔画宽度 style默认 4可选范围 132 共 12 个预设并让样式面板在draw 工具处于激活状态或选中了 draw 形状时显示这 12 个预设点其它形状geo、箭头、文字等继续使用内置的四档尺寸选择器。示例由三个文件组成文件作用StrokeSizePickerExample.tsx全部实现逻辑样式定义、自定义 shape util、选择器组件、面板组装stroke-size-picker.css十二档按钮的网格布局、悬停与激活态样式README.md示例说明与文档元数据二、步骤 1用 StyleProp.define 定义数值型笔画宽度样式示例的第一步是定义一个名为example:strokeSize的数值型样式const strokeSizeStyle StyleProp.define(example:strokeSize, { defaultValue: 4, type: T.number, })这里的StyleProp样式属性是 tldraw 中一类特殊的形状属性。查看 packages/tlschema/src/styles/StyleProp.ts 中StyleProp类的文档注释它的特殊之处有两点同一个值可以同时批量设置到多个形状上例如全选多个形状后改颜色最近一次使用的值会被编辑器自动记忆并应用到之后新画的形状上。这正是样式与普通 props 的本质区别。从源码看StyleProp.define的签名是StyleProp.ts 第 47-53 行static defineType(uniqueId: string, options: { defaultValue: Type; type?: T.ValidatableType }) { const { defaultValue, type T.any } options return new StylePropType(uniqueId, defaultValue, type) }要点uniqueId必须全局唯一官方建议用应用名/库名 语义作为前缀本示例使用example:前缀你的应用应替换为自己的命名空间例如myapp:strokeSize避免与其它插件或形状 util 冲突defaultValue是新建 draw 形状时的默认笔画宽度此处为4type是可选的数据校验器用于 store 的 validator 校验持久化数据。示例使用T.number从tldraw包导出的T其真实来源是tldraw/validate。如果你需要固定取值集合的样式也可以改用StyleProp.defineEnumStyleProp.ts 第 75-81 行。为什么选择 style 而不是普通 prop代码注释示例源码注释 [1]解释得很清楚正因为它是样式编辑器才会记住最近一次值用于下一个新画的形状在样式面板相关时显示它当多选形状取值不一致时报告mixed混合状态。如果你把它做成普通 prop这些机制将全部丢失。三、步骤 2通过声明合并declaration merging补全类型为了让shape.props.strokeSize在 TypeScript 下获得完整类型提示示例使用了模块声明合并declare module tldraw/tlschema { interface TLDrawShapeProps { strokeSize: number } }tldraw 的 schema 包tldraw/tlschema中TLDrawShapeProps接口声明了 draw 形状全部 props 的类型。因为稍后我们要在自定义 util 中为 props 追加一个额外字段在类型层面也同步加上strokeSize: number从而让全文件范围内对shape.props.strokeSize的读写都被类型系统覆盖。这与源码注释 [2]描述的行为一致。四、步骤 3用 DrawShapeUtil.configure 定制 draw 工具并接入渲染这是整个方案最核心的一步。tldraw 为DrawShapeUtil预留了configure类方法示例通过它覆盖 draw 形状展示值display values的解析逻辑class CustomDrawShapeUtil extends DrawShapeUtil.configure({ getCustomDisplayValues(_editor, shape) { return { strokeWidth: shape.props.strokeSize } }, }) { static override props { ...drawShapeProps, strokeSize: strokeSizeStyle } override getDefaultProps() { return { ...super.getDefaultProps(), strokeSize: strokeSizeStyle.defaultValue } } } const shapeUtils [CustomDrawShapeUtil]这里涉及 tldraw 两套展示值display values机制需要理解清楚其分工getDefaultDisplayValues内置DrawShapeUtil的options中定义的默认解析函数DrawShapeUtil.tsx 第 67-81 行它读取shape.props.color/fill/size产出strokeColor、strokeWidth、fillColor、patternFillFallbackColor等展示值getCustomDisplayValues是ShapeOptionsWithDisplayValues接口要求提供的覆盖钩子getDisplayValues.ts 第 14-19 行默认实现返回空对象。两套值最终由getDisplayValues合并getDisplayValues.ts 第 43-46 行const values { ...util.options.getDefaultDisplayValues(util.editor, shape, theme, resolvedColorMode), ...util.options.getCustomDisplayValues(util.editor, shape, theme, resolvedColorMode), }也就是说getCustomDisplayValues返回的{ strokeWidth: shape.props.strokeSize }会覆盖默认的theme.strokeWidth * STROKE_SIZES[size]最终 draw 形状的笔画宽度直接等于我们赋的数值。由于 draw 形状的渲染、几何命中测试getGeometry依赖getDisplayValues(this, shape).strokeWidth见 DrawShapeUtil.tsx 第 120 行以及图片导出都统一经过getDisplayValues这条通道该函数还带 WeakMap 缓存见 getDisplayValues.ts 第 22-48 行所以一处覆盖四处生效——渲染、命中测试、导出都一致地使用自定义数值宽度。子类的两处关键覆写类体内还有两处静态配置static override props { ...drawShapeProps, strokeSize: strokeSizeStyle }先展开内置 draw 形状的全部 props 定义再追加我们的样式。这样编辑器样式系统styles system能感知strokeSizeStyle并参与记忆/应用store 的 validator 会把strokeSize当作可校验字段通过StyleProp自带的validate。override getDefaultProps()在调用super.getDefaultProps()得到内置默认 props 后再补上strokeSize: strokeSizeStyle.defaultValue即4确保新建的 draw 形状带合法默认值。把自定义 util 装进编辑器const shapeUtils [CustomDrawShapeUtil]DrawShapeUtil的静态type是drawTldraw的shapeUtilsprop 中同名类型draw的自定义 util 会替换内置的 draw shape util因此默认的 draw 工具会自动拾取我们定制后的版本无需额外改工具定义——正如源码注释 [3] 所说明的。五、步骤 4编写十二档预设的数值选择器组件在 CSSstroke-size-picker.css和 JSX 之外选择器本体是一个完全自绘的按钮组const STROKE_SIZE_PRESETS [1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 26, 32] function StrokeSizePicker() { const { styles, onValueChange, onHistoryMark } useStylePanelContext() const strokeSize styles.get(strokeSizeStyle) // 当前上下文不包含该样式时回退到内置四档选择器 if (strokeSize undefined) return StylePanelSizePicker / const value strokeSize.type mixed ? null : strokeSize.value return ( div classNamestroke-size-picker {STROKE_SIZE_PRESETS.map((size) ( button key{size} classNamestroke-size-picker__preset >export interface StylePanelContext { styles: ReadonlySharedStyleMap enhancedA11yMode: boolean onHistoryMark(id: string): void onValueChangeT(style: StylePropT, value: T): void onOpacityChange(opacity: number): void }本示例用到的三项含义styles只读样式映射表。styles.get(strokeSizeStyle)返回当前上下文相关的样式条目可能形如{ type: shared, value: 4 }选中形状取值一致或{ type: mixed }多选形状取值不一致在上下文不包含该样式时为undefinedonValueChange(style, value)官方推荐的改样式入口。看 StylePanelContext.tsx 第 36-57 行 的实现它在一个editor.run事务中依次执行setStyleForSelectedShapes(style, value)选中形状立即生效与setStyleForNextShapes(style, value)记住为下一个形状的默认值与内置 picker 行为完全一致它还检测用户是否按住加速键Ctrl/Cmd来区分是否要把样式带给下一个形状以及触发set-style分析埋点onHistoryMark(set stroke size)对应editor.markHistoryStoppingPoint(id)在修改前标记历史记录点让一次点击成为可被撤销独立回退的单个操作。处理 mixed 与点大小随数值增长value在 mixed 状态下为null此时没有任何预设按钮点亮data-active均为 false每个预设点用一个圆形div表示直径由4 size / 2计算例如 size1 → 4.5pxsize32 → 20px让用户通过圆点视觉大小直观感知粗细网格采用grid-template-columns: repeat(6, 1fr)12 个按钮排成两行激活态data-activetrue与悬停态由 stroke-size-picker.css 中的--tl-color-hint/--tl-color-muted-2等 tldraw 主题变量着色保证与其它面板控件观感统一。六、步骤 5让选择器只在 draw 相关时出现这是本示例很巧妙的一处条件渲染const strokeSize styles.get(strokeSizeStyle) if (strokeSize undefined) return StylePanelSizePicker /为什么styles里没有strokeSizeStyle时就要回退到内置选择器这由 tldraw 样式面板的构建方式决定面板的styles映射只包含与当前激活工具/选中形状相关的样式。也就是说当draw 工具被激活如本示例onMount中editor.setCurrentTool(draw)或选中范围内包含 draw 形状时由于 draw 形状 props 里带了strokeSizeStyle样式映射才会包含它此时渲染我们自绘的十二档选择器而当选中 geo 形状、箭头、文字、线等仍使用内置size样式四档枚举的形状时映射中不含strokeSizeStyle代码自动渲染内置的StylePanelSizePicker四档s/m/l/xl。因此自定义样式面板对外呈现的是智能切换画 draw 用 12 档数值画其它形状退回官方四档。用户无需在两个界面间手动跳转。七、步骤 6像搭积木一样组装样式面板最后一步是用DefaultStylePanelStylePanelSection把内置控件按需重组替换掉尺寸选择器这一个槽位function CustomStylePanel(props: TLUiStylePanelProps) { return ( DefaultStylePanel {...props} StylePanelSection StylePanelColorPicker / StylePanelOpacityPicker / /StylePanelSection StylePanelSection StylePanelFillPicker / StylePanelDashPicker / StrokeSizePicker / {/* 原来的 StylePanelSizePicker 被替换 */} /StylePanelSection StylePanelSection StylePanelFontPicker / StylePanelTextAlignPicker / StylePanelLabelAlignPicker / /StylePanelSection StylePanelSection StylePanelGeoShapePicker / StylePanelArrowKindPicker / StylePanelArrowheadPicker / StylePanelSplinePicker / /StylePanelSection /DefaultStylePanel ) } const components: TLComponents { StylePanel: CustomStylePanel, }与默认面板逐行对照将上面的 JSX 与官方默认面板内容 DefaultStylePanelContent.tsx 逐行对照可以发现唯一差异是第 2 个 Section 中把StylePanelSizePicker /换成了StrokeSizePicker /其余控件颜色、透明度、填充、虚线、字体、对齐、图形类型、箭头、样条线原封不动。这体现了 tldraw UI 的组合式设计面板不是黑盒而是由可独立复用的StylePanel*控件 StylePanelSection分组构成官方甚至把每个控件的源码都公开在 DefaultStylePanelContent.tsx 中方便开发者参考每个控件的写法。两个易错点必须转发props给DefaultStylePanelDefaultStylePanel {...props}编辑器在移动端把样式面板渲染到工具栏的 popover 中时会传入isMobile等 props不透传会导致移动端布局异常示例源码注释 [6] 特别提醒了这一点注册方式CustomStylePanel通过TLComponents的StylePanel槽位传入Tldraw components{components} ... /替换的是整个样式面板组件而不是面板里的某个控件。完整挂载export default function StrokeSizePickerExample() { return ( div classNametldraw__editor Tldraw shapeUtils{shapeUtils} components{components} onMount{(editor) { editor.setCurrentTool(draw) // 进入 draw 工具展示自定义选择器 }} / /div ) }配合导入tldraw/tldraw.css与示例自身的样式文件shapeUtils提供替换后的 draw utilcomponents提供定制面板。八、把方案抽象成通用套路本示例虽然是针对 draw 工具的定制但其方法论可直接复用到任意自定义形状上用StyleProp.define/StyleProp.defineEnum定义你自己的样式注意用带命名空间的唯一 ID用 declaration merging 扩充形状 props 的类型保证类型安全继承并configure目标 ShapeUtil在getCustomDisplayValues中把你样式的值映射为展示值ShapeUtil的通用机制渲染、几何、导出统一经getDisplayValues合并保证各环节一致用useStylePanelContext写一个自绘 picker处理undefined回退内置控件与mixed不点亮两种状态用DefaultStylePanel重新组装面板只替换需要替换的槽位并透传props。如果想了解自定义样式配合完全自定义形状而非改造内置形状的写法可以继续阅读 tldraw 官方示例集中关于自定义形状与自定义样式的示例源码同位于 apps/examples/src/examples 下两者配合可实现对形状外观体系的完全掌控。九、如何本地运行该示例该示例位于独立的 examples 应用包名examples.tldraw.com中其启动脚本定义在 apps/examples/package.jsonyarn dev # 在 apps/examples 目录下运行等价于 vite --host启动后打开 Vite 提供的本地地址在示例列表中找到Stroke size picker即可交互验证页面加载后会自动切到 draw 工具样式面板中显示 12 个大小递增的圆点画出几条不同数值的笔迹后切换到选择工具点选其它形状尺寸选择器会自动恢复为内置四档。你也可以在 DrawShapeUtil.tsx 与 StylePanelContext.tsx 中打断点观察getDisplayValues的合并结果与onValueChange内部如何对选中/下一个形状分别设置样式从而把本文中的每一步实现与 SDK 源码一一对应起来。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考