Skip to content

Instantly share code, notes, and snippets.

@warpling
Last active July 26, 2023 07:16
Show Gist options
  • Save warpling/31f8f896f1db7b242ccb62d70b8bbf5f to your computer and use it in GitHub Desktop.
Save warpling/31f8f896f1db7b242ccb62d70b8bbf5f to your computer and use it in GitHub Desktop.
πŸŒ” Drawing an accurate moon with UIBezierPath
//
// Moon.h
// Blackbox
//
// Created by Ryan McLeod on 12/2/14.
// Copyright (c) 2014 Ryan McLeod. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface Moon : UIView
// 0.0 = new moon
// 0.1 = sliver forming/waxing from right
// 0.5 = full mooon! fading/wanning from right
// 0.9 = almost new moon
// 1.0 = new moon
@property float moonPhase;
@property UIColor *moonColor;
@end
//
// Moon.m
// Blackbox
//
// Created by Ryan McLeod on 12/2/14.
// Copyright (c) 2014 Ryan McLeod. All rights reserved.
//
#import "Moon.h"
@implementation Moon
static float moonInset = 1;
- (id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.moonPhase = 0.0f;
self.moonColor = [UIColor fallbackPrimary];
[self setBackgroundColor:[UIColor clearColor]];
self.layer.backgroundColor = [UIColor backgroundColor].CGColor;
self.layer.cornerRadius = (frame.size.width / 2.f) - moonInset;
self.layer.shadowColor = [UIColor colorWithRed:0.75 green:0.32 blue:0.56 alpha:1].CGColor;
self.accessibilityPath = [UIBezierPath bezierPathWithOvalInRect:self.frame];
}
return self;
}
- (void) drawRect:(CGRect)rect {
[super drawRect:rect];
// Outline when close to new moons (inset helps avoid stroke clipping
UIBezierPath *outlineCircle = [[UIBezierPath bezierPathWithOvalInRect:CGRectInset(rect, moonInset, moonInset)] bezierPathByReversingPath];
outlineCircle.usesEvenOddFillRule = NO;
float alpha = CLAMP(5*fabsf(self.moonPhase-0.5f)/0.5f - 4.f, 0.0, 1);
[[self.moonColor colorWithAlphaComponent:alpha] setStroke];
[outlineCircle stroke];
CGRect bounds = self.bounds;
NSAssert(bounds.size.width == bounds.size.height, @"Moon view must be a square!");
CGFloat moonPhase = self.moonPhase;
CGFloat diameter = CGRectGetWidth(bounds);
CGFloat radius = diameter/2.f;
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius-moonInset
startAngle:-M_PI_2
endAngle:M_PI_2
clockwise:(moonPhase < 0.5f)];
// 0.0 -> Full, Subtractive
// 0.125 -> Half, Subtractive
// 0.25 -> Zero, Additive
// 0.375 -> Half, Additive
// 0.5 -> Full, Additive
// 0.625 -> Half, Additive
// 0.75 -> Zero, Additive
// 0.875 -> Half, Subtractive
// 1.0 -> Full, Subtractive
// visible area of the moon is based on the area not the radius so we sqrt our multiplier
CGFloat ovalWidth = 2 * radius * sqrtf(fabs(4.0f * (fmod(moonPhase, 0.5) - 0.25)));
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(CGRectMake(((diameter/2.f) - (ovalWidth/2.f)), 0, ovalWidth, diameter), moonInset, moonInset)];
[self.moonColor setFill];
if (moonPhase < 0.25 || moonPhase > 0.75) {
CGFloat nudgeAmount = 0.5; // Avoid small anti-aliasing lines on some devices
CGFloat originX = (moonPhase < 0.25f ? (bounds.size.width / 2.f + nudgeAmount) : -nudgeAmount);
CGRect otherHalf = CGRectMake(originX, 0, (bounds.size.width / 2.f), bounds.size.height);
[[UIBezierPath bezierPathWithRect:otherHalf] addClip];
UIBezierPath *moonPath = [UIBezierPath bezierPath];
moonPath.usesEvenOddFillRule = YES;
[moonPath appendPath:circlePath];
[moonPath appendPath:ovalPath];
[moonPath fill];
} else {
[circlePath fill];
[ovalPath fill];
}
// Moon shadow movement
float newToFull = -2 * (self.moonPhase - 0.5); // [–1, 0, +1]
float xOffset = newToFull * 20;
float yOffset = newToFull * 20;
self.layer.shadowOffset = CGSizeMake(xOffset, yOffset);
self.layer.shadowOpacity = (fabs(newToFull) * 0.20) + 0.15;
self.layer.shadowRadius = (fabs(newToFull) * 35) + 15;
self.layer.shadowPath = circlePath.CGPath;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment