Pulled out ColorWheelView into its own view class. Added color value to cgpoint calculation relative to color wheel. Removed unused code.

This commit is contained in:
Jonathan Cardasis 2019-04-11 22:14:07 -04:00
parent 5a8f85fbcd
commit 01f0cc0e57
5 changed files with 159 additions and 229 deletions

View File

@ -18,6 +18,7 @@
FC1BD8C02207D7B700817AF3 /* ChromaColorPickerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BD8BF2207D7B700817AF3 /* ChromaColorPickerTests.swift */; };
FC1BD8C22207D7B700817AF3 /* ChromaColorPicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */; };
FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */; };
FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */; };
FCEA4E272235AAA200C0A1B6 /* ChromaColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */; };
/* End PBXBuildFile section */
@ -73,6 +74,7 @@
FC1BD8CC2207FCE100817AF3 /* ChromaAddButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChromaAddButton.swift; sourceTree = "<group>"; };
FC1BD8CD2207FCE100817AF3 /* ColorModeToggleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorModeToggleButton.swift; sourceTree = "<group>"; };
FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWheelView.swift; sourceTree = "<group>"; };
FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorHandle.swift; sourceTree = "<group>"; };
FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorPicker.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -110,6 +112,7 @@
3503B8311F2689BC00750356 /* ChromaColorPicker.h */,
3503B8321F2689BC00750356 /* Info.plist */,
FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */,
FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */,
FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */,
);
path = Source;
@ -319,6 +322,7 @@
buildActionMask = 2147483647;
files = (
FCEA4E272235AAA200C0A1B6 /* ChromaColorPicker.swift in Sources */,
FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */,
FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -12,7 +12,7 @@ import ChromaColorPicker
class ViewController: UIViewController {
@IBOutlet weak var colorDisplayView: UIView!
let colorPicker = ChromaColorPicker2()
let colorPicker = ChromaColorPicker()
override func viewDidLoad() {

View File

@ -0,0 +1,24 @@
//
// ChromaColorHandle.swift
// ChromaColorPicker
//
// Created by Jon Cardasis on 4/11/19.
// Copyright © 2019 Jonathan Cardasis. All rights reserved.
//
import UIKit
public class ChromaColorHandle {
/// Current selected color of the handle.
fileprivate(set) var color: UIColor
/// An image to display above the handle.
var popoverImage: UIImage?
/// A view to display above the handle. Overrides any provided `popoverImage`.
var popoverView: UIView?
init(color: UIColor) {
self.color = color
}
}

View File

@ -14,33 +14,11 @@ public protocol ChromaColorPickerDelegate {
func colorPickerDidChooseColor(_ colorPicker: ChromaColorPicker, color: UIColor)
}
func timeElapsedInSecondsWhenRunningCode(operation: ()->()) -> Double { //TEMP - DEBUG
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
return Double(timeElapsed)
}
public class ChromaColorHandle {
/// Current selected color of the handle.
fileprivate(set) var color: UIColor
/// An image to display above the handle.
var popoverImage: UIImage?
/// A view to display above the handle. Overrides any provided `popoverImage`.
var popoverView: UIView?
init(color: UIColor) {
self.color = color
}
}
@IBDesignable
public class ChromaColorPicker: UIControl {
//MARK: - Initialization
override public init(frame: CGRect) {
super.init(frame: frame)
@ -60,10 +38,6 @@ public class ChromaColorPicker: UIControl {
public override func layoutSubviews() {
super.layoutSubviews()
let minDimensionSize = min(bounds.width, bounds.height)
let colorWheelImage = makeColorWheel(radius: minDimensionSize * 3.0) // TEMP?
colorWheelImageView.image = colorWheelImage
}
public func addHandle(at color: UIColor? = nil) -> ChromaColorHandle {
@ -71,50 +45,24 @@ public class ChromaColorPicker: UIControl {
}
// MARK: - Private
internal var colorWheelImageView: UIImageView!
internal let colorWheelView = ColorWheelView()
internal func commonInit() {
self.backgroundColor = UIColor.clear
self.layer.masksToBounds = false
setupColorWheel()
setupColorWheelView()
applySmoothingMaskToColorWheel()
setupGestures()
// DEBUG - BENCHMARK
func timeElapsedInSecondsWhenRunningCode(operation: ()->()) -> Double {
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
return Double(timeElapsed)
}
var times = [Double]()
for _ in 0..<10 {
let time = timeElapsedInSecondsWhenRunningCode {
_ = makeColorWheel(radius: 1400)
}
times.append(time)
print(time)
}
let avgTime = times.reduce(0, +) / Double(times.count)
print("\n\nAvgTime: \(avgTime)")
// END DEBUG
}
internal func setupColorWheel() {
colorWheelImageView = UIImageView(image: nil)
colorWheelImageView.contentMode = .scaleAspectFit
colorWheelImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(colorWheelImageView)
internal func setupColorWheelView() {
colorWheelView.translatesAutoresizingMaskIntoConstraints = false
addSubview(colorWheelView)
NSLayoutConstraint.activate([
colorWheelImageView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
colorWheelImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
colorWheelImageView.widthAnchor.constraint(equalTo: self.widthAnchor),
colorWheelImageView.heightAnchor.constraint(equalTo: colorWheelImageView.widthAnchor),
colorWheelView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
colorWheelView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
colorWheelView.widthAnchor.constraint(equalTo: self.widthAnchor),
colorWheelView.heightAnchor.constraint(equalTo: colorWheelView.widthAnchor),
])
}
@ -125,177 +73,16 @@ public class ChromaColorPicker: UIControl {
internal func setupGestures() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorWheelTapped(_:)))
colorWheelImageView.isUserInteractionEnabled = true
colorWheelImageView.addGestureRecognizer(tapGesture)
colorWheelView.isUserInteractionEnabled = true
colorWheelView.addGestureRecognizer(tapGesture)
}
@objc
internal func colorWheelTapped(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: colorWheelImageView)
let pixelColor = colorWheelImageView.getPixelColor(at: location)
print()
}
/**
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
image of 200x200 (400x400 pixels on a device with 2x scaling.)
*/
internal func makeColorWheel(radius: CGFloat) -> UIImage {
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
"inputDither": 0,
"inputRadius": radius,
"inputSoftness": 0,
"inputValue": 1
])!
return UIImage(ciImage: filter.outputImage!)
}
/*
internal lazy var context = makeMetalContext()
internal func makeMetalContext() -> CIContext {
let mtlDevice = MTLCreateSystemDefaultDevice()
if let device = mtlDevice, device.supportsFeatureSet(.iOS_GPUFamily1_v1) {
return CIContext(mtlDevice: device, options: [CIContextOption.useSoftwareRenderer: false])
} else if let eaglContext = EAGLContext(api: .openGLES2) {
return CIContext(eaglContext: eaglContext)
} else {
return CIContext()
}
}
internal lazy var lookupImage: CGImage = {
let colorWheelCIImage = makeColorWheel(radius: 50).ciImage!
return context.createCGImage(colorWheelCIImage, from: colorWheelCIImage.extent)!
}()
internal func location(of color: UIColor) -> CGPoint? {
// guard let image = colorWheelImageView.image else { return nil }
//
// let ci = image.ciImage!
// let cgImage = context.createCGImage(ci, from: ci.extent)!
let cgImage = self.lookupImage
var red : CGFloat = 0
var green : CGFloat = 0
var blue : CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
let r = UInt8(red * 255.0)
let g = UInt8(green * 255.0)
let b = UInt8(blue * 255.0)
let width = 100 //Int(image.size.width)
let height = 100 //Int(image.size.height)
if let cfData = cgImage.dataProvider?.data, let pointer = CFDataGetBytePtr(cfData) {
for x in 0..<width {
for y in 0..<height {
// TODO: do a DISTANCE calc and find closest distance
let pixelAddress = x * 4 + y * width * 4
if pointer.advanced(by: pixelAddress).pointee == r && //Red
pointer.advanced(by: pixelAddress + 1).pointee == g && //Green
pointer.advanced(by: pixelAddress + 2).pointee == b { //Blue
print(CGPoint(x: x, y: y)) //temp
return CGPoint(x: x, y: y)
}
}
}
}
return nil
}
*/
}
private let defaultHandleColorPosition: UIColor = .black
internal extension UIImageView {
internal func getPixelColor(at point: CGPoint) -> UIColor {
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 UIColor.white
}
context.translateBy(x: -point.x, y: -point.y)
layer.render(in: context)
let color = UIColor(
red: CGFloat(pixel[0]) / 255.0,
green: CGFloat(pixel[1]) / 255.0,
blue: CGFloat(pixel[2]) / 255.0,
alpha: CGFloat(pixel[3]) / 255.0
)
pixel.deallocate()
return color
let location = gesture.location(in: colorWheelView)
let pixelColor = colorWheelView.pixelColor(at: location)
print(pixelColor)
}
}
extension UIImage {
func resized(newWidth: CGFloat, context: CIContext) -> UIImage? {
let scale = newWidth / self.size.width
let newHeight = self.size.height * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
self.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
extension CGImage {
func resizeImageUsingVImage(size:CGSize) -> CGImage? {
let cgImage = self
var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue), version: 0, decode: nil, renderingIntent: CGColorRenderingIntent.defaultIntent)
var sourceBuffer = vImage_Buffer()
defer {
free(sourceBuffer.data)
}
var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, numericCast(kvImageNoFlags))
guard error == kvImageNoError else { return nil }
// create a destination buffer
//let scale = self.scale
let destWidth = Int(size.width)
let destHeight = Int(size.height)
let bytesPerPixel = self.bitsPerPixel/8
let destBytesPerRow = destWidth * bytesPerPixel
let destData = UnsafeMutablePointer<UInt8>.allocate(capacity: destHeight * destBytesPerRow)
defer {
destData.deallocate(capacity: destHeight * destBytesPerRow)
}
var destBuffer = vImage_Buffer(data: destData, height: vImagePixelCount(destHeight), width: vImagePixelCount(destWidth), rowBytes: destBytesPerRow)
// scale the image
error = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, numericCast(kvImageHighQualityResampling))
guard error == kvImageNoError else { return nil }
// create a CGImage from vImage_Buffer
var destCGImage = vImageCreateCGImageFromBuffer(&destBuffer, &format, nil, nil, numericCast(kvImageNoFlags), &error)?.takeRetainedValue()
guard error == kvImageNoError else { return nil }
// create a UIImage
// defer {
// destCGImage = nil
// }
return destCGImage
// let resizedImage = destCGImage.flatMap { UIImage(cgImage: $0, scale: 0.0, orientation: self.imageOrientation) }
// destCGImage = nil
// return resizedImage
}
}
internal let defaultHandleColorPosition: UIColor = .black

View File

@ -10,4 +10,119 @@ import UIKit
public class ColorWheelView: UIView {
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()
let minDimensionSize = min(bounds.width, bounds.height)
if let colorWheelImage = makeColorWheelImage(radius: minDimensionSize) {
imageView.image = UIImage(ciImage: colorWheelImage)
}
}
/**
Returns the (x,y) location of the color provided within the ColorWheelView.
*/
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))
let colorPoint = CGPoint(x: center.x + colorTranslation.x, y: center.y + colorTranslation.y)
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.
*/
public func pixelColor(at point: CGPoint) -> UIColor? {
guard bounds.contains(point) else { return nil }
let distanceFromCenter: CGFloat = hypot(center.x - point.x, center.y - point.y)
let pointExistsInRadius: Bool = distanceFromCenter <= radius
guard pointExistsInRadius else { return nil }
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)
layer.render(in: context)
let color = UIColor(
red: CGFloat(pixel[0]) / 255.0,
green: CGFloat(pixel[1]) / 255.0,
blue: CGFloat(pixel[2]) / 255.0,
alpha: CGFloat(pixel[3]) / 255.0
)
pixel.deallocate()
return color
}
// MARK: - Private
internal let imageView = UIImageView()
internal var radius: CGFloat {
return bounds.width / 2.0
}
internal func commonInit() {
backgroundColor = .clear
setupImageView()
}
internal func setupImageView() {
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
/**
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
image of 200x200 (400x400 pixels on a device with 2x scaling.)
*/
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
}
}