写一个辅助类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();
    }
}

运行效果

image

 

posted on 2025-10-18 09:14  dalgleish  阅读(2)  评论(0)    收藏  举报