diff --git a/Makefile b/Makefile index 9953039..c1bf807 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,7 @@ endif CFLAGS += -arch arm64 -miphoneos-version-min=11.0 -isysroot $(SYSROOT) LDFLAGS += -arch arm64 OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) -LDFLAGS += -framework UIKit -framework Metal -framework MetalKit -framework AVFoundation -miphoneos-version-min=11.0 -isysroot $(SYSROOT) +LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AVFoundation -miphoneos-version-min=11.0 -isysroot $(SYSROOT) CODESIGN := codesign -fs - else ifeq ($(PLATFORM),Darwin) diff --git a/iOS/GBHorizontalLayout.h b/iOS/GBHorizontalLayout.h new file mode 100644 index 0000000..1eb9e3d --- /dev/null +++ b/iOS/GBHorizontalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBHorizontalLayout : GBLayout + +@end diff --git a/iOS/GBHorizontalLayout.m b/iOS/GBHorizontalLayout.m new file mode 100644 index 0000000..d614afe --- /dev/null +++ b/iOS/GBHorizontalLayout.m @@ -0,0 +1,112 @@ +#define GBLayoutInternal +#import "GBHorizontalLayout.h" + +@implementation GBHorizontalLayout + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + + CGSize resolution = {self.resolution.height - self.cutout, self.resolution.width}; + + CGRect screenRect = {0,}; + screenRect.size.height = self.hasFractionalPixels? resolution.height : floor(resolution.height / 144) * 144; + screenRect.size.width = screenRect.size.height / 144 * 160; + + double horizontalMargin, verticalMargin; + while (true) { + horizontalMargin = (resolution.width - screenRect.size.width) / 2; + verticalMargin = (resolution.height - screenRect.size.height) / 2; + if (horizontalMargin / self.factor < 170) { + if (self.hasFractionalPixels) { + screenRect.size.width = resolution.width - 170 * self.factor * 2; + screenRect.size.height = screenRect.size.width / 160 * 144; + continue; + } + screenRect.size.width -= 160; + screenRect.size.height -= 144; + continue; + } + break; + } + + double screenBorderWidth = screenRect.size.width / 40; + + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + bool drawSameBoyLogo = false; + if (verticalMargin * 2 > screenBorderWidth * 7) { + drawSameBoyLogo = true; + screenRect.origin.y = (resolution.height - screenRect.size.height - screenBorderWidth * 5) / 2; + } + else { + screenRect.origin.y = (resolution.height - screenRect.size.height) / 2; + } + + self.screenRect = screenRect; + + self.dpadLocation = (CGPoint){ + round((screenRect.origin.x - screenBorderWidth) / 2), + round(resolution.height * 3 / 8) + }; + + double wingWidth = (resolution.width - screenRect.size.width) / 2 - screenBorderWidth * 5; + double buttonRadius = 36 * self.factor; + CGSize buttonsDelta = [self buttonDeltaForMaxHorizontalDistance:wingWidth - buttonRadius * 2]; + CGPoint buttonsCenter = { + resolution.width - self.dpadLocation.x, + self.dpadLocation.y, + }; + + self.aLocation = (CGPoint) { + round(buttonsCenter.x + buttonsDelta.width / 2), + round(buttonsCenter.y - buttonsDelta.height / 2) + }; + + self.bLocation = (CGPoint) { + round(buttonsCenter.x - buttonsDelta.width / 2), + round(buttonsCenter.y + buttonsDelta.height / 2) + }; + + self.selectLocation = (CGPoint){ + self.dpadLocation.x, + MIN(round(resolution.height * 3 / 4), self.dpadLocation.y + 180 * self.factor) + }; + + self.startLocation = (CGPoint){ + buttonsCenter.x, + self.selectLocation.y + }; + + resolution.width += self.cutout * 2; + self.screenRect = (CGRect){{self.screenRect.origin.x + self.cutout, self.screenRect.origin.y}, self.screenRect.size}; + self.dpadLocation = (CGPoint){self.dpadLocation.x + self.cutout, self.dpadLocation.y}; + self.aLocation = (CGPoint){self.aLocation.x + self.cutout, self.aLocation.y}; + self.bLocation = (CGPoint){self.bLocation.x + self.cutout, self.bLocation.y}; + self.startLocation = (CGPoint){self.startLocation.x + self.cutout, self.startLocation.y}; + self.selectLocation = (CGPoint){self.selectLocation.x + self.cutout, self.selectLocation.y}; + + UIGraphicsBeginImageContextWithOptions(resolution, true, 1); + [self drawBackground]; + [self drawScreenBezels]; + if (drawSameBoyLogo) { + double bezelBottom = screenRect.origin.y + screenRect.size.height + screenBorderWidth; + double freeSpace = resolution.height - bezelBottom; + [self drawLogoInVerticalRange:(NSRange){bezelBottom + screenBorderWidth * 2, freeSpace - screenBorderWidth * 4}]; + } + + [self drawLabels]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation +{ + if (orientation == UIInterfaceOrientationLandscapeLeft) { + return CGRectMake(-(signed)self.cutout / (signed)self.factor, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); + } + return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); +} +@end diff --git a/iOS/GBLayout.h b/iOS/GBLayout.h new file mode 100644 index 0000000..01a9f90 --- /dev/null +++ b/iOS/GBLayout.h @@ -0,0 +1,40 @@ +#import + +@interface GBLayout : NSObject +@property (readonly) UIImage *background; +@property (readonly) CGRect screenRect; +@property (readonly) CGPoint dpadLocation; +@property (readonly) CGPoint aLocation; +@property (readonly) CGPoint bLocation; +@property (readonly) CGPoint startLocation; +@property (readonly) CGPoint selectLocation; +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation; +@end + +#ifdef GBLayoutInternal + +@interface GBLayout() +@property UIImage *background; +@property CGRect screenRect; +@property CGPoint dpadLocation; +@property CGPoint aLocation; +@property CGPoint bLocation; +@property CGPoint startLocation; +@property CGPoint selectLocation; +@property (readonly) CGSize resolution; +@property (readonly) unsigned factor; +@property (readonly) unsigned minY; +@property (readonly) unsigned cutout; +@property (readonly) bool hasFractionalPixels; + +@property (readonly) UIColor *brandColor; + +- (void)drawBackground; +- (void)drawScreenBezels; +- (void)drawLogoInVerticalRange:(NSRange)range; +- (void)drawLabels; + +- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)distance; +@end + +#endif diff --git a/iOS/GBLayout.m b/iOS/GBLayout.m new file mode 100644 index 0000000..de99ba7 --- /dev/null +++ b/iOS/GBLayout.m @@ -0,0 +1,166 @@ +#define GBLayoutInternal +#import "GBLayout.h" + +@interface UIApplication() +- (double)statusBarHeightForOrientation:(UIInterfaceOrientation)orientation ignoreHidden:(bool)ignoreHidden; +@end + +@implementation GBLayout +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + _factor = [UIScreen mainScreen].scale; + _resolution = [UIScreen mainScreen].bounds.size; + _resolution.width *= _factor; + _resolution.height *= _factor; + if (_resolution.width > _resolution.height) { + _resolution = (CGSize){_resolution.height, _resolution.width}; + } + _minY = [[UIApplication sharedApplication] statusBarHeightForOrientation:UIInterfaceOrientationPortrait + ignoreHidden:true] * _factor; + _cutout = _minY <= 24 * _factor? 0 : _minY; + + // The Plus series will scale things lossily anyway, so no need to bother with integer scale things + // This also "catches" zoomed display modes + _hasFractionalPixels = _factor != [UIScreen mainScreen].nativeScale; + return self; +} + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation +{ + return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); +} + +- (UIColor *)brandColor +{ + static dispatch_once_t onceToken; + static UIColor *ret = nil; + dispatch_once(&onceToken, ^{ + ret = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0]; + }); + return ret; +} + +- (void)drawBackground +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorRef top = [UIColor colorWithRed:192 / 255.0 green:195 / 255.0 blue:199 / 255.0 alpha:1.0].CGColor; + CGColorRef bottom = [UIColor colorWithRed:174 / 255.0 green:176 / 255.0 blue:180 / 255.0 alpha:1.0].CGColor; + CGColorRef colors[] = {top, bottom}; + CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks); + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL); + CGContextDrawLinearGradient(context, + gradient, + (CGPoint){0, 0}, + (CGPoint){0, CGBitmapContextGetHeight(context)}, + 0); + + CFRelease(gradient); + CFRelease(colorsArray); + CFRelease(colorspace); +} + +- (void)drawScreenBezels +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorRef top = [UIColor colorWithWhite:53 / 255.0 alpha:1.0].CGColor; + CGColorRef bottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0].CGColor; + CGColorRef colors[] = {top, bottom}; + CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks); + + double borderWidth = self.screenRect.size.width / 40; + CGRect bezelRect = self.screenRect; + bezelRect.origin.x -= borderWidth; + bezelRect.origin.y -= borderWidth; + bezelRect.size.width += borderWidth * 2; + bezelRect.size.height += borderWidth * 2; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bezelRect cornerRadius:borderWidth]; + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, (CGSize){0,}, borderWidth / 2, [UIColor colorWithWhite:0 alpha:1.0].CGColor); + [[UIColor colorWithWhite:0 alpha:0.25] setFill]; + [path fill]; + [path addClip]; + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL); + CGContextDrawLinearGradient(context, + gradient, + bezelRect.origin, + (CGPoint){bezelRect.origin.x, bezelRect.origin.y + bezelRect.size.height}, + 0); + + CGContextRestoreGState(context); + + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, (CGSize){0,}, borderWidth / 2, [UIColor colorWithWhite:0 alpha:0.25].CGColor); + + [[UIColor blackColor] setFill]; + UIRectFill(self.screenRect); + CGContextRestoreGState(context); + + CFRelease(gradient); + CFRelease(colorsArray); + CFRelease(colorspace); +} + +- (void)drawLogoInVerticalRange:(NSRange)range +{ + UIFont *font = [UIFont fontWithName:@"AvenirNext-BoldItalic" size:range.length * 4 / 3]; + + CGRect rect = CGRectMake(0, + range.location - range.length / 3, + CGBitmapContextGetWidth(UIGraphicsGetCurrentContext()), range.length * 2); + NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; + style.alignment = NSTextAlignmentCenter; + [@"SAMEBOY" drawInRect:rect + withAttributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName:self.brandColor, + NSParagraphStyleAttributeName: style, + }]; +} + +- (void)drawRotatedLabel:(NSString *)label withFont:(UIFont *)font origin:(CGPoint)origin distance:(double)distance +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + CGContextConcatCTM(context, CGAffineTransformMakeTranslation(origin.x, origin.y)); + CGContextConcatCTM(context, CGAffineTransformMakeRotation(-M_PI / 6)); + + NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; + style.alignment = NSTextAlignmentCenter; + + [label drawInRect:CGRectMake(-256, distance, 512, 256) + withAttributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName:self.brandColor, + NSParagraphStyleAttributeName: style, + }]; + CGContextRestoreGState(context); +} + +- (void)drawLabels +{ + + UIFont *labelFont = [UIFont fontWithName:@"AvenirNext-Bold" size:24 * _factor]; + UIFont *smallLabelFont = [UIFont fontWithName:@"AvenirNext-DemiBold" size:20 * _factor]; + + [self drawRotatedLabel:@"A" withFont:labelFont origin:self.aLocation distance:40 * self.factor]; + [self drawRotatedLabel:@"B" withFont:labelFont origin:self.bLocation distance:40 * self.factor]; + [self drawRotatedLabel:@"SELECT" withFont:smallLabelFont origin:self.selectLocation distance:24 * self.factor]; + [self drawRotatedLabel:@"START" withFont:smallLabelFont origin:self.startLocation distance:24 * self.factor]; +} + +- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)maxDistance +{ + CGSize buttonsDelta = {90 * self.factor, 45 * self.factor}; + if (buttonsDelta.width <= maxDistance) { + return buttonsDelta; + } + return (CGSize){maxDistance, floor(sqrt(100 * 100 * self.factor * self.factor - maxDistance * maxDistance))}; +} +@end diff --git a/iOS/GBVerticalLayout.h b/iOS/GBVerticalLayout.h new file mode 100644 index 0000000..a5720ea --- /dev/null +++ b/iOS/GBVerticalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBVerticalLayout : GBLayout + +@end diff --git a/iOS/GBVerticalLayout.m b/iOS/GBVerticalLayout.m new file mode 100644 index 0000000..e426960 --- /dev/null +++ b/iOS/GBVerticalLayout.m @@ -0,0 +1,76 @@ +#define GBLayoutInternal +#import "GBVerticalLayout.h" + +@implementation GBVerticalLayout + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + + CGSize resolution = self.resolution; + + CGRect screenRect = {0,}; + screenRect.size.width = self.hasFractionalPixels? resolution.width : floor(resolution.width / 160) * 160; + screenRect.size.height = screenRect.size.width / 160 * 144; + + double screenBorderWidth = screenRect.size.width / 40; + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + screenRect.origin.y = self.minY + screenBorderWidth * 2; + self.screenRect = screenRect; + + double controlAreaStart = screenRect.origin.y + screenRect.size.height + screenBorderWidth * 2; + + self.selectLocation = (CGPoint){ + MIN(resolution.width / 4, 120 * self.factor), + MIN(resolution.height - 80 * self.factor, (resolution.height - controlAreaStart) * 0.75 + controlAreaStart) + }; + + self.startLocation = (CGPoint){ + resolution.width - self.selectLocation.x, + self.selectLocation.y + }; + + double buttonRadius = 36 * self.factor; + CGSize buttonsDelta = [self buttonDeltaForMaxHorizontalDistance:resolution.width / 2 - buttonRadius * 2 - screenBorderWidth * 2]; + + self.dpadLocation = (CGPoint) { + self.selectLocation.x, + self.selectLocation.y - 140 * self.factor + }; + + CGPoint buttonsCenter = { + resolution.width - self.dpadLocation.x, + self.dpadLocation.y, + }; + + self.aLocation = (CGPoint) { + round(buttonsCenter.x + buttonsDelta.width / 2), + round(buttonsCenter.y - buttonsDelta.height / 2) + }; + + self.bLocation = (CGPoint) { + round(buttonsCenter.x - buttonsDelta.width / 2), + round(buttonsCenter.y + buttonsDelta.height / 2) + }; + + double controlsTop = self.dpadLocation.y - 80 * self.factor; + double middleSpace = self.bLocation.x - buttonRadius - (self.dpadLocation.x + 80 * self.factor); + + UIGraphicsBeginImageContextWithOptions(resolution, true, 1); + [self drawBackground]; + [self drawScreenBezels]; + + if (controlsTop - controlAreaStart > 24 * self.factor + screenBorderWidth * 2 || + middleSpace > 160 * self.factor) { + [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor}]; + } + + [self drawLabels]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +@end diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h new file mode 100644 index 0000000..3e41d5e --- /dev/null +++ b/iOS/GBViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBViewController : UIViewController +@property (nullable, nonatomic, strong) UIWindow *window; +@end diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m new file mode 100644 index 0000000..98b50c2 --- /dev/null +++ b/iOS/GBViewController.m @@ -0,0 +1,92 @@ +#import "GBViewController.h" +#import "GBHorizontalLayout.h" +#import "GBVerticalLayout.h" + +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 + }; + +} + +@implementation GBViewController +{ + GBLayout *_currentLayout; + GBHorizontalLayout *_horizontalLayout; + GBVerticalLayout *_verticalLayout; + UIImageView *_backgroundView; + UIImageView *_dpadView; + UIImageView *_aButtonView; + UIImageView *_bButtonView; + UIImageView *_startButtonView; + UIImageView *_selectButtonView; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + _window = [[UIWindow alloc] init]; + _window.rootViewController = self; + [_window makeKeyAndVisible]; + _horizontalLayout = [[GBHorizontalLayout alloc] init]; + _verticalLayout = [[GBVerticalLayout alloc] init]; + + _backgroundView = [[UIImageView alloc] initWithImage:nil]; + [_window addSubview:_backgroundView]; + + _dpadView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpad"]]; + _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"]]; + + [_backgroundView addSubview:_dpadView]; + [_backgroundView addSubview:_aButtonView]; + [_backgroundView addSubview:_bButtonView]; + [_backgroundView addSubview:_startButtonView]; + [_backgroundView addSubview:_selectButtonView]; + + [self orientationChange]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChange) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; + return true; +} + +- (void)orientationChange +{ + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { + _currentLayout = _verticalLayout; + } + else { + _currentLayout = _horizontalLayout; + } + + _backgroundView.image = _currentLayout.background; + _backgroundView.frame = [_currentLayout viewRectForOrientation:[UIApplication sharedApplication].statusBarOrientation]; + + positionView(_dpadView, _currentLayout.dpadLocation); + positionView(_aButtonView, _currentLayout.aLocation); + positionView(_bButtonView, _currentLayout.bLocation); + positionView(_startButtonView, _currentLayout.startLocation); + positionView(_selectButtonView, _currentLayout.selectLocation); +} + +- (BOOL)prefersHomeIndicatorAutoHidden +{ + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { + return false; + } + return true; +} + +@end diff --git a/iOS/button2@2x.png b/iOS/button2@2x.png new file mode 100644 index 0000000..ccb6a4e Binary files /dev/null and b/iOS/button2@2x.png differ diff --git a/iOS/button2@3x.png b/iOS/button2@3x.png new file mode 100644 index 0000000..8feb468 Binary files /dev/null and b/iOS/button2@3x.png differ diff --git a/iOS/button@2x.png b/iOS/button@2x.png new file mode 100644 index 0000000..5737256 Binary files /dev/null and b/iOS/button@2x.png differ diff --git a/iOS/button@3x.png b/iOS/button@3x.png new file mode 100644 index 0000000..44626ad Binary files /dev/null and b/iOS/button@3x.png differ diff --git a/iOS/dpad@2x.png b/iOS/dpad@2x.png new file mode 100644 index 0000000..ca8fca6 Binary files /dev/null and b/iOS/dpad@2x.png differ diff --git a/iOS/dpad@3x.png b/iOS/dpad@3x.png new file mode 100644 index 0000000..0e40b10 Binary files /dev/null and b/iOS/dpad@3x.png differ diff --git a/iOS/main.m b/iOS/main.m index 4af9f5f..74c22f4 100644 --- a/iOS/main.m +++ b/iOS/main.m @@ -1,6 +1,7 @@ #import +#import "GBViewController.h" int main(int argc, char * argv[]) { - return UIApplicationMain(argc, argv, nil, nil); + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GBViewController class])); }