Path: blob/a-new-beginning/Folium-iOS/Classes/ControllerView/ControllerThumbstick.swift
2 views
//
// ControllerThumbstick.swift
// Folium-iOS
//
// Created by Jarrod Norwell on 10/6/2024.
//
import Foundation
import GameController
import UIKit
protocol ControllerThumbstickDelegate {
func touchBegan(with type: Thumbstick.`Type`, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
func touchEnded(with type: Thumbstick.`Type`, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
func touchMoved(with type: Thumbstick.`Type`, position: (x: Float, y: Float), playerIndex: GCControllerPlayerIndex) async
}
enum ThumbstickClass : String {
case blurredThumbstick = "blurredThumbstick"
case defaultThumbstick = "defaultThumbstick"
}
class ControllerThumbstick : UIView {
var centerXConstraint: NSLayoutConstraint? = nil, centerYConstraint: NSLayoutConstraint? = nil,
widthConstraint: NSLayoutConstraint? = nil, heightConstraint: NSLayoutConstraint? = nil
var stickView: UIView? = nil
var thumbstick: Thumbstick
var skin: Skin
var delegate: ControllerThumbstickDelegate? = nil
init(thumbstick: Thumbstick, skin: Skin, delegate: ControllerThumbstickDelegate? = nil) {
self.thumbstick = thumbstick
self.skin = skin
self.delegate = delegate
super.init(frame: .zero)
stickView = .init()
guard let stickView else {
return
}
stickView.translatesAutoresizingMaskIntoConstraints = false
stickView.clipsToBounds = true
stickView.layer.cornerCurve = .continuous
addSubview(stickView)
centerXConstraint = stickView.centerXAnchor.constraint(equalTo: centerXAnchor)
guard let centerXConstraint else {
return
}
centerXConstraint.isActive = true
centerYConstraint = stickView.centerYAnchor.constraint(equalTo: centerYAnchor)
guard let centerYConstraint else {
return
}
centerYConstraint.isActive = true
widthConstraint = stickView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / 3, constant: -8)
guard let widthConstraint else {
return
}
widthConstraint.isActive = true
heightConstraint = stickView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1 / 3, constant: -8)
guard let heightConstraint else {
return
}
heightConstraint.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let stickView {
stickView.layer.cornerRadius = stickView.frame.height / 2
}
}
fileprivate func position(in view: UIView, with location: CGPoint) -> (x: Float, y: Float) {
let radius = view.frame.width / 2
return (Float((location.x - radius) / radius), Float(-(location.y - radius) / radius))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let delegate, let touch = touches.first else {
return
}
Task {
await delegate.touchBegan(with: thumbstick.type, position: position(in: self, with: touch.location(in: self)), playerIndex: .index1)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let delegate else {
return
}
guard let centerXConstraint, let centerYConstraint/*, let widthConstraint, let heightConstraint*/ else {
return
}
centerXConstraint.constant = 0
centerYConstraint.constant = 0
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
}
Task {
await delegate.touchEnded(with: thumbstick.type, position: (x: 0, y: 0), playerIndex: .index1)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let delegate, let touch = touches.first else {
return
}
guard let stickView, let centerXConstraint, let centerYConstraint else {
return
}
let halfWidth: CGFloat = frame.width / 2
let halfHeight: CGFloat = frame.height / 2
let touchLocation = touch.location(in: self)
let distanceFromCenter = CGPoint(x: touchLocation.x - halfWidth, y: touchLocation.y - halfHeight)
let angle = atan2(distanceFromCenter.y, distanceFromCenter.x)
let distance = sqrt(pow(distanceFromCenter.x, 2) + pow(distanceFromCenter.y, 2))
let maxDraggableDistance = min(halfWidth, halfHeight) * 0.9
let constrainedDistance = min(distance, maxDraggableDistance)
let constrainedX = cos(angle) * constrainedDistance
let constrainedY = sin(angle) * constrainedDistance
centerXConstraint.constant = constrainedX
centerYConstraint.constant = constrainedY
stickView.layoutIfNeeded()
Task {
await delegate.touchMoved(with: thumbstick.type, position: position(in: self, with: touchLocation), playerIndex: .index1)
}
}
}
class BlurredThumbstick : ControllerThumbstick {
fileprivate var visualEffectView: UIVisualEffectView? = nil
override init(thumbstick: Thumbstick, skin: Skin, delegate: (any ControllerThumbstickDelegate)? = nil) {
super.init(thumbstick: thumbstick, skin: skin, delegate: delegate)
visualEffectView = if #available(iOS 26, *) {
.init(effect: UIGlassEffect(style: .regular))
} else {
.init(effect: UIBlurEffect(style: .systemMaterial))
}
guard let stickView, let visualEffectView else {
return
}
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
visualEffectView.layer.shadowColor = UIColor.black.cgColor
visualEffectView.layer.shadowOpacity = 1 / 5
visualEffectView.layer.shadowRadius = 20
visualEffectView.layer.shadowOffset = .init(width: 0, height: 10)
stickView.addSubview(visualEffectView)
visualEffectView.topAnchor.constraint(equalTo: stickView.topAnchor).isActive = true
visualEffectView.leadingAnchor.constraint(equalTo: stickView.leadingAnchor).isActive = true
visualEffectView.bottomAnchor.constraint(equalTo: stickView.bottomAnchor).isActive = true
visualEffectView.trailingAnchor.constraint(equalTo: stickView.trailingAnchor).isActive = true
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
guard let visualEffectView else {
return
}
visualEffectView.layer.cornerRadius = visualEffectView.frame.height / 2
}
}
class DefaultThumbstick : ControllerThumbstick {
fileprivate var imageView: UIImageView? = nil
override init(thumbstick: Thumbstick, skin: Skin, delegate: (any ControllerThumbstickDelegate)? = nil) {
super.init(thumbstick: thumbstick, skin: skin, delegate: delegate)
imageView = .init()
guard let stickView, let imageView else {
return
}
if let backgroundImageName = thumbstick.backgroundImageName, let url = skin.url {
imageView.image = .init(contentsOfFile: url
.appendingPathComponent("thumbsticks", conformingTo: .folder)
.appendingPathComponent(backgroundImageName, conformingTo: .fileURL)
.path
)
} else {
imageView.image = .init(systemName: "circle.fill")?
.applyingSymbolConfiguration(.init(paletteColors: [skin.core == .cytrus ? .white : .label]))
}
imageView.translatesAutoresizingMaskIntoConstraints = false
stickView.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: stickView.topAnchor).isActive = true
imageView.leadingAnchor.constraint(equalTo: stickView.leadingAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: stickView.bottomAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: stickView.trailingAnchor).isActive = true
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/*
class ControllerThumbstick : UIView {
fileprivate var stickImageView: UIImageView!
fileprivate var centerXConstraint, centerYConstraint,
widthConstraint, heightConstraint: NSLayoutConstraint!
var thumbstick: Thumbstick
var skin: Skin
var delegate: ControllerThumbstickDelegate? = nil
init(thumbstick: Thumbstick, skin: Skin, delegate: ControllerThumbstickDelegate? = nil) {
self.thumbstick = thumbstick
self.skin = skin
self.delegate = delegate
super.init(frame: .zero)
stickImageView = .init()
stickImageView.translatesAutoresizingMaskIntoConstraints = false
if let debugging = skin.debugging, debugging {
stickImageView.backgroundColor = .systemRed.withAlphaComponent(1 / 3)
}
if let backgroundImageName = thumbstick.backgroundImageName, let url = skin.url {
stickImageView.image = .init(contentsOfFile: url
.appendingPathComponent("thumbsticks", conformingTo: .folder)
.appendingPathComponent(backgroundImageName, conformingTo: .fileURL)
.path
)
} else {
stickImageView.image = .init(systemName: "circle.fill")?
.applyingSymbolConfiguration(.init(paletteColors: [skin.core == .cytrus ? .white : .label]))
}
addSubview(stickImageView)
centerXConstraint = stickImageView.centerXAnchor.constraint(equalTo: centerXAnchor)
centerXConstraint.isActive = true
centerYConstraint = stickImageView.centerYAnchor.constraint(equalTo: centerYAnchor)
centerYConstraint.isActive = true
widthConstraint = stickImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / 3)
widthConstraint.isActive = true
heightConstraint = stickImageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1 / 3)
heightConstraint.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func position(in view: UIView, with location: CGPoint) -> (x: Float, y: Float) {
let radius = view.frame.width / 2
return (Float((location.x - radius) / radius), Float(-(location.y - radius) / radius))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let delegate, let touch = touches.first else {
return
}
widthConstraint.constant = 10
heightConstraint.constant = 10
stickImageView.layoutIfNeeded()
delegate.touchBegan(with: thumbstick.type, position: position(in: self, with: touch.location(in: self)), playerIndex: .index1)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let delegate else {
return
}
centerXConstraint.constant = 0
centerYConstraint.constant = 0
widthConstraint.constant = 0
heightConstraint.constant = 0
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
}
delegate.touchEnded(with: thumbstick.type, position: (x: 0, y: 0), playerIndex: .index1)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let delegate, let touch = touches.first else {
return
}
let halfWidth: CGFloat = frame.width / 2
let halfHeight: CGFloat = frame.height / 2
let touchLocation = touch.location(in: self)
let distanceFromCenter = CGPoint(x: touchLocation.x - halfWidth, y: touchLocation.y - halfHeight)
let angle = atan2(distanceFromCenter.y, distanceFromCenter.x)
let distance = sqrt(pow(distanceFromCenter.x, 2) + pow(distanceFromCenter.y, 2))
let maxDraggableDistance = min(halfWidth, halfHeight) * 0.9
let constrainedDistance = min(distance, maxDraggableDistance)
let constrainedX = cos(angle) * constrainedDistance
let constrainedY = sin(angle) * constrainedDistance
centerXConstraint.constant = constrainedX
centerYConstraint.constant = constrainedY
stickImageView.layoutIfNeeded()
delegate.touchMoved(with: thumbstick.type, position: position(in: self, with: touchLocation), playerIndex: .index1)
}
}
*/