csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
13 KiB
401 lines
13 KiB
using System;
|
|
using System.Diagnostics;
|
|
using System.Numerics;
|
|
using Avalonia;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Animation.Easings;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media;
|
|
using Avalonia.Rendering.Composition;
|
|
using Avalonia.Rendering.Composition.Animations;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace RenderDemo.Pages
|
|
{
|
|
public class HitTestingPage : UserControl
|
|
{
|
|
private const int GroupColumns = 8;
|
|
private const int GroupRows = 5;
|
|
private const int CellsPerGroupSide = 10;
|
|
private const int CellStride = 10;
|
|
private const int CellSize = 8;
|
|
private const int AnimationTravel = 64;
|
|
|
|
private readonly Canvas _scene;
|
|
private readonly TextBlock _stats;
|
|
private readonly Cell[] _cells = new Cell[GroupColumns * GroupRows * CellsPerGroupSide * CellsPerGroupSide];
|
|
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
|
|
private Compositor? _compositor;
|
|
private int _hitTestsPerFrame = 256;
|
|
private int _updateCount;
|
|
private int _hitTestCount;
|
|
private int _hitCount;
|
|
private int _lastSecondUpdateCount;
|
|
private int _lastSecondHitTestCount;
|
|
private int _lastSecondHitCount;
|
|
private TimeSpan _lastSecondTime;
|
|
private double _lastSecondUpdatesPerSecond;
|
|
private double _lastSecondHitTestsPerSecond;
|
|
private double _lastSecondHitsPerSecond;
|
|
private int _clickCount;
|
|
private bool _isAttached;
|
|
private bool _updateQueued;
|
|
private bool _animationsStarted;
|
|
private Cell? _lastHitCell;
|
|
private Cell? _lastClickedCell;
|
|
|
|
public HitTestingPage()
|
|
{
|
|
_scene = new Canvas
|
|
{
|
|
Width = GroupColumns * CellsPerGroupSide * CellStride,
|
|
Height = GroupRows * CellsPerGroupSide * CellStride,
|
|
Background = Brushes.Transparent
|
|
};
|
|
|
|
_stats = new TextBlock
|
|
{
|
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
|
|
Margin = new Thickness(12),
|
|
Padding = new Thickness(8, 4),
|
|
Background = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255)),
|
|
Foreground = Brushes.Black,
|
|
IsHitTestVisible = false
|
|
};
|
|
|
|
var numberOfHitTests = new NumericUpDown
|
|
{
|
|
Minimum = 0,
|
|
Value = _hitTestsPerFrame,
|
|
Width = 200,
|
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left,
|
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
|
};
|
|
|
|
numberOfHitTests.ValueChanged += (s, e) =>
|
|
{
|
|
if (numberOfHitTests.Value.HasValue)
|
|
{
|
|
_hitTestsPerFrame = (int)numberOfHitTests.Value.Value;
|
|
}
|
|
};
|
|
|
|
var param = new StackPanel
|
|
{
|
|
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left,
|
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
|
|
Margin = new Thickness(12),
|
|
Spacing = 8,
|
|
Children =
|
|
{
|
|
new TextBlock { Text = "Hit tests per update:" },
|
|
numberOfHitTests
|
|
}
|
|
};
|
|
|
|
var root = new Grid
|
|
{
|
|
ClipToBounds = true,
|
|
RowDefinitions =
|
|
{
|
|
new RowDefinition(GridLength.Auto),
|
|
new RowDefinition(GridLength.Auto),
|
|
new RowDefinition(GridLength.Star),
|
|
},
|
|
};
|
|
|
|
Grid.SetRow(param, 0);
|
|
root.Children.Add(param);
|
|
Grid.SetRow(_stats, 1);
|
|
root.Children.Add(_stats);
|
|
Grid.SetRow(_scene, 2);
|
|
root.Children.Add(_scene);
|
|
|
|
Content = root;
|
|
BuildScene();
|
|
ResetState();
|
|
}
|
|
|
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
base.OnAttachedToVisualTree(e);
|
|
ResetState();
|
|
_compositor = ElementComposition.GetElementVisual(this)?.Compositor;
|
|
_isAttached = true;
|
|
RequestNextUpdate();
|
|
}
|
|
|
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
_isAttached = false;
|
|
_updateQueued = false;
|
|
_compositor = null;
|
|
ResetState();
|
|
base.OnDetachedFromVisualTree(e);
|
|
}
|
|
|
|
private void BuildScene()
|
|
{
|
|
var index = 0;
|
|
var groupSize = CellsPerGroupSide * CellStride;
|
|
|
|
for (var groupY = 0; groupY < GroupRows; groupY++)
|
|
{
|
|
for (var groupX = 0; groupX < GroupColumns; groupX++)
|
|
{
|
|
var group = new Canvas
|
|
{
|
|
Width = groupSize,
|
|
Height = groupSize,
|
|
Background = Brushes.Transparent
|
|
};
|
|
Canvas.SetLeft(group, groupX * groupSize);
|
|
Canvas.SetTop(group, groupY * groupSize);
|
|
_scene.Children.Add(group);
|
|
|
|
for (var y = 0; y < CellsPerGroupSide; y++)
|
|
{
|
|
for (var x = 0; x < CellsPerGroupSide; x++)
|
|
{
|
|
var cell = new Cell(index)
|
|
{
|
|
Width = CellSize,
|
|
Height = CellSize,
|
|
Background = CreateBrush(index),
|
|
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative)
|
|
};
|
|
cell.PointerPressed += OnCellPointerPressed;
|
|
|
|
Canvas.SetLeft(cell, x * CellStride);
|
|
Canvas.SetTop(cell, y * CellStride);
|
|
group.Children.Add(cell);
|
|
_cells[index++] = cell;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnCompositionUpdate()
|
|
{
|
|
_updateQueued = false;
|
|
|
|
if (!_isAttached)
|
|
return;
|
|
|
|
if (!_animationsStarted)
|
|
StartAnimations();
|
|
|
|
RunHitTests();
|
|
|
|
_updateCount++;
|
|
if (_stopwatch.Elapsed - _lastSecondTime >= TimeSpan.FromSeconds(1))
|
|
UpdateStats();
|
|
|
|
RequestNextUpdate();
|
|
}
|
|
|
|
private void RequestNextUpdate()
|
|
{
|
|
if (_updateQueued || _compositor == null)
|
|
return;
|
|
|
|
_updateQueued = true;
|
|
_compositor.RequestCompositionUpdate(OnCompositionUpdate);
|
|
}
|
|
|
|
private void StartAnimations()
|
|
{
|
|
var started = 0;
|
|
var easing = new SineEaseInOut();
|
|
|
|
for (var i = 0; i < _cells.Length; i++)
|
|
{
|
|
if (i % 5 != 0)
|
|
continue;
|
|
|
|
var visual = ElementComposition.GetElementVisual(_cells[i]);
|
|
if (visual == null)
|
|
continue;
|
|
|
|
var translation = visual.Compositor.CreateVector3KeyFrameAnimation();
|
|
translation.Target = "Translation";
|
|
translation.Duration = TimeSpan.FromMilliseconds(900 + (i % 700));
|
|
translation.Direction = PlaybackDirection.Alternate;
|
|
translation.IterationBehavior = AnimationIterationBehavior.Forever;
|
|
translation.InsertKeyFrame(0f, new Vector3(0, 0, 0), easing);
|
|
translation.InsertKeyFrame(1f, GetAnimationOffset(i), easing);
|
|
visual.StartAnimation("Translation", translation);
|
|
|
|
started++;
|
|
}
|
|
|
|
_animationsStarted = started > 0;
|
|
}
|
|
|
|
private void StopAnimations()
|
|
{
|
|
for (var i = 0; i < _cells.Length; i++)
|
|
{
|
|
var visual = ElementComposition.GetElementVisual(_cells[i]);
|
|
if (visual == null)
|
|
continue;
|
|
|
|
visual.StopAnimation("Translation");
|
|
visual.Translation = default;
|
|
}
|
|
|
|
_animationsStarted = false;
|
|
}
|
|
|
|
private void RunHitTests()
|
|
{
|
|
var width = Math.Max(1, _scene.Bounds.Width);
|
|
var height = Math.Max(1, _scene.Bounds.Height);
|
|
var baseIndex = _updateCount * 37;
|
|
|
|
for (var i = 0; i < _hitTestsPerFrame; i++)
|
|
{
|
|
_hitTestCount++;
|
|
var sample = baseIndex + (i * 97);
|
|
var point = new Point(sample * 17 % width, sample * 29 % height);
|
|
var hit = _scene.GetVisualAt(point);
|
|
|
|
if (hit is Cell cell)
|
|
{
|
|
SetLastHitCell(cell);
|
|
_hitCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Vector3 GetAnimationOffset(int index)
|
|
{
|
|
var x = index % 4 switch
|
|
{
|
|
0 => -AnimationTravel,
|
|
1 => AnimationTravel,
|
|
2 => -AnimationTravel / 2,
|
|
_ => AnimationTravel / 2
|
|
};
|
|
var y = index / 4 % 4 switch
|
|
{
|
|
0 => -AnimationTravel,
|
|
1 => AnimationTravel,
|
|
2 => AnimationTravel / 2,
|
|
_ => -AnimationTravel / 2
|
|
};
|
|
|
|
return new Vector3(x, y, 0);
|
|
}
|
|
|
|
private void ResetState()
|
|
{
|
|
StopAnimations();
|
|
|
|
_lastClickedCell?.ClearHighlight();
|
|
_lastHitCell = null;
|
|
_lastClickedCell = null;
|
|
|
|
_updateCount = 0;
|
|
_hitTestCount = 0;
|
|
_hitCount = 0;
|
|
_lastSecondUpdateCount = 0;
|
|
_lastSecondHitTestCount = 0;
|
|
_lastSecondHitCount = 0;
|
|
_lastSecondTime = default;
|
|
_lastSecondUpdatesPerSecond = 0;
|
|
_lastSecondHitTestsPerSecond = 0;
|
|
_lastSecondHitsPerSecond = 0;
|
|
_clickCount = 0;
|
|
_stopwatch.Restart();
|
|
UpdateStats();
|
|
}
|
|
|
|
private void UpdateStats()
|
|
{
|
|
var elapsed = _stopwatch.Elapsed;
|
|
var seconds = Math.Max(0.001, (elapsed - _lastSecondTime).TotalSeconds);
|
|
_lastSecondUpdatesPerSecond = (_updateCount - _lastSecondUpdateCount) / seconds;
|
|
_lastSecondHitTestsPerSecond = (_hitTestCount - _lastSecondHitTestCount) / seconds;
|
|
_lastSecondHitsPerSecond = (_hitCount - _lastSecondHitCount) / seconds;
|
|
_lastSecondUpdateCount = _updateCount;
|
|
_lastSecondHitTestCount = _hitTestCount;
|
|
_lastSecondHitCount = _hitCount;
|
|
_lastSecondTime = elapsed;
|
|
|
|
_stats.Text =
|
|
$"Visuals: {_cells.Length} ({_cells.Length / 5} animated), " +
|
|
$"Hit tests/frame: {_hitTestsPerFrame}, " +
|
|
$"Composition updates/s: {_lastSecondUpdatesPerSecond:F1}, Hit tests/s: {_lastSecondHitTestsPerSecond:F0}, " +
|
|
$"Hits/s: {_lastSecondHitsPerSecond:F0}, Misses/s: {_lastSecondHitTestsPerSecond - _lastSecondHitsPerSecond:F0}, " +
|
|
$"Clicks: {_clickCount}";
|
|
}
|
|
|
|
private void OnCellPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (sender is not Cell cell)
|
|
return;
|
|
|
|
SetLastClickedCell(cell);
|
|
_clickCount++;
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void SetLastHitCell(Cell cell)
|
|
{
|
|
if (ReferenceEquals(_lastHitCell, cell))
|
|
return;
|
|
|
|
_lastHitCell = cell;
|
|
}
|
|
|
|
private void SetLastClickedCell(Cell cell)
|
|
{
|
|
if (!ReferenceEquals(_lastClickedCell, cell) && _lastClickedCell != null)
|
|
{
|
|
_lastClickedCell.IsLatestClick = false;
|
|
_lastClickedCell.UpdateHighlight();
|
|
}
|
|
|
|
_lastClickedCell = cell;
|
|
cell.IsLatestClick = true;
|
|
cell.UpdateHighlight();
|
|
}
|
|
|
|
private static IBrush CreateBrush(int index)
|
|
{
|
|
var r = (byte)(80 + (index * 47 % 160));
|
|
var g = (byte)(80 + (index * 91 % 160));
|
|
var b = (byte)(80 + (index * 137 % 160));
|
|
return new SolidColorBrush(Color.FromRgb(r, g, b));
|
|
}
|
|
|
|
private sealed class Cell : Border
|
|
{
|
|
public Cell(int index)
|
|
{
|
|
Index = index;
|
|
BorderThickness = new Thickness(1);
|
|
}
|
|
|
|
public int Index { get; }
|
|
public bool IsLatestClick { get; set; }
|
|
|
|
public void ClearHighlight()
|
|
{
|
|
IsLatestClick = false;
|
|
UpdateHighlight();
|
|
}
|
|
|
|
public void UpdateHighlight()
|
|
{
|
|
BorderBrush = IsLatestClick ? Brushes.White : Brushes.Transparent;
|
|
ZIndex = IsLatestClick ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|