Skip to content

Instantly share code, notes, and snippets.

@tor4kichi
Last active March 6, 2024 13:24
Show Gist options
  • Select an option

  • Save tor4kichi/2d9801372b3638353154e0b7c1b4afc1 to your computer and use it in GitHub Desktop.

Select an option

Save tor4kichi/2d9801372b3638353154e0b7c1b4afc1 to your computer and use it in GitHub Desktop.
UWPで仮想アナログパッド、バーチャルスティックを実装
<myControls:VirtualAnalogPad X="{x:Bind CanvasTransform.TranslateX, Mode=TwoWay}"
Y="{x:Bind CanvasTransform.TranslateY, Mode=TwoWay}"
MinX="{x:Bind _vm.HalfAndNegative(_vm.DefaultCanvasSize.Width), Mode=OneWay}"
MaxX="{x:Bind _vm.Half(_vm.DefaultCanvasSize.Width), Mode=OneWay}"
MinY="{x:Bind _vm.HalfAndNegative(_vm.DefaultCanvasSize.Height), Mode=OneWay}"
MaxY="{x:Bind _vm.Half(_vm.DefaultCanvasSize.Height), Mode=OneWay}"
BaseScale="{x:Bind CanvasTransform.ScaleX, Mode=OneWay}"
Margin="32 8"
Background="{ThemeResource SystemBaseLowColor}"
BorderBrush="{ThemeResource SystemBaseMediumColor}"
BorderThickness="1"
ThumbRadius="16"
InputPlayNormalized="0.15"
VelocityPerSecond="25"
/>
<UserControl
x:Class="Starryboard.Views.Controls.VirtualAnalogPad"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Starryboard.Views.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<Border x:Name="InputAreaBorder"
PointerPressed="InputAreaBorder_PointerPressed"
PointerMoved="InputAreaBorder_PointerMoved"
PointerReleased="InputAreaBorder_PointerReleased"
PointerCanceled="InputAreaBorder_PointerCanceled"
PointerCaptureLost="InputAreaBorder_PointerCaptureLost"
Background="{x:Bind Background, Mode=OneWay}"
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
IsHitTestVisible="True"
>
</Border>
<Border x:Name="ThumbBorder"
Width="{x:Bind ThumbRadius, Mode=OneWay}"
Height="{x:Bind ThumbRadius, Mode=OneWay}"
Background="{x:Bind ThumbColor, Mode=OneWay}"
CornerRadius="1000"
IsHitTestVisible="False"
Opacity="0"
>
<Border.RenderTransform>
<TranslateTransform x:Name="ThumbBorderTransform" />
</Border.RenderTransform>
</Border>
<Border Background="{x:Bind CenterPointColor, Mode=OneWay}"
Width="1"
Height="1" />
</Grid>
</UserControl>
using CommunityToolkit.Diagnostics;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Devices.Input;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.System;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
#nullable enable
namespace Starryboard.Views.Controls;
public sealed partial class VirtualAnalogPad : UserControl
{
public VirtualAnalogPad()
{
this.InitializeComponent();
Loaded += VirtualJoystick_Loaded;
_PositionUpdateTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
_PositionUpdateTimer.Interval = TimeSpan.FromMilliseconds(8);
_PositionUpdateTimer.IsRepeating = true;
_PositionUpdateTimer.Tick += _PositionUpdateTimer_Tick;
}
private void VirtualJoystick_Loaded(object sender, RoutedEventArgs e)
{
double size = InputAreaRadius;
InputAreaBorder.Width = size;
InputAreaBorder.Height = size;
InputAreaBorder.CornerRadius = new CornerRadius(size);
}
#region Properties
public double InputAreaRadius
{
get { return (double)GetValue(InputAreaRadiusProperty); }
set { SetValue(InputAreaRadiusProperty, value); }
}
public static readonly DependencyProperty InputAreaRadiusProperty =
DependencyProperty.Register("InputAreaRadius", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(64d, OnRadiusChanged));
private static void OnRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var _this = (VirtualAnalogPad)d;
double size = (double)e.NewValue;
_this.InputAreaBorder.Width = size;
_this.InputAreaBorder.Height = size;
_this.InputAreaBorder.CornerRadius = new CornerRadius(size);
}
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public static readonly DependencyProperty XProperty =
DependencyProperty.Register("X", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0d));
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, value); }
}
public static readonly DependencyProperty YProperty =
DependencyProperty.Register("Y", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0d));
public double MinX
{
get { return (double)GetValue(MinXProperty); }
set { SetValue(MinXProperty, value); }
}
public static readonly DependencyProperty MinXProperty =
DependencyProperty.Register("MinX", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(-720d));
public double MaxX
{
get { return (double)GetValue(MaxXProperty); }
set { SetValue(MaxXProperty, value); }
}
public static readonly DependencyProperty MaxXProperty =
DependencyProperty.Register("MaxX", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(720d));
public double MinY
{
get { return (double)GetValue(MinYProperty); }
set { SetValue(MinYProperty, value); }
}
public static readonly DependencyProperty MinYProperty =
DependencyProperty.Register("MinY", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(-480d));
public double MaxY
{
get { return (double)GetValue(MaxYProperty); }
set { SetValue(MaxYProperty, value); }
}
public static readonly DependencyProperty MaxYProperty =
DependencyProperty.Register("MaxY", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(480d));
public double BaseScale
{
get { return (double)GetValue(BaseScaleProperty); }
set { SetValue(BaseScaleProperty, value); }
}
public static readonly DependencyProperty BaseScaleProperty =
DependencyProperty.Register("BaseScale", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(1d));
// 最大まで傾けた際のX/Yに毎秒加算する移動量を指定します。
public double VelocityPerSecond
{
get { return (double)GetValue(VelocityPerSecondProperty); }
set { SetValue(VelocityPerSecondProperty, value); }
}
public static readonly DependencyProperty VelocityPerSecondProperty =
DependencyProperty.Register("VelocityPerSecond", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(50d));
// 入力の遊び。スキップする入力量を指定します。
public double InputPlayNormalized
{
get { return (double)GetValue(InputPlayNormalizedProperty); }
set
{
Guard.IsBetweenOrEqualTo(value, 0d, 1d);
SetValue(InputPlayNormalizedProperty, value);
}
}
public static readonly DependencyProperty InputPlayNormalizedProperty =
DependencyProperty.Register("InputPlayNormalized", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(0.05d));
public double ThumbRadius
{
get { return (double)GetValue(ThumbRadiusProperty); }
set { SetValue(ThumbRadiusProperty, value); }
}
public static readonly DependencyProperty ThumbRadiusProperty =
DependencyProperty.Register("ThumbRadius", typeof(double), typeof(VirtualAnalogPad), new PropertyMetadata(32d));
public Brush ThumbColor
{
get { return (Brush)GetValue(ThumbColorProperty); }
set { SetValue(ThumbColorProperty, value); }
}
public static readonly DependencyProperty ThumbColorProperty =
DependencyProperty.Register("ThumbColor", typeof(Brush), typeof(VirtualAnalogPad), new PropertyMetadata(new SolidColorBrush(Colors.White)));
public Brush CenterPointColor
{
get { return (Brush)GetValue(CenterPointColorProperty); }
set { SetValue(CenterPointColorProperty, value); }
}
public static readonly DependencyProperty CenterPointColorProperty =
DependencyProperty.Register("CenterPointColor", typeof(Brush), typeof(VirtualAnalogPad), new PropertyMetadata(new SolidColorBrush(Colors.White)));
public bool IsReverseInput
{
get { return (bool)GetValue(IsReverseInputProperty); }
set { SetValue(IsReverseInputProperty, value); }
}
public static readonly DependencyProperty IsReverseInputProperty =
DependencyProperty.Register("IsReverseInput", typeof(bool), typeof(VirtualAnalogPad), new PropertyMetadata(false));
#endregion
private readonly DispatcherQueueTimer _PositionUpdateTimer;
double _prevX;
double _prevY;
Vector2 _startPointerPos;
bool _isPressed;
Vector2 _thumbStick = Vector2.Zero;
private void InputAreaBorder_PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (InputAreaBorder.CapturePointer(e.Pointer) is false) { return; }
e.Handled = true;
_isPressed = true;
(_prevX, _prevY) = (X, Y);
_startPointerPos = Window.Current.CoreWindow.PointerPosition.ToVector2();
ThumbBorderTransform.X = 0;
ThumbBorderTransform.Y = 0;
ThumbBorder.Opacity = 1;
_thumbStick = Vector2.Zero;
_PositionUpdateTimer.Start();
}
private void InputAreaBorder_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_isPressed is false) { return; }
UpdateThumbPosition(Window.Current.CoreWindow.PointerPosition.ToVector2());
}
void UpdateThumbPosition(Vector2 pointerPos)
{
var moved = pointerPos - _startPointerPos;
if (moved is { X : 0, Y : 0 } ) { return; }
var length = moved.Length();
var dir = Vector2.Normalize(moved);
float radius = (float)InputAreaRadius * 0.5f;
if (length > radius)
{
_thumbStick = dir * radius;
ThumbBorderTransform.X = _thumbStick.X;
ThumbBorderTransform.Y = _thumbStick.Y;
Window.Current.CoreWindow.PointerPosition = (_startPointerPos + _thumbStick).ToPoint();
}
else
{
ThumbBorderTransform.X = moved.X;
ThumbBorderTransform.Y = moved.Y;
// play amount
// |----|-------------|
// | / |
// | / |
// | / |
// |/ |
// |--scaled amount---|
//
// 遊び値を引いたamountを 0 ~ 1 の値域に射影する
float play = (float)InputPlayNormalized;
float invPlay = 1 / (1 - play);
var amount = length / radius;
if (amount >= play)
{
_thumbStick = Vector2.Lerp(Vector2.Zero, dir, (amount - play) * invPlay) * radius;
}
else
{
_thumbStick = Vector2.Zero;
}
}
}
private void _PositionUpdateTimer_Tick(DispatcherQueueTimer sender, object args)
{
if (_thumbStick != default)
{
float reverseFactor = IsReverseInput ? 1 : -1;
double speed = VelocityPerSecond * sender.Interval.TotalSeconds * reverseFactor;
X = Math.Clamp(X + _thumbStick.X * speed, MinX * BaseScale, MaxX * BaseScale);
Y = Math.Clamp(Y + _thumbStick.Y * speed, MinY * BaseScale, MaxY * BaseScale);
}
}
private void InputAreaBorder_PointerReleased(object sender, PointerRoutedEventArgs e)
{
_isPressed = false;
e.Handled = true;
InputAreaBorder.ReleasePointerCapture(e.Pointer);
_PositionUpdateTimer.Stop();
ThumbBorder.Opacity = 0;
}
private void InputAreaBorder_PointerCanceled(object sender, PointerRoutedEventArgs e)
{
InputAreaBorder.ReleasePointerCapture(e.Pointer);
ThumbBorder.Opacity = 0;
(X, Y) = (_prevX, _prevY);
_isPressed = false;
_PositionUpdateTimer.Stop();
}
private void InputAreaBorder_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
if (_isPressed)
{
InputAreaBorder.ReleasePointerCapture(e.Pointer);
ThumbBorder.Opacity = 0;
(X, Y) = (_prevX, _prevY);
_isPressed = false;
_PositionUpdateTimer.Stop();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment