写一个辅助类PathHelper,用于将PathGeometry转换为对应离散的Point,用于路径追踪。
using System; using System.Collections.Generic; using Avalonia; using Avalonia.Media; public class PathHelper { private readonly List<Point> points = new(); // 离散点缓存 private readonly List<double> lengths = new(); // 累计长度缓存 private double totalLength; // 总路径长度 // geometry: 输入路径,step: 离散步长 (0~1) public PathHelper(PathGeometry geometry, double step = 0.01) { BuildPathCache(geometry, step); } // 将 PathGeometry 离散化 private void BuildPathCache(PathGeometry geometry, double step) { double accumulated = 0; // 累计长度 foreach (var figure in geometry.Figures!) { Point start = figure.StartPoint; points.Add(start); lengths.Add(accumulated); foreach (var seg in figure.Segments!) { switch (seg) { case LineSegment line: // 线段处理 accumulated += GetDistance(start, line.Point); start = line.Point; points.Add(start); lengths.Add(accumulated); break; case BezierSegment bezier: // 三次贝塞尔曲线处理 AddBezierSegment(ref accumulated, ref start, bezier.Point1, bezier.Point2, bezier.Point3, step); break; case PolyLineSegment poly: // 多线段处理 foreach (var p in poly.Points) { accumulated += GetDistance(start, p); start = p; points.Add(p); lengths.Add(accumulated); } break; case PolyBezierSegment pbezier: // 多贝塞尔曲线处理 if (pbezier.Points!.Count % 3 != 0) Console.WriteLine("警告: PolyBezierSegment 点数不是 3 的倍数,末尾点将被忽略"); for (int i = 0; i + 2 < pbezier.Points.Count; i += 3) { AddBezierSegment(ref accumulated, ref start, pbezier.Points[i], pbezier.Points[i + 1], pbezier.Points[i + 2], step); } break; case ArcSegment arc: // 弧线处理 var arcPoints = FlattenArcSegment(start, arc, step); Point lastArcPoint = start; foreach (var p in arcPoints) { if (p != lastArcPoint) // 避免重复起点 { accumulated += GetDistance(lastArcPoint, p); points.Add(p); lengths.Add(accumulated); lastArcPoint = p; } } start = arc.Point; // 更新起点为弧终点 break; default: Console.WriteLine($"不支持的 Segment 类型: {seg.GetType().Name}"); break; } } } totalLength = accumulated; // 保存总长度 } // 贝塞尔离散化逻辑 private void AddBezierSegment(ref double accumulated, ref Point start, Point p1, Point p2, Point p3, double step) { Point lastPoint = start; int segments = Math.Max(1, (int)(1.0 / step)); // 离散段数 for (int i = 1; i < segments; i++) { double t = i / (double)segments; var p = EvaluateBezier(start, p1, p2, p3, t); accumulated += GetDistance(lastPoint, p); points.Add(p); lengths.Add(accumulated); lastPoint = p; } // 添加终点 accumulated += GetDistance(lastPoint, p3); points.Add(p3); lengths.Add(accumulated); start = p3; } // 根据 progress 获取路径上对应的点 (0~1) public Point GetPoint(double progress) { if (points.Count == 0) return new Point(); if (progress <= 0) return points[0]; if (progress >= 1) return points[^1]; double targetLength = progress * totalLength; // 二分查找长度区间 int left = 0, right = lengths.Count - 1; while (left < right) { int mid = (left + right) / 2; if (lengths[mid] < targetLength) left = mid + 1; else right = mid; } // 检查精确匹配情况 if (left < lengths.Count && Math.Abs(lengths[left] - targetLength) < 1e-10) { return points[left]; } int index = Math.Max(left - 1, 0); // 避免 index+1 越界 if (index + 1 >= points.Count) return points[^1]; double segLength = lengths[index + 1] - lengths[index]; if (segLength < 1e-10) return points[index]; double t = (targetLength - lengths[index]) / segLength; return Interpolate(points[index], points[index + 1], t); } // 线性插值 private static Point Interpolate(Point p1, Point p2, double t) => new Point(p1.X + (p2.X - p1.X) * t, p1.Y + (p2.Y - p1.Y) * t); // 两点间距离 private static double GetDistance(Point p1, Point p2) => Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2)); // 计算三次贝塞尔曲线上点 private static Point EvaluateBezier(Point p0, Point p1, Point p2, Point p3, double t) { double u = 1 - t; double tt = t * t; double uu = u * u; double uuu = uu * u; double ttt = tt * t; double x = uuu * p0.X + 3 * uu * t * p1.X + 3 * u * tt * p2.X + ttt * p3.X; double y = uuu * p0.Y + 3 * uu * t * p1.Y + 3 * u * tt * p2.Y + ttt * p3.Y; return new Point(x, y); } // 将 ArcSegment 转换为离散点 private static List<Point> FlattenArcSegment(Point start, ArcSegment arc, double step) { var result = new List<Point> { start }; double rx = Math.Abs(arc.Size.Width); double ry = Math.Abs(arc.Size.Height); double phi = arc.RotationAngle * Math.PI / 180.0; bool largeArc = arc.IsLargeArc; bool sweep = arc.SweepDirection == SweepDirection.Clockwise; Point end = arc.Point; if (rx == 0 || ry == 0) { result.Add(end); return result; } double dx2 = (start.X - end.X) / 2.0; double dy2 = (start.Y - end.Y) / 2.0; double x1p = Math.Cos(phi) * dx2 + Math.Sin(phi) * dy2; double y1p = -Math.Sin(phi) * dx2 + Math.Cos(phi) * dy2; double rx_sq = rx * rx; double ry_sq = ry * ry; double x1p_sq = x1p * x1p; double y1p_sq = y1p * y1p; double lambda = x1p_sq / rx_sq + y1p_sq / ry_sq; if (lambda > 1) { double scale = Math.Sqrt(lambda); rx *= scale; ry *= scale; rx_sq = rx * rx; ry_sq = ry * ry; } double sign = (largeArc == sweep) ? -1 : 1; double sq = ((rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq)) / ((rx_sq * y1p_sq) + (ry_sq * x1p_sq)); sq = Math.Max(sq, 0); double coef = sign * Math.Sqrt(sq); double cxp = coef * (rx * y1p) / ry; double cyp = coef * -(ry * x1p) / rx; double cx = Math.Cos(phi) * cxp - Math.Sin(phi) * cyp + (start.X + end.X) / 2.0; double cy = Math.Sin(phi) * cxp + Math.Cos(phi) * cyp + (start.Y + end.Y) / 2.0; double vectorAngle(double ux, double uy, double vx, double vy) { double dot = ux * vx + uy * vy; double det = ux * vy - uy * vx; return Math.Atan2(det, dot); } double theta1 = vectorAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry); double deltaTheta = vectorAngle( (x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry ); if (!sweep && deltaTheta > 0) deltaTheta -= 2 * Math.PI; else if (sweep && deltaTheta < 0) deltaTheta += 2 * Math.PI; int segments = Math.Max((int)(Math.Abs(deltaTheta) / (step * 2 * Math.PI)), 1); for (int i = 1; i <= segments; i++) { double t = i / (double)segments; double angle = theta1 + t * deltaTheta; double x = cx + rx * Math.Cos(angle) * Math.Cos(phi) - ry * Math.Sin(angle) * Math.Sin(phi); double y = cy + rx * Math.Cos(angle) * Math.Sin(phi) + ry * Math.Sin(angle) * Math.Cos(phi); result.Add(new Point(x, y)); } return result; } // 总路径长度 public double TotalLength => totalLength; // 离散点数量 public int PointCount => points.Count; }
PathBasedAnimation.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="381.6" Width="521.6" x:Class="AvaloniaUI.PathBasedAnimation" Title="PathBasedAnimation"> <Window.Resources> <PathGeometry x:Key="pathGeometry">M0,0 A15,10 0 0 1 120,350 A5,5 0 0 1 400,50</PathGeometry> </Window.Resources> <Canvas Margin="10"> <Path Stroke="Red" StrokeThickness="1" Data="{StaticResource pathGeometry}"/> <Image Name="image" Width="20" Height="20"> <Image.Source> <DrawingImage> <DrawingImage.Drawing> <GeometryDrawing Brush="LightSteelBlue"> <GeometryDrawing.Geometry> <GeometryGroup> <EllipseGeometry Center="10,10" RadiusX="9" RadiusY="4"/> <EllipseGeometry Center="10,10" RadiusX="4" RadiusY="9"/> </GeometryGroup> </GeometryDrawing.Geometry> <GeometryDrawing.Pen> <Pen Thickness="1" Brush="Black"/> </GeometryDrawing.Pen> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image> </Canvas> </Window>
PathBasedAnimation.axaml.cs代码
using Avalonia; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; using Shares.Avalonia; using System; using System.Drawing; namespace AvaloniaUI; public partial class PathBasedAnimation : Window { private AnimationPlayer player = new AnimationPlayer(); private PathHelper? helper; public PathBasedAnimation() { InitializeComponent(); var geometry = this.Resources["pathGeometry"] as PathGeometry; helper = new PathHelper(geometry!); player.At(0).PlayLocal(p => { var pt = helper.GetPoint(p); Canvas.SetLeft(image, pt.X - image.Width / 2); Canvas.SetTop(image, pt.Y - image.Height / 2); }); player.Start(); } }
运行效果