diff --git a/DesktopShim/DesktopShim.entitlements b/DesktopShim/DesktopShim.entitlements deleted file mode 100644 index f2ef3ae..0000000 --- a/DesktopShim/DesktopShim.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - - diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.h b/SpriteKitWatchFace WatchKit Extension/FaceScene.h index 182b74d..1430f74 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.h +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.h @@ -60,6 +60,9 @@ typedef enum : NSUInteger { @property BOOL useProgrammaticLayout; @property BOOL useRoundFace; +@property CGSize majorMarkSize; +@property CGSize minorMarkSize; + @end diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index 4617d31..8beac48 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -11,46 +11,212 @@ #if TARGET_OS_IPHONE #define NSFont UIFont #define NSFontWeightMedium UIFontWeightMedium + +@interface NSValue (NSPointSupport) + ++ (NSValue *)valueWithPoint:(CGPoint)point; + +- (CGPoint)pointValue; + +@end + +@implementation NSValue (NSPointSupport) + ++ (NSValue *)valueWithPoint:(CGPoint)point +{ + return [self valueWithCGPoint:point]; +} + +- (CGPoint)pointValue +{ + return self.CGPointValue; +} + +@end + #endif #define PREPARE_SCREENSHOT 0 -CGFloat workingRadiusForFaceOfSizeWithAngle(CGSize faceSize, CGFloat angle) +// Adapted from https://www.particleincell.com/2013/cubic-line-intersection/ +NSArray *intersectionBetweenCubicCurveAndLine(CGPoint curvePointA, CGPoint curvePointB, CGPoint curvePointC, CGPoint curvePointD, CGPoint linePointA, CGPoint linePointB) { - CGFloat faceHeight = faceSize.height; - CGFloat faceWidth = faceSize.width; - - CGFloat workingRadius = 0; - - double vx = cos(angle); - double vy = sin(angle); - - double x1 = 0; - double y1 = 0; - double x2 = faceHeight; - double y2 = faceWidth; - double px = faceHeight/2; - double py = faceWidth/2; - - double t[4]; - double smallestT = 1000; - - t[0]=(x1-px)/vx; - t[1]=(x2-px)/vx; - t[2]=(y1-py)/vy; - t[3]=(y2-py)/vy; - - for (int m = 0; m < 4; m++) - { - double currentT = t[m]; - - if (currentT > 0 && currentT < smallestT) - smallestT = currentT; - } - - workingRadius = smallestT; - - return workingRadius; + CGFloat xAmB = linePointA.x - linePointB.x; + CGFloat xBmA = linePointB.x - linePointA.x; + CGFloat yAmB = linePointA.y - linePointB.y; + CGFloat yBmA = linePointB.y - linePointA.y; + CGFloat lineConstant = linePointA.x*yAmB + linePointA.y*xBmA; + + + CGPoint bezierCoefficient0 = CGPointMake(-curvePointA.x + 3.*curvePointB.x - 3.*curvePointC.x + curvePointD.x, + -curvePointA.y + 3.*curvePointB.y - 3.*curvePointC.y + curvePointD.y); + + CGPoint bezierCoefficient1 = CGPointMake(3.*curvePointA.x - 6.*curvePointB.x + 3.*curvePointC.x, + 3.*curvePointA.y - 6.*curvePointB.y + 3.*curvePointC.y); + + CGPoint bezierCoefficient2 = CGPointMake(-3.*curvePointA.x + 3.*curvePointB.x, + -3.*curvePointA.y + 3.*curvePointB.y); + + CGPoint bezierCoefficient3 = CGPointMake(curvePointA.x, + curvePointA.y); + + CGFloat polynomialCoefficient0 = yBmA*bezierCoefficient0.x + xAmB*bezierCoefficient0.y; // t^3 + CGFloat polynomialCoefficient1 = yBmA*bezierCoefficient1.x + xAmB*bezierCoefficient1.y; // t^2 + CGFloat polynomialCoefficient2 = yBmA*bezierCoefficient2.x + xAmB*bezierCoefficient2.y; // t^1 + CGFloat polynomialCoefficient3 = yBmA*bezierCoefficient3.x + xAmB*bezierCoefficient3.y + lineConstant; // t^0 + + if (polynomialCoefficient0 == 0) return nil; + + CGFloat A = polynomialCoefficient1/polynomialCoefficient0; + CGFloat B = polynomialCoefficient2/polynomialCoefficient0; + CGFloat C = polynomialCoefficient3/polynomialCoefficient0; + + CGFloat Q = (3.*B - A*A)/9.; + CGFloat R = (9.*A*B - 27*C - 2.*A*A*A)/54.; + + CGFloat discriminant = Q*Q*Q + R*R; + + // Possible roots + CGFloat root0 = -1; + CGFloat root1 = -1; + CGFloat root2 = -1; + + if (discriminant >= 0) { // Complex or duplicate roots + CGFloat S = ((R + sqrt(discriminant)) < 0 ? -1. : 1.)*pow(ABS(R + sqrt(discriminant)), 1./3.); + CGFloat T = ((R - sqrt(discriminant)) < 0 ? -1. : 1.)*pow(ABS(R - sqrt(discriminant)), 1./3.); + + root0 = -A/3. + (S + T); // Real root + root1 = -A/3. - (S + T)/2.; // real part of complex root + root2 = -A/3. - (S + T)/2.; // real part of other complex root + CGFloat complexComponent = ABS(sqrt(3.)*(S - T)/2.); + + if (complexComponent) { // We are uninterested in complex roots + root1 = -1; + root2 = -1; + } + } else { // distinct real roots + CGFloat theta = acos(R/sqrt(-Q*Q*Q)); + + root0 = 2.*sqrt(-Q)*cos(theta/3.) - A/3.; + root1 = 2.*sqrt(-Q)*cos((theta + 2.*M_PI)/3.) - A/3.; + root2 = 2.*sqrt(-Q)*cos((theta + 4.*M_PI)/3.) - A/3.; + } + + NSMutableArray *roots = [[NSMutableArray alloc] init]; + + if (root0 >= 0 && root0 <= 1) [roots addObject:@(root0)]; + if (root1 >= 0 && root1 <= 1) [roots addObject:@(root1)]; + if (root2 >= 0 && root2 <= 1) [roots addObject:@(root2)]; + + NSMutableArray *intersections = [[NSMutableArray alloc] init]; + + for (NSNumber *root in roots) { + CGFloat curveTime = root.doubleValue; + CGFloat t = curveTime; + + CGPoint intersectionPoint = CGPointMake(t*t*t*bezierCoefficient0.x + t*t*bezierCoefficient1.x + t*bezierCoefficient2.x + bezierCoefficient3.x, + t*t*t*bezierCoefficient0.y + t*t*bezierCoefficient1.y + t*bezierCoefficient2.y + bezierCoefficient3.y); + + CGFloat lineTime = 0; + + if (xBmA) { + lineTime = (intersectionPoint.x - linePointA.x)/xBmA; + } else { + lineTime = (intersectionPoint.y - linePointA.y)/yBmA; + } + + if (curveTime >= 0 && curveTime <= 1. && lineTime >= 0. && lineTime <= 1.) { + [intersections addObject:[NSValue valueWithPoint:intersectionPoint]]; + } + } + + return intersections; +} + +// Adapted from https://stackoverflow.com/a/565282/1565236 +NSValue *intersectionBetweenLineAndLine(CGPoint line1PointA, CGPoint line1PointB, CGPoint line2PointA, CGPoint line2PointB) +{ + line1PointB.x -= line1PointA.x; + line1PointB.y -= line1PointA.y; + line2PointB.x -= line2PointA.x; + line2PointB.y -= line2PointA.y; + + CGFloat denomenator = line1PointB.x*line2PointB.y - line1PointB.y*line2PointB.x; + + if (denomenator == 0) return nil; // lines are parallel/colinear // handle case when line1 is a point here? Or maybe not necessary + + CGPoint difference = CGPointMake(line2PointA.x - line1PointA.x, + line2PointA.y - line1PointA.y); + + + CGFloat line1Time = (difference.x*line2PointB.y - difference.y*line2PointB.x)/denomenator; + CGFloat line2Time = (difference.x*line1PointB.y - difference.y*line1PointB.x)/denomenator; + + if (line1Time >= 0. && line1Time <= 1. && line2Time >= 0. && line2Time <= 1.) { + CGPoint intersection = CGPointMake(line1PointA.x + line1Time*line1PointB.x, + line1PointA.y + line1Time*line1PointB.y); + + return [NSValue valueWithPoint:intersection]; + } else { + return nil; + } + + return nil; +} + +NSArray *intersectionsBetweenPathAndLinePassingThroughPoints(CGPathRef path, CGPoint linePointA, CGPoint linePointB) +{ + NSMutableArray *intersections = [[NSMutableArray alloc] init]; + + __block CGPoint firstPoint; + __block CGPoint lastPoint; + + CGPathApplyWithBlock(path, ^(const CGPathElement * _Nonnull elementPointer) { + CGPathElement element = *elementPointer; + + if (element.type == kCGPathElementMoveToPoint) { + firstPoint = element.points[0]; + lastPoint = element.points[0]; + } else if (element.type == kCGPathElementAddLineToPoint) { + // get intersection between both lines + + NSValue *localIntersection = intersectionBetweenLineAndLine(lastPoint, element.points[0], linePointA, linePointB); + if (localIntersection) [intersections addObject:localIntersection]; + + lastPoint = element.points[0]; + } else if (element.type == kCGPathElementAddQuadCurveToPoint) { + // get intersection between line and quad curve + + // cheat for now + NSValue *localIntersection = intersectionBetweenLineAndLine(lastPoint, element.points[1], linePointA, linePointB); + if (localIntersection) [intersections addObject:localIntersection]; + + lastPoint = element.points[1]; + } else if (element.type == kCGPathElementAddCurveToPoint) { + // get intersection between line and cubic curve + + NSArray *localIntersections = intersectionBetweenCubicCurveAndLine(lastPoint, element.points[0], element.points[1], element.points[2], linePointA, linePointB); + if (localIntersections.count) [intersections addObjectsFromArray:localIntersections]; + + lastPoint = element.points[2]; + } else if (element.type == kCGPathElementCloseSubpath) { + // get intersection between both lines + + NSValue *localIntersection = intersectionBetweenLineAndLine(lastPoint, firstPoint, linePointA, linePointB); + if (localIntersection) [intersections addObject:localIntersection]; + + lastPoint = firstPoint; + } + }); + + return intersections; +} + +CGPoint intersectionBetweenPathAndLinePassingThroughPoints(CGPathRef path, CGPoint linePointA, CGPoint linePointB) +{ + NSArray *intersections = intersectionsBetweenPathAndLinePassingThroughPoints(path, linePointA, linePointB); + + return intersections.firstObject.pointValue; } @implementation FaceScene @@ -62,9 +228,12 @@ - (instancetype)initWithCoder:(NSCoder *)coder self.theme = ThemeDelay; self.useProgrammaticLayout = YES; - self.useRoundFace = YES; + self.useRoundFace = NO; self.numeralStyle = NumeralStyleAll; self.tickmarkStyle = TickmarkStyleAll; + + self.majorMarkSize = CGSizeMake(3, 9); + self.minorMarkSize = CGSizeMake(1, 4.5); [self setupColors]; [self setupScene]; @@ -141,55 +310,95 @@ -(void)setupTickmarksForRectangularFace CGFloat labelXMargin = 24.0; CGSize faceSize = (CGSize){184, 224}; + CGFloat cornerRadius = 34; + + CGFloat workingRadius = sqrt(faceSize.width/2.*faceSize.width/2. + faceSize.height/2.*faceSize.height/2.); + CGFloat majorMarkHeight = self.majorMarkSize.height; + CGFloat minorMarkHeight = self.minorMarkSize.height; + CGFloat majorMarkWidth = self.majorMarkSize.width/2.; + CGFloat minorMarkWidth = self.minorMarkSize.width/2.; + + CGPathRef outerPath = CGPathCreateWithRoundedRect(CGRectMake(margin - faceSize.width/2., margin - faceSize.height/2., faceSize.width - margin*2., faceSize.height - margin*2.), cornerRadius - margin, cornerRadius - margin, NULL); + + CGPathRef largeTickInnerPath = CGPathCreateWithRoundedRect(CGRectMake(margin + majorMarkHeight - faceSize.width/2., margin + majorMarkHeight - faceSize.height/2., faceSize.width - margin*2. - majorMarkHeight*2., faceSize.height - margin*2. - majorMarkHeight*2.), cornerRadius - margin - majorMarkHeight, cornerRadius - margin - majorMarkHeight, NULL); - /* Major */ for (int i = 0; i < 12; i++) { - CGFloat angle = -(2*M_PI)/12.0 * i; - CGFloat workingRadius = workingRadiusForFaceOfSizeWithAngle(faceSize, angle); - CGFloat longTickHeight = workingRadius/10.0; - - SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.majorMarkColor size:CGSizeMake(2, longTickHeight)]; - - tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, (workingRadius-margin)/longTickHeight); - tick.zRotation = angle; - - tick.zPosition = 0; - - if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMajor) - [self addChild:tick]; + CGFloat angle = -(2*M_PI)/12.0 * i; + + CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); + + CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-majorMarkWidth, 0), rotation); + CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-majorMarkWidth, workingRadius), rotation); + CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(majorMarkWidth, 0), rotation); + CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(majorMarkWidth, workingRadius), rotation); + + CGPoint topLeft = intersectionBetweenPathAndLinePassingThroughPoints(outerPath, lineLeftPointA, lineLeftPointB); + CGPoint topRight = intersectionBetweenPathAndLinePassingThroughPoints(outerPath, lineRightPointA, lineRightPointB); + CGPoint bottomLeft = intersectionBetweenPathAndLinePassingThroughPoints(largeTickInnerPath, lineLeftPointA, lineLeftPointB); + CGPoint bottomRight = intersectionBetweenPathAndLinePassingThroughPoints(largeTickInnerPath, lineRightPointA, lineRightPointB); + + CGMutablePathRef tickPath = CGPathCreateMutable(); + CGPathMoveToPoint(tickPath, NULL, topLeft.x, topLeft.y); + CGPathAddLineToPoint(tickPath, NULL, topRight.x, topRight.y); + CGPathAddLineToPoint(tickPath, NULL, bottomRight.x, bottomRight.y); + CGPathAddLineToPoint(tickPath, NULL, bottomLeft.x, bottomLeft.y); + CGPathCloseSubpath(tickPath); + + SKShapeNode *tick = [SKShapeNode shapeNodeWithPath:tickPath]; + tick.fillColor = self.majorMarkColor; + tick.strokeColor = [SKColor clearColor]; + tick.position = CGPointZero; + + CGPathRelease(tickPath); + + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMajor) + [self addChild:tick]; } + + CGPathRelease(largeTickInnerPath); /* Minor */ - for (int i = 0; i < 60; i++) - { - - CGFloat angle = (2*M_PI)/60.0 * i; - CGFloat workingRadius = workingRadiusForFaceOfSizeWithAngle(faceSize, angle); - CGFloat shortTickHeight = workingRadius/20; - SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.minorMarkColor size:CGSizeMake(1, shortTickHeight)]; - - /* Super hacky hack to inset the tickmarks at the four corners of a curved display instead of doing math */ - if (i == 6 || i == 7 || i == 23 || i == 24 || i == 36 || i == 37 || i == 53 || i == 54) - { - workingRadius -= 8; - } + CGPathRef smallTickInnerPath = CGPathCreateWithRoundedRect(CGRectMake(margin + minorMarkHeight - faceSize.width/2., margin + minorMarkHeight - faceSize.height/2., faceSize.width - margin*2. - minorMarkHeight*2., faceSize.height - margin*2. - minorMarkHeight*2.), cornerRadius - margin - minorMarkHeight, cornerRadius - margin - minorMarkHeight, NULL); + + for (int i = 0; i < 60; i++) + { + if (i % 5 == 0) continue; - tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, (workingRadius-margin)/shortTickHeight); - tick.zRotation = angle; - - tick.zPosition = 0; - - if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMinor) - { - if (i % 5 != 0) - { - [self addChild:tick]; - } - } - } + CGFloat angle = - (2*M_PI)/60.0 * i; + + CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); + + CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-minorMarkWidth, 0), rotation); + CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-minorMarkWidth, workingRadius), rotation); + CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(minorMarkWidth, 0), rotation); + CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(minorMarkWidth, workingRadius), rotation); + + CGPoint topLeft = intersectionBetweenPathAndLinePassingThroughPoints(outerPath, lineLeftPointA, lineLeftPointB); + CGPoint topRight = intersectionBetweenPathAndLinePassingThroughPoints(outerPath, lineRightPointA, lineRightPointB); + CGPoint bottomLeft = intersectionBetweenPathAndLinePassingThroughPoints(smallTickInnerPath, lineLeftPointA, lineLeftPointB); + CGPoint bottomRight = intersectionBetweenPathAndLinePassingThroughPoints(smallTickInnerPath, lineRightPointA, lineRightPointB); + + CGMutablePathRef tickPath = CGPathCreateMutable(); + CGPathMoveToPoint(tickPath, NULL, topLeft.x, topLeft.y); + CGPathAddLineToPoint(tickPath, NULL, topRight.x, topRight.y); + CGPathAddLineToPoint(tickPath, NULL, bottomRight.x, bottomRight.y); + CGPathAddLineToPoint(tickPath, NULL, bottomLeft.x, bottomLeft.y); + CGPathCloseSubpath(tickPath); + + SKShapeNode *tick = [SKShapeNode shapeNodeWithPath:tickPath]; + tick.fillColor = self.minorMarkColor; + tick.strokeColor = [SKColor clearColor]; + tick.position = CGPointZero; + + CGPathRelease(tickPath); + + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMinor) + [self addChild:tick]; + } + + CGPathRelease(outerPath); + CGPathRelease(smallTickInnerPath); /* Numerals */ for (int i = 1; i <= 12; i++) diff --git a/SpriteKitWatchFace.xcodeproj/project.pbxproj b/SpriteKitWatchFace.xcodeproj/project.pbxproj index 439e4c5..0d16a19 100644 --- a/SpriteKitWatchFace.xcodeproj/project.pbxproj +++ b/SpriteKitWatchFace.xcodeproj/project.pbxproj @@ -107,7 +107,6 @@ B0EB72FA216E69AB0098CF27 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; B0EB72FC216E69AB0098CF27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B0EB72FD216E69AB0098CF27 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - B0EB72FF216E69AB0098CF27 /* DesktopShim.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DesktopShim.entitlements; sourceTree = ""; }; B0EB7305216E6C380098CF27 /* FaceScene.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FaceScene.h; sourceTree = ""; }; B0EB7306216E6C380098CF27 /* FaceScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FaceScene.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -213,7 +212,6 @@ B0EB72F9216E69AB0098CF27 /* Main.storyboard */, B0EB72FC216E69AB0098CF27 /* Info.plist */, B0EB72FD216E69AB0098CF27 /* main.m */, - B0EB72FF216E69AB0098CF27 /* DesktopShim.entitlements */, ); path = DesktopShim; sourceTree = ""; @@ -311,6 +309,11 @@ }; B0EB72E7216E69AA0098CF27 = { CreatedOnToolsVersion = 10.0; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 0; + }; + }; }; }; }; @@ -686,11 +689,10 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = DesktopShim/DesktopShim.entitlements; - CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 2ZDN69KUUV; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = DesktopShim/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -707,11 +709,10 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = DesktopShim/DesktopShim.entitlements; - CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 2ZDN69KUUV; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = DesktopShim/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)",