# Conflicts:
#	Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml
This commit is contained in:
ltx 2025-10-18 15:16:45 +08:00
commit 3bd2e0c5b4
15 changed files with 366 additions and 188 deletions

View File

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

View File

@ -155,7 +155,7 @@ namespace Wpf_AiSportsMicrospace.Common
}
var duration = DateTime.Now - _raiseStartTime.Value;
if (duration.TotalSeconds >= 3)
if (duration.TotalSeconds >= 1)
{
_raiseStartTime = null;
_wristStartTime = null;

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

@ -50,6 +50,8 @@ namespace Dto
}
};
public Dictionary<int, List<double>> MusicBeatsDic;
public MusicJumpRopeContext()
{
CirclePositions = new List<(double XNorm, double YNorm)>
@ -58,6 +60,8 @@ namespace Dto
(0.50, 0.88),
};
MusicBeatsDic = MusicBeats["1"].GroupBy(b => (int)Math.Ceiling(b) - 1).ToDictionary(g => g.Key, g => g.ToList());
UserList = new List<SportUserItem>();
UserNumberList = new List<string>();
Sports = new List<SportBase>();

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

@ -38,9 +38,9 @@
</Grid>
<!-- 矩形进度条 Stroke="{Binding ProgressColor}" -->
<Path StrokeThickness="13" Visibility="{Binding IsSelected, Converter={StaticResource BoolToVisibilityConverter}}" StrokeStartLineCap="Round" StrokeEndLineCap="Round"
Data="{Binding Progress, Converter={StaticResource ProgressToRectangleGeometryConverter}, ConverterParameter='372,456,22,13'}">
<Path StrokeThickness="13" Visibility="{Binding IsSelected, Converter={StaticResource BoolToVisibilityConverter}}" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat"
Data="{Binding Progress, Converter={StaticResource ProgressToRectangleGeometryConverter}, ConverterParameter='372,456,22,13'}">
<Path.Stroke>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="{Binding ProgressColor1}"/>

View File

@ -116,7 +116,7 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
{
if (_currentItem.Progress < 1)
{
_currentItem.Progress += 0.00556; // 控制速度
_currentItem.Progress += 0.01; // 控制速度
if (spark != null)
{
var pos = GetSparkPosition(_currentItem.Progress, 150, 200);

View File

@ -0,0 +1,25 @@
<UserControl x:Class="Wpf_AiSportsMicrospace.MyUserControl.WxProgressBar"
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"
x:Name="root"
Height="30" Width="600">
<Canvas>
<Border x:Name="LeftBar" Background="#95dfce" HorizontalAlignment="Left" Height="30"/>
<Border x:Name="RightBar" Background="#e0585b" HorizontalAlignment="Right" Height="30"/>
<!-- 中间分割线图片 -->
<Image x:Name="MiddleLine"
Source="/Resources/Img/Album/vs.png"
Width="30" Height="50" Panel.ZIndex="100"/>
<!-- 中间百分比文本 -->
<TextBlock x:Name="ProgressTextBlock"
Foreground="White"
FontSize="14"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Canvas>
</UserControl>

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
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.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wpf_AiSportsMicrospace.MyUserControl
{
/// <summary>
/// WxProgressBar.xaml 的交互逻辑
/// </summary>
public partial class WxProgressBar : UserControl
{
public WxProgressBar()
{
InitializeComponent();
this.SizeChanged += WxProgressBar_SizeChanged;
}
private void WxProgressBar_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateVisual();
}
#region
private double _leftProgress;
public double LeftProgress
{
get => _leftProgress;
set
{
_leftProgress = Clamp(value, 0, 1);
_rightProgress = 1 - _leftProgress;
UpdateVisual();
}
}
private double _rightProgress;
public double RightProgress
{
get => _rightProgress;
set
{
_rightProgress = Clamp(value, 0, 1);
_leftProgress = 1 - _rightProgress;
UpdateVisual();
}
}
#endregion
private void UpdateVisual()
{
double totalWidth = ActualWidth;
double totalHeight = ActualHeight;
if (totalWidth <= 0 || totalHeight <= 0) return;
double barHeight = 30; // 进度条高度
double middleLineHeight = MiddleLine.Height;
// 左侧红条
LeftBar.Width = totalWidth * _leftProgress;
LeftBar.Height = barHeight;
Canvas.SetLeft(LeftBar, 0);
Canvas.SetTop(LeftBar, (totalHeight - barHeight) / 2);
// 右侧绿条
RightBar.Width = totalWidth * _rightProgress;
RightBar.Height = barHeight;
Canvas.SetLeft(RightBar, totalWidth - RightBar.Width);
Canvas.SetTop(RightBar, (totalHeight - barHeight) / 2);
// 分割线居中在红绿交界点
double middleX = LeftBar.Width - MiddleLine.Width / 2;
if (middleX < 0) middleX = 0;
if (middleX > totalWidth - MiddleLine.Width) middleX = totalWidth - MiddleLine.Width;
Canvas.SetLeft(MiddleLine, middleX);
Canvas.SetTop(MiddleLine, (totalHeight - middleLineHeight) / 2);
// 百分比文本居中显示
ProgressTextBlock.Text = $"{Math.Round(_leftProgress * 100)}% : {Math.Round(_rightProgress * 100)}%";
Canvas.SetLeft(ProgressTextBlock, (totalWidth - ProgressTextBlock.ActualWidth) / 2);
Canvas.SetTop(ProgressTextBlock, (totalHeight - ProgressTextBlock.ActualHeight) / 2);
}
private double Clamp(double value, double min, double max) => value < min ? min : value > max ? max : value;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -216,7 +216,7 @@ namespace Wpf_AiSportsMicrospace.Views
}
}
private int _currentCountdown = 3;
private double _currentCountdown = 3600;
private DateTime _lastUpdateTime = DateTime.Now;
private void StartCountdown(int start = 3)
{
@ -233,10 +233,10 @@ namespace Wpf_AiSportsMicrospace.Views
private void UpdateCountdown()
{
if ((DateTime.Now - _lastUpdateTime).TotalSeconds >= 1)
if ((DateTime.Now - _lastUpdateTime).TotalMilliseconds >= 600)
{
_lastUpdateTime = DateTime.Now;
_currentCountdown--;
_currentCountdown -= 600;
if (_currentCountdown > 0)
{

View File

@ -35,29 +35,9 @@
TextAlignment="Center"
/>
</Grid>
<Grid x:Name="BottomBeatPanel" VerticalAlignment="Bottom" Height="200" Panel.ZIndex="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="600"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左边滚动条 -->
<ScrollViewer x:Name="BeatScrollLeft" Width="660" Height="80"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
Grid.Column="0">
<Canvas x:Name="beatCanvasLeft" Height="100"/>
</ScrollViewer>
<!-- 右边滚动条 -->
<ScrollViewer x:Name="BeatScrollRight" Width="660" Height="80"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
Grid.Column="2">
<Canvas x:Name="beatCanvasRight" Height="100"/>
</ScrollViewer>
</Grid>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" >
<local:WxProgressBar x:Name="PkBar"/>
</StackPanel>
<!--<Grid Width="3" Height="140" Margin="00,600,580,0" Background="#fedd40" ClipToBounds="False"></Grid>
<Grid Width="580" Height="87" Margin="00,600,0,0" Background="#25ffffff" ClipToBounds="False">

View File

@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@ -37,40 +38,13 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
private readonly object _updateLock = new object();
private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>();
private MusicJumpRopeContext _musicJumpRopeContext;
private GameState _currentGameState = GameState.NotStarted;
List<RankItem> RankingItemList = new();
private List<double> _musicBeats = new List<double>()
{
0.545,1.09,1.635,2.18,2.725,3.27,3.815,4.36,4.905,5.45,
5.995,6.54,7.085,7.63,8.175,8.72,9.265,9.81,10.355,10.9,
11.445,11.99,12.535,13.08,13.625,14.17,14.715,15.26,15.805,16.35,
16.895,17.44,17.985,18.53,19.075,19.62,20.165,20.71,21.255,21.8,
22.345,22.89,23.435,23.98,24.525,25.07,25.615,26.16,26.705,27.25,
27.795,28.34,28.885,29.43,29.975,30.52,31.065,31.61,32.155,32.7,
33.245,33.79,34.335,34.88,35.425,35.97,36.515,37.06,37.605,38.15,
38.695,39.24,39.785,40.33,40.875,41.42,41.965,42.51,43.055,43.6,
44.145,44.69,45.235,45.78,46.325,46.87,47.415,47.96,48.505,49.05,
49.595,50.14,50.685,51.23,51.775,52.32,52.865,53.41,53.955,54.5,
55.045,55.59,56.135,56.68,57.225,57.77,58.315,58.86,59.405,59.95,
60.495,61.04,61.585,62.13,62.675,63.22,63.765,64.31,64.855,65.4,
65.945,66.49,67.035,67.58,68.125,68.67,69.215,69.76,70.305,70.85,
71.395,71.94,72.485,73.03,73.575,74.12,74.665,75.21,75.755,76.3,
76.845,77.39,77.935,78.48,79.025,79.57,80.115,80.66,81.205,81.75,
82.295,82.84,83.385,83.93,84.475,85.02,85.565,86.11,86.655,87.2,
87.745,88.29,88.835,89.38,89.925,90.47,91.015,91.56,92.105,92.65,
93.195,93.74,94.285,94.83,95.375,95.92,96.465,97.01,97.555,98.1,
98.645,99.19,99.735,100.28,100.825,101.37,101.915,102.46,103.005,103.55,
104.095,104.64,105.185,105.73,106.275,106.82,107.365,107.91,108.455
};
private List<TextBlock> _musicBeatTextBlock = new List<TextBlock>();
// 容忍时间(节拍误差)
public double _beatTolerance = 0.15; // ±150ms
private int _totalDots = 0;
// 滚动显示的节拍点集合
public ObservableCollection<BeatItem> BeatDisplayLeft { get; set; } = new();
public ObservableCollection<BeatItem> BeatDisplayRight { get; set; } = new();
@ -81,9 +55,6 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded;
_musicJumpRopeContext = new MusicJumpRopeContext();
// 初始化节拍点数据
InitBeatDots();
}
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
@ -91,6 +62,18 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
DrawCirclesWithText();
// 播放音乐
PlayMusic("raisehand.mp3");
Random rnd = new Random();
// 模拟 20 次随机更新
for (int i = 0; i < 200; i++)
{
// 随机左右进度变化,保证总和=1
double left = rnd.NextDouble(); // 0~1
PkBar.LeftProgress = left; // RightProgress 自动 1-left
await Task.Delay(1000); // 200ms 间隔不卡UI线程
}
}
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
@ -276,8 +259,6 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
// 播放背景音乐(循环)
Utils.PlayBackgroundMusic("1.MP3", true);
//StartBeatScrollTimer();
for (int i = seconds; i >= 0; i--)
{
countdownText.Text = i.ToString();
@ -491,30 +472,29 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
userItem.ImageState = "2";
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; // 复制一份当前循环索引
if (Math.Abs(beats[indexCopy2] - currentTime) <= _beatTolerance)
{
_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]}";
});
}
_musicJumpRopeContext.UserBeatSyncList[indexCopy]++;
_musicBeatTextBlock[indexCopy].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[indexCopy]}";
//if (indexCopy == 0)
// LeftBeats.SetSelected(currentSecond, true);
//else
// RightBeats.SetSelected(currentSecond, true);
//Application.Current.Dispatcher.BeginInvoke(() =>
//{
// if (indexCopy == 0)
// LeftBeats.SetSelected(currentSecond, true);
// else
// RightBeats.SetSelected(currentSecond, true);
//});
}
// 滚动条跳跃到当前点附近
UpdateBeatScrollSudden(currentTime);
};
sport.Start();
@ -522,30 +502,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)
{
double radiusNormX = 0.07;
@ -672,84 +628,5 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
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 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();
}
}
}

View File

@ -28,6 +28,7 @@
<None Remove="Resources\Img\Album\home_play.png" />
<None Remove="Resources\Img\Album\home_test.png" />
<None Remove="Resources\Img\Album\title.png" />
<None Remove="Resources\Img\Album\vs.png" />
<None Remove="Resources\Img\Badge\1.jpg" />
<None Remove="Resources\Img\Badge\2.jpg" />
<None Remove="Resources\Img\Badge\3.jpg" />
@ -208,6 +209,9 @@
<Resource Include="Resources\Img\Album\title.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\vs.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Badge\1.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>