diff --git a/ChromaColorPicker.xcodeproj/project.pbxproj b/ChromaColorPicker.xcodeproj/project.pbxproj index 47f6255..55f86ac 100644 --- a/ChromaColorPicker.xcodeproj/project.pbxproj +++ b/ChromaColorPicker.xcodeproj/project.pbxproj @@ -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 = ""; }; FC1BD8CD2207FCE100817AF3 /* ColorModeToggleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorModeToggleButton.swift; sourceTree = ""; }; FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWheelView.swift; sourceTree = ""; }; + FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorHandle.swift; sourceTree = ""; }; FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorPicker.swift; sourceTree = ""; }; /* 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; diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 68e496d..fcb9331 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -12,7 +12,7 @@ import ChromaColorPicker class ViewController: UIViewController { @IBOutlet weak var colorDisplayView: UIView! - let colorPicker = ChromaColorPicker2() + let colorPicker = ChromaColorPicker() override func viewDidLoad() { diff --git a/Source/ChromaColorHandle.swift b/Source/ChromaColorHandle.swift new file mode 100644 index 0000000..d626c7b --- /dev/null +++ b/Source/ChromaColorHandle.swift @@ -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 + } +} diff --git a/Source/ChromaColorPicker.swift b/Source/ChromaColorPicker.swift index 03ef2ef..688d7fe 100644 --- a/Source/ChromaColorPicker.swift +++ b/Source/ChromaColorPicker.swift @@ -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.. UIColor { - let pixel = UnsafeMutablePointer.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.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 diff --git a/Source/ColorWheelView.swift b/Source/ColorWheelView.swift index cc11efd..a05d713 100644 --- a/Source/ColorWheelView.swift +++ b/Source/ColorWheelView.swift @@ -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.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 + } }