「闪白切换」UI 特效 - 在 Winform/WPF/WinUI3/Avalonia/Web 的实现

发布时间:2026/7/17 21:38:20
「闪白切换」UI 特效 - 在 Winform/WPF/WinUI3/Avalonia/Web 的实现 目录1. 前言2. WPF2.1. 透明过渡动画2.2. 径向缩放模糊动画2.3. 亮度动画3. Winform3.1. 透明过渡动画​编辑3.2. 径向缩放模糊动画​编辑3.3. 亮度动画​编辑4. Avalonia4.1. 透明过渡动画4.2. 径向缩放模糊动画4.3. 亮度动画5. WinUI35.1. 透明过渡动画5.2. 径向缩放模糊动画5.3. 亮度动画6. Web6.1. 透明过渡动画6.2. 径向缩放模糊动画​编辑6.3. 亮度动画7. 五种比对7.1. 屏幕录制7.2. 手机录制8. 总结8.1. 难点8.2. 原理与特性WPFHTML / WebGLAvalonia-GPUWinForms-GPUWinUI3-GPU8.3. 优势特色9. 源码下载1. 前言这样的【闪白切换】特效看起来真的非常的酷那么它是怎样实现的呢我们最直观的感觉它肯定有两个图片在进行透明的切换也可以看到有缩放的跳跃呼吸感然后中间有一个白色闪屏。那么这三者结合能否达到相同的效果呢其实这是一个非常让人容易容易轻敌的特效。我们来观看它的慢放动画我们可以从慢放动画中发现其实它的处理效果是非常复杂的可以包含3种1透明渐变动画负责基础的过渡变换使过渡平滑柔和2径向缩放模糊动画产生幻影错层感焦点聚集很有电影画面感轻微的缩放有呼吸跳跃感更立体3亮度动画像是相机拍照时的闪光灯非常有场景感。那么我们来分别实现它们试试。2. WPF2.1. 透明过渡动画在WPF中透明渐变动画实现的XAML:DoubleAnimationUsingKeyFrames Storyboard.TargetNameimg1 Storyboard.TargetPropertyOpacity LinearDoubleKeyFrame KeyTime0:0:0.5 Value1/ LinearDoubleKeyFrame KeyTime0:0:1.5 Value0.3/ LinearDoubleKeyFrame KeyTime0:0:1.9 Value0/ /DoubleAnimationUsingKeyFrames DoubleAnimationUsingKeyFrames Storyboard.TargetNameimg2 Storyboard.TargetPropertyOpacity LinearDoubleKeyFrame KeyTime0:0:0 Value0/ LinearDoubleKeyFrame KeyTime0:0:1.5 Value0.8/ LinearDoubleKeyFrame KeyTime0:0:2 Value1/ /DoubleAnimationUsingKeyFrames效果:2.2. 径向缩放模糊动画DoubleAnimationUsingKeyFrames Storyboard.TargetNamezoomBlurEffect Storyboard.TargetPropertyBlurAmount LinearDoubleKeyFrame KeyTime0:0:1.1 Value0/ LinearDoubleKeyFrame KeyTime0:0:1.5 Value0.08/ LinearDoubleKeyFrame KeyTime0:0:1.65 Value0/ /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.TargetNamecontrastAdjustEffect Storyboard.TargetPropertyBrightness LinearDoubleKeyFrame KeyTime0:0:1.4 Value0/ LinearDoubleKeyFrame KeyTime0:0:1.5 Value1/ LinearDoubleKeyFrame KeyTime0:0:1.6 Value0/ /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. WinformWinform的实现方式我们既可以使用CPU算法也可以使用GPU算法这里我们使用更高性能的GPU方式处理GPU · OpenTK GLControl GLSL 330。GPU 入口OpenTK.GLControlCore Profile 3.3· 每帧MakeCurrent → Uniform → DrawArrays → SwapBuffers渲染管线片元着色器所有动画共用原理与 Avalonia/HTML 同构语法为#version 330 coretexture()。 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. AvaloniaAvalonia的实现方式我们既可以使用CPU算法也可以使用GPU算法这里我们使用更高性能的GPU方式处理GPU · OpenGlControlBase GLSL ESGPU 入口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→1Blur/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→0O1 保持 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. WinUI3WinUI3的实现方式我们既可以使用CPU算法也可以使用GPU算法这里我们使用更高性能的GPU方式处理GPU · Win2D PixelShaderEffect HLSLGPU 入口Microsoft.Graphics.Canvas·CanvasAnimatedControl每帧 Draw · 自定义像素着色器编译为Shaders/FlashTransition.bin渲染管线HLSL 像素着色器所有动画共用原理Win2DPixelShaderEffect把两张图作为 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 入口着色器 / 效果形态动画时间模型原理本质WPFUIElement.EffectShaderEffectHLSLps_2/3 风格ZoomBlur.fxContrastAdjust.fxXAMLStoryboard关键帧驱动依赖属性控件树挂效果链Storyboard 改 Opacity / BlurAmount / BrightnessGPU 实时着色桌面 XAML 原生像素特效管线HTMLCanvas WebGLGLSL ES片元着色器单 pass 合并三效果requestAnimationFrameJS 插值关键帧双纹理绑定 uniform浏览器合成层上的可编程管线Web 标准图形管线AvaloniaGPU 版OpenGlControlBaseGLSL ES2 风格控件内嵌着色器字符串DispatcherTimer≈16ms采样后RequestNextFrameRendering跨平台 UI 壳 显式 OpenGL 表面参数进 uniformAvalonia 上的托管 OpenGL 特效面WinFormsGPU 版OpenTKGLControlGLSL 330 Core全屏 TriangleStripWinFormsTimerInvalidate → Paint → SwapBuffers经典 Win32 窗体嵌原生 GL 上下文无内置特效 API自建管线WinForms 外壳 桌面 OpenGLWinUI 3GPU 版Win2DCanvasAnimatedControlHLSL D2D helpersPixelShaderEffectCanvas 自带帧循环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原理窗体内嵌 OpenTKGLControlCore 3.3Load 时建 VAO/纹理/ProgramPaint 时更新 uniform 并 SwapBuffers。特性WinForms 本身无像素着色器 APIGPU 能力完全来自嵌入的 OpenGL 运行时。擅长存量 WinForms 应用中「局部开一块真 GPU 特效面」而不重写整个 UI。约束GL 上下文与 WinForms 消息泵协作需自理不是框架内建特效模型。WinUI3-GPU原理Win2DCanvasAnimatedControl驱动帧循环预编译 HLSL →FlashTransition.bin→PixelShaderEffectProperties 映射 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。对齐靠统一时间轴与公式性能靠各自 GPU9. 源码下载完整源码下载地址https://download.csdn.net/download/LateFrames/93137732再来欣赏一下WPF版本的效果吧我觉得时间节点的控制还算不错