AIGC标识 TKCode.Arc 1.0 发布:一套 Avalonia 12 组件库的设计令牌、弹簧动效与性能规则

黑洞删除动画

这是一个删除动画:选中的内容被吸进黑洞,周围的内容跟着弯曲,点撤销再转出来。它是一个 Avalonia 控件,写在 C# 里。

TKCode.Arc 1.0.0 今天正式发布。这是我做的一套 Avalonia 12 组件库,MIT 协议。这篇不讲情怀,主要讲它怎么搭起来的:设计令牌、弹簧动效、性能规则,以及用 Claude Code 写这么大一个项目的真实账单。

先说清楚:视觉语言和动效来自 Arc(uiarc.dev),我把它的 112 个免费组件移植成了 Avalonia 原生控件,这是我的个人项目,不是 Arc 官方出品。黑洞、全息卡这类特效组件是原创设计,交互和动效参考了 Arc Pro。

先跑起来

需要 .NET 10 SDK。这三条命令会生成一个带对话列表和流式回复的 AI 聊天桌面应用:

dotnet new install TKCode.Arc.Templates
dotnet new tkcode-arc-ai-chat -n Assistant
dotnet run --project Assistant

回复默认是预设的示例内容,换成你自己的模型客户端就能接真模型。把模板名换成 tkcode-arc-admin 是管理后台,换成 tkcode-arc-devtool 是 IDE 式开发工具,tkcode-arc-lib 用来起一个你自己的控件库。

已有项目也可以直接加包,Application 类和 App.axaml 都不用为它改动:

AppBuilder.Configure<App>()
          .UsePlatformDetect()
          .WithInterFont()
          .WithArcFonts()
          .UseArc(arc => arc
              .UseArcControls()
              .WithTheme(t => t.Appearance(ArcAppearance.System)
                               .Accent(ArcAccentPreset.Neutral)));

里面有什么

包 内容
TKCode.Arc 112 个 Arc 组件,以及布局面板和 Markdown、PropertyGrid、Cascader、TransferBox 等扩展控件
TKCode.Arc.Blocks 页面级组合:AI 对话、定价与结账、账单、仪表盘、团队管理
TKCode.Arc.AdvancedCharts DataGrid、SankeyFlow、Sunburst、RaceBarChart 等
TKCode.Arc.Special 特效组件:表面、加载、语音、指针特效
TKCode.Arc.Workflow / .Dock / .Terminal 工作流画布、IDE 式停靠布局、基于 libghostty-vt 的终端
TKCode.Arc.Pdf / .Audio PDF 查看器(桌面 PDFium,浏览器 pdf.js)、音频播放
TKCode.Arc.Foundation 令牌、主题、语言和包注册运行时,只依赖 Avalonia

一共 21 个 NuGet 包,同一个版本一起发布,面向 net8.0、net10.0 和 net11.0(终端包只有后两个),基于 Avalonia 12.1.4。300 多个文档页都有在线示例和自动生成的 API 表格。

设计令牌:控件不写死任何颜色

颜色、圆角、阴影和字体全部来自全局令牌,主题文件里这样取:

<Setter Property="Background" Value="{arc:ArcDesignTokenResource Surface}" />

单个控件的几何参数放在控件令牌里,和控件放在一起:

[ControlDesignToken]
internal sealed class ButtonToken : AbstractControlDesignToken
{
    public Thickness InlinePaddingMD { get; set; }
    public double HoverOpacity { get; set; }

    public override void CalculateTokenValues(bool isDarkMode)
    {
        base.CalculateTokenValues(isDarkMode);
        InlinePaddingMD = new Thickness(16, 0);
        HoverOpacity = 0.91;
    }
}

切主题只有一个入口,每个令牌按外观和强调色重新计算,只写入变化的值,控件原地更新:

ArcTheme.Apply(t => t.Appearance(ArcAppearance.Dark));

这里有个坑:ArcThemeManager.ApplyAsync 的发布步骤要回到 UI 线程,在 UI 线程上 .Wait() 或 .Result 会死锁。要么 await,要么调用 Apply。

规则靠源生成器兜底。比如每个公开控件必须有对应的令牌类,令牌属性类型有限制,同一个 ControlTheme 的 Key 只能由一个文件定义,违反了会直接报 ARCF 开头的诊断,不用等到运行时才发现。主题文件也由生成器加载,不需要手工合并 *Themes.axaml。

动效:写在主题里,不写在代码里

全息卡

大部分动效声明在 ControlTheme 里,按动效级别分开:

<ControlTheme x:Key="{x:Type arc:Foo}" TargetType="arc:Foo">
    <Setter Property="arc:ArcMotion.Tracks" Value="True" />
    <Style Selector="^:motion-full">
        <Setter Property="Transitions">
            <Transitions>
                <arc:ArcBrushTransition Property="Background" />
                <arc:SpringTransformOperationsTransition
                    Property="RenderTransform" Spring="Snappy" />
            </Transitions>
        </Setter>
    </Style>
    <Style Selector="^:motion-reduced">
        <Setter Property="Transitions">
            <Transitions>
                <arc:ArcBrushTransition Property="Background" Tween="Instant" />
            </Transitions>
        </Setter>
    </Style>
</ControlTheme>

ArcMotion.Tracks 让控件在 Loaded 之后才拿到这两个伪类,所以首帧不会播动画。动效级别有 Full、Reduced、Off 三档,是可继承的附加属性,Off 下两个伪类都不存在。

弹簧按呈现时长和回弹定义,不用手调物理常数,比如 Snappy 是 0.26 秒、回弹 0.12。声明式弹簧被打断时从当前值以零速度重新开始。需要保留速度的跟手动画才用代码:

_progress ??= new MotionValue<double>(this, 0, MotionVectors.Double);
_progress.Changed += _ => InvalidateVisual();
await _progress.AnimateTo(1, MotionSpec.Spring(ArcSpring.Snappy));

AnimateTo 返回 Task<bool>,到达目标为 true,被打断为 false。帧由统一的调度器通过 TopLevel.RequestAnimationFrame 驱动,空闲时不请求帧。

性能:几条踩出来的规则

桑基图

动画组件一多,性能问题就很具体。这些写进了项目的实现规则:

  • 逐帧绘制的文本缓存 TextLayout,不要缓存 FormattedText。 Avalonia 12 的 FormattedText.Draw 每次都重新分行和塑形,项目记录的开销是拉丁文约 3 KB / 0.3 ms,中文约 22 KB。TextLayout 保留排好的行,绘制时只输出字形。
  • 枚举状态用伪类,不用属性选择器。 ^[Variant=Primary] 每个实例都会建一个激活器、订阅属性并装箱比较。把枚举映射成 :primary 再用 ^:primary,按钮、列表项这类重复出现的控件优先改。
  • 少用的部件按需创建。 每个模板节点挂载约 5 到 10 KB,隐藏的节点照样付费。焦点环、加载转圈、悬停气泡都在第一次需要时才创建。
  • 重复项自绘,动画不触发布局。 图表的点和格子用一个控件的 Render 画,动画只改变换、透明度和自绘状态。
  • 循环动画要有边界。 只在可见、已挂载、在视口内且动效级别允许时运行。

效果举个例子:GanttTimeline 缩放 60 个任务,每帧分配从约 1.8 MB 降到约 100 KB,视图外的行、条和箭头也不再绘制。

AOT 和按需编译

库不使用反射,按兼容裁剪和 NativeAOT 的方式构建:

dotnet publish -c Release -p:PublishAot=true

如果用 .NET 11 SDK 做 AOT 发布要注意:它的优化发布会丢掉 Avalonia 12.1 编译好的 XAML,启动时报 No precompiled XAML found。在项目文件里加上 <UseOptimizedPublish>false</UseOptimizedPublish>。

Lucide 图标也不用全部打进包里。源生成器会扫描 C# 和 XAML,只编译用到的图标,运行时才决定的图标在项目文件里声明:

<ItemGroup>
    <LucideIcon Include="Bell;ArrowRight" />
</ItemGroup>

枚举选择器 EnumSelector 也是同样的思路:成员和显示文字由生成器产出,不靠反射。

也能干正经活

PDF 查看器

PdfViewer 有缩略图、目录、搜索、文本选择和链接。没有后端时文档会报错并给出提示,不会显示假页面。

赛跑条形图

DataGrid 支持区域选择、行内编辑、填充柄、剪贴板、分组和导出。工作流画布支持平移、缩放、自动排列和撤销,停靠布局可以存成 JSON。

这套东西花了多少钱

从第一次提交到 1.0 用了 19 天,282 次提交,其中 263 次带着 Claude 的共同署名。Claude 订阅加 API 费用近 2 万元,都是我自己付的,不含我自己的时间。

我扫了一遍本机的 Claude Code 会话记录,这个项目大约用了:

  • 请求:约 9 万次
  • Token 总量:约 243 亿
  • 其中缓存读取:约 237 亿,占 97.6%
  • 模型实际输出:约 9300 万

项目的约束尽量写成了能被机器检查的东西:AGENTS.md 里的硬规则、生成器的诊断、缺文档页就失败的测试,以及令牌和预设的基线快照。仓库里现在有 1 万多个测试方法。

能不能用,还是请你自己判断。代码和提交记录都是公开的,每个版本修了哪些 bug 都列在 CHANGELOG 里。

还没做好的

  • 移动端组件只在桌面的手机外框里预览过,还没上过 Android 和 iOS 真机。
  • 视频组件还在仓库里,没有发布到 NuGet。
  • PDF 查看器不支持触屏拖选文字;没有内嵌中日韩字体的文档,显示效果取决于机器上的字体。

从 1.0 起遵循语义化版本,破坏性 API 变更只在新的主版本出现。和上一个预览版对比了 11 个包的 API,没有发现破坏性变更。不过很多不常显示的部件已经移出模板,如果你的自定义 ControlTheme 或代码查找这些 PART_ 名称,要处理它们不存在的情况。

地址

有问题欢迎提 Issue。如果你正好在用 Avalonia,我最想听的是哪个控件用起来不顺手。

posted on 2026-11-09 11:31  239573049  阅读(40)  评论(1)    收藏  举报