Sunday, March 8, 2015

Code Jam : Swift : Gradient and Graphics

Today I am going to enter graphics in swift, or at least the basics of it. 
In a simple view controller, ( I assume you know how to create a view controller by now, if not do refer to my previous blogs), Create a view. A view is nothing but a sub window inside a view controller. It is a subclass of UIView, and can be used to draw elements on screen. 

This week, I am going to create a bezier path with transformations, and color it with gradient color. This would be the simplest of all the graphics we can do in swift, but this is where it starts. 

In viewController.swift (or the file name you have for the main view controller), create a new class called MyView (or any other name you prefer), and subclass it to UIView.

class MyView : UIView{

Override the function drawRect().

override func drawRect(rect: CGRect) {

1. Transformations : I assume the readers know about graphics transformations. If not here are few good pages on it (http://www.cs.uic.edu/~jbell/CourseNotes/ComputerGraphics/2DTransforms.html and http://www.cs.utexas.edu/~fussell/courses/cs384g-fall2010/lectures/lecture07-Affine.pdf)
In this week, I am going to add rotation, and translation to the bezier curve. Rotation is created by Core Graphics function  CGAffineTransformMakeRotation.  cThis function creates a rotation matrix from unit matrix. The argument is angle in radians.CGAffineTransformMakeRotation is one of the methods to rotate objects on screen. The other way is to directly use CGContextRotateCTM, which rotates within a context. For this week, we will see affine transformation matrix. Eventually I will end up using context to append the transformation matrices.

 var rot = CGAffineTransformMakeRotation(CGFloat(M_PI/ 4)

The angle of rotation is 45 degrees ( Pi/4).  Then I translate the object to bring it inside the view. 


var trans = CGAffineTransformMakeTranslation(100, -150)

This creates another matrix that would translate the object by 100 units along x axis and -150 units along y axis. Since iOS coordinate system origin is at top left corner of screen, this would move the object towards right and up.

Next I am saving the current context, this is to make sure, we can restore back to the previous context after our customized changes to the current transformation matrix (CTM)

 let context = UIGraphicsGetCurrentContext()
 CGContextSaveGState(context)
UIGraphicsGetCurrentContext This function returns back a  CGContextRef of the current context ( https://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html ) being used. Current context is nil by default. the UIView object (MyView class object in our case) pushes a valid context into the stack, making it current, to draw objects, and this happens just before calling drawRect() method. In case you are not using UIView object, then you must manually push context using UIGraphicsPushContext method. 

 CGContextConcatCTM(context, rot)
 CGContextConcatCTM(context, trans)

These lines concatenates rotation matrix, and translation matrix to the existing matrix of the context. The order is important. These lines would first rotate the object then translate. 

2. Bezier Curve : Next is to draw a bezier curve (  http://web.iitd.ac.in/~hegde/cad/lecture/L13_Bezi ercurve.pdf ) . To create the curve programmatically. I am creating a rectangle with a circle at its bottom to look similar to a pendulum but not exactly one. This would include two bezier paths inside one. 

First create the rectangle path. 
  var bezierPath = UIBezierPath();
  let r = CGRectInset(rect, rect.size.width * 0.475, rect.size.height * 0.05)
  var rpath = UIBezierPath(roundedRect: r, cornerRadius: 60.0)
      
Create a new UIBezierPath, then create a rounded rectangle by using CGRectInset with rectangle as the view’s rectangle itself, and the roundness to reduce the width by a lot, but height by a small fraction, so that a long slender rectangle is created. Then create a bezier path using the provided easy API UIBezierPath instead of creating your own control points using  addLineToPoint or addCurveToPoint functions. 

Then create another bezier path that would create a circle. 
let newrect = CGRectMake(20, 105, 250  , 250)
let h = CGRectInset(newrect, newrect.size.width * 0.3, newrect.size.width * 0.3)
var headpath = UIBezierPath(ovalInRect: h);
let rad = 20.0
        
This creates another rectangle that would position the circle at the bottom part of the rectangle that I already created. CGRectInset is used to create circle with this rectangle. UIBezierPath is used to create bezier path. using ovalInRect parameter. 

 rpath.appendPath(headpath)
 bezierPath.appendPath(rpath);
 bezierPath.addClip()

Add headpath to rpath ( circle to rectangle), then append that combined path to the main empty bezierPath. Then do addClip(). This method intersects the shape with current clipping region of the graphics context. This method is important to create the gradient color filling to the path. 

3. Gradient filling:

 let colorspace = CGColorSpaceCreateDeviceRGB()
 let g1 = UIColor(red: 0.5, green: 0.2, blue: 0.8, alpha: 1)
 let g2 = UIColor(red: 1, green: 0.6, blue: 0.2, alpha: 1
 let gc : CFArray = [g1.CGColor, g2.CGColor, UIColor.blueColor().CGColor, UIColor.redColor().CGColor]
 let gl : [CGFloat] = [0, 0.3,0.5,1]
 let gra = CGGradientCreateWithColors(colorspace, gc, gl)
       
 CGContextDrawLinearGradient(context, gra, CGPointMake(self.bounds.width/2, self.bounds.height - 20), CGPointMake(self.bounds.width / 2, 20), 0)

First create device dependent RGB color space by using CGColorSpaceCreateDeviceRGB . This is important to set the colors for the gradient. 
Create two colors g1 and g2, and setup 4 colors for the gradient to change from. Then create color array using 4 colors (g1, g2, blue, red). Create coordinates array, which specifies where the color should change. This is again an array of 4 elements. 
Then create gradient with colors and coordinates. using CGGradientCreateWithColors . Then draw gradient colors. Since we already clipped using addClip, the gradient colors will be applied to the bezier path. 

 CGContextDrawLinearGradient(context, gra, CGPointMake(self.bounds.width/2, self.bounds.height - 20), CGPointMake(self.bounds.width / 2, 20), 0)
       
 CGContextRestoreGState(context)

CGContextDrawLinearGradient is used to draw the gradient colors to the bezier shapes.  The third and fourth parameters are CGPoints that define the coordinates for starting and ending points.  CGContextRestoreGState is used to restore the context that was used before we saved it and changed it. 
For a simple project like this one, saving and restoring might not be necessary. But it is required for complex ones with multiple objects to be rendered on the screen. 

The final rendered view looks like this :

The viewController.swift code :


import UIKit

class MyView : UIView{
   
override func drawRect(rect: CGRect) {
       
       
       
       
// 1. transformations
        var rot = CGAffineTransformMakeRotation(CGFloat(M_PI) / 4)
       
var trans = CGAffineTransformMakeTranslation(100, -150)
       
       
let context = UIGraphicsGetCurrentContext()
       
CGContextSaveGState(context)
       
CGContextConcatCTM(context, rot)
       
CGContextConcatCTM(context, trans)
       
   
       
//bezier path
        var bezierPath = UIBezierPath();
       
       
let r = CGRectInset(rect, rect.size.width * 0.475, rect.size.height * 0.05)
       
var rpath = UIBezierPath(roundedRect: r, cornerRadius: 60.0)
      
       
       
let newrect = CGRectMake(20, 105, 250  , 250)
       
let h = CGRectInset(newrect, newrect.size.width * 0.3, newrect.size.width * 0.3)
       
var headpath = UIBezierPath(ovalInRect: h);
       
let rad = 20.0
     
        rpath.
appendPath(headpath)
   
       
        bezierPath.
appendPath(rpath);
        bezierPath.
addClip()
      
       
//gradient
        let colorspace = CGColorSpaceCreateDeviceRGB()
       
let g1 = UIColor(red: 0.5, green: 0.2, blue: 0.8, alpha: 1)
       
let g2 = UIColor(red: 1, green: 0.6, blue: 0.2, alpha: 1)
       
let gc : CFArray = [g1.CGColor, g2.CGColor, UIColor.blueColor().CGColor, UIColor.redColor().CGColor]
       
let gl : [CGFloat] = [0, 0.3,0.5,1]
       
       
let gra = CGGradientCreateWithColors(colorspace, gc, gl)
       
       
CGContextDrawLinearGradient(context, gra, CGPointMake(self.bounds.width/2, self.bounds.height - 20), CGPointMake(self.bounds.width / 2, 20), 0)
       
       
CGContextRestoreGState(context)
    
    }
}


class MyViewController: UIViewController{
  
   
   
let myview = MyView(frame: CGRectMake( 10, 100, 300, 300))
   
@IBOutlet var colorLabel: UILabel!
   
override func viewDidLoad() {
       
super.viewDidLoad()
       
// Do any additional setup after loading the view, typically from a nib.
        view.addSubview(myview);
    }

   
override func didReceiveMemoryWarning() {
       
super.didReceiveMemoryWarning()
       
// Dispose of any resources that can be recreated.
    }
}




Error & Solution : If you get a blank screen instead of the main view controller, and an error in the debug window stating "perhaps the designated entry point is not set?”, then it means there is no initial view controller for the story board. Select the storyboard->then the navigation controller or the view controller that you want as the first screen -> attributes inspector -> check the “Set as initial view controller” option. It should be enabled.

Code Jam for the week of Mar 1 2015

No comments:

Post a Comment