Swift 中的亮度饱和度圆形垫

发布于 2025-01-21 03:24:16 字数 3986 浏览 2 评论 0原文

序言

我正在尝试使用 SwiftUI 创建一个圆形亮度饱和度垫,既用于指示(从给定颜色,显示光标位置)又用于使用(通过移动光标,获取结果颜色)。

我为我糟糕的英语道歉。

演示代码

首先,我使用两个 Circle 元素绘制圆形,并填充 LinearGradient 元素和亮度平淡模式。

    public var body: some View {
        ZStack(alignment: .top) {
            //MARK: - Inner Color circle
            Group {
                Circle()
                    .fill(
                        LinearGradient(
                            colors: [self.minSaturatedColor, self.maxSaturatedColor],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                Circle()
                    .fill(
                        LinearGradient(
                            colors: [.white, .black],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                    .blendMode(.luminosity)
            }
            .frame(width: self.diameter, height: self.diameter, alignment: .center)
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged(self.updateCursorPosition)
                    .onEnded({ _ in
                        self.startIndication = nil
                        self.changed(self.finalColor)
                    })
            )

            //MARK: - Indicator

            Circle()
                .stroke(Color.black, lineWidth: 2)
                .foregroundColor(Color.clear)
                .frame(width: self.cursorDiameter, height: self.cursorDiameter, alignment: .center)
                .position(cursorPosition)
        }
        .frame(width: self.diameter, height: self.diameter, alignment: .center)
    }

理论

这是关于颜色饱和度与亮度混合的理论。 输入图片这里的描述

我不确定这是正确的方法,但看起来是这样,因为它产生了以下具有(可能)所有色调的圆圈:
输入图片这里的描述

问题 1

我用来确定光标位置(从给定颜色)的简化代码是:

    public var cursorPosition: CGPoint {
        let hsb = self.finalColor.hsba
        let x: CGFloat = hsb.saturation * self.diameter
        let y: CGFloat = hsb.brightness * self.diameter 

        // Adjust the Apple reversed ordinate
        return CGPoint(x: x, y: self.diameter - y)
    }

不幸的是,这似乎不正确,因为通过给出某种颜色(即#bd4d22) 它使指针放置在错误的位置。 输入图片这里的描述

问题 2

还有另一个问题:组件应该允许移动光标,从而使用正确的亮度和饱和度更新 finalColor State 属性。

    private func updateCursorPosition(_ value: DragGesture.Value) -> Void {
        let hsb = self.baseColor.hsba
        
        let saturation = value.location.x / self.diameter
        let brightness = (0 - value.location.y + self.diameter) / self.diameter
        
        self.finalColor = Color(
            hue: hsb.hue,
            saturation: saturation,
            brightness: brightness
        )
    }

但结果并不是预期的,因为即使光标跟随该点(意味着计算值是正确的),所得的颜色也不是在指针后面呈现的颜色!

示例 pt. 1

enter图片描述在这里在这里,正如您所看到的,完全饱和的颜色(理论上应该是圆周的 2π)是不正确的。

示例 pt. 2

enter图片描述在这里在这里,正如您所看到的,走出圆圈就会出现问题。

问题

  1. 谁能解释一下我哪里失败了?
  2. 如果光标被拖出圆圈,任何人都可以帮助我阻止 updateCursorPosition 结果颜色值吗?
  3. 我是否以错误的方式做其他事情?

谢谢大家的耐心等待

Preamble

I'm trying to create a rounded Brightness Saturation pad using SwiftUI both for indication (from a given color, show the cursor position) and use (by moving the cursor, obtain the resulting color).

I apologize for my bad English ????.

Presentation code

Firstly I draw the Circle using two Circle elements, filled with a LinearGradient element and the luminosity bland mode.

    public var body: some View {
        ZStack(alignment: .top) {
            //MARK: - Inner Color circle
            Group {
                Circle()
                    .fill(
                        LinearGradient(
                            colors: [self.minSaturatedColor, self.maxSaturatedColor],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                Circle()
                    .fill(
                        LinearGradient(
                            colors: [.white, .black],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                    .blendMode(.luminosity)
            }
            .frame(width: self.diameter, height: self.diameter, alignment: .center)
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged(self.updateCursorPosition)
                    .onEnded({ _ in
                        self.startIndication = nil
                        self.changed(self.finalColor)
                    })
            )

            //MARK: - Indicator

            Circle()
                .stroke(Color.black, lineWidth: 2)
                .foregroundColor(Color.clear)
                .frame(width: self.cursorDiameter, height: self.cursorDiameter, alignment: .center)
                .position(cursorPosition)
        }
        .frame(width: self.diameter, height: self.diameter, alignment: .center)
    }

Theory

Here's the theory above the mixtures of the color saturation with the brightness.
enter image description here

I'm not sure that this is the correct approach, but it seems so because it produces the following circle having (probably) all the shades:
enter image description here

Problem 1

The simplified code that I used to determine the Cursor position (from a given color) is:

    public var cursorPosition: CGPoint {
        let hsb = self.finalColor.hsba
        let x: CGFloat = hsb.saturation * self.diameter
        let y: CGFloat = hsb.brightness * self.diameter 

        // Adjust the Apple reversed ordinate
        return CGPoint(x: x, y: self.diameter - y)
    }

Unfortunately, this it seems not correct because by giving a certain color (I.e. #bd4d22) it makes the pointer placed in a wrong position.
enter image description here

Problem 2

There's also another problem: the component should allow to move the cursor and so update the finalColor State property with the correct amount of brightness and saturation.

    private func updateCursorPosition(_ value: DragGesture.Value) -> Void {
        let hsb = self.baseColor.hsba
        
        let saturation = value.location.x / self.diameter
        let brightness = (0 - value.location.y + self.diameter) / self.diameter
        
        self.finalColor = Color(
            hue: hsb.hue,
            saturation: saturation,
            brightness: brightness
        )
    }

yet the result is not what expected because, event tough the cursor follows the point (meaning that the computed value is correct), the resulting color is not what rendered behind the pointer!

Example pt. 1

enter image description here
Here, as you can see, the full saturated color (that should theoretically be 2π of the circumference) is not correct.

Example pt. 2

enter image description here
Here, as you can see, there's an issue by going outside of the circle.

Question

  1. Can anyone explain where am I failing?
  2. Can anyone help me blocking the updateCursorPosition resulting color value, if the cursor is dragged out of the circle?
  3. Am I doing something else in the wrong way?

Thank you for the patience ????????!

Ps. To accomplish this, I take clue form what done by Procreate and what asked here Circular saturation-brightness gradient for color wheel

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

情痴 2025-01-28 03:24:16

好吧,这花了一段时间,因为这不是一回事。我最终将您的代码拆开并更简单地重建它,并从颜色视图中重构了指示器。为了修复该指标,我从这个答案中获得了灵感。给它一个碰撞。

至于色轮本身,我将其简化为一个 Circle() 并叠加了第二个 Circle()。由于我不是设计师,所以直接确定颜色需要花费更多时间,因此我不得不不太直观地处理颜色。在处理这个问题时,我有两个认识。

为了处理最大和最小饱和度,唯一可以使用的变量是色调。亮度和饱和度是固定的,饱和度为 0 或 1,亮度为 1,否则基色渐变关闭。

这导致我必须处理亮度梯度。颜色已经处于最大亮度,因此从白色到黑色的渐变渲染再次错误。我的另一个认识是亮度渐变应该从透明到黑色。这样就解决了颜色问题。代码如下并注释。

色轮:

struct ColorWheelView: View {
    
    @State private var position: CGPoint
    @Binding public var selectedColor: Color
    
    private var saturation: CGFloat {
        position.x / diameter
    }
    private var brightness: CGFloat {
        (diameter - position.y) / diameter
    }
    
    private let diameter: CGFloat
    private let cursorDiameter: CGFloat
    
    private let baseHSB: (hue: Double, saturation: Double, brightness: Double, alpha: Double)
    private let minSaturatedColor: Color
    private let maxSaturatedColor: Color
    
    public init(
        //there needs to be an intitial fixed color to set the hue from
        color: Color,
        //this returns the selected color. It can start as anything as it is set to
        //color in .onAppear()
        selectedColor: Binding<Color>,
        diameter: CGFloat = 200,
        cursorDiameter: CGFloat = 20
    ) {
        let hsb = color.hsba
        // Because the view uses the different parts of color, it made sense to just have
        // an hsba here. I would consider making this its own Type just for readability.
        // The only thing that this entire variable is used for is setting the initial selected
        // color. Otherwise I could have just kept the hue.
        baseHSB = Color(hue: color.hsba.hue, saturation: color.hsba.saturation, brightness: color.hsba.brightness).hsba
    
        // This sets the initial indicator position to be at the correct place on the wheel as
        //The initial color sent in, including the initial saturation and brightness.
        _position = State(initialValue: CGPoint(x: hsb.saturation * diameter, y: diameter - (hsb.brightness * diameter)))
        // This is the return color.
        _selectedColor = selectedColor
        // self is only needed to avoid confusion for the compiler.
        self.diameter = diameter
        self.cursorDiameter = cursorDiameter
        // Note these are set to maximum brightness.
        minSaturatedColor = Color(hue: baseHSB.hue, saturation: 0, brightness: 1)
        maxSaturatedColor = Color(hue: baseHSB.hue, saturation: 1, brightness: 1)
    }
    
    
    var body: some View {
        VStack {
        Circle()
            .fill(
                LinearGradient(
                    colors: [self.minSaturatedColor, self.maxSaturatedColor],
                    startPoint: .leading,
                    endPoint: .trailing
                )
            )
            .overlay(
                Circle()
                    .fill(
                        LinearGradient(
                            // Instead of a gradient of white to black, this is clear to black
                            // as the saturation gradients are at maximum brightness already.
                            colors: [.clear, .black],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                    .blendMode(.luminosity)
            )
            .overlay(
                IndicatorView(position: $position, diameter: diameter, cursorDiameter: cursorDiameter)
            )
            .frame(width: self.diameter, height: self.diameter)
            .onAppear {
                // Sets the bound color to the original color.
                selectedColor = Color(hue: baseHSB.hue, saturation: baseHSB.saturation, brightness: baseHSB.brightness, opacity: baseHSB.alpha)
            }
            .onChange(of: position) { _ in
                // When position changes, this changes the saturation and brightness.
                selectedColor = Color(
                    hue: baseHSB.hue,
                    saturation: saturation,
                    brightness: brightness
                )
            }
        }
    }
}

指示器:

struct IndicatorView: View {
    
    @Binding var position: CGPoint
    private let diameter: CGFloat
    private let cursorDiameter: CGFloat
    private let radius: CGFloat
    private let center: CGPoint
    
    init(position: Binding<CGPoint>, diameter: CGFloat, cursorDiameter: CGFloat) {
        _position = position
        self.diameter = diameter
        self.cursorDiameter = cursorDiameter
        self.radius = diameter / 2
        // the center of the circle is the center of the frame which is CGPoint(radius, radius)
        self.center = CGPoint(x: radius, y: radius)
    }
    var body: some View {
        VStack {
            Circle()
                // This circle is simply to keep the indicator aligned with the ColorWheelView
                .fill(Color.clear)
                .overlay(
                    Circle()
                        .stroke(Color.black, lineWidth: 2)
                        .foregroundColor(Color.clear)
                        .frame(width: cursorDiameter, height: self.cursorDiameter)
                        .position(position)
                        .gesture(DragGesture()
                            .onChanged { value in
                                updatePosition(value.location)
                            }
                        )
                )
                .frame(width: diameter, height: diameter)
        }
        .onAppear {
            updatePosition(position)
        }
    }
    
    private func updatePosition(_ point: CGPoint) {
        let currentLocation = point
        let center = CGPoint(x: radius, y: radius)
        let distance = center.distance(to:currentLocation)
        // This triggers if the drag goes outside of the circle.
        if distance > radius {
            let overDrag = radius / distance
            //These coordinates are guaranteed inside of the circle.
            let newLocationX = (currentLocation.x - center.x) * overDrag + center.x
            let newLocationY = (currentLocation.y - center.y) * overDrag + center.y
            self.position = CGPoint(x: newLocationX, y: newLocationY)
        }else{
            self.position = point
        }
    }
}
    

extension CGPoint {
    //This is simply the Pythagorean theorem. Distance is the hypotenuse.
    func distance(to point: CGPoint) -> CGFloat {
        return sqrt(pow((point.x - x), 2) + pow((point.y - y), 2))
    }
}

此外,作为旁注,您拥有的大多数变量都是常量,应该是 let 而不是 vars。结构内部的任何内容都应该是私有的。 @State 变量始终是私有的。

Okay, this took a while as it wasn't one thing. I ended up pulling your code apart and rebuilding it a little more simply, as well as refactoring the indicator from the color view. To fix the indicator, I took inspiration from this answer. Give it a bump.

As to the color wheel itself, I simplified it into a Circle() with an overlay of the second Circle(). Getting the colors straight took a lot more time as I am not a designer, so I had to work through dealing with the colors less intuitively. I had a two realizations while dealing with this.

To handle the maximum and minimum saturations, the only variable that could be used was the hue. The brightness and saturation were fixed, either 0 or 1 for saturation and 1 for brightness, otherwise the base color gradient was off.

That caused me to have to deal with the brightness gradient. The color was already at maximum brightness, so putting a gradient from white to black rendered incorrectly, yet again. Another realization I had was that the brightness gradient should go from clear to black. That resolved the color issues. The code is below and commented.

The Color Wheel:

struct ColorWheelView: View {
    
    @State private var position: CGPoint
    @Binding public var selectedColor: Color
    
    private var saturation: CGFloat {
        position.x / diameter
    }
    private var brightness: CGFloat {
        (diameter - position.y) / diameter
    }
    
    private let diameter: CGFloat
    private let cursorDiameter: CGFloat
    
    private let baseHSB: (hue: Double, saturation: Double, brightness: Double, alpha: Double)
    private let minSaturatedColor: Color
    private let maxSaturatedColor: Color
    
    public init(
        //there needs to be an intitial fixed color to set the hue from
        color: Color,
        //this returns the selected color. It can start as anything as it is set to
        //color in .onAppear()
        selectedColor: Binding<Color>,
        diameter: CGFloat = 200,
        cursorDiameter: CGFloat = 20
    ) {
        let hsb = color.hsba
        // Because the view uses the different parts of color, it made sense to just have
        // an hsba here. I would consider making this its own Type just for readability.
        // The only thing that this entire variable is used for is setting the initial selected
        // color. Otherwise I could have just kept the hue.
        baseHSB = Color(hue: color.hsba.hue, saturation: color.hsba.saturation, brightness: color.hsba.brightness).hsba
    
        // This sets the initial indicator position to be at the correct place on the wheel as
        //The initial color sent in, including the initial saturation and brightness.
        _position = State(initialValue: CGPoint(x: hsb.saturation * diameter, y: diameter - (hsb.brightness * diameter)))
        // This is the return color.
        _selectedColor = selectedColor
        // self is only needed to avoid confusion for the compiler.
        self.diameter = diameter
        self.cursorDiameter = cursorDiameter
        // Note these are set to maximum brightness.
        minSaturatedColor = Color(hue: baseHSB.hue, saturation: 0, brightness: 1)
        maxSaturatedColor = Color(hue: baseHSB.hue, saturation: 1, brightness: 1)
    }
    
    
    var body: some View {
        VStack {
        Circle()
            .fill(
                LinearGradient(
                    colors: [self.minSaturatedColor, self.maxSaturatedColor],
                    startPoint: .leading,
                    endPoint: .trailing
                )
            )
            .overlay(
                Circle()
                    .fill(
                        LinearGradient(
                            // Instead of a gradient of white to black, this is clear to black
                            // as the saturation gradients are at maximum brightness already.
                            colors: [.clear, .black],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                    .blendMode(.luminosity)
            )
            .overlay(
                IndicatorView(position: $position, diameter: diameter, cursorDiameter: cursorDiameter)
            )
            .frame(width: self.diameter, height: self.diameter)
            .onAppear {
                // Sets the bound color to the original color.
                selectedColor = Color(hue: baseHSB.hue, saturation: baseHSB.saturation, brightness: baseHSB.brightness, opacity: baseHSB.alpha)
            }
            .onChange(of: position) { _ in
                // When position changes, this changes the saturation and brightness.
                selectedColor = Color(
                    hue: baseHSB.hue,
                    saturation: saturation,
                    brightness: brightness
                )
            }
        }
    }
}

The Indicator:

struct IndicatorView: View {
    
    @Binding var position: CGPoint
    private let diameter: CGFloat
    private let cursorDiameter: CGFloat
    private let radius: CGFloat
    private let center: CGPoint
    
    init(position: Binding<CGPoint>, diameter: CGFloat, cursorDiameter: CGFloat) {
        _position = position
        self.diameter = diameter
        self.cursorDiameter = cursorDiameter
        self.radius = diameter / 2
        // the center of the circle is the center of the frame which is CGPoint(radius, radius)
        self.center = CGPoint(x: radius, y: radius)
    }
    var body: some View {
        VStack {
            Circle()
                // This circle is simply to keep the indicator aligned with the ColorWheelView
                .fill(Color.clear)
                .overlay(
                    Circle()
                        .stroke(Color.black, lineWidth: 2)
                        .foregroundColor(Color.clear)
                        .frame(width: cursorDiameter, height: self.cursorDiameter)
                        .position(position)
                        .gesture(DragGesture()
                            .onChanged { value in
                                updatePosition(value.location)
                            }
                        )
                )
                .frame(width: diameter, height: diameter)
        }
        .onAppear {
            updatePosition(position)
        }
    }
    
    private func updatePosition(_ point: CGPoint) {
        let currentLocation = point
        let center = CGPoint(x: radius, y: radius)
        let distance = center.distance(to:currentLocation)
        // This triggers if the drag goes outside of the circle.
        if distance > radius {
            let overDrag = radius / distance
            //These coordinates are guaranteed inside of the circle.
            let newLocationX = (currentLocation.x - center.x) * overDrag + center.x
            let newLocationY = (currentLocation.y - center.y) * overDrag + center.y
            self.position = CGPoint(x: newLocationX, y: newLocationY)
        }else{
            self.position = point
        }
    }
}
    

extension CGPoint {
    //This is simply the Pythagorean theorem. Distance is the hypotenuse.
    func distance(to point: CGPoint) -> CGFloat {
        return sqrt(pow((point.x - x), 2) + pow((point.y - y), 2))
    }
}

Also, as a side note, most of the variables you had were constants and should be lets and not vars. Anything internal to the struct should be private. @State variables are always private.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文