using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace PamhagenSysCtrl.Helpers.Pipeline { internal sealed class FlowArrows { public const double SPEED = 4; public const double SPACING = 3; public const double LENGTH = 1.4; public const double WIDTH = 1.4; private static readonly Stopwatch Clock = Stopwatch.StartNew(); private double _scale = 5; private readonly Canvas _layer; private readonly Point[] _path; private Point[] _points = []; private double _pathOffset; private bool _isActive; public double Length { get; init; } private FlowArrows(Point[] path) { _path = path; Length = _path.Length <= 1 ? 0 : Enumerable.Range(0, _path.Length - 1).Sum(i => GetLength(_path[i], _path[i + 1])); _layer = new Canvas() { IsHitTestVisible = false, Visibility = Visibility.Collapsed, }; _layer.Unloaded += (_, _) => Stop(); } public static FlowArrows Create(Canvas canvas, IEnumerable points) { var arrows = new FlowArrows([.. points]); canvas.Children.Add(arrows._layer); return arrows; } public void Scale(double scale) { _scale = scale; } public void Update((bool TowardEnd, double PathOffset)? flow) { if (flow == null) { Stop(); return; } _points = flow.Value.TowardEnd ? _path : [.. _path.Reverse()]; _pathOffset = flow.Value.PathOffset; if (!_isActive) { CompositionTarget.Rendering += OnRendering; _isActive = true; } _layer.Visibility = Visibility.Visible; Render(); } private void OnRendering(object? sender, EventArgs args) { Render(); } private void Render() { var distance = ((Clock.Elapsed.TotalSeconds * SPEED - _pathOffset) % SPACING + SPACING) % SPACING; var arrowIndex = 0; while (distance <= Length) { var arrow = GetArrow(arrowIndex++); SetPosition(arrow, distance); arrow.Visibility = Visibility.Visible; distance += SPACING; } while (arrowIndex < _layer.Children.Count) { _layer.Children[arrowIndex++].Visibility = Visibility.Collapsed; } } private Polygon GetArrow(int index) { if (index < _layer.Children.Count) { return (Polygon)_layer.Children[index]; } var arrow = new Polygon() { Fill = new SolidColorBrush(Color.FromArgb(0x60, 0, 0, 0)), IsHitTestVisible = false, Points = [ new(-LENGTH / 2, -WIDTH / 2), new(0, -WIDTH / 2), new(LENGTH / 2, 0), new(0, WIDTH / 2), new(-LENGTH / 2, WIDTH / 2), new(0, 0), ], RenderTransform = new MatrixTransform(Matrix.Identity), }; _layer.Children.Add(arrow); return arrow; } private void SetPosition(Polygon arrow, double distance) { const double blendDistance = WIDTH / 2; double firstLength = GetLength(_points[0], _points[1]); double blendStartDistance = firstLength - blendDistance; double blendEndDistance = firstLength + blendDistance; Vector v1 = Normalize(_points[0], _points[1]); Vector v2 = Normalize(_points[1], _points[2]); Point blendStart = _points[1] - v1 * blendDistance; Point blendEnd = _points[1] + v2 * blendDistance; Point position; Vector direction; if (distance < blendStartDistance) { position = _points[0] + v1 * distance; direction = v1; } else if (distance <= blendEndDistance) { double t = (distance - blendStartDistance) / (blendEndDistance - blendStartDistance); double u = 1.0 - t; position = new Point( u * u * blendStart.X + 2 * u * t * _points[1].X + t * t * blendEnd.X, u * u * blendStart.Y + 2 * u * t * _points[1].Y + t * t * blendEnd.Y); direction = new Vector( 2 * u * (_points[1].X - blendStart.X) + 2 * t * (blendEnd.X - _points[1].X), 2 * u * (_points[1].Y - blendStart.Y) + 2 * t * (blendEnd.Y - _points[1].Y)); direction.Normalize(); } else { double d = distance - firstLength; position = _points[1] + v2 * d; direction = v2; } double ux = direction.X * _scale; double uy = direction.Y * _scale; (arrow.RenderTransform as MatrixTransform)?.Matrix = new Matrix(ux, uy, -uy, ux, position.X * _scale, position.Y * _scale); } private void Stop() { if (_isActive) { CompositionTarget.Rendering -= OnRendering; _isActive = false; } _layer.Visibility = Visibility.Collapsed; } private static double GetLength(Point start, Point end) { var dx = end.X - start.X; var dy = end.Y - start.Y; return Math.Sqrt(dx * dx + dy * dy); } private static Vector Normalize(Point p1, Point p2) { Vector v = p2 - p1; if (v.Length > 0) v.Normalize(); return v; } } }