Skip to content

Instantly share code, notes, and snippets.

@eliyap
Last active November 2, 2022 01:49
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliyap/779dbe188c5c9a3c54a5408ac5b02c84 to your computer and use it in GitHub Desktop.
Save eliyap/779dbe188c5c9a3c54a5408ac5b02c84 to your computer and use it in GitHub Desktop.
import SwiftUI
struct ContentView: View {
@State private var spinRadians: CGFloat = .zero
@State private var tiltRadians: CGFloat = .zero
private var spin: Angle { .radians(spinRadians) }
private var tilt: Angle { .radians(tiltRadians) }
var body: some View {
VStack {
iPhone12Sim(spin: spin, tilt: tilt)
Slider(value: $spinRadians, in: -CGFloat.pi...CGFloat.pi)
Slider(value: $tiltRadians, in: 0...CGFloat.pi/2)
}
.padding()
}
}
struct iPhone12Sim: View {
public let spin: Angle
public let tilt: Angle
public let scale: CGFloat = 2.5
var body: some View {
ZStack {
iPhone12Shape(layer: .bottom, spin: spin, tilt: tilt, scale: scale)
.fill(Color.blue)
iPhone12Shape.Filler(
corner: .topLeft,
spin: spin,
tilt: tilt,
scale: scale
)
.fill(Color.blue)
iPhone12Shape.Filler(
corner: .bottomRight,
spin: spin,
tilt: tilt,
scale: scale
)
.fill(Color.blue)
iPhone12Shape(layer: .top, spin: spin, tilt: tilt, scale: scale)
.fill(Color.blue)
.brightness(-0.05)
iPhone12Shape.Face(spin: spin, tilt: tilt, scale: scale)
iPhone12Shape.Screen(spin: spin, tilt: tilt, scale: scale)
.fill(Color.gray)
iPhone12Shape.Island(spin: spin, tilt: tilt, scale: scale)
}
.border(Color.red)
}
}
struct iPhone12Shape: Shape {
/// https://www.apple.com/iphone-14-pro/specs/
static let _height: CGFloat = 147.5
static let _width: CGFloat = 71.5
static let _depth: CGFloat = 7.85 /// Slightly thicker than true value
let height: CGFloat = Self._height
let width: CGFloat = Self._width
let depth: CGFloat = Self._depth
enum Layer { case top, bottom }
let layer: Layer
var z: CGFloat {
switch layer {
case .top: return -depth/2
case .bottom: return +depth/2
}
}
public var spin: Angle
public var tilt: Angle
public var scale: CGFloat
/**
According to https://www.apple.com/iphone-12/specs/
screen is 2532‑by‑1170-pixel resolution at 460 ppi
i.e. 2.543in, or 64.6mm
That puts the bezel at ~`(71-64)/2 = 3.5mm`
The internal corner radius according to https://github.com/kylebshr/ScreenCorners
is 47.33pts, which at 3pt-per-pixels, according to https://useyourloaf.com/blog/iphone-12-screen-sizes/
is `(47.33 * 3 / 460) = 7.84mm`
So we'll approximate the corner radius as `12mm`.
*/
static let _bezel: CGFloat = 3.5
static let _radius: CGFloat = 12
let radius: CGFloat = Self._radius
func path(in rect: CGRect) -> Path {
let topDown = Path { path in
path.move(to: CGPoint(x: radius, y: 0))
path.addArc(
center: CGPoint(x: radius, y: radius),
radius: radius,
startAngle: -.quarter,
endAngle: -.half,
clockwise: true
)
path.addLine(to: CGPoint(x: 0, y: height-radius))
path.addArc(
center: CGPoint(x: radius, y: height-radius),
radius: radius,
startAngle: -.half,
endAngle: .quarter,
clockwise: true
)
path.addLine(to: CGPoint(x: width-radius, y: height))
path.addArc(
center: CGPoint(x: width-radius, y: height-radius),
radius: radius,
startAngle: .quarter,
endAngle: .zero,
clockwise: true
)
path.addLine(to: CGPoint(x: width, y: radius))
path.addArc(
center: CGPoint(x: width-radius, y: radius),
radius: radius,
startAngle: .zero,
endAngle: -.quarter,
clockwise: true
)
path.closeSubpath()
}
let transform = CGAffineTransform.identity
/// Center at origin.
.concatenating(CGAffineTransform(translationX: -width/2, y: -height/2))
/// Apply scale.
.concatenating(CGAffineTransform(scaleX: scale, y: scale))
/// Spin around origin.
.concatenating(CGAffineTransform(rotationAngle: spin.radians))
/// Tilt around origin.
.concatenating(CGAffineTransform.tilt(tilt, z: z * scale))
/// Center in frame.
.concatenating(CGAffineTransform(translationX: rect.width/2, y: rect.height/2))
return topDown.applying(transform)
}
/// Fills in the corner bumps to preserve illusion of a solid object.
/// Treats the iPhone corners as thick "coins".
/// - Coins are round, and (cross-sectionally) can ignore `spin`.
/// - Coins only need 1 cardboard rectangle in the middle as filler.
///
// __==TTTT==__ <- Top Surface
// [ ^^=__=^^ ]
// [ ] <- Filler Rectangle
// [ ]
// TT--____--TT <- Bottom Edge
///
struct Filler: Shape {
enum Corner { case topLeft, topRight, bottomLeft, bottomRight }
let corner: Corner
let height: CGFloat = iPhone12Shape._height
let width: CGFloat = iPhone12Shape._width
let depth: CGFloat = iPhone12Shape._depth
let radius: CGFloat = iPhone12Shape._radius
public var spin: Angle
public var tilt: Angle
public var scale: CGFloat
func path(in rect: CGRect) -> Path {
let center: CGPoint
let x = width/2 - radius
let y = height/2 - radius
switch corner {
case .topLeft: center = CGPoint(x: -x, y: -y)
case .topRight: center = CGPoint(x: +x, y: -y)
case .bottomLeft: center = CGPoint(x: -x, y: +y)
case .bottomRight: center = CGPoint(x: +x, y: +y)
}
let transform = CGAffineTransform.identity
/// Spin around origin.
.concatenating(CGAffineTransform(rotationAngle: spin.radians))
/// Apply scale.
.concatenating(CGAffineTransform(scaleX: scale, y: scale))
/// Tilt around origin.
.concatenating(CGAffineTransform.tilt(tilt, z: 0))
/// Center in frame.
.concatenating(CGAffineTransform(translationX: rect.width/2, y: rect.height/2))
let newCenter = center.applying(transform)
let rectHeight = depth * sin(tilt.radians) * scale
let rectWidth = 2 * radius * scale
let origin = CGPoint(
x: newCenter.x - rectWidth/2,
y: newCenter.y - rectHeight/2
)
return Path { path in
path.move(to: origin)
path.addRect(CGRect(origin: origin, size: CGSize(
width: rectWidth,
height: rectHeight
)))
}
}
}
/// The device's metal case intrudes slightly on the face.
/// This makes the black "dead screen area" appear smaller.
/// This represents the black face of the screen, excluding the front-facing metal.
struct Face: Shape {
/// In the official Apple product shot PNGs, we have
/// - 58 pixels from screen to device edge
/// - 37 pixels from screen to black edge
static let metal: CGFloat = iPhone12Shape._bezel * (21.0/58.0)
let height: CGFloat = iPhone12Shape._height - 2*Self.metal
let width: CGFloat = iPhone12Shape._width - 2*Self.metal
let depth: CGFloat = iPhone12Shape._depth
let radius: CGFloat = iPhone12Shape._radius - Self.metal
var z: CGFloat { -depth / 2 }
public var spin: Angle
public var tilt: Angle
public var scale: CGFloat
func path(in rect: CGRect) -> Path {
let topDown = Path { path in
path.move(to: CGPoint(x: radius, y: 0))
path.addArc(
center: CGPoint(x: radius, y: radius),
radius: radius,
startAngle: -.quarter,
endAngle: -.half,
clockwise: true
)
path.addLine(to: CGPoint(x: 0, y: height-radius))
path.addArc(
center: CGPoint(x: radius, y: height-radius),
radius: radius,
startAngle: -.half,
endAngle: .quarter,
clockwise: true
)
path.addLine(to: CGPoint(x: width-radius, y: height))
path.addArc(
center: CGPoint(x: width-radius, y: height-radius),
radius: radius,
startAngle: .quarter,
endAngle: .zero,
clockwise: true
)
path.addLine(to: CGPoint(x: width, y: radius))
path.addArc(
center: CGPoint(x: width-radius, y: radius),
radius: radius,
startAngle: .zero,
endAngle: -.quarter,
clockwise: true
)
path.closeSubpath()
}
let transform = CGAffineTransform.identity
/// Center at origin.
.concatenating(CGAffineTransform(translationX: -width/2, y: -height/2))
/// Apply scale.
.concatenating(CGAffineTransform(scaleX: scale, y: scale))
/// Spin around origin.
.concatenating(CGAffineTransform(rotationAngle: spin.radians))
/// Tilt around origin.
.concatenating(CGAffineTransform.tilt(tilt, z: z * scale))
/// Center in frame.
.concatenating(CGAffineTransform(translationX: rect.width/2, y: rect.height/2))
return topDown.applying(transform)
}
}
struct Screen: Shape {
let height: CGFloat = iPhone12Shape._height - 2*iPhone12Shape._bezel
let width: CGFloat = iPhone12Shape._width - 2*iPhone12Shape._bezel
let depth: CGFloat = iPhone12Shape._depth
let radius: CGFloat = iPhone12Shape._radius - iPhone12Shape._bezel
var z: CGFloat { -depth / 2 }
public var spin: Angle
public var tilt: Angle
public var scale: CGFloat
func path(in rect: CGRect) -> Path {
let topDown = Path { path in
path.move(to: CGPoint(x: radius, y: 0))
path.addArc(
center: CGPoint(x: radius, y: radius),
radius: radius,
startAngle: -.quarter,
endAngle: -.half,
clockwise: true
)
path.addLine(to: CGPoint(x: 0, y: height-radius))
path.addArc(
center: CGPoint(x: radius, y: height-radius),
radius: radius,
startAngle: -.half,
endAngle: .quarter,
clockwise: true
)
path.addLine(to: CGPoint(x: width-radius, y: height))
path.addArc(
center: CGPoint(x: width-radius, y: height-radius),
radius: radius,
startAngle: .quarter,
endAngle: .zero,
clockwise: true
)
path.addLine(to: CGPoint(x: width, y: radius))
path.addArc(
center: CGPoint(x: width-radius, y: radius),
radius: radius,
startAngle: .zero,
endAngle: -.quarter,
clockwise: true
)
path.closeSubpath()
}
let transform = CGAffineTransform.identity
/// Center at origin.
.concatenating(CGAffineTransform(translationX: -width/2, y: -height/2))
/// Apply scale.
.concatenating(CGAffineTransform(scaleX: scale, y: scale))
/// Spin around origin.
.concatenating(CGAffineTransform(rotationAngle: spin.radians))
/// Tilt around origin.
.concatenating(CGAffineTransform.tilt(tilt, z: z * scale))
/// Center in frame.
.concatenating(CGAffineTransform(translationX: rect.width/2, y: rect.height/2))
return topDown.applying(transform)
}
}
struct Island: Shape {
/// 3x retina, 25.4 mm per inch, 460ppi.
static let mmPerPixel = (3 * 25.4 / 460)
/// https://betterprogramming.pub/dynamic-island-animation-5869fbce41e6
/// > The dynamic island has 11-pixel top padding. Its width is 126 and its height is 37.33.
let padding: CGFloat = 11 * Self.mmPerPixel
let height: CGFloat = 37.33 * Self.mmPerPixel
let width: CGFloat = 126 * Self.mmPerPixel
let depth: CGFloat = iPhone12Shape._depth
var radius: CGFloat { height / 2 }
var z: CGFloat { -depth / 2 }
public var spin: Angle
public var tilt: Angle
public var scale: CGFloat
func path(in rect: CGRect) -> Path {
let topDown = Path { path in
let origin = CGPoint(x: 0, y: iPhone12Shape._bezel + padding + height/2 - iPhone12Shape._height/2)
path.move(to: origin)
path.addRoundedRect(
in: CGRect(origin: origin, size: CGSize(width: width, height: height)),
cornerSize: CGSize(width :radius, height: radius),
style: .circular,
transform: .identity
)
}
let transform = CGAffineTransform.identity
/// Center at origin.
.concatenating(CGAffineTransform(translationX: -width/2, y: -height/2))
/// Apply scale.
.concatenating(CGAffineTransform(scaleX: scale, y: scale))
/// Spin around origin.
.concatenating(CGAffineTransform(rotationAngle: spin.radians))
/// Tilt around origin.
.concatenating(CGAffineTransform.tilt(tilt, z: z * scale))
/// Center in frame.
.concatenating(CGAffineTransform(translationX: rect.width/2, y: rect.height/2))
return topDown.applying(transform)
}
}
}
extension CGAffineTransform {
static func tilt(_ angle: Angle, z: CGFloat) -> CGAffineTransform {
/// `x` unchanged.
let (a, c, tx): (CGFloat, CGFloat, CGFloat) = (1, 0, 0)
/// `y` incorporates `z` when tilting.
let (b, d, ty): (CGFloat, CGFloat, CGFloat) = (0, cos(angle.radians), z * sin(angle.radians))
return CGAffineTransformMake(a, b, c, d, tx, ty)
}
}
extension Angle {
static let quarter = Angle(radians: .pi / 2)
static let half = Angle(radians: .pi)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment