2 changed files with 404 additions and 0 deletions
@ -0,0 +1,401 @@ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue