SameBoy/iOS/GBBackgroundView.m

468 lines
16 KiB
Objective-C

#import "GBBackgroundView.h"
#import "GBViewMetal.h"
#import "GBHapticManager.h"
#import "GBMenuViewController.h"
#import "GBViewController.h"
#import "GBROMManager.h"
double CGPointSquaredDistance(CGPoint a, CGPoint b)
{
double deltaX = a.x - b.x;
double deltaY = a.y - b.y;
return deltaX * deltaX + deltaY * deltaY;
}
double CGPointAngle(CGPoint a, CGPoint b)
{
double deltaX = a.x - b.x;
double deltaY = a.y - b.y;
return atan2(deltaY, deltaX);
}
static void positionView(UIImageView *view, CGPoint position)
{
double center = view.image.size.width / 2 * [UIScreen mainScreen].scale;
view.frame = (CGRect){
{
round(position.x - center) / [UIScreen mainScreen].scale,
round(position.y - center) / [UIScreen mainScreen].scale
},
view.image.size
};
}
static GB_key_mask_t angleToKeyMask(double angle)
{
signed quantizedAngle = round(angle / M_PI * 16);
if (quantizedAngle < 0) {
quantizedAngle += 32;
}
switch (quantizedAngle) {
case 32:
case 0: return GB_KEY_RIGHT_MASK;
case 1: return GB_KEY_RIGHT_MASK;
case 2: return GB_KEY_RIGHT_MASK;
case 3: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK;
case 4: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK;
case 5: return GB_KEY_DOWN_MASK;
case 6: return GB_KEY_DOWN_MASK;
case 7: return GB_KEY_DOWN_MASK;
case 8: return GB_KEY_DOWN_MASK;
case 9: return GB_KEY_DOWN_MASK;
case 10: return GB_KEY_DOWN_MASK;
case 11: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK;
case 12: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK;
case 13: return GB_KEY_LEFT_MASK;
case 14: return GB_KEY_LEFT_MASK;
case 15: return GB_KEY_LEFT_MASK;
case 16: return GB_KEY_LEFT_MASK;
case 17: return GB_KEY_LEFT_MASK;
case 18: return GB_KEY_LEFT_MASK;
case 19: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK;
case 20: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK;
case 21: return GB_KEY_UP_MASK;
case 22: return GB_KEY_UP_MASK;
case 23: return GB_KEY_UP_MASK;
case 24: return GB_KEY_UP_MASK;
case 25: return GB_KEY_UP_MASK;
case 26: return GB_KEY_UP_MASK;
case 27: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK;
case 28: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK;
case 29: return GB_KEY_RIGHT_MASK;
case 30: return GB_KEY_RIGHT_MASK;
case 31: return GB_KEY_RIGHT_MASK;
}
return 0;
}
@implementation GBBackgroundView
{
NSMutableSet<UITouch *> *_touches;
UITouch *_swipePadTouch;
CGPoint _padSwipeOrigin;
UITouch *_screenTouch;
CGPoint _screenSwipeOrigin;
bool _screenSwiped;
UIImageView *_dpadView;
UIImageView *_dpadShadowView;
UIImageView *_aButtonView;
UIImageView *_bButtonView;
UIImageView *_startButtonView;
UIImageView *_selectButtonView;
UILabel *_screenLabel;
UIVisualEffectView *_overlayView;
UIView *_overlayViewContents;
NSTimer *_fadeTimer;
GB_key_mask_t _lastMask;
}
- (instancetype)init
{
self = [super initWithImage:nil];
if (!self) return nil;
_touches = [NSMutableSet set];
_screenLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library.";
_screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium];
_screenLabel.textAlignment = NSTextAlignmentCenter;
_screenLabel.textColor = [UIColor whiteColor];
_screenLabel.lineBreakMode = NSLineBreakByWordWrapping;
_screenLabel.numberOfLines = 0;
[self addSubview:_screenLabel];
_dpadView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpad"]];
_dpadShadowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpadShadow"]];
_dpadShadowView.hidden = true;
_aButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button"]];
_bButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button"]];
_startButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button2"]];
_selectButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button2"]];
_gbView = [[GBViewMetal alloc] initWithFrame:CGRectZero];
[self addSubview:_dpadView];
[self addSubview:_aButtonView];
[self addSubview:_bButtonView];
[self addSubview:_startButtonView];
[self addSubview:_selectButtonView];
[self addSubview:_gbView];
[_dpadView addSubview:_dpadShadowView];
UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleDark];
_overlayView = [[UIVisualEffectView alloc] initWithEffect:effect];
_overlayView.frame = CGRectMake(8, 8, 32, 32);
_overlayView.layer.cornerRadius = 12;
_overlayView.layer.masksToBounds = true;
_overlayView.alpha = 0;
if (@available(iOS 13.0, *)) {
_overlayViewContents = [[UIImageView alloc] init];
_overlayViewContents.tintColor = [UIColor whiteColor];
}
else {
_overlayViewContents = [[UILabel alloc] init];
((UILabel *)_overlayViewContents).font = [UIFont systemFontOfSize:UIFont.systemFontSize weight:UIFontWeightMedium];
((UILabel *)_overlayViewContents).textColor = [UIColor whiteColor];
}
_overlayViewContents.frame = CGRectMake(8, 8, 160, 20.5);
[_overlayView.contentView addSubview:_overlayViewContents];
[_gbView addSubview:_overlayView];
return self;
}
- (GBViewController *)viewController
{
return (GBViewController *)[UIApplication sharedApplication].delegate;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static const double dpadRadius = 75;
CGPoint dpadLocation = _layout.dpadLocation;
double factor = [UIScreen mainScreen].scale;
dpadLocation.x /= factor;
dpadLocation.y /= factor;
for (UITouch *touch in touches) {
CGPoint point = [touch locationInView:self];
if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) {
if (self.viewController.runMode != GBRunModeNormal) {
self.viewController.runMode = GBRunModeNormal;
[self fadeOverlayOut];
}
else {
_screenTouch = touch;
_screenSwipeOrigin = point;
_screenSwiped = false;
_overlayView.alpha = 0;
[_fadeTimer invalidate];
_fadeTimer = nil;
}
}
if (_usesSwipePad && !_swipePadTouch) {
if (fabs(point.x - dpadLocation.x) <= dpadRadius &&
fabs(point.y - dpadLocation.y) <= dpadRadius) {
_swipePadTouch = touch;
_padSwipeOrigin = point;
}
}
}
[_touches unionSet:touches];
[self touchesChanged];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if ([touches containsObject:_swipePadTouch]) {
_swipePadTouch = nil;
}
if ([touches containsObject:_screenTouch]) {
_screenTouch = nil;
if (!_screenSwiped) {
[self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil];
}
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) {
if (self.viewController.runMode != GBRunModeNormal) {
self.viewController.runMode = GBRunModeNormal;
[self fadeOverlayOut];
}
}
}
[_touches minusSet:touches];
[self touchesChanged];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self touchesEnded:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self touchesChanged];
}
- (void)touchesChanged
{
if (!GB_is_inited(_gbView.gb)) return;
GB_key_mask_t mask = 0;
double factor = [UIScreen mainScreen].scale;
double buttonRadiusSquared = 36 * 36 * factor * factor;
double dpadRadius = 75 * factor;
bool dpadHandled = false;
if (_usesSwipePad) {
dpadHandled = true;
if (_swipePadTouch) {
CGPoint point = [_swipePadTouch locationInView:self];
double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin);
if (squaredDistance > 16 * 16) {
double angle = CGPointAngle(point, _padSwipeOrigin);
mask |= angleToKeyMask(angle);
if (squaredDistance > 24 * 24) {
double deltaX = point.x - _padSwipeOrigin.x;
double deltaY = point.y - _padSwipeOrigin.y;
double distance = sqrt(squaredDistance);
_padSwipeOrigin.x = point.x - deltaX / distance * 24;
_padSwipeOrigin.y = point.y - deltaY / distance * 24;
}
}
}
}
for (UITouch *touch in _touches) {
if (touch == _swipePadTouch) continue;
CGPoint point = [touch locationInView:self];
if (touch == _screenTouch) {
if (_screenSwiped) continue;
if (point.x - _screenSwipeOrigin.x > 32) {
[self turboSwipe];
}
else if (point.x - _screenSwipeOrigin.x < -32) {
[self rewindSwipe];
}
else if (point.y - _screenSwipeOrigin.y > 32) {
[self saveSwipe];
}
else if (point.y - _screenSwipeOrigin.y < -32) {
[self loadSwipe];
}
continue;
}
point.x *= factor;
point.y *= factor;
if (CGPointSquaredDistance(point, _layout.aLocation) <= buttonRadiusSquared) {
mask |= GB_KEY_A_MASK;
}
else if (CGPointSquaredDistance(point, _layout.bLocation) <= buttonRadiusSquared) {
mask |= GB_KEY_B_MASK;
}
else if (CGPointSquaredDistance(point, _layout.startLocation) <= buttonRadiusSquared) {
mask |= GB_KEY_START_MASK;
}
else if (CGPointSquaredDistance(point, _layout.selectLocation) <= buttonRadiusSquared) {
mask |= GB_KEY_SELECT_MASK;
}
else if (!dpadHandled &&
fabs(point.x - _layout.dpadLocation.x) <= dpadRadius &&
fabs(point.y - _layout.dpadLocation.y) <= dpadRadius) {
dpadHandled = true; // Don't handle the dpad twice
double angle = CGPointAngle(point, _layout.dpadLocation);
mask |= angleToKeyMask(angle);
}
}
if (mask != _lastMask) {
_aButtonView.image = [UIImage imageNamed:(mask & GB_KEY_A_MASK)? @"buttonPressed" : @"button"];
_bButtonView.image = [UIImage imageNamed:(mask & GB_KEY_B_MASK)? @"buttonPressed" : @"button"];
_startButtonView.image = [UIImage imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"];
_selectButtonView.image = [UIImage imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"];
bool hidden = false;
bool diagonal = false;
double rotation = 0;
switch (mask & (GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK | GB_KEY_LEFT_MASK | GB_KEY_UP_MASK)) {
case GB_KEY_RIGHT_MASK: break;
case GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK: diagonal = true; break;
case GB_KEY_DOWN_MASK: rotation = M_PI_2; break;
case GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK: diagonal = true; rotation = M_PI_2; break;
case GB_KEY_LEFT_MASK: rotation = M_PI; break;
case GB_KEY_LEFT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = M_PI; break;
case GB_KEY_UP_MASK: rotation = -M_PI_2; break;
case GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = -M_PI_2; break;
default:
hidden = true;
}
_dpadShadowView.hidden = hidden;
if (!hidden) {
if (_usesSwipePad) {
_dpadShadowView.image = [UIImage imageNamed:diagonal? @"swipepadShadowDiagonal" : @"swipepadShadow"];
}
else {
_dpadShadowView.image = [UIImage imageNamed:diagonal? @"dpadShadowDiagonal" : @"dpadShadow"];
}
_dpadShadowView.transform = CGAffineTransformMakeRotation(rotation);
}
GB_set_key_mask(_gbView.gb, mask);
if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) {
[[GBHapticManager sharedManager] doTapHaptic];
}
_lastMask = mask;
}
}
- (BOOL)isMultipleTouchEnabled
{
return true;
}
- (BOOL)isUserInteractionEnabled
{
return true;
}
- (void)setLayout:(GBLayout *)layout
{
_layout = layout;
self.image = layout.background;
positionView(_dpadView, layout.dpadLocation);
positionView(_aButtonView, layout.aLocation);
positionView(_bButtonView, layout.bLocation);
positionView(_startButtonView, layout.startLocation);
positionView(_selectButtonView, layout.selectLocation);
CGRect screenFrame = layout.screenRect;
screenFrame.origin.x /= [UIScreen mainScreen].scale;
screenFrame.origin.y /= [UIScreen mainScreen].scale;
screenFrame.size.width /= [UIScreen mainScreen].scale;
screenFrame.size.height /= [UIScreen mainScreen].scale;
_gbView.frame = screenFrame;
screenFrame.origin.x += 8;
screenFrame.origin.y += 8;
screenFrame.size.width -= 16;
screenFrame.size.height -= 16;
_screenLabel.frame = screenFrame;
}
- (void)setUsesSwipePad:(bool)usesSwipePad
{
_usesSwipePad = usesSwipePad;
_dpadView.image = [UIImage imageNamed:usesSwipePad? @"swipepad" : @"dpad"];
}
- (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title
{
if (@available(iOS 13.0, *)) {
((UIImageView *)_overlayViewContents).image = [UIImage systemImageNamed:imageName
withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]];
}
else {
((UILabel *)_overlayViewContents).text = title;
}
[_overlayViewContents sizeToFit];
CGRect bounds = _overlayViewContents.bounds;
bounds.origin = (CGPoint){8, 8};
bounds.size.width += 16;
bounds.size.height += 16;
_overlayView.frame = bounds;
_overlayView.alpha = 1.0;
}
- (void)fadeOverlayOut
{
[UIView animateWithDuration:1 animations:^{
_overlayView.alpha = 0;
}];
[_fadeTimer invalidate];
_fadeTimer = nil;
}
- (void)turboSwipe
{
_screenSwiped = true;
[self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"];
self.viewController.runMode = GBRunModeTurbo;
}
- (void)rewindSwipe
{
_screenSwiped = true;
[self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"];
self.viewController.runMode = GBRunModeRewind;
}
- (NSString *)swipeStateFile
{
return [[GBROMManager sharedManager] stateFile:1];
}
- (void)saveSwipe
{
_screenSwiped = true;
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
return;
}
[self displayOverlayWithImage:@"square.and.arrow.down" orTitle:@"Saved state to Slot 1"];
_fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) {
[self fadeOverlayOut];
}];
[self.viewController stop];
[self.viewController saveStateToFile:self.swipeStateFile];
[self.viewController start];
}
- (void)loadSwipe
{
_screenSwiped = true;
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
return;
}
[self displayOverlayWithImage:@"square.and.arrow.up" orTitle:@"Loaded state from Slot 1"];
_fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) {
[self fadeOverlayOut];
}];
[self.viewController stop];
[self.viewController loadStateFromFile:self.swipeStateFile];
[self.viewController start];
}
@end