From 4bbdeac111f875d3d55667dbaee18db1d0bf017d Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 07:58:58 -0700 Subject: [PATCH 1/8] Remove desktop shim code signing --- DesktopShim/DesktopShim.entitlements | 10 ---------- SpriteKitWatchFace.xcodeproj/project.pbxproj | 17 +++++++++-------- 2 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 DesktopShim/DesktopShim.entitlements 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.xcodeproj/project.pbxproj b/SpriteKitWatchFace.xcodeproj/project.pbxproj index 047c162..1651d5f 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)", From 24a0cf4d48b9f8796c0a3cbcf29283c0f02a38aa Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 08:44:34 -0700 Subject: [PATCH 2/8] Use a mask for the square face instead of messing with math --- .../FaceScene.m | 105 ++++++++---------- 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index e145f7b..e120736 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -12,44 +12,6 @@ #define NSFont UIFont #endif -CGFloat workingRadiusForFaceOfSizeWithAngle(CGSize faceSize, CGFloat angle) -{ - 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; -} - @implementation FaceScene - (instancetype)initWithCoder:(NSCoder *)coder @@ -59,7 +21,7 @@ - (instancetype)initWithCoder:(NSCoder *)coder self.theme = ThemeHermesPink; self.useProgrammaticLayout = YES; - self.useRoundFace = YES; + self.useRoundFace = NO; [self setupColors]; [self setupScene]; @@ -132,41 +94,64 @@ -(void)setupTickmarksForRectangularFace CGFloat labelXMargin = 24.0; CGSize faceSize = (CGSize){184, 224}; + CGFloat cornerRadius = 16; + + CGFloat workingRadius = faceSize.width/2; + CGFloat longTickHeight = workingRadius/10.; + CGFloat shortTickHeight = workingRadius/20.; + + /* Major */ + SKCropNode *majorTicksLayer = [[SKCropNode alloc] init]; + majorTicksLayer.position = CGPointZero; + majorTicksLayer.zPosition = 0; + + SKShapeNode *majorTicksMask = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(faceSize.width - margin*2. - longTickHeight, faceSize.height - margin*2. - longTickHeight) cornerRadius:cornerRadius - margin - longTickHeight/2.]; + majorTicksMask.lineWidth = longTickHeight; + majorTicksMask.strokeColor = [SKColor colorWithWhite:1 alpha:1]; + majorTicksMask.position = CGPointZero; + majorTicksMask.antialiased = NO; + + majorTicksLayer.maskNode = majorTicksMask; + [self addChild:majorTicksLayer]; - /* 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.markColor size:CGSizeMake(2, longTickHeight)]; - - tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, (workingRadius-margin)/longTickHeight); - tick.zRotation = angle; - - tick.zPosition = 0; - - [self addChild:tick]; + CGFloat angle = -(2*M_PI)/12.0 * i; + + SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.markColor size:CGSizeMake(2, workingRadius)]; + + tick.position = CGPointZero; + tick.anchorPoint = CGPointMake(0.5, 1.5); + tick.zRotation = angle; + + [majorTicksLayer addChild:tick]; } /* Minor */ + SKCropNode *minorTicksLayer = [[SKCropNode alloc] init]; + minorTicksLayer.position = CGPointZero; + minorTicksLayer.zPosition = 0; + + SKShapeNode *minorTicksMask = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(faceSize.width - margin*2. - shortTickHeight, faceSize.height - margin*2. - shortTickHeight) cornerRadius:cornerRadius - margin - shortTickHeight/2.]; + minorTicksMask.lineWidth = shortTickHeight; + minorTicksMask.strokeColor = [SKColor colorWithWhite:1 alpha:1]; + minorTicksMask.position = CGPointZero; + minorTicksMask.antialiased = NO; + + minorTicksLayer.maskNode = minorTicksMask; + [self addChild:minorTicksLayer]; + 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.markColor size:CGSizeMake(1, shortTickHeight)]; + SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.markColor size:CGSizeMake(1, workingRadius)]; tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, (workingRadius-margin)/shortTickHeight); + tick.anchorPoint = CGPointMake(0.5, 1.5); tick.zRotation = angle; - tick.zPosition = 0; if (i % 5 != 0) - [self addChild:tick]; + [minorTicksLayer addChild:tick]; } /* Numerals */ From d6b2cece03ece8b03e942523508407996cb04699 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 20:19:36 -0700 Subject: [PATCH 3/8] Actually, go ahead and throw some math at the problem --- .../FaceScene.m | 292 +++++++++++++++--- 1 file changed, 250 insertions(+), 42 deletions(-) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index e120736..61cd48d 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -12,6 +12,191 @@ #define NSFont UIFont #endif +// 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 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; + } + }); + + if (intersections.count == 0) { + NSLog(@"No intersections for %@ - %@", NSStringFromPoint(linePointA), NSStringFromPoint(linePointB)); + } + + return intersections; +} + +CGPoint intersectionBetweenPathAndLinePassingThroughPoints(CGPathRef path, CGPoint linePointA, CGPoint linePointB) +{ + NSArray *intersections = intersectionsBetweenPathAndLinePassingThroughPoints(path, linePointA, linePointB); + + return intersections.firstObject.pointValue; +} + @implementation FaceScene - (instancetype)initWithCoder:(NSCoder *)coder @@ -94,65 +279,88 @@ -(void)setupTickmarksForRectangularFace CGFloat labelXMargin = 24.0; CGSize faceSize = (CGSize){184, 224}; - CGFloat cornerRadius = 16; + CGFloat cornerRadius = 34; - CGFloat workingRadius = faceSize.width/2; - CGFloat longTickHeight = workingRadius/10.; - CGFloat shortTickHeight = workingRadius/20.; + CGFloat workingRadius = sqrt(faceSize.width/2.*faceSize.width/2. + faceSize.height/2.*faceSize.height/2.); + CGFloat longTickHeight = round(faceSize.width/2./10.); + CGFloat shortTickHeight = longTickHeight/2.; - /* Major */ - SKCropNode *majorTicksLayer = [[SKCropNode alloc] init]; - majorTicksLayer.position = CGPointZero; - majorTicksLayer.zPosition = 0; + 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); - SKShapeNode *majorTicksMask = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(faceSize.width - margin*2. - longTickHeight, faceSize.height - margin*2. - longTickHeight) cornerRadius:cornerRadius - margin - longTickHeight/2.]; - majorTicksMask.lineWidth = longTickHeight; - majorTicksMask.strokeColor = [SKColor colorWithWhite:1 alpha:1]; - majorTicksMask.position = CGPointZero; - majorTicksMask.antialiased = NO; - - majorTicksLayer.maskNode = majorTicksMask; - [self addChild:majorTicksLayer]; + CGPathRef largeTickInnerPath = CGPathCreateWithRoundedRect(CGRectMake(margin + longTickHeight - faceSize.width/2., margin + longTickHeight - faceSize.height/2., faceSize.width - margin*2. - longTickHeight*2., faceSize.height - margin*2. - longTickHeight*2.), cornerRadius - margin - longTickHeight, cornerRadius - margin - longTickHeight, NULL); for (int i = 0; i < 12; i++) { CGFloat angle = -(2*M_PI)/12.0 * i; - SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.markColor size:CGSizeMake(2, workingRadius)]; + CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); + + CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-1, 0), rotation); + CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-1, workingRadius), rotation); + CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(1, 0), rotation); + CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(1, 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.markColor; + tick.strokeColor = [SKColor clearColor]; tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, 1.5); - tick.zRotation = angle; - [majorTicksLayer addChild:tick]; + [self addChild:tick]; } + + CGPathRelease(largeTickInnerPath); /* Minor */ - SKCropNode *minorTicksLayer = [[SKCropNode alloc] init]; - minorTicksLayer.position = CGPointZero; - minorTicksLayer.zPosition = 0; - SKShapeNode *minorTicksMask = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(faceSize.width - margin*2. - shortTickHeight, faceSize.height - margin*2. - shortTickHeight) cornerRadius:cornerRadius - margin - shortTickHeight/2.]; - minorTicksMask.lineWidth = shortTickHeight; - minorTicksMask.strokeColor = [SKColor colorWithWhite:1 alpha:1]; - minorTicksMask.position = CGPointZero; - minorTicksMask.antialiased = NO; + CGPathRef smallTickInnerPath = CGPathCreateWithRoundedRect(CGRectMake(margin + shortTickHeight - faceSize.width/2., margin + shortTickHeight - faceSize.height/2., faceSize.width - margin*2. - shortTickHeight*2., faceSize.height - margin*2. - shortTickHeight*2.), cornerRadius - margin - shortTickHeight, cornerRadius - margin - shortTickHeight, NULL); - minorTicksLayer.maskNode = minorTicksMask; - [self addChild:minorTicksLayer]; + for (int i = 0; i < 60; i++) + { + if (i % 5 == 0) continue; + + CGFloat angle = - (2*M_PI)/60.0 * i; + + CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); + + CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-0.5, 0), rotation); + CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-0.5, workingRadius), rotation); + CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(0.5, 0), rotation); + CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(0.5, 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.markColor; + tick.strokeColor = [SKColor clearColor]; + tick.position = CGPointZero; + + [self addChild:tick]; + } - for (int i = 0; i < 60; i++) - { - CGFloat angle = - (2*M_PI)/60.0 * i; - SKSpriteNode *tick = [SKSpriteNode spriteNodeWithColor:self.markColor size:CGSizeMake(1, workingRadius)]; - - tick.position = CGPointZero; - tick.anchorPoint = CGPointMake(0.5, 1.5); - tick.zRotation = angle; - - if (i % 5 != 0) - [minorTicksLayer addChild:tick]; - } + CGPathRelease(outerPath); + CGPathRelease(smallTickInnerPath); /* Numerals */ for (int i = 1; i <= 12; i++) From 02e7363c9bb7651ad0cd49e1e57d31555c0238ea Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 20:25:01 -0700 Subject: [PATCH 4/8] Fixed some merge issues --- SpriteKitWatchFace WatchKit Extension/FaceScene.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index 8a484f4..a569c41 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -325,7 +325,7 @@ -(void)setupTickmarksForRectangularFace tick.strokeColor = [SKColor clearColor]; tick.position = CGPointZero; - if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMajor) + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMajor) [self addChild:tick]; } @@ -360,11 +360,11 @@ -(void)setupTickmarksForRectangularFace CGPathCloseSubpath(tickPath); SKShapeNode *tick = [SKShapeNode shapeNodeWithPath:tickPath]; - tick.fillColor = self.markColor; + tick.fillColor = self.minorMarkColor; tick.strokeColor = [SKColor clearColor]; tick.position = CGPointZero; - if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMinor) + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMinor) [self addChild:tick]; } From 2e9b3589d23b1006b71ca0f5bb3ac050bc82c261 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 22:00:52 -0700 Subject: [PATCH 5/8] Release tick mark paths --- SpriteKitWatchFace WatchKit Extension/FaceScene.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index a569c41..f1de223 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -325,6 +325,8 @@ -(void)setupTickmarksForRectangularFace tick.strokeColor = [SKColor clearColor]; tick.position = CGPointZero; + CGPathRelease(tickPath); + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMajor) [self addChild:tick]; } @@ -364,6 +366,8 @@ -(void)setupTickmarksForRectangularFace tick.strokeColor = [SKColor clearColor]; tick.position = CGPointZero; + CGPathRelease(tickPath); + if (self.tickmarkStyle == TickmarkStyleAll || self.tickmarkStyle == TickmarkStyleMinor) [self addChild:tick]; } From e30a1bf313809ae1c9f9063ef30b0fdb21bc29ae Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 22:42:54 -0700 Subject: [PATCH 6/8] Made tick mark size customizable --- .../FaceScene.h | 3 ++ .../FaceScene.m | 29 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) 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 f1de223..8619e28 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -211,6 +211,9 @@ - (instancetype)initWithCoder:(NSCoder *)coder self.useRoundFace = NO; self.numeralStyle = NumeralStyleAll; self.tickmarkStyle = TickmarkStyleAll; + + self.majorMarkSize = CGSizeMake(3, 9); + self.minorMarkSize = CGSizeMake(1, 4.5); [self setupColors]; [self setupScene]; @@ -290,12 +293,14 @@ -(void)setupTickmarksForRectangularFace CGFloat cornerRadius = 34; CGFloat workingRadius = sqrt(faceSize.width/2.*faceSize.width/2. + faceSize.height/2.*faceSize.height/2.); - CGFloat longTickHeight = round(faceSize.width/2./10.); - CGFloat shortTickHeight = longTickHeight/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 + longTickHeight - faceSize.width/2., margin + longTickHeight - faceSize.height/2., faceSize.width - margin*2. - longTickHeight*2., faceSize.height - margin*2. - longTickHeight*2.), cornerRadius - margin - longTickHeight, cornerRadius - margin - longTickHeight, 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); for (int i = 0; i < 12; i++) { @@ -303,10 +308,10 @@ -(void)setupTickmarksForRectangularFace CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); - CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-1, 0), rotation); - CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-1, workingRadius), rotation); - CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(1, 0), rotation); - CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(1, workingRadius), rotation); + 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); @@ -334,7 +339,7 @@ -(void)setupTickmarksForRectangularFace CGPathRelease(largeTickInnerPath); /* Minor */ - CGPathRef smallTickInnerPath = CGPathCreateWithRoundedRect(CGRectMake(margin + shortTickHeight - faceSize.width/2., margin + shortTickHeight - faceSize.height/2., faceSize.width - margin*2. - shortTickHeight*2., faceSize.height - margin*2. - shortTickHeight*2.), cornerRadius - margin - shortTickHeight, cornerRadius - margin - shortTickHeight, NULL); + 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++) { @@ -344,10 +349,10 @@ -(void)setupTickmarksForRectangularFace CGAffineTransform rotation = CGAffineTransformMakeRotation(angle); - CGPoint lineLeftPointA = CGPointApplyAffineTransform(CGPointMake(-0.5, 0), rotation); - CGPoint lineLeftPointB = CGPointApplyAffineTransform(CGPointMake(-0.5, workingRadius), rotation); - CGPoint lineRightPointA = CGPointApplyAffineTransform(CGPointMake(0.5, 0), rotation); - CGPoint lineRightPointB = CGPointApplyAffineTransform(CGPointMake(0.5, workingRadius), rotation); + 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); From 22ad58ce7050e5e9d3a779484fbf3eecd7451b43 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 22:53:44 -0700 Subject: [PATCH 7/8] Make sure it compiles on watchOS too --- .../FaceScene.m | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index 8619e28..e86a910 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -11,6 +11,29 @@ #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 @@ -185,10 +208,6 @@ } }); - if (intersections.count == 0) { - NSLog(@"No intersections for %@ - %@", NSStringFromPoint(linePointA), NSStringFromPoint(linePointB)); - } - return intersections; } From ce3772edef37f1250a33fa033386b701f01a183e Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Oct 2018 22:59:10 -0700 Subject: [PATCH 8/8] Somehow lost the reference to cubic spline intersection algorithm along the way --- SpriteKitWatchFace WatchKit Extension/FaceScene.m | 1 + 1 file changed, 1 insertion(+) diff --git a/SpriteKitWatchFace WatchKit Extension/FaceScene.m b/SpriteKitWatchFace WatchKit Extension/FaceScene.m index e86a910..8beac48 100644 --- a/SpriteKitWatchFace WatchKit Extension/FaceScene.m +++ b/SpriteKitWatchFace WatchKit Extension/FaceScene.m @@ -38,6 +38,7 @@ - (CGPoint)pointValue #define PREPARE_SCREENSHOT 0 +// 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 xAmB = linePointA.x - linePointB.x;