Skip to content

Instantly share code, notes, and snippets.

@CodeSlicing
Created December 5, 2020 08:22
Show Gist options
  • Save CodeSlicing/c3b0b16c80eeb9339adbdb528cae52f5 to your computer and use it in GitHub Desktop.
Save CodeSlicing/c3b0b16c80eeb9339adbdb528cae52f5 to your computer and use it in GitHub Desktop.
Code for the Settings icon
//
// SettingsIcon.swift
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Copyright © 2020 Adam Fordyce. All rights reserved.
//
import SwiftUI
import PureSwiftUI
private let startColor = Color.rgb8(228, 229, 231)
private let endColor = Color.rgb8(139, 143, 150)
private let gradient = LinearGradient([startColor, endColor], to: .bottom)
private let numTeeth = 54
private let segmentsPerTooth = 20
private let numSegments = numTeeth * segmentsPerTooth
private let teethLayoutConfig = LayoutGuideConfig.polar(rings: [0.885, 1], segments: numSegments)
private let halfSpokeWidth: CGFloat = 0.025
private let spokePolarConfig = LayoutGuideConfig.polar(rings: [0.78], segments: 60)
private let spokeGridConfig = LayoutGuideConfig.grid(columns: [0.5 - halfSpokeWidth, 0.5 + halfSpokeWidth], rows: [0.1, 0.25, 0.46])
private let defaultAngle = -30.5.degrees
struct SettingsIcon: View {
@State private var animating = false
var body: some View {
let debug = false
ZStack {
Color(white: 0.183)
.clipCircle()
.opacityIf(debug, 0)
Shroud()
.iconStyle(debug: debug)
Cog(scale: 0.3, debug: debug)
.rotateIfNot(debug, -defaultAngle)
.rotateIf(!debug && animating, -720.degrees)
.iconStyle(debug: debug)
Cog(inner: true, scale: 0.53, debug: debug)
.rotateIfNot(debug, defaultAngle)
.rotateIf(!debug && animating, -360.degrees)
.iconStyle(debug: debug)
let primaryCogScale: CGFloat = 0.8
Cog(scale: primaryCogScale, debug: debug)
.rotateIfNot(debug, defaultAngle)
.rotateIf(!debug && animating, 360.degrees)
.iconStyle(debug: debug)
// .layoutGuide(teethLayoutConfig.scaled(primaryCogScale), color: .green)
.layoutGuide(spokePolarConfig.scaled(primaryCogScale), color: .blue, lineWidth: 1)
.layoutGuide(spokeGridConfig.scaled(primaryCogScale), color: .red, lineWidth: 1)
.layoutGuide(spokeGridConfig.scaled(primaryCogScale).rotated(120.degrees), color: .red, lineWidth: 1)
}
.shadowIfNot(debug, color: .black, radius: 10)
.showLayoutGuides(debug)
.onAppear {
withAnimation(Animation.linear(duration: 15).repeatForever(autoreverses: false)) {
animating = true
}
}
}
}
private extension Shape {
@ViewBuilder func iconStyle(debug: Bool = false) -> some View {
if debug {
stroke(Color.white, style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round))
} else {
fill(gradient, style: .init(eoFill: true))
}
}
}
private struct Shroud: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.roundedRect(rect, cornerRadius: rect.widthScaled(0.22))
path.ellipse(rect.center, rect.sizeScaled(0.87), anchor: .center)
return path
}
}
private struct Cog: Shape {
var inner = false
var scale: CGFloat
var debug = false
func path(in rect: CGRect) -> Path {
var path = Path()
var pt = teethLayoutConfig.scaled(scale).layout(in: rect)
let rotationDelta = pt.angleTo(0, segmentsPerTooth)
pt = pt
.rotated(-pt.angleTo(1, 5))
path.move(pt[0, 0])
for tooth in 0..<numTeeth {
let pt = pt
.rotated(rotationDelta * tooth)
path.line(pt[1, 4])
path.line(pt[1, 6])
path.line(pt[0, 10])
path.line(pt[0, 20])
}
path.closeSubpath()
if !inner {
var p = spokePolarConfig.scaled(scale).layout(in: rect)
var g = spokeGridConfig.scaled(scale).layout(in: rect)
for _ in 0..<3 {
path.move(g[1, 2])
path.line(g[1, 1])
path.curve(p[0, 3], cp1: g[1, 0], cp2: p[0, 2], showControlPoints: debug)
path.arc(rect.center, radius: p.radiusTo(0, 0), startAngle: p.angleTo(0, 3), endAngle: p.angleTo(0, 17))
g = g
.rotated(120.degrees)
path.curve(g[0, 1], cp1: p[0, 18], cp2: g[0, 0], showControlPoints: debug)
path.line(g[0, 2])
path.closeSubpath()
p = p
.rotated(120.degrees)
}
path.circle(rect.center, diameter: rect.widthScaled(0.04 * scale))
} else {
path.circle(rect.center, diameter: rect.widthScaled(0.72 * scale))
}
return path
}
}
struct SettingsIcon_Previews: PreviewProvider {
struct SettingsIcon_Harness: View {
var body: some View {
SettingsIcon()
.frame(400)
.greedyFrame()
.background(Color(white: 0.1).ignoresSafeArea())
}
}
static var previews: some View {
SettingsIcon_Harness()
.previewDevice(.iPhone_12_Pro_Max)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment