In the third challenge for the Drawing app, the following is asked:
Create a ColorCyclingRectangle
shape that is the rectangular cousin of ColorCyclingCircle
, allowing us to control the position of the gradient using one or more properties.
I was not sure what exactly is meant. Do we literally have to use a Gradient
? The original solution did not, but worked with insets, all stroked with a different color.
Also, we are asked to use a rectangle, but is this just the surrounding shape or do the different colors also need to have rectangular shape?
In the end, I decided to make two solutions. The user can switch using a toggle.
The first is analogous to the given solution in that it is circular, however embedded in a (rounded) rectangular shape and implemented using a RadialGradient
. The center can be moved through sliders for X and Y. This one was not too difficult.
The second was to have (rounded) rectangular shapes everywhere, getting smaller and smaller and so that they "disappear" in a point. This point can also be moved. At first I thought to use inset(by:)
, but that appears not to be possible in SwiftUI: one cannot have separate values for the four directions. And I need to, in order to be able to move the center. So I had to forego inset(by:)
, and size and position all rectangles explicitly.
I did consider creating my own inset(by insets: EdgeInsets)
modifier, but found it not necessary, and probably more complicated than necessary, once I found out how simple it is to size and position a rectangle.
It cost me quite some time to arrive at this solution and I had a few false starts, but in the end it does not seem too complicated. Any comments or suggestions are welcome.
import SwiftUI
struct ColorsView: View {
@State private var colorCycle = 0.0
@State private var locX = 50.0
@State private var locY = 50.0
@State private var useGradient = true
let baseCornerRadius = 25.0
var body: some View {
VStack {
Toggle("Use gradient?", isOn: $useGradient)
ZStack {
GeometryReader { geo in
let w = geo.size.width
let h = geo.size.height
let m = min(w,h)
let centerX = locX * w / 100
let centerY = locY * h / 100
VStack {
if useGradient {
let center = UnitPoint(x: centerX / w, y: centerY / h)
RoundedRectangle(cornerRadius: baseCornerRadius)
.fill(
RadialGradient(gradient: Gradient(colors: colors(steps: Int(m), amount: colorCycle)), center: center, startRadius: 0, endRadius: m/2))
} else {
ZStack {
ForEach(0..<Int(m), id: \.self) { value in
let v = CGFloat(value)
let cornerRadius = baseCornerRadius * (m - Double(value)) / m
RoundedRectangle(cornerRadius: cornerRadius)
.size(width: w-(v*w/m), height: h-(v*h/m))
.offset(x: v*(centerX/m), y: v*(centerY/m))
.stroke(color(for: value, amount: colorCycle, steps: Int(m)), lineWidth: 2)
}
}
}
}
}
}
Text("Color cycle")
Slider(value: $colorCycle)
Text("centerX")
Slider(value: $locX, in: 5...95, step: 1)
Text("centerY")
Slider(value: $locY, in: 5...95, step: 1)
}
}
func color(for value: Int, amount: Double, steps: Int) -> Color {
var targetHue = Double(value) / Double(steps) + amount
targetHue = targetHue.truncatingRemainder(dividingBy: 1)
return Color(hue: targetHue, saturation: 1, brightness: 1)
}
func colors(steps: Int, amount: Double) -> [Color] {
var colors = [Color]()
for i in 0..<steps {
colors.append(color(for: i, amount: amount, steps: steps))
}
return colors
}
}