2019-04-12 04:38:30 +03:00
|
|
|
//
|
|
|
|
// ColorWheelView.swift
|
|
|
|
// ChromaColorPicker
|
|
|
|
//
|
|
|
|
// Created by Jon Cardasis on 4/11/19.
|
|
|
|
// Copyright © 2019 Jonathan Cardasis. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
2019-11-09 02:32:51 +03:00
|
|
|
/// This value is used to expand the imageView's bounds and then mask back to its normal size
|
|
|
|
/// such that any displayed image may have perfectly rounded corners.
|
|
|
|
private let defaultImageViewCurveInset: CGFloat = 1.0
|
|
|
|
|
2019-04-12 04:38:30 +03:00
|
|
|
public class ColorWheelView: UIView {
|
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
public override init(frame: CGRect) {
|
|
|
|
super.init(frame: frame)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
public required init?(coder aDecoder: NSCoder) {
|
|
|
|
super.init(coder: aDecoder)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
public override func layoutSubviews() {
|
|
|
|
super.layoutSubviews()
|
2019-04-12 06:08:00 +03:00
|
|
|
layer.masksToBounds = false
|
|
|
|
layer.cornerRadius = radius
|
2019-04-12 05:14:07 +03:00
|
|
|
|
2019-05-02 18:55:00 +03:00
|
|
|
let screenScale: CGFloat = UIScreen.main.scale
|
2019-11-09 02:32:51 +03:00
|
|
|
if let colorWheelImage: CIImage = makeColorWheelImage(radius: radius * screenScale) {
|
2019-05-02 18:55:00 +03:00
|
|
|
imageView.image = UIImage(ciImage: colorWheelImage, scale: screenScale, orientation: .up)
|
2019-04-12 05:14:07 +03:00
|
|
|
}
|
2019-11-09 02:32:51 +03:00
|
|
|
|
|
|
|
// Mask imageview so the generated colorwheel has smooth edges.
|
|
|
|
// We mask the imageview instead of image so we get the benefits of using the CIImage
|
|
|
|
// rendering directly on the GPU.
|
|
|
|
imageViewMask.frame = imageView.bounds.insetBy(dx: defaultImageViewCurveInset, dy: defaultImageViewCurveInset)
|
|
|
|
imageViewMask.layer.cornerRadius = imageViewMask.bounds.width / 2.0
|
|
|
|
imageView.mask = imageViewMask
|
2019-04-12 05:14:07 +03:00
|
|
|
}
|
2019-11-09 02:32:51 +03:00
|
|
|
|
2019-04-12 06:08:00 +03:00
|
|
|
public var radius: CGFloat {
|
|
|
|
return max(bounds.width, bounds.height) / 2.0
|
|
|
|
}
|
|
|
|
|
2019-04-20 22:36:22 +03:00
|
|
|
public var middlePoint: CGPoint {
|
|
|
|
return CGPoint(x: bounds.midX, y: bounds.midY)
|
|
|
|
}
|
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
/**
|
|
|
|
Returns the (x,y) location of the color provided within the ColorWheelView.
|
2019-04-20 01:26:24 +03:00
|
|
|
Disregards color's brightness component.
|
2019-04-12 05:14:07 +03:00
|
|
|
*/
|
|
|
|
public func location(of color: UIColor) -> CGPoint {
|
|
|
|
var hue: CGFloat = 0
|
|
|
|
var saturation: CGFloat = 0
|
|
|
|
color.getHue(&hue, saturation: &saturation, brightness: nil, alpha: nil)
|
|
|
|
|
|
|
|
let radianAngle = hue * (2 * .pi)
|
|
|
|
let distance = saturation * radius
|
|
|
|
let colorTranslation = CGPoint(x: distance * cos(radianAngle), y: -distance * sin(radianAngle))
|
2019-04-20 22:36:22 +03:00
|
|
|
let colorPoint = CGPoint(x: bounds.midX + colorTranslation.x, y: bounds.midY + colorTranslation.y)
|
2019-04-12 05:14:07 +03:00
|
|
|
|
|
|
|
return colorPoint
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns the color on the wheel on a given point relative to the view. nil is returned if
|
|
|
|
the point does not exist within the bounds of the color wheel.
|
|
|
|
*/
|
2019-09-09 02:16:58 +03:00
|
|
|
// TODO: replace this function with a mathmatically based one in ChromaColorPicker
|
2019-04-12 05:14:07 +03:00
|
|
|
public func pixelColor(at point: CGPoint) -> UIColor? {
|
2019-04-14 20:11:00 +03:00
|
|
|
guard pointIsInColorWheel(point) else { return nil }
|
2019-04-12 05:14:07 +03:00
|
|
|
|
2019-04-20 04:39:42 +03:00
|
|
|
// Values on the edge of the circle should be calculated instead of obtained
|
|
|
|
// from the rendered view layer. This ensures we obtain correct values where
|
|
|
|
// image smoothing may have taken place.
|
|
|
|
guard !pointIsOnColorWheelEdge(point) else {
|
2019-04-20 22:36:22 +03:00
|
|
|
let angleToCenter = atan2(point.x - middlePoint.x, point.y - middlePoint.y)
|
2019-04-20 04:39:42 +03:00
|
|
|
return edgeColor(for: angleToCenter)
|
|
|
|
}
|
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
let pixel = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: 4)
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
|
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
|
|
|
guard let context = CGContext(
|
|
|
|
data: pixel,
|
|
|
|
width: 1,
|
|
|
|
height: 1,
|
|
|
|
bitsPerComponent: 8,
|
|
|
|
bytesPerRow: 4,
|
|
|
|
space: colorSpace,
|
|
|
|
bitmapInfo: bitmapInfo.rawValue
|
|
|
|
) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
context.translateBy(x: -point.x, y: -point.y)
|
2019-04-19 23:45:47 +03:00
|
|
|
imageView.layer.render(in: context)
|
2019-04-12 05:14:07 +03:00
|
|
|
let color = UIColor(
|
|
|
|
red: CGFloat(pixel[0]) / 255.0,
|
|
|
|
green: CGFloat(pixel[1]) / 255.0,
|
|
|
|
blue: CGFloat(pixel[2]) / 255.0,
|
2019-04-19 23:45:47 +03:00
|
|
|
alpha: 1.0
|
2019-04-12 05:14:07 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
pixel.deallocate()
|
|
|
|
return color
|
|
|
|
}
|
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
/**
|
|
|
|
Returns whether or not the point is in the circular area of the color wheel.
|
|
|
|
*/
|
|
|
|
public func pointIsInColorWheel(_ point: CGPoint) -> Bool {
|
2019-04-20 04:06:41 +03:00
|
|
|
guard bounds.insetBy(dx: -1, dy: -1).contains(point) else { return false }
|
2019-04-20 01:26:24 +03:00
|
|
|
|
2019-04-20 22:36:22 +03:00
|
|
|
let distanceFromCenter: CGFloat = hypot(middlePoint.x - point.x, middlePoint.y - point.y)
|
2019-04-20 01:26:24 +03:00
|
|
|
let pointExistsInRadius: Bool = distanceFromCenter <= (radius - layer.borderWidth)
|
|
|
|
return pointExistsInRadius
|
|
|
|
}
|
|
|
|
|
2019-04-20 04:39:42 +03:00
|
|
|
public func pointIsOnColorWheelEdge(_ point: CGPoint) -> Bool {
|
2019-04-20 22:36:22 +03:00
|
|
|
let distanceToCenter = hypot(middlePoint.x - point.x, middlePoint.y - point.y)
|
2019-04-20 04:39:42 +03:00
|
|
|
let isPointOnEdge = distanceToCenter >= radius - 1.0
|
|
|
|
return isPointOnEdge
|
|
|
|
}
|
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
// MARK: - Private
|
|
|
|
internal let imageView = UIImageView()
|
2019-11-09 02:32:51 +03:00
|
|
|
internal let imageViewMask = UIView()
|
2019-04-12 05:14:07 +03:00
|
|
|
|
|
|
|
internal func commonInit() {
|
|
|
|
backgroundColor = .clear
|
|
|
|
setupImageView()
|
|
|
|
}
|
|
|
|
|
|
|
|
internal func setupImageView() {
|
|
|
|
imageView.contentMode = .scaleAspectFit
|
2019-11-09 02:32:51 +03:00
|
|
|
imageViewMask.backgroundColor = .black
|
2019-04-12 05:14:07 +03:00
|
|
|
|
|
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(imageView)
|
2019-11-09 02:32:51 +03:00
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
NSLayoutConstraint.activate([
|
2019-11-09 02:32:51 +03:00
|
|
|
imageView.widthAnchor.constraint(equalTo: widthAnchor, constant: defaultImageViewCurveInset * 2),
|
|
|
|
imageView.heightAnchor.constraint(equalTo: heightAnchor, constant: defaultImageViewCurveInset * 2),
|
|
|
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
2019-04-12 05:14:07 +03:00
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Generates a color wheel image from a given radius.
|
|
|
|
- Parameters:
|
|
|
|
- radius: The radius of the wheel in points. A radius of 100 would generate an
|
2019-05-02 18:55:00 +03:00
|
|
|
image of 200x200 points (400x400 pixels on a device with 2x scaling.)
|
2019-04-12 05:14:07 +03:00
|
|
|
*/
|
|
|
|
internal func makeColorWheelImage(radius: CGFloat) -> CIImage? {
|
|
|
|
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
|
|
|
|
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
|
|
|
|
"inputDither": 0,
|
|
|
|
"inputRadius": radius,
|
|
|
|
"inputSoftness": 0,
|
|
|
|
"inputValue": 1
|
|
|
|
])
|
|
|
|
return filter?.outputImage
|
|
|
|
}
|
2019-04-20 04:39:42 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
Returns a color for a provided radian angle on the color wheel.
|
|
|
|
- Note: Adjusts angle for the local color space and returns a color of
|
|
|
|
max saturation and brightness with variable hue.
|
|
|
|
*/
|
|
|
|
internal func edgeColor(for angle: CGFloat) -> UIColor {
|
|
|
|
var normalizedAngle = angle + .pi // normalize to [0, 2pi]
|
|
|
|
normalizedAngle += (.pi / 2) // rotate pi/2 for color wheel
|
|
|
|
var hue = normalizedAngle / (2 * .pi)
|
|
|
|
if hue > 1 { hue -= 1 }
|
|
|
|
return UIColor(hue: hue, saturation: 1, brightness: 1.0, alpha: 1.0)
|
|
|
|
}
|
2019-04-12 04:38:30 +03:00
|
|
|
}
|