Last active
November 2, 2015 22:23
-
-
Save algal/26474d5cdd48edb1669c to your computer and use it in GitHub Desktop.
Define gradient as usual with finite stops, then extract the color from any position
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Exposes all colors in a gradient, which is defined as usual by a finite number of color stops | |
/// - version: known good on Xcode 7.1 playgrounds and iOS 9.1 | |
class ColorGradient | |
{ | |
// array of specified colors for specified locations along the gradient | |
let colors:[UIColor] | |
// array of specified locations (in 0...1) for the specified colors in the gradient | |
let locations:[CGFloat] | |
let gradientImage:UIImage | |
// Number of distinct colors the instance can provide via `colorAtGradientLocation(_:)` | |
let granularity:Int = 1000 | |
// the entire color gradient, in a bitmap with `granularity` pixels | |
private let pixelData:NSData | |
// - requires: every location is in 0...1 | |
init(colorLocationPairs:[(UIColor,CGFloat)]) | |
{ | |
(self.colors,self.locations) = unzip(colorLocationPairs) | |
// generate image with horizontal gradient of `granularity` pixels | |
let ctx = CreateRGBA32PremultipliedBitmapContext(pixelSize: CGSize(width: granularity, height: 1)) | |
var mutableLocations = locations | |
let grad = CGGradientCreateWithColors(nil, colors.map({ $0.CGColor }), &mutableLocations)! | |
CGContextDrawLinearGradient(ctx, grad, CGPoint(x: 0, y: 0), CGPoint(x:CGFloat(granularity),y:0), []) | |
guard let cgImage = CGBitmapContextCreateImage(ctx) else { | |
fatalError("error creating bitmap context") | |
} | |
// grab the bitmap array of pixel data from the image | |
let provider = CGImageGetDataProvider(cgImage) | |
guard let data = CGDataProviderCopyData(provider) else { fatalError("error getting data") } | |
self.pixelData = data | |
// expose the gradient as UIImage just for debugging | |
self.gradientImage = UIImage(CGImage: cgImage, scale: 1, orientation: .Up) | |
} | |
func colorAtGradientLocation(location:CGFloat) -> UIColor | |
{ | |
precondition(ClosedInterval<CGFloat>(0,1).contains(location), "location must be in 0...1") | |
// index of pixel with color we want to explore | |
let pixelIndex:Int = Int(location * CGFloat(self.granularity - 1)) | |
let ptr:UnsafePointer<Void> = pixelData.bytes | |
let pixelArray:UnsafePointer<PixelDataRGBA32Premultiplied> = unsafeBitCast(ptr, UnsafePointer<PixelDataRGBA32Premultiplied>.self) | |
let pixel = pixelArray[pixelIndex] | |
return pixel.color | |
} | |
} | |
/// struct for representing a single 32-bit RGBA-premultiplied pixel | |
private struct PixelDataRGBA32Premultiplied | |
{ | |
var red:UInt8 | |
var green:UInt8 | |
var blue:UInt8 | |
var alpha:UInt8 | |
} | |
extension PixelDataRGBA32Premultiplied | |
{ | |
var color:UIColor { | |
let alphaFloat = CGFloat(alpha) / CGFloat(255) | |
let redF = CGFloat(red) / (CGFloat(255) * alphaFloat) | |
let greenF = CGFloat(green) / (CGFloat(255) * alphaFloat) | |
let blueF = CGFloat(blue) / (CGFloat(255) * alphaFloat) | |
return UIColor(red: redF, green: greenF, blue: blueF, alpha: alphaFloat) | |
} | |
} | |
/// Creates a `CGBitmapContext` with 32-bits per pixel, 8 bits per channel, with channels ordered RGBA, with the RGB channel values premultiplied by the A value | |
/// - parameter size: size in pixels (not points) | |
/// - returns: a `CGContext` context, or nil | |
private func CreateRGBA32PremultipliedBitmapContext(pixelSize size:CGSize) -> CGContext? | |
{ | |
let (width,height) = (Int(size.width),Int(size.height)) | |
// setup an alpha-premultiplied 32 byte RGBA pixel bitmap | |
let bitsPerComponent = 8 | |
let components = 4 | |
let bitsPerPixel = bitsPerComponent * components | |
let bytesPerPixel = bitsPerPixel / 8 | |
let pixelsPerRow = width | |
let bytesPerRow = pixelsPerRow * bytesPerPixel | |
let colorSpace = CGColorSpaceCreateDeviceRGB() | |
let alphaInfo = CGImageAlphaInfo.PremultipliedLast | |
let bitmapInfoAlpha = CGBitmapInfo(rawValue:alphaInfo.rawValue) | |
let bitmapInfoBig32 = CGBitmapInfo.ByteOrder32Big | |
let bitmapInfo = bitmapInfoBig32.union(bitmapInfoAlpha) | |
let ctx = CGBitmapContextCreate(nil, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo.rawValue) | |
return ctx | |
} | |
/** | |
Returns a fresh `UIImage` backed by a bitmap with 32-bit RGBA-Premultiplied pixels. | |
- parameter image: image to copy | |
- returns: new copy of the image, with a guaranteed bitmap representation | |
*/ | |
private func RGBA32PremultipliedImageFromImage(image:UIImage) -> UIImage? | |
{ | |
let pixelSize = CGSize(width: Int(image.size.width * image.scale),height: Int(image.size.height * image.scale)) | |
let ctx = CreateRGBA32PremultipliedBitmapContext(pixelSize: pixelSize) | |
CGContextDrawImage(ctx, CGRect(origin: .zero, size: pixelSize), image.CGImage) | |
let newImage = CGBitmapContextCreateImage(ctx) | |
guard let newImage2 = newImage else { return nil } | |
return UIImage(CGImage: newImage2, scale: image.scale, orientation: image.imageOrientation) | |
} | |
private func unzip<T,U>(pairs:[(T,U)]) -> ([T],[U]) { | |
var ts:[T] = [] | |
var us:[U] = [] | |
for (t,u) in pairs { | |
ts.append(t) | |
us.append(u) | |
} | |
return (ts,us) | |
} | |
/** | |
Returns the color at a point in a UIImage, as long as that UIImage's underlying bitmap representation is 32-bit RGBA premultiplied pixels. | |
*/ | |
func colorAtPoint(point:CGPoint,inImage image:UIImage) -> UIColor | |
{ | |
let (x,y) = (Int(point.x),Int(point.y)) | |
let imageWidth = Int(image.size.width * image.scale) | |
let provider = CGImageGetDataProvider(image.CGImage) | |
let pixelData:NSData = CGDataProviderCopyData(provider)! | |
let pixelDataStart:UnsafePointer<Void> = pixelData.bytes | |
let pixelArray:UnsafePointer<PixelDataRGBA32Premultiplied> = unsafeBitCast(pixelDataStart, UnsafePointer<PixelDataRGBA32Premultiplied>.self) | |
let index = x + y * imageWidth | |
let pixel = pixelArray[index] | |
return pixel.color | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment