Skip to content

Instantly share code, notes, and snippets.

@algal
Last active November 2, 2015 22:23
Show Gist options
  • Save algal/26474d5cdd48edb1669c to your computer and use it in GitHub Desktop.
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
/// 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