This commit is contained in:
tanglong 2025-10-17 14:18:01 +08:00
parent 0e8c41fb61
commit ec46f3799c
6 changed files with 257 additions and 136 deletions

View File

@ -2,6 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views" xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views"
xmlns:conv="clr-namespace:Wpf_AiSportsMicrospace.Converter"
StartupUri="Views/Main.xaml"> StartupUri="Views/Main.xaml">
<!--StartupUri="Views/JumpRope/GroupJumpRope.xaml">--> <!--StartupUri="Views/JumpRope/GroupJumpRope.xaml">-->
@ -10,6 +12,8 @@
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="./Resoures/Fonts/myFontFamily"/> <Setter Property="FontFamily" Value="./Resoures/Fonts/myFontFamily"/>
</Style> </Style>
<conv:DotColorConverter x:Key="DotColorConverter"/>
<!--<conv:SpacingConverter x:Key="SpacingConverter"/>-->
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using Wpf_AiSportsMicrospace.MyUserControl;
namespace Wpf_AiSportsMicrospace.Converter
{
public class DotColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b && b)
return Brushes.Red;
return Brushes.Green;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View File

@ -0,0 +1,29 @@
<UserControl x:Class="Wpf_AiSportsMicrospace.MyUserControl.BeatScrollDots"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Converter"
mc:Ignorable="d"
Height="100" Width="400">
<UserControl.Resources>
<local:DotColorConverter x:Key="DotColorConverter"/>
</UserControl.Resources>
<ScrollViewer x:Name="scrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled" Opacity="0.5">
<ItemsControl ItemsSource="{Binding Dots, ElementName=root}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="20" Height="20"
Margin="{Binding ElementName=root, Path=DotSpacing}"
Fill="{Binding IsSelected, Converter={StaticResource DotColorConverter}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,136 @@
using Emgu.CV.Flann;
using HandyControl.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wpf_AiSportsMicrospace.MyUserControl
{
public partial class BeatScrollDots : UserControl
{
public ObservableCollection<DotItem> Dots { get; set; } = new ObservableCollection<DotItem>();
public BeatScrollDots()
{
InitializeComponent();
DataContext = this;
}
public int DotCount
{
get => (int)GetValue(DotCountProperty);
set => SetValue(DotCountProperty, value);
}
public static readonly DependencyProperty DotCountProperty =
DependencyProperty.Register(nameof(DotCount), typeof(int), typeof(BeatScrollDots),
new PropertyMetadata(0, OnDotCountChanged));
private static void OnDotCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BeatScrollDots ctrl)
ctrl.GenerateDots();
}
private void GenerateDots()
{
Dots.Clear();
for (int i = 0; i < DotCount; i++)
Dots.Add(new DotItem { IsSelected = false });
}
public void SetSelected(int index, bool selected)
{
if (index >= 0 && index < Dots.Count)
Dots[index].IsSelected = selected;
}
public Thickness DotSpacing
{
get => (Thickness)GetValue(DotSpacingProperty);
set => SetValue(DotSpacingProperty, value);
}
public static readonly DependencyProperty DotSpacingProperty =
DependencyProperty.Register(nameof(DotSpacing), typeof(Thickness), typeof(BeatScrollDots), new PropertyMetadata(new Thickness(5, 0, 5, 0)));
public void ScrollToDotCenter(int index, bool mirror = false)
{
if (scrollViewer == null || Dots.Count == 0) return;
double step = 20 + DotSpacing.Left + DotSpacing.Right;
double totalWidth = Dots.Count * step;
double dotCenter = index * step + step / 2;
double targetOffset = !mirror
? dotCenter - scrollViewer.ActualWidth / 2
: totalWidth - dotCenter - scrollViewer.ActualWidth / 2;
targetOffset = System.Math.Max(0, System.Math.Min(targetOffset, scrollViewer.ScrollableWidth));
scrollViewer.ScrollToHorizontalOffset(targetOffset);
}
public void UpdateSpacingForCount(int count)
{
if (count <= 0) count = 1;
double spacing = (ActualWidth / count) - 20;
if (spacing < 2) spacing = 2;
DotSpacing = new Thickness(spacing / 2, 0, spacing / 2, 0);
}
public Dictionary<int, List<double>> MusicBeatsDic
{
get => (Dictionary<int, List<double>>)GetValue(MusicBeatsDicProperty);
set => SetValue(MusicBeatsDicProperty, value);
}
public static readonly DependencyProperty MusicBeatsDicProperty =
DependencyProperty.Register(nameof(MusicBeatsDic), typeof(Dictionary<int, List<double>>), typeof(BeatScrollDots),
new PropertyMetadata(null, OnMusicBeatsDicChanged));
private static void OnMusicBeatsDicChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BeatScrollDots ctrl)
{
// 可选:根据第一个 key 初始化 DotCount
if (ctrl.MusicBeatsDic != null && ctrl.MusicBeatsDic.Any())
{
int firstKey = ctrl.MusicBeatsDic.Keys.Min();
ctrl.DotCount = ctrl.MusicBeatsDic[firstKey].Count;
ctrl.UpdateSpacingForCount(ctrl.DotCount);
}
}
}
}
public class DotItem : INotifyPropertyChanged
{
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

View File

@ -35,28 +35,18 @@
TextAlignment="Center" TextAlignment="Center"
/> />
</Grid> </Grid>
<Grid x:Name="BottomBeatPanel" VerticalAlignment="Bottom" Height="200" Panel.ZIndex="100"> <Grid x:Name="BottomBeatPanel" VerticalAlignment="Bottom" Height="200">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="600"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 左边滚动条 --> <!-- 左控件 -->
<ScrollViewer x:Name="BeatScrollLeft" Width="660" Height="80" <local:BeatScrollDots x:Name="LeftBeats" Grid.Column="0" Background="Gray"/>
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
Grid.Column="0">
<Canvas x:Name="beatCanvasLeft" Height="100"/>
</ScrollViewer>
<!-- 右边滚动条 --> <!-- 右控件 -->
<ScrollViewer x:Name="BeatScrollRight" Width="660" Height="80" <local:BeatScrollDots x:Name="RightBeats" Grid.Column="2" Background="Gray"/>
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
Grid.Column="2">
<Canvas x:Name="beatCanvasRight" Height="100"/>
</ScrollViewer>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
@ -37,7 +38,7 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
private readonly object _updateLock = new object(); private readonly object _updateLock = new object();
private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>(); private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>();
private MusicJumpRopeContext _musicJumpRopeContext; private MusicJumpRopeContext _musicJumpRopeContext;
public Dictionary<int, List<double>> _musicBeatsDic;
private GameState _currentGameState = GameState.NotStarted; private GameState _currentGameState = GameState.NotStarted;
List<RankItem> RankingItemList = new(); List<RankItem> RankingItemList = new();
@ -70,7 +71,7 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
// 容忍时间(节拍误差) // 容忍时间(节拍误差)
public double _beatTolerance = 0.15; // ±150ms public double _beatTolerance = 0.15; // ±150ms
private int _totalDots = 0;
// 滚动显示的节拍点集合 // 滚动显示的节拍点集合
public ObservableCollection<BeatItem> BeatDisplayLeft { get; set; } = new(); public ObservableCollection<BeatItem> BeatDisplayLeft { get; set; } = new();
public ObservableCollection<BeatItem> BeatDisplayRight { get; set; } = new(); public ObservableCollection<BeatItem> BeatDisplayRight { get; set; } = new();
@ -81,9 +82,17 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
Loaded += UserControl_Loaded; Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded; Unloaded += UserControl_Unloaded;
_musicJumpRopeContext = new MusicJumpRopeContext(); _musicJumpRopeContext = new MusicJumpRopeContext();
_musicBeatsDic = _musicJumpRopeContext.MusicBeatsDic;
// 初始化节拍点数据 _totalDots = _musicJumpRopeContext.MusicBeats["1"].Count();
InitBeatDots();
// 左控件
LeftBeats.DotCount = _totalDots;
LeftBeats.MusicBeatsDic = _musicBeatsDic;
// 右控件
RightBeats.DotCount = _totalDots;
RightBeats.MusicBeatsDic = _musicBeatsDic;
} }
private async void UserControl_Loaded(object sender, RoutedEventArgs e) private async void UserControl_Loaded(object sender, RoutedEventArgs e)
@ -234,11 +243,10 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
private DateTime _lastUpdateTime = DateTime.Now; private DateTime _lastUpdateTime = DateTime.Now;
private void StartCountdown(int start = 3) private void StartCountdown(int start = 3)
{ {
_mainWin.ShowCountDownAnimation(); _currentCountdown = start;
//_currentCountdown = start; countdownText.Text = _currentCountdown.ToString();
//countdownText.Text = _currentCountdown.ToString(); countdownGrid.Visibility = Visibility.Visible;
//countdownGrid.Visibility = Visibility.Visible; _lastUpdateTime = DateTime.Now;
//_lastUpdateTime = DateTime.Now;
Utils.PlayBackgroundMusic("countdown_3.mp3", false); Utils.PlayBackgroundMusic("countdown_3.mp3", false);
} }
@ -492,30 +500,29 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
userItem.ImageState = "2"; userItem.ImageState = "2";
var currentTime = Utils.GetMusicCurrentTime(); var currentTime = Utils.GetMusicCurrentTime();
var beats = _musicJumpRopeContext.MusicBeats["1"]; int currentSecond = (int)Math.Floor(currentTime);
for (int j = 0; j < beats.Count; j++) // 判断是否命中节拍
var beats = _musicJumpRopeContext.MusicBeats["1"];
bool hit = beats.Any(b => Math.Abs(b - currentTime) <= _beatTolerance);
if (hit)
{ {
int indexCopy2 = j; // 复制一份当前循环索引 _musicJumpRopeContext.UserBeatSyncList[indexCopy]++;
if (Math.Abs(beats[indexCopy2] - currentTime) <= _beatTolerance) _musicBeatTextBlock[indexCopy].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[indexCopy]}";
{
_musicJumpRopeContext.UserBeatSyncList[indexCopy]++; if (indexCopy == 0)
Application.Current.Dispatcher.BeginInvoke(() => LeftBeats.SetSelected(currentSecond, true);
{ else
if (indexCopy == 0) RightBeats.SetSelected(currentSecond, true);
{
((Ellipse)beatCanvasLeft.Children[indexCopy2]).Fill = Brushes.Red; //Application.Current.Dispatcher.BeginInvoke(() =>
} //{
else // if (indexCopy == 0)
{ // LeftBeats.SetSelected(currentSecond, true);
((Ellipse)beatCanvasRight.Children[indexCopy2]).Fill = Brushes.Red; // else
} // RightBeats.SetSelected(currentSecond, true);
_musicBeatTextBlock[indexCopy].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[indexCopy]}"; //});
});
}
} }
// 滚动条跳跃到当前点附近
UpdateBeatScrollSudden(currentTime);
}; };
sport.Start(); sport.Start();
@ -523,30 +530,6 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
} }
} }
private void UpdateBeatScrollSudden(double currentTime)
{
int maxVisible = 5; // 最多显示 5 个点
var beats = _musicJumpRopeContext.MusicBeats["1"];
// 找到最近的点索引
int currentIndex = beats.FindIndex(bt => bt >= currentTime);
if (currentIndex == -1) currentIndex = beats.Count - 1;
// 取前后最多 5 个点
int startIndex = Math.Max(0, currentIndex - maxVisible / 2);
int endIndex = Math.Min(beats.Count - 1, startIndex + maxVisible - 1);
// 左侧 ScrollViewer
double leftCenterX = BeatDisplayLeft[currentIndex].X - BeatScrollLeft.ViewportWidth / 2;
leftCenterX = Math.Max(0, Math.Min(leftCenterX, beatCanvasLeft.Width - BeatScrollLeft.ViewportWidth));
BeatScrollLeft.ScrollToHorizontalOffset(leftCenterX);
// 右侧 ScrollViewer
double rightCenterX = BeatDisplayRight[currentIndex].X - BeatScrollRight.ViewportWidth / 2;
rightCenterX = Math.Max(0, Math.Min(rightCenterX, beatCanvasRight.Width - BeatScrollRight.ViewportWidth));
BeatScrollRight.ScrollToHorizontalOffset(rightCenterX);
}
private void UpdateCircleCounts(List<Human> humans) private void UpdateCircleCounts(List<Human> humans)
{ {
double radiusNormX = 0.07; double radiusNormX = 0.07;
@ -673,82 +656,37 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
return userItem; return userItem;
} }
private List<TextBlock> leftDots = new();
private List<TextBlock> rightDots = new();
public double BeatPanelWidth { get; set; }
private void InitBeatDots()
{
BeatDisplayLeft.Clear();
BeatDisplayRight.Clear();
beatCanvasLeft.Children.Clear();
beatCanvasRight.Children.Clear();
var beats = _musicJumpRopeContext.MusicBeats["1"];
double lastX = 0;
double scale = 300; // 间隔放大系数,可调
for (int i = 0; i < beats.Count; i++)
{
double interval = i == 0 ? beats[0] : beats[i] - beats[i - 1];
lastX += interval * scale;
}
double totalWidth = lastX + 50;
// 左侧点
lastX = 0;
for (int i = 0; i < beats.Count; i++)
{
double interval = i == 0 ? beats[0] : beats[i] - beats[i - 1];
double xPos = lastX + interval * scale;
lastX = xPos;
var leftItem = new BeatItem { X = xPos, Color = Brushes.Black };
BeatDisplayLeft.Add(leftItem);
var leftEllipse = new Ellipse { Width = 8, Height = 8, Fill = leftItem.Color };
Canvas.SetLeft(leftEllipse, leftItem.X);
Canvas.SetTop(leftEllipse, 40);
beatCanvasLeft.Children.Add(leftEllipse);
}
beatCanvasLeft.Width = totalWidth;
// 右侧点(倒序排列,但间隔顺序保持一致)
lastX = 0;
for (int i = 0; i < beats.Count; i++)
{
double interval = i == 0 ? beats[0] : beats[i] - beats[i - 1];
double xPos = lastX + interval * scale;
lastX = xPos;
// 倒序显示
double rightX = totalWidth - xPos;
var rightItem = new BeatItem { X = rightX, Color = Brushes.Black };
BeatDisplayRight.Add(rightItem);
var rightEllipse = new Ellipse { Width = 8, Height = 8, Fill = rightItem.Color };
Canvas.SetLeft(rightEllipse, rightItem.X);
Canvas.SetTop(rightEllipse, 40);
beatCanvasRight.Children.Add(rightEllipse);
}
beatCanvasRight.Width = totalWidth;
}
private DispatcherTimer _beatScrollTimer; private DispatcherTimer _beatScrollTimer;
private double totalTime = 108.455; // 音乐总时长 private double totalTime = 108.455; // 音乐总时长
private int _lastSecond = -1; // 上一次已经处理的秒数
private void StartBeatScrollTimer() private void StartBeatScrollTimer()
{ {
if (_beatScrollTimer != null) _beatScrollTimer.Stop();
_beatScrollTimer = new DispatcherTimer(); _beatScrollTimer = new DispatcherTimer();
_beatScrollTimer.Interval = TimeSpan.FromMilliseconds(1000); _beatScrollTimer.Interval = TimeSpan.FromMilliseconds(500); // 每秒一次
int lastSecond = -1;
_beatScrollTimer.Tick += (s, e) => _beatScrollTimer.Tick += (s, e) =>
{ {
double currentTime = Utils.GetMusicCurrentTime(); double currentTime = Utils.GetMusicCurrentTime();
int currentSecond = (int)Math.Floor(currentTime)+1;
double progress = currentTime / totalTime; if (currentSecond <= lastSecond) return;
double maxOffsetLeft = beatCanvasLeft.Width - BeatScrollLeft.ViewportWidth; lastSecond = currentSecond;
double maxOffsetRight = beatCanvasRight.Width - BeatScrollRight.ViewportWidth; // 左控件
BeatScrollLeft.ScrollToHorizontalOffset(progress * (beatCanvasLeft.Width - BeatScrollLeft.ViewportWidth)); if (_musicBeatsDic.TryGetValue(currentSecond, out var leftBeats))
BeatScrollRight.ScrollToHorizontalOffset((1 - progress) * (beatCanvasRight.Width - BeatScrollRight.ViewportWidth)); {
LeftBeats.SetSelected(currentSecond, true);
LeftBeats.ScrollToDotCenter(currentSecond, mirror: true);
}
// 右控件
if (_musicBeatsDic.TryGetValue(currentSecond, out var rightBeats))
{
RightBeats.SetSelected(currentSecond, true);
RightBeats.ScrollToDotCenter(currentSecond, mirror: false);
}
}; };
_beatScrollTimer.Start(); _beatScrollTimer.Start();
} }