1
0
forked from MeloNX/MeloNX

Merge pull request '上传文件至 src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick' (#1) from phoenixr-patch-1 into XC-ios-ht

Reviewed-on: #1
This commit is contained in:
PhoenixR 2025-03-18 09:19:19 +00:00
commit fd4bc5ccbb

View File

@ -0,0 +1,592 @@
//
// Joystick4FTG.swift
// MeloNX
//
// Created by RZH on 2025/3/7.
//
import SwiftUI
public struct Joystick4FTG: View {
//
@State var iscool: Bool? = nil
@State private var joystickPositionText = ""
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
public var body: some View {
VStack{
//
//FTGJoystick
/*FTGJoystickBuilder { relativePosition in
let scaledX = Double(relativePosition.x )
let scaledY = Double(relativePosition.y )
//print("Joystick Position: (\(scaledX), \(scaledY))")
*/
//
/*
EnhancedEightWayJoystick{relativePosition in
let scaledX = Double(relativePosition.x * 10)
let scaledY = Double(relativePosition.y * 10)
let formattedText = String(format: "X: %5.2f, Y: %5.2f", scaledX, scaledY)
print("Joystick Position: \(formattedText)")
//
//self.joystickPositionText = formattedText
*/
/*
//
FreeRoamJoystick { position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
//print("Joystick Position: \(formattedText)")
//
//self.joystickPositionText = formattedText
*/
//
/*
NineGridJoystick { position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
*/
//
DynamicNineGridJoystick{ position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: scaledX, y: scaledY)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: scaledX, y: scaledY)
}
}
}
}
}
struct EnhancedEightWayJoystick: View {
var onDirectionChanged: ((x: Int, y: Int)) -> Void
@State private var currentDirection: (x: Int, y: Int) = (0, 0)
private let radius: CGFloat = 50
private let centerPoint = CGPoint(x: 50, y: 50)
var body: some View {
GeometryReader { geometry in
ZStack {
//
Circle()
.fill(Color.gray.opacity(0.2))
.frame(width: 200, height: 200)
.contentShape(Circle())
//
Circle()
.fill(Color.white.opacity(0.7))
.frame(width: 75, height: 75)
.offset(x: CGFloat(currentDirection.x), y: CGFloat(currentDirection.y))
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let location = gesture.location
//使Geometrydxdy
let viewFrame = geometry.frame(in: .local)
let center = CGPoint(
x: viewFrame.midX,
y: viewFrame.midY
)
//let dx = location.x - centerPoint.x
//let dy = location.y - centerPoint.y
let dx = location.x - center.x
let dy = location.y - center.y
//
let radians = atan2(dy, dx)
var degrees = radians * 180 / .pi
if degrees < 0 { degrees += 360 }
//
let direction = get45DegreeDirection(from: degrees)
//
if direction.x != currentDirection.x || direction.y != currentDirection.y {
currentDirection = direction
onDirectionChanged(direction)
}
}
.onEnded { _ in
currentDirection = (0, 0)
onDirectionChanged((0, 0))
}
)
}
}
// 45
//Y
//scale 168
private func get45DegreeDirection(from degrees: CGFloat) -> (x: Int, y: Int) {
switch degrees {
// (337.5°-22.5°)
case 337.5...360, 0..<22.5:
return (50, 0)
// (22.5°-67.5°)
case 22.5..<67.5:
//return (50, -50)
return (50, 50)
// (67.5°-112.5°)
case 67.5..<112.5:
//return (0, -50)
return (0, 50)
// (112.5°-157.5°)
case 112.5..<157.5:
//return (-50, -50)
return (-50, 50)
// (157.5°-202.5°)
case 157.5..<202.5:
return (-50, 0)
// (202.5°-247.5°)
case 202.5..<247.5:
//return (-50, 50)
return (-50, -50)
// (247.5°-292.5°)
case 247.5..<292.5:
//return (0, 50)
return (0, -50)
// (292.5°-337.5°)
case 292.5..<337.5:
//return (50, 50)
return (50, -50)
default:
return (0, 0)
}
}
}
struct FreeRoamJoystick: View {
var onPositionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 90
private let thumbRadius: CGFloat = 75
@State private var isEightWay = 0 // 0: 1:
@State private var position = CGPoint.zero
@State private var viewCenter: CGPoint = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
Spacer()
//
Circle()
.stroke(Color.gray.opacity(0.5), lineWidth: 4)
.frame(width: trackRadius*2, height: trackRadius*2)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.contentShape(Circle()) // 1
//
let maxMoveRadius = trackRadius - thumbRadius
Circle()
.fill(Color.white.opacity(0.4))
.frame(width: thumbRadius*2, height: thumbRadius*2)
//.offset(x: position.x, y: position.y)
.offset(
x: min(max(position.x, -maxMoveRadius), maxMoveRadius),
y: min(max(position.y, -maxMoveRadius), maxMoveRadius)
)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
.gesture(
DragGesture(minimumDistance: 0) // 2
.onChanged { gesture in
let viewFrame = geometry.frame(in: .local)
let touchPoint = gesture.location
//
let center = CGPoint(
x: viewFrame.midX,
y: viewFrame.midY
)
let delta = CGPoint(
x: touchPoint.x - center.x,
y: touchPoint.y - center.y
)
//
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
let clampedDelta = distance > trackRadius ? CGPoint(
x: delta.x * trackRadius / distance,
y: delta.y * trackRadius / distance
) : delta
var test_data = clampedDelta
//
//
if isEightWay == 1 {
test_data = getEightDirection(for: test_data)
}
//position = clampedDelta
position = test_data
//onPositionChanged(clampedDelta)
onPositionChanged(test_data)
}
.onEnded { _ in
position = .zero
onPositionChanged(.zero)
}
)
.onAppear {
viewCenter = CGPoint(
x: geometry.size.width/2,
y: geometry.size.height/2
)
}
}
.frame(width: trackRadius, height: trackRadius)
//.frame(alignment: .bottomLeading)
}
private func getEightDirection(for delta: CGPoint) -> CGPoint {
let angle = atan2(-delta.y, delta.x)
let degrees = angle * 180 / .pi
let normalized = degrees < 0 ? degrees + 360 : degrees
switch normalized {
case 337.5...360, 0..<22.5: //
return CGPoint(x: trackRadius, y: 0)
case 22.5..<67.5: //
return CGPoint(x: trackRadius, y: -trackRadius)
case 67.5..<112.5: //
return CGPoint(x: 0, y: -trackRadius)
case 112.5..<157.5: //
return CGPoint(x: -trackRadius, y: -trackRadius)
case 157.5..<202.5: //
return CGPoint(x: -trackRadius, y: 0)
case 202.5..<247.5: //
return CGPoint(x: -trackRadius, y: trackRadius)
case 247.5..<292.5: //
return CGPoint(x: 0, y: trackRadius)
case 292.5..<337.5: //
return CGPoint(x: trackRadius, y: trackRadius)
default:
return .zero
}
}
}
struct NineGridJoystick: View {
var onDirectionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 90
private let thumbRadius: CGFloat = 35
@State private var position = CGPoint.zero
@State private var activeColor = Color.white.opacity(0.4)
//
private var gridThreshold: CGFloat {
trackRadius / 3 // 56
}
var body: some View {
GeometryReader { geometry in
ZStack {
//
gridBackgroundView()
//
Circle()
.fill(activeColor)
.frame(width: thumbRadius*2, height: thumbRadius*2)
.offset(x: position.x, y: position.y)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
//.animation(.spring(), value: position)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2)
let delta = CGPoint(
x: gesture.location.x - center.x,
y: gesture.location.y - center.y
)
let direction = getGridDirection(for: delta)
updatePosition(to: direction)
}
.onEnded { _ in
resetPosition()
}
)
}
.frame(width: trackRadius*2, height: trackRadius*2)
}
// MARK: -
private func gridBackgroundView() -> some View {
ZStack {
// 线
Path { path in
// 线
path.move(to: CGPoint(x: trackRadius - gridThreshold, y: 0))
path.addLine(to: CGPoint(x: trackRadius - gridThreshold, y: trackRadius*2))
path.move(to: CGPoint(x: trackRadius + gridThreshold, y: 0))
path.addLine(to: CGPoint(x: trackRadius + gridThreshold, y: trackRadius*2))
// 线
path.move(to: CGPoint(x: 0, y: trackRadius - gridThreshold))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius - gridThreshold))
path.move(to: CGPoint(x: 0, y: trackRadius + gridThreshold))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + gridThreshold))
}
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
//
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(width: gridThreshold*2, height: gridThreshold*2)
}
}
// MARK: -
private func getGridDirection(for delta: CGPoint) -> CGPoint {
let xVal = delta.x
let yVal = delta.y
switch (xVal, yVal) {
case (..<(-gridThreshold), ..<(-gridThreshold)): //
return CGPoint(x: -trackRadius, y: -trackRadius)
case ((-gridThreshold)...gridThreshold, ..<(-gridThreshold)): //
return CGPoint(x: 0, y: -trackRadius)
case (gridThreshold..., ..<(-gridThreshold)): //
return CGPoint(x: trackRadius, y: -trackRadius)
case (..<(-gridThreshold), (-gridThreshold)...gridThreshold): //
return CGPoint(x: -trackRadius, y: 0)
case ((-gridThreshold)...gridThreshold, (-gridThreshold)...gridThreshold): //
return .zero
case (gridThreshold..., (-gridThreshold)...gridThreshold): //
return CGPoint(x: trackRadius, y: 0)
case (..<(-gridThreshold), gridThreshold...): //
return CGPoint(x: -trackRadius, y: trackRadius)
case ((-gridThreshold)...gridThreshold, gridThreshold...): //
return CGPoint(x: 0, y: trackRadius)
case (gridThreshold..., gridThreshold...): //
return CGPoint(x: trackRadius, y: trackRadius)
default:
return .zero
}
}
// MARK: -
private func updatePosition(to direction: CGPoint) {
//withAnimation(.spring()) {
position = direction
activeColor = direction == .zero ?
Color.white.opacity(0.4) :
Color.blue.opacity(0.6)
onDirectionChanged(direction)
//}
}
private func resetPosition() {
//withAnimation(.spring()) {
position = .zero
activeColor = Color.white.opacity(0.4)
onDirectionChanged(.zero)
//}
}
}
struct DynamicNineGridJoystick: View {
var onDirectionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 100
private let thumbRadius: CGFloat = 35
@State private var position = CGPoint.zero
@State private var activeColor = Color.white.opacity(0.4)
@State private var lastDirection: CGPoint = .zero
//
private var gridThreshold: CGFloat { trackRadius / 3 }
private let hapticGenerator = UIImpactFeedbackGenerator(style: .soft)
var body: some View {
GeometryReader { geometry in
ZStack {
//
gridBackgroundView()
//
Circle()
.fill(activeColor)
.frame(width: thumbRadius*2, height: thumbRadius*2)
.offset(x: position.x, y: position.y)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
//.animation(.spring(), value: position)
}
//
.contentShape(Rectangle()) // 使GeometryReader
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
handleGestureChange(
location: gesture.location,
center: CGPoint(x: geometry.size.width/2,
y: geometry.size.height/2)
)
}
.onEnded { _ in
resetPosition()
}
)
}
.frame(width: trackRadius*2, height: trackRadius*2)
.onAppear {
hapticGenerator.prepare()
}
}
// MARK: -
private func handleGestureChange(location: CGPoint, center: CGPoint) {
let delta = CGPoint(
x: location.x - center.x,
y: location.y - center.y
)
let currentDirection = getGridDirection(for: delta)
//
if currentDirection != lastDirection {
updatePosition(to: currentDirection)
triggerHapticFeedback()
lastDirection = currentDirection
}
}
// MARK: -
private func getGridDirection(for delta: CGPoint) -> CGPoint {
let xVal = delta.x
let yVal = delta.y
//
let isLeft = xVal < -gridThreshold
let isRight = xVal > gridThreshold
let isUp = yVal < -gridThreshold
let isDown = yVal > gridThreshold
switch (isLeft, isRight, isUp, isDown) {
case (true, false, true, false): //
return CGPoint(x: -trackRadius, y: -trackRadius)
case (false, false, true, false): //
return CGPoint(x: 0, y: -trackRadius)
case (false, true, true, false): //
return CGPoint(x: trackRadius, y: -trackRadius)
case (true, false, false, false): //
return CGPoint(x: -trackRadius, y: 0)
case (false, false, false, false): //
return .zero
case (false, true, false, false): //
return CGPoint(x: trackRadius, y: 0)
case (true, false, false, true): //
return CGPoint(x: -trackRadius, y: trackRadius)
case (false, false, false, true): //
return CGPoint(x: 0, y: trackRadius)
case (false, true, false, true): //
return CGPoint(x: trackRadius, y: trackRadius)
default: // 线
return calculateEdgeCase(delta: delta)
}
}
//
private func calculateEdgeCase(delta: CGPoint) -> CGPoint {
let angle = atan2(delta.y, delta.x) * 180 / .pi
let normalized = angle < 0 ? angle + 360 : angle
switch normalized {
case 45..<135: //
return CGPoint(x: 0, y: -trackRadius)
case 135..<225: //
return CGPoint(x: -trackRadius, y: 0)
case 225..<315: //
return CGPoint(x: 0, y: trackRadius)
default: //
return CGPoint(x: trackRadius, y: 0)
}
}
// MARK: -
private func updatePosition(to direction: CGPoint) {
//withAnimation(.interactiveSpring()) {
position = direction
activeColor = direction == .zero ?
Color.white.opacity(0.4) :
Color.blue.opacity(0.6)
onDirectionChanged(direction)
}
private func resetPosition() {
//withAnimation(.spring()) {
position = .zero
activeColor = Color.white.opacity(0.4)
lastDirection = .zero
onDirectionChanged(.zero)
//}
}
private func triggerHapticFeedback() {
guard position != .zero else { return }
hapticGenerator.impactOccurred(intensity: 0.7)
}
// MARK: -
private func gridBackgroundView() -> some View {
ZStack {
// 线
Path { path in
let offsets = [-gridThreshold, 0, gridThreshold]
for offset in offsets {
path.move(to: CGPoint(x: trackRadius + offset, y: 0))
path.addLine(to: CGPoint(x: trackRadius + offset, y: trackRadius*2))
path.move(to: CGPoint(x: 0, y: trackRadius + offset))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + offset))
}
}
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.1))
.frame(width: gridThreshold*2, height: gridThreshold*2)
}
}
}