Automatic layout generation

This commit is contained in:
Lior Halphon 2023-01-13 16:44:36 +02:00
parent d9bf739f52
commit 0441967332
16 changed files with 504 additions and 2 deletions

View File

@ -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)

5
iOS/GBHorizontalLayout.h Normal file
View File

@ -0,0 +1,5 @@
#import "GBLayout.h"
@interface GBHorizontalLayout : GBLayout
@end

112
iOS/GBHorizontalLayout.m Normal file
View File

@ -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

40
iOS/GBLayout.h Normal file
View File

@ -0,0 +1,40 @@
#import <UIKit/UIKit.h>
@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

166
iOS/GBLayout.m Normal file
View File

@ -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

5
iOS/GBVerticalLayout.h Normal file
View File

@ -0,0 +1,5 @@
#import "GBLayout.h"
@interface GBVerticalLayout : GBLayout
@end

76
iOS/GBVerticalLayout.m Normal file
View File

@ -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

5
iOS/GBViewController.h Normal file
View File

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface GBViewController : UIViewController <UIApplicationDelegate>
@property (nullable, nonatomic, strong) UIWindow *window;
@end

92
iOS/GBViewController.m Normal file
View File

@ -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

BIN
iOS/button2@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
iOS/button2@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
iOS/button@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
iOS/button@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
iOS/dpad@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
iOS/dpad@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -1,6 +1,7 @@
#import <UIKit/UIKit.h>
#import "GBViewController.h"
int main(int argc, char * argv[])
{
return UIApplicationMain(argc, argv, nil, nil);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([GBViewController class]));
}