Compare commits

...

5 Commits

Author SHA1 Message Date
ltx
4ef4da7781 update:合并 2025-09-26 16:48:35 +08:00
ltx
66d6b237d1 update:ui 2025-09-26 16:46:30 +08:00
ltx
3d05aa48be update:
大小&位置&背景
2025-09-25 09:37:53 +08:00
ltx
06900606c8 Merge branch 'dev' of http://8.153.108.90:3000/YD/Wpf_AiSportsMicrospace into dev 2025-09-23 16:05:59 +08:00
ltx
d7cdcbe411 1604 2025-09-23 16:05:56 +08:00
24 changed files with 853 additions and 154 deletions

View File

@ -0,0 +1,193 @@
using HandyControl.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Wpf_AiSportsMicrospace.Enum;
using Yztob.AiSports.Inferences.Abstractions;
using Yztob.AiSports.Inferences.Things;
using Yztob.AiSports.Postures;
using Yztob.AiSports.Postures.Abstractions;
using Yztob.AiSports.Postures.Sports;
using Yztob.AiSports.Postures.Things;
using Yztob.AiSports.Sensors.Abstractions;
using Yztob.AiSports.Sensors.Things;
namespace Wpf_AiSportsMicrospace.Common
{
/// <summary>
/// 运动检测操作类
/// </summary>
public class SportOperate
{
IPointTracker _leftTracker;
IPointTracker _rightTracker;
IPointTracker _leftElbow;
IPointTracker _rightElbow;
WebcamClient _webcamClient;
private DateTime _lastActionTime = DateTime.MinValue;
private Point? _lastLeftWrist = null;
private Point? _lastRightWrist = null;
private DateTime? _raiseStartTime = null;
private DateTime? _wristStartTime = null;
public SportOperate()
{
_leftTracker = PostureCalculate.CreatePointTracker("left_wrist", 0);
_rightTracker = PostureCalculate.CreatePointTracker("right_wrist", 0);
_leftElbow = PostureCalculate.CreatePointTracker("left_elbow", 0);
_rightElbow = PostureCalculate.CreatePointTracker("right_elbow", 0);
//_leftTracker.Amplitude = 0.05f;
//_rightTracker.Amplitude = 0.05f;
//_leftElbow.Amplitude = 0.05f;
//_rightElbow.Amplitude = 0.05f;
}
public WebcamClient CreateRTSP()
{
_webcamClient = WebcamClient.CreateRTSP("192.168.3.64", "admin", "yd708090", 554u);
return _webcamClient;
}
public int VerifyWavingAction(Human human)
{
var leftWrist = human.Keypoints.FirstOrDefault(x => x.Name == "left_wrist");
var leftElbow = human.Keypoints.FirstOrDefault(x => x.Name == "left_elbow");
var rightWrist = human.Keypoints.FirstOrDefault(x => x.Name == "right_wrist");
var rightElbow = human.Keypoints.FirstOrDefault(x => x.Name == "right_elbow");
// 左手逻辑
if (leftWrist != null && leftElbow != null)
{
var result = RecognizeLeftHandGesture(
new Point(leftWrist.X, leftWrist.Y),
new Point(leftElbow.X, leftElbow.Y));
if (result != 0) return result;
}
// 右手逻辑
if (rightWrist != null && rightElbow != null)
{
var result = RecognizeRightHandGesture(
new Point(rightWrist.X, rightWrist.Y),
new Point(rightElbow.X, rightElbow.Y));
if (result != 0) return result;
}
return 0;
}
/// <summary>
/// 统一的水平挥手检测
/// </summary>
private int DetectHorizontalWave(Point wrist, Point elbow, Point? lastWrist, bool isLeft)
{
if (lastWrist != null)
{
double dx = wrist.X - lastWrist.Value.X;
double dy = Math.Abs(wrist.Y - lastWrist.Value.Y);
// 挥手:水平位移明显,垂直位移小,且接近肘部水平
if (Math.Abs(dx) > 30 && dy < 40 && Math.Abs(wrist.Y - elbow.Y) < 100)
{
if (CheckCooldown())
{
if (isLeft && dx > 0)
return (int)WavingAction.LeftWave; // 左手往右挥
if (!isLeft && dx < 0)
return (int)WavingAction.RightWave; // 右手往左挥
}
}
}
return (int)WavingAction.None;
}
/// <summary>
/// 识别左手动作
/// </summary>
public int RecognizeLeftHandGesture(Point wrist, Point elbow)
{
int result = DetectHorizontalWave(wrist, elbow, _lastLeftWrist, true);
_lastLeftWrist = wrist; // 更新记录
return result;
}
private bool _firstHandTriggered = false;
/// <summary>
/// 识别右手动作
/// </summary>
/// <param name="wrist"></param>
/// <param name="elbow"></param>
/// <returns></returns>
public int RecognizeRightHandGesture(Point wrist, Point elbow)
{
// --- 先判断水平挥手 ---
int waveResult = DetectHorizontalWave(wrist, elbow, _lastRightWrist, false);
_lastRightWrist = wrist; // 更新记录
if (waveResult != (int)WavingAction.None)
return waveResult;
// --- 举手逻辑 ---
double verticalRise = elbow.Y - wrist.Y; // 手腕在肘上方 → 正值
if (verticalRise > 100) // 举手阈值
{
// 初始化计时
if (_raiseStartTime == null)
_raiseStartTime = DateTime.Now;
if (_wristStartTime == null)
_wristStartTime = DateTime.Now;
var wristDuration = DateTime.Now - _wristStartTime.Value;
// 保持 >1 秒才触发一次 FirstHand
if (!_firstHandTriggered && wristDuration.TotalSeconds >= 1)
{
_firstHandTriggered = true;
return (int)WavingAction.FirstHand; // 举手开始,只触发一次
}
// 判断是否完成3秒举手
var duration = DateTime.Now - _raiseStartTime.Value;
if (duration.TotalSeconds >= 4)
{
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false; // 重置状态
return (int)WavingAction.RaiseHand; // 举手完成
}
else
{
return (int)WavingAction.Raising; // 举手中
}
}
else
{
// 手放下,重置计时和状态
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false;
}
return (int)WavingAction.None;
}
/// <summary>
/// 冷却防抖(避免重复触发)
/// </summary>
private bool CheckCooldown(int cooldownMs = 1000)
{
if ((DateTime.Now - _lastActionTime).TotalMilliseconds < cooldownMs)
return false;
_lastActionTime = DateTime.Now;
return true;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wpf_AiSportsMicrospace.Enum
{
public enum WavingAction
{
None = 0,
LeftWave = 1,
RightWave = 2,
FirstHand = 3, // 举手开始
Raising = 4, // 举手中
RaiseHand = 5 // 举手完成
}
}

View File

@ -2,8 +2,30 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.MyUserControl"
Title="Home" Height="600" Width="800" Loaded="Window_Loaded">
<Grid>
<local:CoverFlowControl1 x:Name="coverFlow" HorizontalAlignment="Center" VerticalAlignment="Center"/>
Title="Home" Height="1080" Width="1920" Loaded="Window_Loaded">
<Grid Height="1065" VerticalAlignment="Bottom">
<Grid.Background>
<ImageBrush ImageSource="/Resources/Img/Album/home_bg.png" Stretch="UniformToFill"/>
</Grid.Background>
<!-- 顶部图片 -->
<Image
Source="/Resources/Img/Album/title.png"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="615"
Margin="0,100,0,0"
/>
<!-- CoverFlowControl距离图片80 -->
<local:CoverFlowControl
x:Name="coverFlow"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Height="900"
Width="1920"
Margin="0,250,0,0"
Padding="0,100"
/>
</Grid>
</Window>

View File

@ -4,6 +4,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@ -13,7 +14,9 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Wpf_AiSportsMicrospace.Common;
using Wpf_AiSportsMicrospace.Enum;
using Wpf_AiSportsMicrospace.MyUserControl;
using Wpf_AiSportsMicrospace.Views;
using Yztob.AiSports.Inferences.Abstractions;
using Yztob.AiSports.Postures;
using Yztob.AiSports.Postures.Abstractions;
@ -30,19 +33,10 @@ namespace Wpf_AiSportsMicrospace
public partial class Home : Window
{
private IHumanPredictor _humanPredictor;
private IObjectDetector _objectDetector;
private HumanGraphicsRenderer _humanGraphicsRenderer;
private readonly List<SportDescriptor> _sports;
private SportBase _sport;
private readonly SportDetectionQueue _detectQueue;
private IPointTracker _leftTracker;
private IPointTracker _rightTracker;
private IPointTracker _leftElbow;
private IPointTracker _rightElbow;
private WebcamClient _webcamClient;
private ConcurrentQueue<VideoFrame> _frameQueue = new();
private CancellationTokenSource _cts = new();
private DateTime _lastSlideTime = DateTime.MinValue;
private SportOperate _sportOperate;
public Home()
{
@ -59,49 +53,73 @@ namespace Wpf_AiSportsMicrospace
string albumPath = Path.Combine(projectRoot, "Resources", "Img", "Album");
// 转换为 Uri
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "1.jpg")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "2.jpg")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "3.jpg")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_play.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_test.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_history.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "4.jpg")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "5.jpg")));
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "1.jpg")) });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "2.jpg")) });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "3.jpg")) });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "4.jpg")) });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "5.jpg")) });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_test.png")) , ProgressColor1 = "#215bc7" , ProgressColor2 = "#fc640e" });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_play.png")), ProgressColor1 = "#e73d42", ProgressColor2 = "#fd8212" });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_history.png")), ProgressColor1 = "#e73d42", ProgressColor2 = "#215bc7" });
// 默认选中第3张
coverFlow.SelectedIndex = 2;
coverFlow.SelectedIndex = 0;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_leftTracker = PostureCalculate.CreatePointTracker("left_wrist", 0);
_rightTracker = PostureCalculate.CreatePointTracker("right_wrist", 0);
_leftElbow = PostureCalculate.CreatePointTracker("left_elbow", 0);
_rightElbow = PostureCalculate.CreatePointTracker("right_elbow", 0);
_sportOperate = new SportOperate();
_webcamClient = _sportOperate.CreateRTSP();
_leftTracker.Amplitude = 0.05f;
_rightTracker.Amplitude = 0.05f;
_leftElbow.Amplitude = 0.05f;
_rightElbow.Amplitude = 0.05f;
StartRTSP();
StartFrameProcessing();
}
private void StartRTSP()
{
_webcamClient = WebcamClient.CreateRTSP("192.168.3.64", "admin", "yd708090", 554u);
_webcamClient.OnExtractFrame += frame =>
{
if (frame != null)
_frameQueue.Enqueue(frame);
};
_webcamClient.StartExtract();
StartFrameProcessing();
coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
}
private void CoverFlow_ProgressCompleted(CoverFlowItem item)
{
// 停止抽帧线程/释放资源
try
{
_cts.Cancel(); // 停止后台处理线程
_webcamClient?.StopExtract(); // 停止摄像头抽帧
_webcamClient = null;
}
catch (Exception ex)
{
Debug.WriteLine($"停止抽帧异常: {ex.Message}");
}
// 解绑事件,防止重复触发
//coverFlow.ProgressCompleted -= CoverFlow_ProgressCompleted;
// 根据图片跳转新窗口
string uri = item.ImageUri.ToString();
Window newWindow = null;
if (uri.EndsWith("1.jpg"))
newWindow = new GroupJumpRope();
else if (uri.EndsWith("2.jpg"))
newWindow = new GroupJumpRope();
else if (uri.EndsWith("3.jpg"))
newWindow = new GroupJumpRope();
if (newWindow != null)
{
Dispatcher.BeginInvoke(() =>
{
newWindow.Show(); // 先显示新窗口
this.Close(); // 再关闭当前窗口
});
}
}
private void StartFrameProcessing()
@ -116,6 +134,15 @@ namespace Wpf_AiSportsMicrospace
}
else
{
//_webcamClient.OnExtractFrame += frame =>
//{
// if (frame != null)
// _frameQueue.Enqueue(frame);
//};
//_webcamClient.StartExtract();
_webcamClient.StartExtract();
Thread.Sleep(5);
}
}
@ -128,55 +155,60 @@ namespace Wpf_AiSportsMicrospace
{
var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray();
var humanResult = _humanPredictor.Predicting(buffer, frame.Number);
var humans = humanResult?.Humans?.ToList();
if (humans == null || humans.Count == 0) return;
if (humans == null || humans.Count == 0)
return;
//var human = humans
// .Where(h =>
// h.Keypoints.Any(kp => kp.Name == "left_ankle" && kp.X < 1020 && kp.Y > 900 && kp.Y < 1020) &&
// h.Keypoints.Any(kp => kp.Name == "right_ankle" && kp.X > 750 && kp.Y > 900 && kp.Y < 1020)
// )
// .FirstOrDefault();
var human = humans.FirstOrDefault();
if (human == null) return;
var leftResult = _leftTracker.Tracking(human);
var rightResult = _rightTracker.Tracking(human);
var leftElbowResult = _leftElbow.Tracking(human);
var rightElbowResult = _rightElbow.Tracking(human);
//检测挥手动作
var wavingaction = _sportOperate.VerifyWavingAction(human);
if ((DateTime.Now - _lastSlideTime).TotalMilliseconds < 500) return;
_lastSlideTime = DateTime.Now;
// 把低 8 位作为动作类型,高 8 位作为进度
int actionType = wavingaction & 0xFF;
int progress = (wavingaction >> 8) & 0xFF;
if (leftResult != 0 && leftElbowResult != 1)
Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
if (rightResult != 0 && rightElbowResult != 1)
Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
switch (actionType)
{
case (int)WavingAction.LeftWave: // 左手挥动
Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
break;
case (int)WavingAction.RightWave: // 右手挥动
Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
break;
case (int)WavingAction.FirstHand: // 举手开始
Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
break;
case (int)WavingAction.Raising: // 举手中,实时更新进度
break;
case (int)WavingAction.RaiseHand: // 举手完成
_cts.Cancel();
break;
default: // 没有动作 → 取消进度
Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
break;
}
}
catch (Exception ex)
{
Console.WriteLine("ProcessFrame error: " + ex.Message);
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_cts.Cancel();
_webcamClient?.StopExtract();
}
/// <summary>
/// 更新 UI线程安全
/// </summary>
private void SlideCoverFlow(Action slideAction)
{
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
try
{
slideAction?.Invoke();
}
catch (Exception ex)
{
Console.WriteLine("UI update error: " + ex.Message);
}
}), System.Windows.Threading.DispatcherPriority.Background);
}
}
}

View File

@ -4,8 +4,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.MyUserControl"
Height="300" Width="600">
<Grid ClipToBounds="True" Background="Transparent">
Width="1080">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<local:ProgressToRectangleGeometryConverter x:Key="ProgressToRectangleGeometryConverter"/>
</UserControl.Resources>
<Grid ClipToBounds="False" Background="Transparent">
<ItemsControl x:Name="ItemsHost" ItemsSource="{Binding Images, RelativeSource={RelativeSource AncestorType=UserControl}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -15,15 +19,36 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="ImageContainer" RenderTransformOrigin="0.5,0.5"
MouseLeftButtonDown="Image_MouseLeftButtonDown">
<Border x:Name="ImageContainer" RenderTransformOrigin="0.5,0.5" CornerRadius="28"
MouseLeftButtonDown="Image_MouseLeftButtonDown" Background="#fff">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="scale" ScaleX="1" ScaleY="1"/>
<TranslateTransform x:Name="translate" X="0" Y="0"/>
</TransformGroup>
</Border.RenderTransform>
<Image Source="{Binding}" Stretch="UniformToFill" Width="150" Height="200"/>
<Grid ClipToBounds="False">
<Grid Width="372" Height="456" Background="Transparent" >
<Image Source="{Binding ImageUri}" Stretch="UniformToFill" Width="346" Height="430">
<Image.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Image.RenderTransform>
</Image>
</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.Stroke>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="{Binding ProgressColor1}"/>
<GradientStop Offset="0.9" Color="{Binding ProgressColor2}"/>
</LinearGradientBrush>
</Path.Stroke>
</Path>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

@ -16,6 +16,7 @@ using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace Wpf_AiSportsMicrospace.MyUserControl
{
@ -24,7 +25,28 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
/// </summary>
public partial class CoverFlowControl : UserControl
{
public ObservableCollection<Uri> Images { get; set; } = new ObservableCollection<Uri>();
//public ObservableCollection<Uri> Images { get; set; } = new ObservableCollection<Uri>();
public ObservableCollection<CoverFlowItem> Images { get; set; } = new ObservableCollection<CoverFlowItem>();
// 新增事件:进度条完成
public event Action<CoverFlowItem> ProgressCompleted;
// 添加附加属性帮助类
public static class LayoutHelper
{
public static int GetZIndex(DependencyObject obj)
{
return (int)obj.GetValue(ZIndexProperty);
}
public static void SetZIndex(DependencyObject obj, int value)
{
obj.SetValue(ZIndexProperty, value);
}
public static readonly DependencyProperty ZIndexProperty =
DependencyProperty.RegisterAttached("ZIndex", typeof(int), typeof(LayoutHelper), new PropertyMetadata(0));
}
private int _selectedIndex = 0;
public int SelectedIndex
@ -34,23 +56,112 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
{
if (Images.Count == 0) return;
// 循环处理
if (value < 0)
_selectedIndex = Images.Count - 1;
else if (value >= Images.Count)
_selectedIndex = 0;
else
_selectedIndex = value;
if (value < 0) _selectedIndex = Images.Count - 1;
else if (value >= Images.Count) _selectedIndex = 0;
else _selectedIndex = value;
for (int i = 0; i < Images.Count; i++)
{
Images[i].IsSelected = i == _selectedIndex;
//if (SelectedIndex == 0)
//{
// Images[i].ProgressColor = (Brush)new BrushConverter().ConvertFromString("#e73d42")!;
//}
//else if (SelectedIndex == Images.Count - 1)
//{
// Images[i].ProgressColor = (Brush)new BrushConverter().ConvertFromString("#215bc7")!;
//}
//else
//{
// Images[i].ProgressColor = (Brush)new BrushConverter().ConvertFromString("#fc640e")!;
//}
}
UpdateLayoutWithAnimation();
// 启动进度条动画
//var current = Images[_selectedIndex];
//StartProgress(current);
}
}
private EventHandler _renderHandler;
private CoverFlowItem _currentItem;
private void StartProgress(CoverFlowItem item)
{
StopProgress();
_currentItem = item;
_currentItem.Progress = 0;
_renderHandler = (s, e) =>
{
if (_currentItem.Progress < 1)
{
_currentItem.Progress += 1.0 / (3 * 60.0); // 3秒完成一圈假设60帧/s
if (_currentItem.Progress > 1) _currentItem.Progress = 1;
}
else
{
StopProgress();
}
};
CompositionTarget.Rendering += _renderHandler;
}
private void StopProgress()
{
if (_renderHandler != null)
{
CompositionTarget.Rendering -= _renderHandler;
_renderHandler = null;
}
}
public void StartSelectedProgress()
{
if (SelectedIndex >= 0 && SelectedIndex < Images.Count)
{
var current = Images[SelectedIndex];
if (current.Progress >= 1) return; // 已完成,直接返回,不再启动
StartProgress(current);
}
}
public void CancelSelectedProgress()
{
if (SelectedIndex >= 0 && SelectedIndex < Images.Count)
{
var current = Images[SelectedIndex];
// 停止动画/计时
StopProgress();
// 清零当前的进度值
current.Progress = 0;
}
}
public CoverFlowControl()
{
InitializeComponent();
DataContext = this;
Loaded += (s, e) => UpdateLayoutWithAnimation(true);
//var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(5) };
//timer.Tick += (s, e) =>
//{
// if (Images.Count == 0) return;
// var current = Images[_selectedIndex];
// if (current.Progress < 1)
// current.Progress += 0.01; // 调整速度
//};
//timer.Start();
}
private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@ -69,9 +180,9 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
private void UpdateLayoutWithAnimation(bool instant = false)
{
double centerX = ActualWidth / 2;
double spacing = 180;
double sideScale = 0.8;
double centerScale = 1.2;
double spacing = 500;
double sideScale = 1;
double centerScale = 1.33;
for (int i = 0; i < ItemsHost.Items.Count; i++)
{
@ -82,7 +193,7 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
if (border == null) continue;
var transformGroup = border.RenderTransform as TransformGroup;
var scale = transformGroup.Children[0] as ScaleTransform;
var scale = transformGroup!.Children[0] as ScaleTransform;
var translate = transformGroup.Children[1] as TranslateTransform;
double targetX;
@ -91,48 +202,62 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
if (i == SelectedIndex)
{
targetX = centerX - 75;
targetX = centerX - 190;
targetScale = centerScale;
targetOpacity = 1.0;
}
else if (i == SelectedIndex - 1 || (SelectedIndex == 0 && i == Images.Count - 1))
{
// 左边图片,循环处理
targetX = centerX - spacing - 75;
targetX = centerX - spacing - 190;
targetScale = sideScale;
targetOpacity = 1.0;
}
else if (i == SelectedIndex + 1 || (SelectedIndex == Images.Count - 1 && i == 0))
{
// 右边图片,循环处理
targetX = centerX + spacing - 75;
targetX = centerX + spacing - 190;
targetScale = sideScale;
targetOpacity = 1.0;
}
else
{
targetX = centerX - 75;
targetX = centerX - 190;
targetScale = sideScale;
targetOpacity = 0.0;
}
if (instant)
{
translate.X = targetX;
scale.ScaleX = scale.ScaleY = targetScale;
translate!.X = targetX;
scale!.ScaleX = scale.ScaleY = targetScale;
border.Opacity = targetOpacity;
}
else
{
translate.BeginAnimation(TranslateTransform.XProperty,
translate!.BeginAnimation(TranslateTransform.XProperty,
new DoubleAnimation(targetX, TimeSpan.FromMilliseconds(400)) { EasingFunction = new QuadraticEase() });
scale.BeginAnimation(ScaleTransform.ScaleXProperty,
scale!.BeginAnimation(ScaleTransform.ScaleXProperty,
new DoubleAnimation(targetScale, TimeSpan.FromMilliseconds(400)) { EasingFunction = new QuadraticEase() });
scale.BeginAnimation(ScaleTransform.ScaleYProperty,
new DoubleAnimation(targetScale, TimeSpan.FromMilliseconds(400)) { EasingFunction = new QuadraticEase() });
border.BeginAnimation(Border.OpacityProperty,
new DoubleAnimation(targetOpacity, TimeSpan.FromMilliseconds(400)));
}
var item = Images[i];
if (i == SelectedIndex)
{
item.IsSelected = true;
StartProgress(item);
}
else
{
item.IsSelected = false;
item.Progress = 0;
}
}
}

View File

@ -30,23 +30,49 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
public ObservableCollection<CoverFlowItem> Images { get; set; } = new ObservableCollection<CoverFlowItem>();
private int _selectedIndex = 0;
private EventHandler _renderHandler;
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (Images.Count == 0) return;
if (value < 0) _selectedIndex = Images.Count - 1;
else if (value >= Images.Count) _selectedIndex = 0;
else _selectedIndex = value;
for (int i = 0; i < Images.Count; i++)
{
Images[i].IsSelected = (i == _selectedIndex);
if (i == _selectedIndex) Images[i].Progress = 0;
}
Images[i].IsSelected = i == _selectedIndex;
UpdateLayoutWithAnimation();
// 启动进度条动画
var current = Images[_selectedIndex];
StartProgress(current);
}
}
private void StartProgress(CoverFlowItem item)
{
StopProgress();
_renderHandler = (s, e) =>
{
if (item.Progress < 1)
item.Progress += 0.005; // 每帧增加一点
else
item.Progress = 0;
};
CompositionTarget.Rendering += _renderHandler;
}
private void StopProgress()
{
if (_renderHandler != null)
{
CompositionTarget.Rendering -= _renderHandler;
_renderHandler = null;
}
}

View File

@ -5,30 +5,70 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace Wpf_AiSportsMicrospace.MyUserControl
{
public class CoverFlowItem : INotifyPropertyChanged
{
private bool _isSelected;
private double _progress; // 0~1
public Uri ImageUri { get; set; }
public bool IsSelected
private double _progress;
private String _progressColor1 = "#e73d42";
public String ProgressColor1
{
get => _isSelected;
set { _isSelected = value; OnPropertyChanged(nameof(IsSelected)); }
get => _progressColor1;
set
{
if (_progressColor1 != value)
{
_progressColor1 = value;
}
}
}
private String _progressColor2 = "#215bc7";
public String ProgressColor2
{
get => _progressColor2;
set
{
if (_progressColor2 != value)
{
_progressColor2 = value;
}
}
}
public double Progress
{
get => _progress;
set { _progress = value; OnPropertyChanged(nameof(Progress)); }
set
{
if (_progress != value)
{
_progress = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Progress)));
}
}
}
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;
protected void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

View File

@ -1,75 +1,186 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Ink;
using System.Windows.Media;
namespace Wpf_AiSportsMicrospace.MyUserControl
{
/// <summary>
/// 将进度值转换为表示矩形边框部分路径的几何图形StreamGeometry的转换器。
/// 该转换器用于绘制一个带圆角的矩形边框并根据进度值绘制其部分轮廓线常用于进度条等UI控件中。
/// </summary>
public class ProgressToRectangleGeometryConverter : IValueConverter
{
// parameter: "width,height"
/// <summary>
/// 将进度值转换为表示矩形边框部分路径的几何图形StreamGeometry对象
/// </summary>
/// <param name="value">当前进度值,应为 [0.0, 1.0] 范围内的 double 类型</param>
/// <param name="targetType">目标类型,应为 Geometry 类型</param>
/// <param name="parameter">参数字符串,格式为 "width,height,radius",分别表示矩形的宽度、高度和圆角半径</param>
/// <param name="culture">当前区域性信息</param>
/// <returns>返回一个 StreamGeometry 对象,表示根据进度绘制的部分矩形边框</returns>
// parameter: "width,height,radius"
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double progress = (double)value;
string[] sizes = parameter.ToString().Split(',');
string[] sizes = parameter.ToString()!.Split(',');
double width = double.Parse(sizes[0]);
double height = double.Parse(sizes[1]);
double radius = double.Parse(sizes[2]);
double stroke = sizes.Length > 3 ? double.Parse(sizes[3]) : 0;
var geo = new StreamGeometry();
using (var ctx = geo.Open())
double inset = stroke / 2.0;
double x = inset;
double y = inset;
double w = width - stroke;
double h = height - stroke;
// 保持原始圆角半径
double r = radius;
// 计算总周长
double perimeter = 2 * (w + h - 4 * r) + 2 * Math.PI * r;
double length = perimeter * progress;
var geometry = new StreamGeometry();
using (var ctx = geometry.Open())
{
ctx.BeginFigure(new Point(0, 0), false, false);
// 从左上角圆角起点,顺时针
ctx.BeginFigure(new Point(x + r, y), false, false);
double total = 2 * (width + height);
double len = progress * total;
// top line
double top = Math.Min(length, w - 2 * r);
ctx.LineTo(new Point(x + r + top, y), true, false);
length -= top;
if (length <= 0) return geometry;
// 上边
if (len <= width)
// top-right arc
double arc = Math.Min(length, Math.PI / 2 * r);
if (arc > 0)
{
ctx.LineTo(new Point(len, 0), true, true);
return geo;
double angle = arc / r;
double endX = x + w - r + Math.Sin(angle) * r;
double endY = y + r - Math.Cos(angle) * r;
if (arc < Math.PI / 2 * r)
{
ctx.ArcTo(
new Point(endX, endY),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
return geometry;
}
else
{
ctx.ArcTo(
new Point(x + w, y + r),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
length -= Math.PI / 2 * r;
}
}
ctx.LineTo(new Point(width, 0), true, true);
len -= width;
if (length <= 0) return geometry;
// 右边
if (len <= height)
{
ctx.LineTo(new Point(width, len), true, true);
return geo;
}
ctx.LineTo(new Point(width, height), true, true);
len -= height;
// right line
double right = Math.Min(length, h - 2 * r);
ctx.LineTo(new Point(x + w, y + r + right), true, false);
length -= right;
if (length <= 0) return geometry;
// 下边
if (len <= width)
// bottom-right arc
arc = Math.Min(length, Math.PI / 2 * r);
if (arc > 0)
{
ctx.LineTo(new Point(width - len, height), true, true);
return geo;
double angle = arc / r;
double endX = x + w - r + Math.Cos(angle) * r;
double endY = y + h - r + Math.Sin(angle) * r;
if (arc < Math.PI / 2 * r)
{
ctx.ArcTo(
new Point(endX, endY),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
return geometry;
}
else
{
ctx.ArcTo(
new Point(x + w - r, y + h),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
length -= Math.PI / 2 * r;
}
}
ctx.LineTo(new Point(0, height), true, true);
len -= width;
if (length <= 0) return geometry;
// 左边
if (len <= height)
// bottom line
double bottom = Math.Min(length, w - 2 * r);
ctx.LineTo(new Point(x + w - r - bottom, y + h), true, false);
length -= bottom;
if (length <= 0) return geometry;
// bottom-left arc
arc = Math.Min(length, Math.PI / 2 * r);
if (arc > 0)
{
ctx.LineTo(new Point(0, height - len), true, true);
double angle = arc / r;
double endX = x + r - Math.Sin(angle) * r;
double endY = y + h - r + Math.Cos(angle) * r;
if (arc < Math.PI / 2 * r)
{
ctx.ArcTo(
new Point(endX, endY),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
return geometry;
}
else
{
ctx.ArcTo(
new Point(x, y + h - r),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
length -= Math.PI / 2 * r;
}
}
else
if (length <= 0) return geometry;
// left line
double left = Math.Min(length, h - 2 * r);
ctx.LineTo(new Point(x, y + h - r - left), true, false);
length -= left;
if (length <= 0) return geometry;
// top-left arc
arc = Math.Min(length, Math.PI / 2 * r);
if (arc > 0)
{
ctx.LineTo(new Point(0, 0), true, true);
double angle = arc / r;
double endX = x + r - Math.Cos(angle) * r;
double endY = y + r - Math.Sin(angle) * r;
if (arc < Math.PI / 2 * r)
{
ctx.ArcTo(
new Point(endX, endY),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
return geometry;
}
else
{
ctx.ArcTo(
new Point(x + r, y),
new Size(r, r),
0, false, SweepDirection.Clockwise, true, false);
}
}
}
geo.Freeze();
return geo;
geometry.Freeze();
return geometry;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,12 @@
<Window x:Class="Wpf_AiSportsMicrospace.Views.GroupJumpRope"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views"
mc:Ignorable="d"
Title="多人跳绳" Height="450" Width="800">
<Grid>
</Grid>
</Window>

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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.Shapes;
namespace Wpf_AiSportsMicrospace.Views
{
/// <summary>
/// GroupJumpRope.xaml 的交互逻辑
/// </summary>
public partial class GroupJumpRope : Window
{
public GroupJumpRope()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,12 @@
<Window x:Class="Wpf_AiSportsMicrospace.Views.MusicJumpRope"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views"
mc:Ignorable="d"
Title="MusicJumpRope" Height="450" Width="800">
<Grid>
</Grid>
</Window>

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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.Shapes;
namespace Wpf_AiSportsMicrospace.Views
{
/// <summary>
/// MusicJumpRope.xaml 的交互逻辑
/// </summary>
public partial class MusicJumpRope : Window
{
public MusicJumpRope()
{
InitializeComponent();
}
}
}

View File

@ -14,6 +14,13 @@
<None Remove="Resources\Img\Album\3.jpg" />
<None Remove="Resources\Img\Album\4.jpg" />
<None Remove="Resources\Img\Album\5.jpg" />
<None Remove="Resources\Img\Album\action_user.png" />
<None Remove="Resources\Img\Album\change_bg.png" />
<None Remove="Resources\Img\Album\home_bg.png" />
<None Remove="Resources\Img\Album\home_history.png" />
<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\Badge\1.jpg" />
<None Remove="Resources\Img\Badge\2.jpg" />
<None Remove="Resources\Img\Badge\3.jpg" />
@ -87,6 +94,27 @@
<Resource Include="Resources\Img\Album\5.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\action_user.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\change_bg.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\home_bg.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\home_history.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\home_play.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\home_test.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Album\title.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\Badge\1.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>