目录

1. 前言
这样的【闪白切换】特效看起来真的非常的酷,那么它是怎样实现的呢?我们最直观的感觉,它肯定有两个图片在进行透明的切换;也可以看到有缩放的跳跃呼吸感;然后中间有一个白色闪屏。那么这三者结合,能否达到相同的效果呢?其实,这是一个非常让人容易容易轻敌的特效。我们来观看它的慢放动画:

我们可以从慢放动画中发现,其实它的处理效果是非常复杂的,可以包含3种:
1,透明渐变动画:负责基础的过渡变换,使过渡平滑柔和;
2,径向缩放模糊动画:产生幻影错层感,焦点聚集,很有电影画面感;轻微的缩放有呼吸跳跃感,更立体;
3,亮度动画:像是相机拍照时的闪光灯,非常有场景感。
那么我们来分别实现它们试试。
2. WPF
2.1. 透明过渡动画
在WPF中,透明渐变动画实现的XAML:
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="img1"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.5" Value="0.3"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.9" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="img2"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.5" Value="0.8"/>
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="1"/>
</DoubleAnimationUsingKeyFrames>
效果:

2.2. 径向缩放模糊动画
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="zoomBlurEffect"
Storyboard.TargetProperty="BlurAmount">
<LinearDoubleKeyFrame KeyTime="0:0:1.1" Value="0"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.5" Value="0.08"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.65" Value="0"/>
</DoubleAnimationUsingKeyFrames>
模糊效果依赖于着色器:
public class ZoomBlurEffect : ShaderEffect
{
public static readonly DependencyProperty InputProperty =
RegisterPixelShaderSamplerProperty(nameof(Input), typeof(ZoomBlurEffect), 0);
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
nameof(Center),
typeof(Point),
typeof(ZoomBlurEffect),
new UIPropertyMetadata(new Point(0.5, 0.5), PixelShaderConstantCallback(0)));
public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register(
nameof(BlurAmount),
typeof(double),
typeof(ZoomBlurEffect),
new UIPropertyMetadata(0.1d, PixelShaderConstantCallback(1)));
public ZoomBlurEffect()
{
PixelShader = new PixelShader
{
UriSource = new Uri("pack://application:,,,/Shaders/ZoomBlurEffect.ps")
};
UpdateShaderValue(InputProperty);
UpdateShaderValue(CenterProperty);
UpdateShaderValue(BlurAmountProperty);
}
public Brush Input
{
get => (Brush)GetValue(InputProperty);
set => SetValue(InputProperty, value);
}
/// <summary>模糊中心(相对坐标,0.5,0.5 为元素中心)。</summary>
public Point Center
{
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
/// <summary>模糊强度。0 为无效果。</summary>
public double BlurAmount
{
get => (double)GetValue(BlurAmountProperty);
set => SetValue(BlurAmountProperty, value);
}
}
效果:

2.3. 亮度动画
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="contrastAdjustEffect"
Storyboard.TargetProperty="Brightness">
<LinearDoubleKeyFrame KeyTime="0:0:1.4" Value="0"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.5" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:1.6" Value="0"/>
</DoubleAnimationUsingKeyFrames>
亮度动画也依赖着色器:
public class ContrastAdjustEffect : ShaderEffect
{
public static readonly DependencyProperty InputProperty =
RegisterPixelShaderSamplerProperty(nameof(Input), typeof(ContrastAdjustEffect), 0);
public static readonly DependencyProperty BrightnessProperty = DependencyProperty.Register(
nameof(Brightness),
typeof(double),
typeof(ContrastAdjustEffect),
new UIPropertyMetadata(0d, PixelShaderConstantCallback(0)));
public static readonly DependencyProperty ContrastProperty = DependencyProperty.Register(
nameof(Contrast),
typeof(double),
typeof(ContrastAdjustEffect),
new UIPropertyMetadata(1d, PixelShaderConstantCallback(1)));
public ContrastAdjustEffect()
{
PixelShader = new PixelShader
{
UriSource = new Uri("pack://application:,,,/Shaders/ContrastAdjustEffect.ps")
};
UpdateShaderValue(InputProperty);
UpdateShaderValue(BrightnessProperty);
UpdateShaderValue(ContrastProperty);
}
public Brush Input
{
get => (Brush)GetValue(InputProperty);
set => SetValue(InputProperty, value);
}
/// <summary>亮度偏移。0 为原图,增大变亮。</summary>
public double Brightness
{
get => (double)GetValue(BrightnessProperty);
set => SetValue(BrightnessProperty, value);
}
/// <summary>对比度倍率。1 为原图。</summary>
public double Contrast
{
get => (double)GetValue(ContrastProperty);
set => SetValue(ContrastProperty, value);
}
}
效果:

以上三个动画综合在一起效果:

3. Winform
Winform的实现方式,我们既可以使用CPU算法,也可以使用GPU算法,这里我们使用更高性能的GPU方式处理:GPU · OpenTK GLControl + GLSL 330。
GPU 入口:OpenTK.GLControl(Core Profile 3.3)· 每帧 MakeCurrent → Uniform → DrawArrays → SwapBuffers
渲染管线片元着色器(所有动画共用)
原理:与 Avalonia/HTML 同构,语法为 #version 330 core + texture()。 WinForms.Timer 采样关键帧后 _gl.Invalidate() 触发 Paint。
private const string Fs = """
#version 330 core
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uTex1;
uniform sampler2D uTex2;
uniform float uOpacity1;
uniform float uOpacity2;
uniform float uBlur;
uniform float uBrightness;
void main() {
vec2 center = vec2(0.5, 0.5);
vec2 centered = vUv - center;
vec4 c = vec4(0.0);
for (int i = 0; i < 15; i++) {
float scale = 1.0 - uBlur * (float(i) / 14.0);
vec2 sampleUv = centered * scale + center;
vec4 s1 = texture(uTex1, sampleUv);
vec4 s2 = texture(uTex2, sampleUv);
c += s1 * uOpacity1 + s2 * uOpacity2;
}
c /= 15.0;
c.rgb += uBrightness;
fragColor = vec4(clamp(c.rgb, 0.0, 1.0), 1.0);
}
""";
透明过渡/缩放模糊/亮度/合并:
原理:按钮只切换 AnimDef;同一套 Timer + Paint 路径。 差异仅在关键帧表(见上方共用表)。
每帧绘制OnGlPaint
原理:把当前四个参数写入 uniform,绑定双纹理,画全屏 TriangleStrip。 像素运算全部在 GPU 着色器内完成。
private void OnGlPaint(object? sender, PaintEventArgs e)
{
_gl.MakeCurrent();
GL.Viewport(0, 0, _gl.Width, _gl.Height);
GL.Clear(ClearBufferMask.ColorBufferBit);
GL.UseProgram(_program);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, _tex1);
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(TextureTarget.Texture2D, _tex2);
GL.Uniform1(_locO1, _o1);
GL.Uniform1(_locO2, _o2);
GL.Uniform1(_locBlur, _blur);
GL.Uniform1(_locBright, _bright);
GL.BindVertexArray(_vao);
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
_gl.SwapBuffers();
}
3.1. 透明过渡动画

3.2. 径向缩放模糊动画

3.3. 亮度动画

合成效果:

4. Avalonia
Avalonia的实现方式,我们既可以使用CPU算法,也可以使用GPU算法,这里我们使用更高性能的GPU方式处理:GPU · OpenGlControlBase + GLSL ES
GPU 入口:Avalonia.OpenGL.Controls.OpenGlControlBase · 片元着色器在 GPU 执行 · 时间轴用 DispatcherTimer(约 16ms)驱动
渲染管线片元着色器(所有动画共用)
原理:全屏四边形 + ES2 风格 GLSL。四种动画只改 uniform,不换着色器。 RequestNextFrameRendering() 触发 OnOpenGlRender 真正画到 GPU 表面。
private const string FragmentShaderEs2 = """
precision mediump float;
varying vec2 vUv;
uniform sampler2D uTex1;
uniform sampler2D uTex2;
uniform float uOpacity1;
uniform float uOpacity2;
uniform float uBlur;
uniform float uBrightness;
void main() {
vec2 center = vec2(0.5, 0.5);
vec2 centered = vUv - center;
vec4 c = vec4(0.0);
for (int i = 0; i < 15; i++) {
float scale = 1.0 - uBlur * (float(i) / 14.0);
vec2 sampleUv = centered * scale + center;
vec4 s1 = texture2D(uTex1, sampleUv);
vec4 s2 = texture2D(uTex2, sampleUv);
c += s1 * uOpacity1 + s2 * uOpacity2;
}
c /= 15.0;
c.rgb += uBrightness;
gl_FragColor = vec4(clamp(c.rgb, 0.0, 1.0), 1.0);
}
""";
透明过渡Fade
原理:1 秒内 O1 从 1→0、O2 从 0→1;Blur/Bright 恒为 0。 着色器里退化为 mix(tex1, tex2) 式交叉溶解(仍走 15 次循环,但 scale 恒为 1)。
private void OnFade(...) => Preview.Play(AnimTimeline.Fade);
public void Play(AnimTimeline.AnimDef def)
{
Opacity1 = 1; Opacity2 = 0; BlurAmount = 0; Brightness = 0;
var sw = Stopwatch.StartNew();
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
_timer.Tick += (_, _) =>
{
var t = Math.Min((float)sw.Elapsed.TotalSeconds, def.Duration);
Opacity1 = AnimTimeline.Sample(def.O1, t);
Opacity2 = AnimTimeline.Sample(def.O2, t);
BlurAmount = AnimTimeline.Sample(def.Blur, t);
Brightness = AnimTimeline.Sample(def.Bright, t);
RequestNextFrameRendering(); // → OnOpenGlRender 写 uniform 并 DrawArrays
if (t >= def.Duration) _timer.Stop();
};
_timer.Start();
}
缩放模糊
原理:0.6s 内 BlurAmount 脉冲 0→0.06→0;O1 保持 1。 GPU 按径向 scale 多次采样,形成向放大方向发散的拖影。
private void OnZoom(...) => Preview.Play(AnimTimeline.Zoom);
// Blur 关键帧:0 → 0.06(at 0.3s) → 0(at 0.6s)
// 着色器:scale = 1.0 - uBlur * (i / 14.0)
亮度BrightAnim
原理:0.4s 内 Brightness 0→1→0;着色器末尾 c.rgb += uBrightness,形成闪白脉冲。
private void OnBright(...) => Preview.Play(AnimTimeline.BrightAnim);
// Bright 关键帧:0 → 1(at 0.2s) → 0(at 0.4s)
4.1. 透明过渡动画

4.2. 径向缩放模糊动画

4.3. 亮度动画

合成效果:

5. WinUI3
WinUI3的实现方式,我们既可以使用CPU算法,也可以使用GPU算法,这里我们使用更高性能的GPU方式处理:GPU · Win2D PixelShaderEffect + HLSL
GPU 入口:Microsoft.Graphics.Canvas · CanvasAnimatedControl 每帧 Draw · 自定义像素着色器编译为 Shaders/FlashTransition.bin
渲染管线HLSL 像素着色器(所有动画共用)
原理:Win2D PixelShaderEffect 把两张图作为 Source1/Source2, 常量缓冲写入 Opacity/Blur/Brightness。使用 D2D 助手宏做复杂采样(非 1:1)。
#define D2D_INPUT_COUNT 2
#define D2D_INPUT0_COMPLEX
#define D2D_INPUT1_COMPLEX
#include "d2d1effecthelpers.hlsli"
cbuffer constants : register(b0)
{
float Opacity1;
float Opacity2;
float BlurAmount;
float Brightness;
};
D2D_PS_ENTRY(main)
{
float2 uv = D2DGetInputCoordinate(0).xy;
float2 center = float2(0.5, 0.5);
float2 centered = uv - center;
float4 c = 0;
[unroll]
for (int i = 0; i < 15; i++)
{
float scale = 1.0 - BlurAmount * (i / 14.0);
float2 sampleUv = centered * scale + center;
c += D2DSampleInput(0, sampleUv) * Opacity1;
c += D2DSampleInput(1, sampleUv) * Opacity2;
}
c /= 15.0;
c.rgb += Brightness;
return float4(saturate(c.rgb), 1.0);
}
装载效果PixelShaderEffect
原理:图片先 Cover 到 400×600 的 CanvasRenderTarget(仅加载时一次), 再挂到 PixelShaderEffect;运行期只改 Properties,不再碰 CPU 逐像素。
var bytecode = await File.ReadAllBytesAsync(shaderPath);
_effect = new PixelShaderEffect(bytecode)
{
Source1 = _img1,
Source2 = _img2,
Source1BorderMode = EffectBorderMode.Hard,
Source2BorderMode = EffectBorderMode.Hard,
Source1Mapping = SamplerCoordinateMapping.Unknown,
Source2Mapping = SamplerCoordinateMapping.Unknown,
};
private void ApplyUniforms()
{
_effect.Properties["Opacity1"] = _o1;
_effect.Properties["Opacity2"] = _o2;
_effect.Properties["BlurAmount"] = _blur;
_effect.Properties["Brightness"] = _bright;
}
透明过渡/缩放模糊/亮度/合并
原理:CanvasAnimatedControl 自带帧循环; 在 OnDraw 里按经过时间采样关键帧、写 Properties、再 DrawImage(_effect)。 各按钮只是 Play(不同 AnimDef)。
private void OnFade(...) => Play(AnimTimeline.Fade);
private void OnZoom(...) => Play(AnimTimeline.Zoom);
private void OnBright(...) => Play(AnimTimeline.BrightAnim);
private void OnSum(...) => Play(AnimTimeline.Sum);
private void Play(AnimTimeline.AnimDef def)
{
_playing = def;
_sw = Stopwatch.StartNew();
_o1 = 1; _o2 = 0; _blur = 0; _bright = 0;
ApplyUniforms();
}
private void OnDraw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
if (_playing is not null && _sw is not null)
{
var t = Math.Min((float)_sw.Elapsed.TotalSeconds, _playing.Duration);
_o1 = AnimTimeline.Sample(_playing.O1, t);
_o2 = AnimTimeline.Sample(_playing.O2, t);
_blur = AnimTimeline.Sample(_playing.Blur, t);
_bright = AnimTimeline.Sample(_playing.Bright, t);
if (t >= _playing.Duration) _playing = null;
ApplyUniforms();
}
args.DrawingSession.DrawImage(_effect, dest, sourceRect);
}
5.1. 透明过渡动画

5.2. 径向缩放模糊动画

5.3. 亮度动画

合成效果:

6. Web
实现方式:单一片段着色器同时完成透明度混合 + 径向模糊 + 亮度;用 JS 关键帧驱动 uniform。
6.1. 透明过渡动画
时间轴定义 + 着色器里 s1*uOpacity1 + s2*uOpacity2。
fade: {
duration: 1,
o1: [{ t: 0, v: 1 }, { t: 1, v: 0 }],
o2: [{ t: 0, v: 0 }, { t: 1, v: 1 }],
blur: [{ t: 0, v: 0 }],
bright: [{ t: 0, v: 0 }],
}
着色器混合部分
vec4 s1 = texture2D(uTex1, sampleUv);
vec4 s2 = texture2D(uTex2, sampleUv);
c += s1 * uOpacity1 + s2 * uOpacity2;

6.2. 径向缩放模糊动画
片段着色器循环
zoom: {
duration: 0.6,
o1: [{ t: 0, v: 1 }],
o2: [{ t: 0, v: 0 }],
blur: [{ t: 0, v: 0 }, { t: 0.3, v: 0.06 }, { t: 0.6, v: 0 }],
bright: [{ t: 0, v: 0 }],
}
// WebGL fragment:
for (int i = 0; i < 15; i++) {
float scale = 1.0 - uBlur * (float(i) / 14.0);
vec2 sampleUv = centered * scale + uCenter;
// sample & accumulate...
}
c /= 15.0;
6.3. 亮度动画
着色器偏置
bright: {
duration: 0.4,
o1: [{ t: 0, v: 1 }],
o2: [{ t: 0, v: 0 }],
blur: [{ t: 0, v: 0 }],
bright: [{ t: 0, v: 0 }, { t: 0.2, v: 1 }, { t: 0.4, v: 0 }],
}
// WebGL fragment:
c.rgb += uBrightness;
驱动(三动画共用)
function play(def) {
const start = performance.now();
const tick = (now) => {
const t = Math.min((now - start) / 1000, def.duration);
state.o1 = lerpKeyframes(def.o1, t);
state.o2 = lerpKeyframes(def.o2, t);
state.blur = lerpKeyframes(def.blur, t);
state.bright = lerpKeyframes(def.bright, t);
draw(); // 写入 uniform 并绘制
if (t < def.duration) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}

合成效果:

7. 五种比对
7.1. 屏幕录制

7.2. 手机录制

8. 总结
8.1. 难点
实现这样的ui动画,最难的是哪一点呢?经过对动画拆分之后,单独的某一个动画实现起来是比较简单的,复杂的是把几种动画综合在一起,并且让不同的动画出现在正确的、符合人眼视觉的时机,这一点是最难的。在这个过程中,即使100毫秒的差异,所带给人的【感觉】都可能是不同的。我们上面虽然使用5种技术框架来模拟参考的效果,但仍然差强人意,有一些细节没有完全控制到位,这是最难调整的,也是最考验动画感的。
比如在调整动画的过程中, 以WPF的UI代码为例, 红色框选位置的这些时间点,就需要反复调整,花费非常多时间,才初步得到了还算ok的效果。(花费这样的时间,对于一个追求效率的快节奏公司来说通常是不可接受的)

所以调整ui动画,其实非常考验耐心,和考验开发者的【感觉】,但是这种细微差异带来的不同的感觉是 AI 无法达成的。每次在做一个新的UI动画之前,我会先描述给AI去处理,但基本上没有一次它能完成我预期的效果。
上面的制作过程,我一般是先把动画拆分之后,用WPF来完成所有的细节调整,然后再AI迁移到其他的 Ui框架里面去, AI对于现有逻辑的【迁移】和【模仿】是非常擅长的。因此就完成了以上的5种方式的处理。
但是我们也能看到,每一种框架的处理,尤其是在闪白的控制上面,何时淡入、何时峰值模糊、何时冲白,表现出来的细微的延迟是不太一样的,每种框架在 交叉溶解、径向 ZoomBlur、亮度偏置 多方面的性能差异导致的细微时间延迟到不同,都会让人产生节点错开的感觉, 正是这类工作真正耗时的地方。
8.2. 原理与特性
五种技术都在做同一件事——用 GPU 着色器(或等价像素效果)完成「双图叠化 + ZoomBlur + 亮度闪白」, 差别不在「能否计算出来」,而在动画时间模型、着色器方言、以及框架原生特效入口不同。
| 框架 | GPU 入口 | 着色器 / 效果形态 | 动画时间模型 | 原理 | 本质 |
|---|---|---|---|---|---|
| WPF | UIElement.EffectShaderEffect | HLSL(ps_2/3 风格)ZoomBlur.fx + ContrastAdjust.fx | XAML Storyboard关键帧驱动依赖属性 | 控件树挂效果链;Storyboard 改 Opacity / BlurAmount / Brightness,GPU 实时着色 | 桌面 XAML 原生像素特效管线 |
| HTML | Canvas + WebGL | GLSL ES(片元着色器) 单 pass 合并三效果 | requestAnimationFrameJS 插值关键帧 | 双纹理绑定 + uniform;浏览器合成层上的可编程管线 | Web 标准图形管线 |
| Avalonia GPU 版 | OpenGlControlBase | GLSL ES2 风格 控件内嵌着色器字符串 | DispatcherTimer ≈16ms采样后 RequestNextFrameRendering | 跨平台 UI 壳 + 显式 OpenGL 表面;参数进 uniform | Avalonia 上的托管 OpenGL 特效面 |
| WinForms GPU 版 | OpenTK GLControl | GLSL 330 Core 全屏 TriangleStrip | WinForms TimerInvalidate → Paint → SwapBuffers | 经典 Win32 窗体嵌原生 GL 上下文;无内置特效 API,自建管线 | WinForms 外壳 + 桌面 OpenGL |
| WinUI 3 GPU 版 | Win2DCanvasAnimatedControl | HLSL + D2D helpersPixelShaderEffect | Canvas 自带帧循环OnDraw 内采样关键帧 | 现代 Windows UI + Direct2D 自定义像素效果;字节码 .bin 装载 | WinUI 生态内的 D2D/Win2D 特效 |
WPF
- 原理:视觉树节点挂
ShaderEffect;内层 ZoomBlur、外层 ContrastAdjust(亮度)。Storyboard 直接动画依赖属性(含 Effect 参数与 Image.Opacity)。 - 特性:声明式时间轴与特效深度绑定;设计师/开发者同一套 XAML 心智;无需自管 GL 上下文。
- 擅长:桌面 .NET 里「控件 + GPU 像素特效」一体化体验最成熟。
- 约束:着色器方言偏旧(经典 ps 档)、主要限 Windows + WPF 栈。
HTML / WebGL
- 原理:一张 Canvas 拿 WebGL 上下文;顶点铺全屏四边形;片元着色器内一次完成叠化、ZoomBlur、亮度。
- 特性:零安装、跨浏览器;可用纯静态页(含内嵌图)打开;与桌面栈完全解耦。
- 擅长:演示、分发、对照实验成本最低。
- 约束:依赖浏览器 GL 能力;与桌面控件树无直接互操作。
Avalonia-GPU
- 原理:自定义
OpenGlControlBase,在OnOpenGlInit/Render中编译链接 GLSL、上传双纹理、每帧写 uniform 并绘制。 - 特性:保留 Avalonia 的跨平台 UI 壳,特效面显式走 OpenGL(经 Avalonia/ANGLE 等上下文管理)。
- 擅长:需要 Avalonia 应用壳、又要可编程 GPU 特效时的「官方 OpenGL 控件」路径。
- 约束:不像 WPF 有一等公民 ShaderEffect;需自管着色器与资源生命周期。
WinForms-GPU
- 原理:窗体内嵌 OpenTK
GLControl(Core 3.3);Load 时建 VAO/纹理/Program;Paint 时更新 uniform 并 SwapBuffers。 - 特性:WinForms 本身无像素着色器 API,GPU 能力完全来自嵌入的 OpenGL 运行时。
- 擅长:存量 WinForms 应用中「局部开一块真 GPU 特效面」而不重写整个 UI。
- 约束:GL 上下文与 WinForms 消息泵协作需自理;不是框架内建特效模型。
WinUI3-GPU
- 原理:Win2D
CanvasAnimatedControl驱动帧循环;预编译 HLSL →FlashTransition.bin→PixelShaderEffect;Properties 映射 cbuffer。 - 特性:站在现代 Windows 图形栈(D2D/Win2D)上;与 WinUI 控件并存自然。
- 擅长:WinUI 3 / Windows App SDK 产品中的自定义后处理特效。
- 约束:需按 D2D Effect 约定编译着色器;打包/运行时依赖 Windows App SDK(本工程已自包含)。
8.3. 优势特色
五种方式个有特色:WPF 声明式真源、Web 易分发、Avalonia 跨平台 GL 面、WinForms 存量嵌入、WinUI 现代 D2D/Win2D。对齐靠统一时间轴与公式;性能靠各自 GPU;

9. 源码下载

完整源码下载地址:
https://download.csdn.net/download/LateFrames/93137732
再来欣赏一下WPF版本的效果吧,我觉得时间节点的控制还算不错:



被折叠的 条评论
为什么被折叠?



