diff --git a/Examples/Pixel Data/main.js b/Examples/Pixel Data/main.js new file mode 100644 index 0000000..b253da9 --- /dev/null +++ b/Examples/Pixel Data/main.js @@ -0,0 +1,48 @@ +var layer = new Layer() +var pusheen = new Image({name:"pusheen"}) + +var start +var end + +var dimension = (400*400); + +//var start = new Date() +//pusheen.loadPixels() +//var end = new Date() +//console.log("Loading pixel array done! Took:"+ (end-start)); +//console.log(pusheen.pixels.length); + +start = new Date() +var pixels = pusheen.toPixels() +end = new Date() +console.log("Load done! Took:"+ (end-start)); + +start = new Date() +for (var i=0; i < dimension; i++) { +// var color = pusheen.pixels[i] + var color = pixels.colorAt(i) + + pixels.setColorAt(i,255-color[0],255-color[1],255-color[2]) +} +end = new Date() +console.log("Modifications done! Took:"+ (end-start)); + +start = new Date() +pusheen = pixels.toImage() +end = new Date() +console.log("Update done! Took:"+ (end-start)); + +layer.image = pusheen + +layer.position = new Point({ x: Layer.root.width * 0.5, y: Layer.root.height * 0.5 }) + + +//////////// + +//this.pixels = []; +//for (var rgb in this.pixelNumbers) { +//var red = (rgb >> 16) & 0xFF; +//var green = (rgb >> 8) & 0xFF; +//var blue = rgb & 0xFF; +//this.pixels.push([red,green,blue]); +//} \ No newline at end of file diff --git a/Examples/Pixel Data/pusheen.png b/Examples/Pixel Data/pusheen.png new file mode 100644 index 0000000..1350dbf Binary files /dev/null and b/Examples/Pixel Data/pusheen.png differ diff --git a/Prototope.xcodeproj/xcshareddata/xcschemes/Protoscope.xcscheme b/Prototope.xcodeproj/xcshareddata/xcschemes/Protoscope.xcscheme index eea9e6b..258d5fc 100644 --- a/Prototope.xcodeproj/xcshareddata/xcschemes/Protoscope.xcscheme +++ b/Prototope.xcodeproj/xcshareddata/xcschemes/Protoscope.xcscheme @@ -68,7 +68,7 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" - buildConfiguration = "Debug" + buildConfiguration = "Release" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" allowLocationSimulation = "YES"> diff --git a/Prototope/Color.swift b/Prototope/Color.swift index 4a4ea6a..3e3adfb 100644 --- a/Prototope/Color.swift +++ b/Prototope/Color.swift @@ -21,6 +21,11 @@ public struct Color { public init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) { uiColor = UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha)) } + + /** Constructs a color from RGB and alpha values. Arguments range from 0.0 to 1.0. */ + public init(red: Float, green: Float, blue: Float, alpha: Float = 1.0) { + uiColor = UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha)) + } /** Constructs a grayscale color. Arguments range from 0.0 to 1.0. */ public init(white: Double, alpha: Double = 1.0) { @@ -53,6 +58,34 @@ public struct Color { init(_ uiColor: UIColor) { self.uiColor = uiColor } + + /** Returns the R, G, B and A values of the Color. */ + public func getRGBAValues() -> (red: Float, green: Float, blue: Float, alpha: Float) { + let colorSpace = CGColorGetColorSpace(self.uiColor.CGColor); + let colorSpaceModel = CGColorSpaceGetModel(colorSpace); + + var r = 0 as CGFloat + var g = 0 as CGFloat + var b = 0 as CGFloat + var a = 0 as CGFloat + + switch colorSpaceModel.value { + + case kCGColorSpaceModelMonochrome.value: + var w = 0 as CGFloat + self.uiColor.getWhite(&w, alpha: &a) + r = w + g = w + b = w + case kCGColorSpaceModelRGB.value: + self.uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + + default: + fatalError("unsupported color space for color: \(self)") + } + + return (Float(r), Float(g), Float(b), Float(a)) + } public static var black: Color { return Color(UIColor.blackColor()) } public static var darkGray: Color { return Color(UIColor.darkGrayColor()) } diff --git a/Prototope/Image.swift b/Prototope/Image.swift index 67f38b8..56f7f2a 100644 --- a/Prototope/Image.swift +++ b/Prototope/Image.swift @@ -34,4 +34,217 @@ public struct Image { init(_ image: UIImage) { uiImage = image } + + public func toPixels() -> PixelBitmap { + return PixelBitmap(image: self) + } } + +/** A representation of an image bitmap that allows pixel data access */ +public struct PixelBitmap : MutableCollectionType { + typealias Index = Int + + public var pixelWidth: Int { return width } + public var pixelHeight: Int { return height } + + private let width: Int + private let height: Int + + public let startIndex: Int = 0 + public var endIndex: Int { return height * width } + + private let scale: Double + + public var data: UnsafeMutablePointer + + private let dataDestroyer: PixelDataDestroyer + + init(image: Image) { + scale = Double(image.uiImage.scale) + width = Int(image.size.width * scale) + height = Int(image.size.height * scale) + + data = UnsafeMutablePointer.alloc(width * height) + dataDestroyer = PixelDataDestroyer(data: data, width: width, height: height) + + let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue) + let bytesPerRow = width * 4 + + let context = CGBitmapContextCreate(data, width, height, 8, bytesPerRow, colorSpace, bitmapInfo) + + let cgImageSize = CGSize(width: CGFloat(width), height: CGFloat(height)) + CGContextDrawImage(context, CGRect(origin: CGPointZero, size: cgImageSize), image.uiImage.CGImage) + } + + public subscript (position: Int) -> Pixel { + get { + return data[position] + } + set { + data[position] = newValue + } + } + + /** Returns the pixel data at a given position. */ + public func pixelAt(#position: Int) -> Pixel? { + if position < height * width { + return self[position] + } else { + return nil + } + } + + /** Sets the pixel data at a given position. */ + public mutating func setPixelAt(#position: Int, value: Pixel) { + self[position] = value + } + + public subscript (#row: Int, #column: Int) -> Pixel { + get { + let idx = indexOf(row: row, column: column) + return self[idx] + } + set { + let idx = indexOf(row: row, column: column) + self[idx] = newValue + } + } + + /** Returns the pixel data at a given row and column. */ + public func pixelAt(#row: Int, column: Int) -> Pixel? { + if row < pixelHeight && column < pixelWidth { + return self[row: row, column: column] + } else { + return nil + } + } + + /** Sets the pixel data at a given row and column. */ + public mutating func setPixelAt(#row: Int, column: Int, value: Pixel) { + self[row: row, column: column] = value + } + + public func generate() -> IndexingGenerator { + return IndexingGenerator(self) + } + + /** Returns the Image representation of the PixelBitmap. */ + public func toImage() -> Image { + let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue) + let context = CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpace, bitmapInfo) + let cgImage = CGBitmapContextCreateImage(context) + + let image = UIImage(CGImage: cgImage, scale: CGFloat(scale), orientation: .Up)! + + return Image(image) + } + + /** Constructs a new PixelBitmap by applying the transform on each pixel the original. */ + public func map(transform: Pixel -> Pixel) -> PixelBitmap { + var newBitmap = self + for idx in startIndex.. Pixel) -> PixelBitmap { + var newBitmap = self + for idx in startIndex.. Pixel) -> PixelBitmap { + var newBitmap = self + for idx in startIndex.. Int { + return row * width + column + } + + private class PixelDataDestroyer { + let pixelDataToCleanUp: UnsafeMutablePointer + let width: Int + let height: Int + + init(data: UnsafeMutablePointer, width: Int, height: Int) { + self.pixelDataToCleanUp = data + self.width = width + self.height = height + } + + deinit { + pixelDataToCleanUp.dealloc(width*height) + } + } +} + + +/** A representation of a single pixel in an RGBA bitmap image. */ +public struct Pixel { + public var redRaw: UInt8 + public var greenRaw: UInt8 + public var blueRaw: UInt8 + public var alphaRaw: UInt8 + + public init(_ redRaw: Int, _ greenRaw: Int, _ blueRaw: Int, _ alphaRaw: Int) { + self.redRaw = UInt8(redRaw) + self.greenRaw = UInt8(greenRaw) + self.blueRaw = UInt8(blueRaw) + self.alphaRaw = UInt8(alphaRaw) + } + + public init(_ red: Float, _ green: Float, _ blue: Float, _ alpha: Float) { + self.redRaw = UInt8(min(1, max(0, red))*255) + self.greenRaw = UInt8(min(1, max(0, green))*255) + self.blueRaw = UInt8(min(1, max(0, blue))*255) + self.alphaRaw = UInt8(min(1, max(0, alpha))*255) + } + + public var red: Float { + get { return Float(redRaw)/255 } + set { self.redRaw = UInt8(min(1, max(0, newValue))*255) } + } + + public var green: Float { + get { return Float(greenRaw)/255 } + set { self.greenRaw = UInt8(min(1, max(0, newValue))*255) } + } + + public var blue: Float { + get { return Float(blueRaw)/255 } + set { self.blueRaw = UInt8(min(1, max(0, newValue))*255) } + } + + public var alpha: Float { + get { return Float(alphaRaw)/255 } + set { self.alphaRaw = UInt8(min(1, max(0, newValue))*255) } + } + + public var color: Color { + get { return Color(red: red, green: green, blue: blue, alpha: alpha) } + set { + let (r, g, b, a) = newValue.getRGBAValues() + self.red = Float(r) + self.green = Float(g) + self.blue = Float(b) + self.alpha = Float(a) + } + } +} + diff --git a/PrototopeJSBridge/Context.swift b/PrototopeJSBridge/Context.swift index 2bd7791..b0370dc 100644 --- a/PrototopeJSBridge/Context.swift +++ b/PrototopeJSBridge/Context.swift @@ -82,5 +82,7 @@ public class Context { TextAlignmentBridge.addToContext(context) CameraLayerBridge.addToContext(context) CameraPositionBridge.addToContext(context) + PixelBitmapBridge.addToContext(context) + PixelBridge.addToContext(context) } } \ No newline at end of file diff --git a/PrototopeJSBridge/ImageBridge.swift b/PrototopeJSBridge/ImageBridge.swift index 7172958..5060bb5 100644 --- a/PrototopeJSBridge/ImageBridge.swift +++ b/PrototopeJSBridge/ImageBridge.swift @@ -12,11 +12,20 @@ import JavaScriptCore @objc public protocol ImageJSExport: JSExport { init?(args: NSDictionary) + +// var pixels: JSValue? { get set } + + func toPixels() -> PixelBitmapJSExport + func loadPixels() + func updatePixels() } @objc public class ImageBridge: NSObject, ImageJSExport, BridgeType { var image: Image! - + +// public var pixels: JSValue? + private var loadedBitmap: PixelBitmap? + public class func addToContext(context: JSContext) { context.setObject(self, forKeyedSubscript: "Image") } @@ -35,4 +44,249 @@ import JavaScriptCore self.image = image super.init() } + + public func toPixels() -> PixelBitmapJSExport { + let pixelBitmap = image.toPixels() + return PixelBitmapBridge(pixelBitmap) + } + + public func loadPixels() { + loadPixelsArr() + } + + public func loadPixelsArr() { + let pixelBitmap = image.toPixels() + loadedBitmap = pixelBitmap + + var array = [[Int]]() + array.reserveCapacity(pixelBitmap.endIndex) + + for pixel in pixelBitmap { + array.append([Int(pixel.redRaw), Int(pixel.greenRaw), Int(pixel.blueRaw)]) + } + + println("started") + let before = CACurrentMediaTime() + let jsPixels = JSValue(object: array, inContext: JSContext.currentContext()) + JSContext.currentThis().setObject(jsPixels, forKeyedSubscript: "pixels") + let after = CACurrentMediaTime() + println("took \(after-before)s") + + } + + public func updatePixels() { + let bridgedArray = JSContext.currentThis().objectForKeyedSubscript("pixels")!.toArray()! + var idx = 0 + for pixelArray in bridgedArray { + loadedBitmap![idx] = Pixel(pixelArray[0] as! Int, pixelArray[1] as! Int, pixelArray[2] as! Int, Int(loadedBitmap![idx].alphaRaw)) + idx++ + } + self.image = loadedBitmap?.toImage() + } +} + +@objc public protocol PixelBitmapJSExport: JSExport { + var pixelWidth: Int { get } + var pixelHeight: Int { get } + + func toImage() -> ImageJSExport + + func pixelAt(args: NSDictionary) -> PixelJSExport? + func setPixelAt(args: NSDictionary) + + func map(args: JSValue) -> PixelBitmapJSExport + + func colorAt(position: Int) -> JSValue + func colorAt(row: Int, _ column: Int) -> JSValue + func setColorAt(position: Int, _ red: Int, _ green: Int, _ blue: Int) + func setColorAt(row: Int, _ column: Int, _ red: Int, _ green: Int, _ blue: Int) } + +@objc public class PixelBitmapBridge: NSObject, PixelBitmapJSExport, BridgeType { + var pixelBitmap: PixelBitmap! + + public class func addToContext(context: JSContext) { + context.setObject(self, forKeyedSubscript: "PixelBitmap") + } + + init(_ pixelBitmap: PixelBitmap) { + self.pixelBitmap = pixelBitmap + super.init() + } + + public var pixelWidth: Int { + return pixelBitmap.pixelWidth + } + + public var pixelHeight: Int { + return pixelBitmap.pixelHeight + } + + public func toImage() -> ImageJSExport { + let image = pixelBitmap.toImage() + return ImageBridge(image) + } + + public func setColorAt(position: Int, _ red: Int, _ green: Int, _ blue: Int) { + let pixel = Pixel(red, green, blue, Int(self.pixelBitmap[position].alphaRaw)) + self.pixelBitmap.setPixelAt(position: position, value: pixel) + } + + public func setColorAt(row: Int, _ column: Int, _ red: Int, _ green: Int, _ blue: Int) { + let pixel = Pixel(red, green, blue, Int(self.pixelBitmap[row: row, column: column].alphaRaw)) + self.pixelBitmap.setPixelAt(row: row, column: column, value: pixel) + } + + public func colorAt(position: Int) -> JSValue { + let pixel = self.pixelBitmap.pixelAt(position: position)! + return JSValue(object: [Int(pixel.redRaw), Int(pixel.greenRaw), Int(pixel.blueRaw)], inContext: JSContext.currentContext()) + } + + public func colorAt(row: Int, _ column: Int) -> JSValue { + let pixel = self.pixelBitmap.pixelAt(row: row, column: row)! + return JSValue(object: [Int(pixel.redRaw), Int(pixel.greenRaw), Int(pixel.blueRaw)], inContext: JSContext.currentContext()) + } + + public func pixelAt(args: NSDictionary) -> PixelJSExport? { + var pixel: Pixel? + + if let position = (args["position"] as! Int?) { + pixel = self.pixelBitmap.pixelAt(position: position) + } else if let row = (args["row"] as! Int?), let column = (args["column"] as! Int?) { + pixel = self.pixelBitmap.pixelAt(row: row, column: column) + } else { + Environment.currentEnvironment?.exceptionHandler("Invalid parameters passed into Pixel.pixelAt") + } + + if let pixel = pixel { + return PixelBridge(pixel) + } else { + return nil + } + } + + public func setPixelAt(args: NSDictionary) { + if let position = (args["position"] as! Int?), + let value = (args["value"] as! PixelBridge?) { + self.pixelBitmap.setPixelAt(position: position, value: value.pixel) + } else if let row = (args["row"] as! Int?), + let column = (args["column"] as! Int?), + let value = (args["value"] as! PixelBridge?) { + self.pixelBitmap.setPixelAt(row: row, column: column, value: value.pixel) + } else { + Environment.currentEnvironment?.exceptionHandler("Invalid parameters passed into Pixel.setPixelAt") + } + + } + + public func map(args: JSValue) -> PixelBitmapJSExport { + let transformValue = args.objectForKeyedSubscript("transform") + let numberOfArguments = transformValue.objectForKeyedSubscript("length").toInt32() + + let result: PixelBitmap + + func safeTryCallingTransformFunction(transformValue: JSValue, withArgs args: [AnyObject], fallback fallbackBridgedPixel: PixelBridge) -> PixelBridge! { + if let bridgedOutput = (transformValue.callWithArguments(args).toObject() as? PixelBridge) { + return bridgedOutput + } else { + Environment.currentEnvironment?.exceptionHandler("PixelBridge's map transform function didn't return a transformed pixel") + // Crashing here for now + return nil + } + } + + switch numberOfArguments { + case 1: + result = pixelBitmap.map { (input: Pixel) -> Pixel in + let bridgedInput = PixelBridge(input) + let bridgedOutput = safeTryCallingTransformFunction(transformValue, withArgs: [bridgedInput], fallback: bridgedInput) + return bridgedOutput.pixel + } + case 2: + result = pixelBitmap.map { (position:Int, input: Pixel) -> Pixel in + let bridgedInput = PixelBridge(input) + let bridgedOutput = safeTryCallingTransformFunction(transformValue, withArgs: [position, bridgedInput], fallback: bridgedInput) + return bridgedOutput.pixel + } + case 3: + result = pixelBitmap.map { (row: Int, column: Int, input: Pixel) -> Pixel in + let bridgedInput = PixelBridge(input) + let bridgedOutput = safeTryCallingTransformFunction(transformValue, withArgs: [row, column, bridgedInput], fallback: bridgedInput) + return bridgedOutput.pixel + } + default: + Environment.currentEnvironment?.exceptionHandler("PixelBitmap's map called with an invalid transform parameter") + result = self.pixelBitmap + } + + return PixelBitmapBridge(result) + } +} + +@objc public protocol PixelJSExport: JSExport { + var red: Float { get set } + var green: Float { get set } + var blue: Float { get set } + var alpha: Float { get set } + + var color: ColorJSExport { get set } +} + +@objc public class PixelBridge: NSObject, PixelJSExport, BridgeType { + var pixel: Pixel! + + public class func addToContext(context: JSContext) { + context.setObject(self, forKeyedSubscript: "Pixel") + } + + init(_ pixel: Pixel) { + self.pixel = pixel + super.init() + } + + public var red: Float { + get { + return pixel.red + } + set { + pixel.red = newValue + } + } + + public var green: Float { + get { + return pixel.green + } + set { + pixel.green = newValue + } + } + + public var blue: Float { + get { + return pixel.blue + } + set { + pixel.blue = newValue + } + } + + public var alpha: Float { + get { + return pixel.alpha + } + set { + pixel.alpha = newValue + } + } + + public var color: ColorJSExport { + get { + return ColorBridge(pixel.color) + } + set { + pixel.color = (newValue as JSExport as! ColorBridge).color + } + } +} + diff --git a/PrototopeTestApp/ViewController.swift b/PrototopeTestApp/ViewController.swift index 1488a9c..06dc5af 100644 --- a/PrototopeTestApp/ViewController.swift +++ b/PrototopeTestApp/ViewController.swift @@ -20,10 +20,10 @@ class ViewController: UIViewController { Environment.currentEnvironment = Environment.defaultEnvironmentWithRootView(view) // You might write a prototype in Swift... - //runSwiftPrototype() + runSwiftPrototype() // ... or in JavaScript. (uncomment one; comment out the other) - runJSPrototope() +// runJSPrototope() } func runSwiftPrototype() { @@ -34,7 +34,40 @@ class ViewController: UIViewController { func makeRedLayer(name: String, y: Double) -> Layer { let redLayer = Layer(parent: Layer.root, name: name) - redLayer.image = Image(name: "paint") + + let redImage = Image(name: "paint") + let pixels = redImage.toPixels() + + let width = redImage.size.width + + var modified = pixels + + // White-out every pixel in even rows and columns + modified = modified.map { row, column, pixel in + var newPixel = pixel + newPixel.red = (row % 2 == 0) || (column % 2 == 0) ? 1.0 : newPixel.red + newPixel.green = (row % 2 == 0) || (column % 2 == 0) ? 1.0 : newPixel.green + newPixel.blue = (row % 2 == 0) || (column % 2 == 0) ? 1.0 : newPixel.blue + return newPixel + } + + // Make every 7th pixel blue + modified = modified.map { idx, pixel in + var newPixel = pixel + newPixel.blue = idx % 7 == 0 ? 1.0 : pixel.blue + return newPixel + } + + // Invert all pixels + modified = modified.map { (pixel: Pixel) -> Pixel in + var newPixel = pixel + newPixel.red = 1.0 - pixel.red + newPixel.green = 1.0 - pixel.green + newPixel.blue = 1.0 - pixel.blue + return newPixel + } + + redLayer.image = modified.toImage() tunable(50, name: "x") { value in redLayer.frame.origin = Point(x: value, y: y) } redLayer.backgroundColor = Color.red redLayer.cornerRadius = 10