使用HSB而不是RGB來定義顏色

有多種方法可以在代碼中定義顏色。最常用的方法是指定三種基色的值 - 紅色、綠色和藍色 (RGB)。本文通過指定色調、飽和度和亮度 (HSB) 的值來探索替代機制的使用。可以以更直觀的方式使用 HSB 屬性來創建顏色搭配良好的調色板。

網上有很多關於顏色的資源,我發現 Jonathan 的 Learn about Hue, Saturation and Brightness colours 以及 Erik Kennedy 的 The HSB Color System: A Practitioner's Primer 特別有用。

RGB 顏色 (紅色、綠色 & 藍色)

定義顏色的最常見方法是指定顏色的紅色、綠色和藍色屬性。每個屬性可以是 0 到 255 之間的十進制值,但通常以十六進制格式給出,因此顏色可以用 6 個字符表示。 Mac 上的 數碼測色計 可用於檢查屏幕上的任何區域並給出所選顏色的 RGB 值。可以在 SwiftUI 中創建一個調色板以顯示可能的顏色。

struct RgbColorPaletteView: View {
    var body: some View {
        VStack(spacing:5) {
            VStack {
                HStack {
                    Text("Red")
                        .frame(width: cellWidth)
                    Text("Green")
                        .frame(width: cellWidth * 11.0)
                    Text("Blue")
                        .frame(width: cellWidth)
                    Spacer()
                }
                HStack(spacing:1) {
                    Spacer()
                        .frame(width:cellWidth)
                    ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { myGreen in
                        Text("\(myGreen, specifier: "%0.1F")")
                            .font(.footnote)
                            .multilineTextAlignment(.center)
                            .frame(width:cellWidth)
                    }
                    Spacer()
                }
            }
                
            ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { myRed in
                HStack(spacing:1) {
                    Text("\(myRed, specifier: "%0.1F")")
                        .frame(width:cellWidth)
                    ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { myGreen in
                        HStack {
                            VStack(spacing:1) {
                                ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { myBlue in
                                    Color(red: myRed, green: myGreen, blue: myBlue)
                                }
                                .frame(width:cellWidth)
                            }
                        }
                    }
                    VStack(spacing:13) {
                        Text(myRed == 0.0 ? "0.0" : "")
                        Image(systemName: "arrow.down")
                            .foregroundColor(myRed == 0.0 ? Color.black : .clear)
                        Text(myRed == 0.0 ? "1.0" : "")
                    }
                    .font(.footnote)
                    .frame(width:cellWidth * 0.7)

                    Spacer()
                }
            }
            Spacer()
        }
        .padding()
    }
    
    let cellWidth: CGFloat = 100
}

HSB 顏色(色調、飽和度 & 亮度)

HSB 顏色模型被認爲更符合我們對顏色的看法。下面是通過改變色調、飽和度和亮度的值來顯示調色板的代碼。請注意,色相(Hue) 通常被賦予一個以角度爲單位的值,表示色環周圍的角度,值在 0 到 360 之間,SwiftUI 使用 0.0 到 1.0 之間的值,其中 1.0 表示 360 度。

struct HsbColorPaletteView: View {
    var body: some View {
        VStack(spacing:5) {
            VStack {
                HStack {
                    Text("Hue")
                        .frame(width: cellWidth)
                    Text("Saturation")
                        .frame(width: cellWidth * 11.0)
                    Text("Brightness")
                        .frame(width: cellWidth)
                    Spacer()
                }
                HStack(spacing:1) {
                    Spacer()
                        .frame(width:cellWidth)
                    ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { mySat in
                        Text("\(mySat, specifier: "%0.1F")")
                            .font(.footnote)
                            .multilineTextAlignment(.center)
                            .frame(width:cellWidth)
                    }
                    Spacer()
                }
            }
                
            ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { myHue in
                HStack(spacing:1) {
                    Text("\(myHue, specifier: "%0.1F")")
                        .frame(width:cellWidth)
                    ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { mySat in
                        HStack {
                            VStack(spacing:1) {
                                //ForEach([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0], id: \.self) { myBright in
                                ForEach([1.0, 0.9, 0.8, 0.7, 0.6, 0.5], id: \.self) { myBright in
                                    Color(hue: myHue,
                                          saturation: mySat,
                                          brightness: myBright)
                                }
                                .frame(width:cellWidth)
                            }
                        }
                    }
                    VStack(spacing:13) {
                        Text(myHue == 0.0 ? "1.0" : "")
                        Image(systemName: "arrow.down")
                            .foregroundColor(myHue == 0.0 ? Color.black : .clear)
                        Text(myHue == 0.0 ? "0.5" : "")
                    }
                    .font(.footnote)
                    .frame(width:cellWidth * 0.5)

                    Spacer()
                }
            }
            Spacer()
        }
        .padding()
    }
    
    let cellWidth: CGFloat = 100
}

色調、飽和度和亮度

  • 色調:通過彩虹的顏色代表從紅色到紫色的基色。
  • 飽和度:表示顏色的強度。當亮度爲 1.0 時,無論指定的色調如何,飽和度值爲 0 都將是白色。
  • 亮度:表示顏色的亮度或明度。無論指定的色調如何,亮度爲 0 都將是黑色。

下圖顯示了一個個第一行基於色調增加的不同顏色,第二行和第三行具有相同的色調,分別顯示增加飽和度和亮度的效果。可以通過將飽和度保持爲 0 並調整亮度來定義灰度顏色。

struct ChangeHsbView: View {
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.17, brightness: 1.0)
                .edgesIgnoringSafeArea(.all)
            
            VStack() {
                VStack {
                    Text("Colors defined with")
                    Text("Hue, Saturation & Brightness")
                }
                .font(.title)
                .fontWeight(.bold)
                
                HueView()
                    .frame(height:200)
                
                SatView()
                    .frame(height:200)
                
                BrightView()
                    .frame(height:200)
                
                
                Spacer()
            }
            .padding(.horizontal, 150)
        }
    }
}
struct HueView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("1. Hue changes the Color")
                .font(.title2)
            HStack {
                ForEach([0.0, 0.2, 0.4, 0.6, 0.8, 1.0], id: \.self) { myHue in
                    VStack {
                        RoundedRectangle(cornerRadius: 20)
                            .fill(Color(hue: myHue, saturation: 1.0, brightness: 1.0))
                            .shadow(radius: 3, x:5, y:5)
                        Text("H: \(myHue, specifier: "%0.2F")")
                            .foregroundColor(.red)
                        Text("S: 1.00")
                        Text("B: 1.00")
                    }
                }
            }
        }
        .padding(.vertical, 20)
    }
}
struct SatView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("2. Saturation changes color Intensity")
                .font(.title2)
            
            HStack {
                ForEach([0.0, 0.2, 0.4, 0.6, 0.8, 1.0], id: \.self) { mySat in
                    VStack {
                        RoundedRectangle(cornerRadius: 20)
                            .fill(Color(hue: 0.75, saturation: mySat, brightness: 1.0))
                            .shadow(radius: 3, x:5, y:5)
                        Text("H: 0.75")
                        Text("S: \(mySat, specifier: "%0.2F")")
                            .foregroundColor(.red)
                        Text("B: 1.00")
                    }
                }
            }
        }
        .padding(.vertical, 20)
    }
}
struct BrightView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("3. Brightness changes whiteness")
                .font(.title2)
            
            HStack {
                ForEach([0.0, 0.2, 0.4, 0.6, 0.8, 1.0], id: \.self) { myBright in
                    VStack {
                        RoundedRectangle(cornerRadius: 20)
                            .fill(Color(hue: 0.75, saturation: 1.00, brightness: myBright))
                            .shadow(radius: 3, x:5, y:5)
                        Text("H: 0.75")
                        Text("S: 1.00")
                        Text("B: \(myBright, specifier: "%0.2F")")
                            .foregroundColor(.red)
                    }
                }
            }
        }
        .padding(.vertical, 20)
    }
}

色輪

在 HSB 顏色模型中,色調錶示基色,可以通過圍繞色環的角度(以度爲單位)來指定,其中紅色位於頂部,顏色沿順時針方向跟隨彩虹的顏色。 SwiftUI 使用 0 到 1 之間的值來表示從 0 到 360 度的色調值。以下代碼在類似於在在 SwiftUI 中創建一個環形 Slider中的環形Slider用於顯示色調選項。移動滑塊可選擇色調,所選色調會顯示不同的飽和度和亮度值。

struct ColorWheelView: View {
    @State private var hue: Double = 180.0
        
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.17, brightness: 1.0)
                    .edgesIgnoringSafeArea(.all)
            
            VStack(spacing: 40) {
                VStack(spacing: 5) {
                    Text("Select Hue")
                        .font(.system(size: 40, weight: .bold, design:.rounded))
                    HStack {
                        CircularSliderView(value: $hue, in: 0...360)
                            .frame(width: 300, height: 300)
                    }
                }
                
                VStack(spacing: 5) {
                    Text("Selected Hue with decreasing Saturation")
                        .font(.system(size: 40, weight: .bold, design:.rounded))
                    HStack() {
                        ForEach([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0], id: \.self) { mySat in
                            VStack {
                                RoundedRectangle(cornerRadius: 10)
                                    .fill(Color(hue: hue/360,
                                                saturation: mySat,
                                                brightness: 1.0))
                                    .frame(height:100)
                                    .overlay {
                                        Text("\(mySat, specifier: "%0.1F")")
                                            .font(.system(size: 30))
                                }
                                Text("H: \(hue/360, specifier: "%0.2F")")
                                Text("S: \(mySat, specifier: "%0.2F")")
                                Text("B: 1.00")
                            }
                        }
                    }
                    .padding(.horizontal, 100)
                }
                            
                VStack(spacing: 5) {
                    Text("Selected Hue with decreasing Brightness")
                        .font(.system(size: 40, weight: .bold, design:.rounded))
                    HStack() {
                        ForEach([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0], id: \.self) { myBright in
                            VStack {
                                RoundedRectangle(cornerRadius: 10)
                                    .fill(Color(hue: hue/360,
                                                saturation: 1.0,
                                                brightness: myBright))
                                    .frame(height:100)
                                    .overlay {
                                        Text("\(myBright, specifier: "%0.1F")")
                                            .font(.system(size: 30))
                                            .foregroundColor(myBright > 0.5 ? Color.black : .white)
                                }
                                Text("H: \(hue/360, specifier: "%0.2F")")
                                Text("S: 1.00")
                                Text("B: \(myBright, specifier: "%0.2F")")
                            }
                        }
                    }
                    .padding(.horizontal, 100)
                }
                
                Spacer()
            }
        }
    }
}
struct CircularSliderView: View {
    @Binding var progress: Double
    
    @State private var rotationAngle = Angle(degrees: 0)
    private var minValue = 0.0
    private var maxValue = 1.0
    
    init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
        self._progress = progress
        
        self.minValue = Double(bounds.first ?? 0)
        self.maxValue = Double(bounds.last ?? 1)
        self.rotationAngle = Angle(degrees: progressFraction * 360.0)
    }
    
    var body: some View {
        GeometryReader { gr in
            let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
            let sliderWidth = radius * 0.3
            
            VStack(spacing:0) {
                ZStack {
                    Circle()
                        .strokeBorder(hueAngularGradient,
                                      style: StrokeStyle(lineWidth: sliderWidth))
                        .rotationEffect(Angle(degrees: -90))
                        .overlay() {
                            Text("\(progress, specifier: "%.0f")")
                                .font(.system(size: radius * 0.5, weight: .bold, design:.rounded))
                        }
                    Circle()
                        .fill(Color.white)
                        .shadow(radius: (sliderWidth * 0.3))
                        .frame(width: sliderWidth, height: sliderWidth)
                        .offset(y: -(radius - (sliderWidth * 0.5)))
                        .rotationEffect(rotationAngle)
                        .gesture(
                            DragGesture(minimumDistance: 0.0)
                                .onChanged() { value in
                                    changeAngle(location: value.location)
                                }
                        )
                }
                .frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
                .padding(radius * 0.1)
            }
            .onAppear {
                self.rotationAngle = Angle(degrees: progressFraction * 360.0)
            }
        }
    }
    
    private var progressFraction: Double {
        return ((progress - minValue) / (maxValue - minValue))
    }
    
    private func changeAngle(location: CGPoint) {
        // Create a Vector for the location (reversing the y-coordinate system on iOS)
        let vector = CGVector(dx: location.x, dy: -location.y)
        
        // Calculate the angle of the vector
        let angleRadians = atan2(vector.dx, vector.dy)
        
        // Convert the angle to a range from 0 to 360 (rather than having negative angles)
        let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians
        
        // Update slider progress value based on angle
        progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
        rotationAngle = Angle(radians: positiveAngle)
    }
    
    let hueAngularGradient = AngularGradient(
        gradient: Gradient(colors: [
            Color(hue: 0.0, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.1, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.2, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.3, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.4, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.5, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.6, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.7, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.8, saturation: 1.0, brightness: 1.0),
            Color(hue: 0.9, saturation: 1.0, brightness: 1.0),
            Color(hue: 1.0, saturation: 1.0, brightness: 1.0),
        ]),
        center: .center,
        startAngle: .degrees(0),
        endAngle: .degrees(360.0))
}

配色

將 HSB 用於顏色的優勢之一是可以輕鬆找到可以很好地協同工作的合適顏色。第一個選項可能是使用相同的色調並更改飽和度或亮度。這對於從較低飽和度到較高飽和度的漸變或使用相同色調的較暗邊框或框架非常有效。下一個選項是通過將色調改變幾度來使用相鄰或相似的顏色。

互補色是色輪上彼此相對的顏色。即使在考慮飽和度和亮度的使用之前,這些顏色也提供了很好的對比度並且可以很好地協同工作。還有一種三色配色方案,其中三種顏色均勻分佈在色輪周圍。這三種顏色可以很好地搭配使用,但需要注意不要讓視圖顯得過於擁擠。通常最好使用一種主色。

定義 ColorModel 以在更改所選色調時創建各種配色方案。 MatchingColorView 在使用圓環滑塊更改色調時顯示不同的匹配顏色集。

model

struct ColorModel {
    var hueDegrees: Double
    private var sat: Double
    private var bright: Double

    let totalDegrees = 360.0
    
    init(hueDegrees: Double, sat: Double, bright: Double) {
        self.hueDegrees = hueDegrees
        self.sat = sat
        self.bright = bright
    }
    
    init() {
        self.init(hueDegrees: 0, sat: 1.0, bright: 1.0)
    }
    
    var hueDouble: Double {
        return Double(self.hueDegrees) / totalDegrees
    }

    var color: Color {
        return Color(hue: hueDouble, saturation: sat, brightness: bright)
    }
    
    // Monochromatic
    var monochromaticColors: [Color] {
        return [
            Color(hue: hueDouble, saturation: sat, brightness: bright),
            Color(hue: hueDouble, saturation: (sat * 0.8), brightness: bright),
            Color(hue: hueDouble, saturation: (sat * 0.6), brightness: bright),
            Color(hue: hueDouble, saturation: (sat * 0.4), brightness: bright)
        ]
    }
    
    private func adjustHue(_ value: Double, percent adjustment: Double) -> Double {
        return Double((Int((value * 100) + adjustment)) % 100) / 100.0
    }
    
    // Analogous
    var analogousColors: [Color] {
        let hue1 = adjustHue(hueDouble, percent: 4)
        let hue2 = adjustHue(hueDouble, percent: -4)
        return [
            Color(hue: hueDouble, saturation: sat, brightness: bright),
            Color(hue: hue1, saturation: sat, brightness: bright),
            Color(hue: hue2, saturation: sat, brightness: bright)
        ]
    }

    // Complementary
    var complementaryColors: [Color] {
        let hue1 = adjustHue(hueDouble, percent: 50)
        return [
            Color(hue: hueDouble, saturation: sat, brightness: bright),
            Color(hue: hue1, saturation: sat, brightness: bright)
        ]
    }
    
    // Triadic
    var triadicColors: [Color] {
        let hue1 = adjustHue(hueDouble, percent: 33.33)
        let hue2 = adjustHue(hueDouble, percent: 66.66)
        return [
            Color(hue: hueDouble, saturation: sat, brightness: bright),
            Color(hue: hue1, saturation: sat, brightness: bright),
            Color(hue: hue2, saturation: sat, brightness: bright)
        ]
    }
}

viewmodel

class ColorViewModel: ObservableObject {
    @Published var colorModel: ColorModel
    
    init() {
        self.colorModel = ColorModel()
    }
    
    var selectedColor: Color {
        colorModel.color
    }
    
    var monochromaticColors: [Color] {
        return colorModel.monochromaticColors
    }
    
    var analogousColors: [Color] {
        return colorModel.analogousColors
    }
    
    var complementaryColors: [Color] {
        return colorModel.complementaryColors
    }

    var triadicColors: [Color] {
        return colorModel.triadicColors
    }
}

view

struct MatchingColorView: View {
    @ObservedObject private var colorVm = ColorViewModel()
    
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.17, brightness: 1.0)
                .edgesIgnoringSafeArea(.all)
            
            VStack(spacing:30) {
                HStack {
                    VStack(spacing: 5) {
                        Text("Select Hue")
                            .font(.system(size: 40, weight: .bold, design:.rounded))
                        HStack {
                            CircularSliderView(value: $colorVm.colorModel.hueDegrees, in: 0...360)
                                .frame(width: 300, height: 300)
                        }
                    }
                    
                    Spacer().frame(width: 200)
                    
                    VStack {
                        RoundedRectangle(cornerRadius: 20)
                            .fill(colorVm.selectedColor)
                            .frame(width: 300, height: 250, alignment: .center)
                        Text("Selected Color")
                            .font(.system(size: 30, weight: .bold, design:.rounded))
                    }
                }
                
                VStack(spacing:20) {
                    HStack {
                        Text("Monochromatic")
                            .frame(width: 300, alignment: .trailing)
                        ForEach(colorVm.monochromaticColors, id: \.self) { col in
                            RoundedRectangle(cornerRadius: 20)
                                .fill(col)
                        }
                    }
                    HStack {
                        Text("Analogous")
                            .frame(width: 300, alignment: .trailing)
                        ForEach(colorVm.analogousColors, id: \.self) { col in
                            RoundedRectangle(cornerRadius: 20)
                                .fill(col)
                        }
                    }
                    HStack {
                        Text("Complementary")
                            .frame(width: 300, alignment: .trailing)
                        ForEach(colorVm.complementaryColors, id: \.self) { col in
                            RoundedRectangle(cornerRadius: 20)
                                .fill(col)
                        }
                    }
                    HStack {
                        Text("Triadic")
                            .frame(width: 300, alignment: .trailing)
                        ForEach(colorVm.triadicColors, id: \.self) { col in
                            RoundedRectangle(cornerRadius: 20)
                                .fill(col)
                        }
                    }
                }
                .padding(.horizontal, 100)
                .font(.system(size: 30, weight: .bold, design:.rounded))
            }
            .padding(50)
        }
    }
}

總結

我發現使用 HSB 定義顏色是一種更直觀的顏色定義方式。使用 RGB 顏色模型沒有錯,如果您有 RGB 值,則使用它們。但是,當從 RGB 值開始時,有時很難識別搭配得很好的顏色。堅持使用相同的色調並調整飽和度或亮度以在不改變顏色的情況下爲屏幕布局添加一些變化會更容易。 HSB 比 RGB 更容易識別相鄰色或互補色。

譯自Define colors with Hue, Saturation and Brightness rather than Red, Green and Blue properties

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章