diff --git a/Wpf_AiSportsMicrospace/Dto/GroupJumpRopeContext.cs b/Wpf_AiSportsMicrospace/Dto/GroupJumpRopeContext.cs index a396dd7..98dbf52 100644 --- a/Wpf_AiSportsMicrospace/Dto/GroupJumpRopeContext.cs +++ b/Wpf_AiSportsMicrospace/Dto/GroupJumpRopeContext.cs @@ -69,6 +69,7 @@ namespace Dto public int Rank { get; set; } public string Name { get; set; } public int Number { get; set; } + public double Score { get; set; } } - } + diff --git a/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs b/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs index 5da4829..3f01bd6 100644 --- a/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs +++ b/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; -using System.Drawing; +using System.Linq; using System.Text; +using System.Windows.Controls; +using System.Windows.Shapes; using Wpf_AiSportsMicrospace.MyUserControl; using Yztob.AiSports.Postures.Sports; @@ -14,7 +16,7 @@ namespace Dto public List<(double XNorm, double YNorm)> CirclePositions { get; private set; } public List UserList { get; private set; } public List UserNumberList { get; private set; } - public List UserScoreList { get; private set; } + public List UserScoreList { get; private set; } public List Sports { get; private set; } public List UseNameList { get; private set; } = new() { "一号位", "二号位" }; public List UserBeatSyncList = new List(); @@ -53,13 +55,12 @@ namespace Dto CirclePositions = new List<(double XNorm, double YNorm)> { (0.21, 0.88), - (0.78, 0.88), + (0.50, 0.88), }; UserList = new List(); UserNumberList = new List(); Sports = new List(); - } // 更新排行榜方法 @@ -83,5 +84,47 @@ namespace Dto return rankList; } + public List UpdateScoreRankList() + { + int totalBeats = MusicBeats.Count; // 总节拍数 + const double maxScore = 100.0; // 满分 100 + + var rankList = UserBeatSyncList + .Select((beatCount, index) => new + { + Index = index, + Name = UseNameList[index], + BeatCount = beatCount, + // 按照 100 分制计算得分 + Score = Math.Round((beatCount / (double)totalBeats) * maxScore, 1) + }) + .OrderByDescending(x => x.Score) + .Select((x, rank) => new RankItem + { + Rank = rank + 1, + Name = x.Name, + Number = x.BeatCount, // 卡点个数 + Score = x.Score // 总分(百分制) + }) + .ToList(); + + // 同步更新 UserScoreList + UserScoreList = rankList.Select(x => x.Score).ToList(); + return rankList; + } + } + public class UserBeatBar + { + public Canvas Canvas { get; set; } // 用于显示节拍条的 Canvas + public List BeatPoints { get; set; } // 每个节拍点 + public List BeatTimes { get; set; } + public bool IsLeftToRight { get; set; } // 滑动方向 + public double BarWidth { get; set; } // 横杠宽度 + public double BarHeight { get; set; } // 横杠高度 + + public UserBeatBar() + { + BeatPoints = new List(); + } } } diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/BeatDot.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/BeatDot.cs new file mode 100644 index 0000000..704ed38 --- /dev/null +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/BeatDot.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text; +using System.Windows; +using System.Windows.Media; + +namespace Views.JumpRope +{ + public class BeatItem : INotifyPropertyChanged + { + private Brush _color = Brushes.Black; + public double X { get; set; } // 横坐标 + public Brush Color + { + get => _color; + set { _color = value; OnPropertyChanged(nameof(Color)); } + } + + public event PropertyChangedEventHandler PropertyChanged; + protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + +} diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/BeatMarginConverter.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/BeatMarginConverter.cs new file mode 100644 index 0000000..917f741 --- /dev/null +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/BeatMarginConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Windows; +using System.Windows.Data; + +namespace Views.JumpRope +{ + public class BeatMarginConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is double leftMargin) + { + return new Thickness(leftMargin, 0, 0, 0); + } + return new Thickness(0); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml index 14d5cea..52d3bf4 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml @@ -17,7 +17,9 @@ Width="615" Margin="0,0,0,0" /> - + + + @@ -33,5 +35,28 @@ TextAlignment="Center" /> + + + + + + + + + + + + + + + + + diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs index 5e84d90..937cf72 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs @@ -2,6 +2,8 @@ using Enum; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Text; using System.Windows; @@ -15,6 +17,7 @@ using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; +using Views.JumpRope; using Wpf_AiSportsMicrospace.Common; using Wpf_AiSportsMicrospace.Enum; using Wpf_AiSportsMicrospace.MyUserControl; @@ -64,15 +67,23 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope }; private List _musicBeatTextBlock = new List(); + // 容忍时间(节拍误差) public double _beatTolerance = 0.15; // ±150ms + // 滚动显示的节拍点集合 + public ObservableCollection BeatDisplayLeft { get; set; } = new(); + public ObservableCollection BeatDisplayRight { get; set; } = new(); + public MusicJumpRope() { InitializeComponent(); Loaded += UserControl_Loaded; Unloaded += UserControl_Unloaded; _musicJumpRopeContext = new MusicJumpRopeContext(); + + // 初始化节拍点数据 + InitBeatDots(); } private async void UserControl_Loaded(object sender, RoutedEventArgs e) @@ -252,11 +263,8 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope { countdownText.Text = "GO!"; countdownGrid.Visibility = Visibility.Hidden; - - // 播放背景音乐(循环) - Utils.PlayBackgroundMusic("1.MP3", true); // 启动60秒倒计时(独立任务) - StartGameCountdown(108); + StartGameCountdown(105); } private async void StartGameCountdown(int seconds) @@ -265,29 +273,42 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope countdownGrid.Visibility = Visibility.Visible; + // 播放背景音乐(循环) + Utils.PlayBackgroundMusic("1.MP3", true); + + //StartBeatScrollTimer(); + for (int i = seconds; i >= 0; i--) { countdownText.Text = i.ToString(); await Task.Delay(1000); } + // 停止音乐 + Utils.StopBackgroundMusic(); + // 停止实时检测和视频流 + countdownGrid.Visibility = Visibility.Hidden; - - _musicJumpRopeContext.UserList.ForEach(x => - { - x.ImageState = "1"; - }); - _mainWin.HumanFrameUpdated -= OnHumanFrameUpdated; _mainWin.WebcamClient.StopExtract(); - RankingItemList = _musicJumpRopeContext.UpdateRankList(); - //ShowRankingBoard(RankingItemList); - Utils.StopBackgroundMusic(); + // 计算排名 + var rankList = _musicJumpRopeContext.UpdateRankList(); + var scoreList = _musicJumpRopeContext.UpdateScoreRankList(); + + // 更新游戏状态 _currentGameState = GameState.Finished; - _mainWin.HumanFrameUpdated += OnHumanFrameUpdated; - _mainWin.WebcamClient.StartExtract(); + + // 恢复视频流 + Application.Current.Dispatcher.Invoke(() => + { + _mainWin.HumanFrameUpdated += OnHumanFrameUpdated; + _mainWin.WebcamClient.StartExtract(); + + // 更新 UI(排行榜或分数板) + // ShowRankingBoard(rankList); + }); } private DateTime? _raiseStartTime; @@ -435,6 +456,8 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope _musicJumpRopeContext.Sports.Clear(); _musicJumpRopeContext.UserList.Clear(); _musicJumpRopeContext.UserNumberList.Clear(); + _musicJumpRopeContext.UserBeatSyncList.Clear(); + _musicBeatTextBlock.Clear(); double imgWidth = userBox.ActualWidth; double imgHeight = userBox.ActualHeight; @@ -467,29 +490,62 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope if (userItem.ImageState != "2") userItem.ImageState = "2"; - var _currentTime = Utils.GetMusicCurrentTime(); + var currentTime = Utils.GetMusicCurrentTime(); + var beats = _musicJumpRopeContext.MusicBeats["1"]; - bool isOnBeat = _musicBeats.Any(bt => Math.Abs(bt - _currentTime) <= _beatTolerance); - if (isOnBeat) + for (int j = 0; j < beats.Count; j++) { - _musicJumpRopeContext.UserBeatSyncList[i]++; - - if (_musicJumpRopeContext.UserBeatSyncList[i] < count) + int indexCopy2 = j; // 复制一份当前循环索引 + if (Math.Abs(beats[indexCopy2] - currentTime) <= _beatTolerance) { - _musicBeatTextBlock[i].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[i]}"; + _musicJumpRopeContext.UserBeatSyncList[indexCopy]++; + Application.Current.Dispatcher.BeginInvoke(() => + { + if (indexCopy == 0) + { + ((Ellipse)beatCanvasLeft.Children[indexCopy2]).Fill = Brushes.Red; + } + else + { + ((Ellipse)beatCanvasRight.Children[indexCopy2]).Fill = Brushes.Red; + } + _musicBeatTextBlock[indexCopy].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[indexCopy]}"; + }); } - - //Application.Current.Dispatcher.BeginInvoke(() => - //{ - // _musicBeatTextBlock[i].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[i]}"; - //}); } + // 滚动条跳跃到当前点附近 + UpdateBeatScrollSudden(currentTime); }; sport.Start(); _musicJumpRopeContext.Sports.Add(sport); } } + + 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 humans) { double radiusNormX = 0.07; @@ -536,10 +592,8 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope } // 推送计数 - if (hasHuman) - _musicJumpRopeContext.Sports[i].Pushing(humanInCircle); - - + //if (hasHuman) + _musicJumpRopeContext.Sports[i].Pushing(humanInCircle); } } private string GetJumpState(int circleIndex, bool humanInCircle) @@ -587,7 +641,7 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope userItem.VerticalAlignment = VerticalAlignment.Top; userItem.HorizontalAlignment = HorizontalAlignment.Left; userItem.ImageState = "1"; - userItem.Margin = new Thickness(centerX - 120, centerY - 700, 0, 0); + userItem.Margin = new Thickness(centerX + 120, centerY - 700, 0, 0); // ----------- 创建上方 TextBlock ------------ var textBlock = new TextBlock @@ -618,5 +672,84 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope return userItem; } + private List leftDots = new(); + private List 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 double totalTime = 108.455; // 音乐总时长 + private void StartBeatScrollTimer() + { + if (_beatScrollTimer != null) _beatScrollTimer.Stop(); + + _beatScrollTimer = new DispatcherTimer(); + _beatScrollTimer.Interval = TimeSpan.FromMilliseconds(1000); + _beatScrollTimer.Tick += (s, e) => + { + double currentTime = Utils.GetMusicCurrentTime(); + + double progress = currentTime / totalTime; + double maxOffsetLeft = beatCanvasLeft.Width - BeatScrollLeft.ViewportWidth; + double maxOffsetRight = beatCanvasRight.Width - BeatScrollRight.ViewportWidth; + BeatScrollLeft.ScrollToHorizontalOffset(progress * (beatCanvasLeft.Width - BeatScrollLeft.ViewportWidth)); + BeatScrollRight.ScrollToHorizontalOffset((1 - progress) * (beatCanvasRight.Width - BeatScrollRight.ViewportWidth)); + }; + _beatScrollTimer.Start(); + } } }