Expression Software Blog

3/21/15

iOS Scene Kit Jet

The Scene Kit 3D Jet in flight with clockwise roll, in iPad airspace.
Trailing transparent snapshots are 3D model clones of the jet at variable intervals.
Use SCNView allowsCameraControl to zoom and rotate the entire scene, including models, for different points of view.

* The 3D Jet ship mesh is part of the default Xcode Scene Kit project.





@import SceneKit;
@import GLKit;


@interface ESJet: NSObject

    @property (nonatomic) SCNNode * node;
    @property (nonatomic, assign) CGFloat velocity;
    @property (nonatomic, assign) BOOL roll;
    @property (nonatomic, assign) CGFloat rollRate;


    -(void)update:(NSTimeInterval)aTime isVisible:(BOOL)aVisible;

    -(instancetype)init;
    -(instancetype)initWithNode:(SCNNode *)aNode zPosition:(int)aZPosition;
@end


@implementation ESJet

    -(void)update:(NSTimeInterval)aTime isVisible:(BOOL)aVisible;
    {
        CGFloat nextZPosition = self.velocity;

     // NSLog(@"jet position: %@", NSStringFromGLKVector3(SCNVector3ToGLKVector3(self.node.position)));

        if (aVisible)
        {
            NSLog(@"jet visible");
        }
        else
        {
            //when jet flys off right-side of screen, reposition on left
            NSLog(@"...");
            nextZPosition = -(self.node.position.x * 1.95);
        }

        GLKVector3 nextFramePosition = GLKVector3Make(0, 0, nextZPosition);
        GLKMatrix4 transform = SCNMatrix4ToGLKMatrix4(self.node.transform);
        self.node.transform = SCNMatrix4FromGLKMatrix4(GLKMatrix4TranslateWithVector3(transform, nextFramePosition));


        if (self.roll)
        {
            //rotate jet ~ 1 degree per frame on X axis
            GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis((M_1_PI * self.rollRate), 1, 0, 0);
            GLKQuaternion orientation = GLKQuaternionMultiply(rot, GLKQuaternionMake(self.node.orientation.x,
                                                                                     self.node.orientation.y,
                                                                                     self.node.orientation.z,
                                                                                     self.node.orientation.w));

            self.node.orientation = SCNVector4Make(orientation.x, orientation.y, orientation.z, orientation.w);
        }
    }

    -(instancetype)init
    {
        if (self = [super init])
        {
            self.velocity = 0.33f;
            self.rollRate = (self.velocity * 0.364);
        }

        return self;
    }

    -(instancetype)initWithNode:(SCNNode *)aNode zPosition:(int)aZPosition
    {
        if ([self init])
        {
            self.node = aNode;
            [self.node removeAllActions];

            //rotate jet 90 degrees on Y axis, facing right edge of screen
            GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis(M_PI_2, 0, 1, 0);
            self.node.orientation = SCNVector4Make(rot.x, rot.y, rot.z, rot.w);
            self.node.position = SCNVector3Make(0, 0, aZPosition);
        }

        return self;
    }
@end


@interface GameViewController: UIViewController<SCNSceneRendererDelegate>
@end

@implementation GameViewController
{
    SCNScene * skScene;
    SCNView * skView;
    SCNNode * cameraNode;

    NSMutableArray * nodeSnapshots;
    NSDate * frameTimer;

    ESJet * jet;
}

    -(void)viewDidLoad
    {
        //default project code here...

        skScene = scene;
        skView = scnView;
        skView.delegate = self;
        skView.playing = YES;

        nodeSnapshots = [NSMutableArray array];

        jet = [[ESJet alloc] initWithNode:ship zPosition:-20];
        jet.roll = YES;
    }

    //SCNSceneRendererDelegate
    -(void)renderer:(id<SCNSceneRenderer>)aRenderer updateAtTime:(NSTimeInterval)time
    {
                void (^cloneNode)() = ^
                { 
                    dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
                    dispatch_async(q, ^
                                   {
                                       SCNNode * clone = [jet.node clone];
                                       clone.opacity = 0.20f;
                                       [nodeSnapshots addObject:clone];
                                       [skScene.rootNode addChildNode:clone];
                                   });
                };

                void (^cloneNodeOnTimer)(float) = ^(float interval)
                {
                    if ((frameTimer == nil) || (frameTimer.timeIntervalSinceNow <= -interval))
                    {
                        frameTimer = [NSDate date];
                        cloneNode();
                    }
                };


        //SCNSceneRenderer protocol
        BOOL nodeIsVisible = [aRenderer isNodeInsideFrustum:jet.node withPointOfView:cameraNode];

        if (nodeIsVisible)
        {
            cloneNodeOnTimer(0.6f);
        }
        else
        {
            skView.playing = NO;
            jet.node.hidden = YES;

            if (nodeSnapshots.count > 0)
            {
                //remove transparency from last cloned node
                ((SCNNode *)[nodeSnapshots lastObject]).opacity = 1.0;
            }
        }


        [jet update:time isVisible:nodeIsVisible];
    }

    //tap any node to render new scene
    -(void)handleTap:(UIGestureRecognizer*)gestureRecognize
    {
        CGPoint tp = [gestureRecognize locationInView:skView];

        NSArray * hitTestResults = [skView hitTest:tp options:nil];
        if (hitTestResults.count > 0)
        {
            [nodeSnapshots enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop)
            {
                [(SCNNode *)obj removeFromParentNode];
            }];

            [nodeSnapshots removeAllObjects];

            jet.node.hidden = NO;
            skView.playing = YES;
        }
    }

3/15/15

iOS Gradient View with Clipped Path

Custom gradient view with support for 3 shapes: default, circle, circle-outline.
Copy path with CGPathCreateCopyByStrokingPath, clip, then draw gradient.




//swift

import UIKit

class ViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()

        var gv = [ESGradientView]()
        var x = 100
        for i in 0...5
        {
            var g = ESGradientView()
            g.frame = CGRect(x:x, y:200, width:100, height:100)
            g.gradientColors = i < 3 ? g.gradientColors : [UIColor.redColor().CGColor, UIColor.blackColor().CGColor]
            gv.append(g)
            self.view.addSubview(g)
            x += 110
        }

        gv[1].shape = .Circle
        gv[2].shape = .CircleOutline
        gv[4].shape = .Circle
        gv[5].shape = .CircleOutline
        gv[5].circleOutlineWidth = 1
    }
}


class ESGradientView: UIView
{

    enum Shape
    {
        case Default, Circle, CircleOutline
    }

    var shape = Shape.Default
    var circleOutlineWidth:Int = 4
    var gradientColors = [UIColor.redColor().CGColor, UIColor.blueColor().CGColor]
    var gradientLocations:[CGFloat] = [0, 1]
    var gradientEndOffset:CGFloat = 20


    override func drawRect(rect: CGRect)
    {
        var context = UIGraphicsGetCurrentContext()

        if (shape == .Circle)
        {
            let path = getCirclePath(1)
            path.addClip()
        }
        else if (shape == .CircleOutline)
        {
            let path = getCirclePath(circleOutlineWidth + 2)
            var pathRef = CGPathCreateCopyByStrokingPath(path.CGPath, nil, CGFloat(circleOutlineWidth), kCGLineCapRound, kCGLineJoinRound, 1)
            UIBezierPath(CGPath: pathRef).addClip()
        }

        var gradient = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), gradientColors, gradientLocations)
        let gradientEndPoint = CGPointMake(bounds.width - gradientEndOffset, bounds.height - gradientEndOffset)
        CGContextDrawLinearGradient(context, gradient!, CGPointZero, gradientEndPoint, UInt32(kCGGradientDrawsAfterEndLocation))
    }   

    func getCirclePath(inset:Int) -> UIBezierPath
    {
        let center = CGFloat(bounds.width * 0.5)
        let radius = CGFloat(center * CGFloat(100 - inset) * 0.01)
        let centerPoint = CGPointMake(center, center)
        var cp = UIBezierPath(arcCenter:centerPoint, radius:radius, startAngle:0, endAngle:CGFloat(M_PI * 2), clockwise:true)

        return cp
    }
}

3/7/15

We Can Has WWDC Session Videoz Descriptions?

Javascript to show all WWDC session video descriptions.
Expands sessions to show descriptions and view search matches. *
To use, copy and paste script into browser console and then call show() function.

var sessions = $$('li.session')

function show()
{
    toggle('session active');
}

function hide()
{
    toggle('session');
}

function toggle(css)
{
    for (var i = 0; i < sessions.length; i++)
    {
        sessions[i].className = css;
    }
}

WWDC
2014
2013
2012
2011
2010


* Usability clue for Apple Web Dev Team
"We can't wait to show you."

2/28/15

iOS Draw Along Bezier Quad Curve Path


//swift

var bp = UIBezierPath()
let startPoint = CGPointMake(200, 400)
let endPoint = CGPointMake(500, 400)
let controlPoint = CGPointMake(350, 150)
bp.moveToPoint(startPoint)
bp.addQuadCurveToPoint(endPoint, controlPoint:controlPoint)

var vp = CAShapeLayer()  //debug visual path
vp.path = bp.CGPath
vp.fillColor = UIColor.clearColor().CGColor
vp.strokeColor = UIColor.darkGrayColor().CGColor
vp.lineWidth = 1.0
self.view.layer.addSublayer(vp)

    //http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_B.C3.A9zier_curves
    func getBezierQuadXY(t:CGFloat, p0:CGFloat, p1:CGFloat, p2:CGFloat) -> CGFloat 
    {
        return ((1-t) * (1-t)) * p0 + 2 * (1-t) * t * p1 + t * t * p2
    }

    func addNode(pathPercent:CGFloat)
    {
        var node = CALayer()
        node.frame = CGRectMake(0, 0, 20, 20)
        node.cornerRadius = 10.0
        node.borderColor = UIColor.darkGrayColor().CGColor
        node.borderWidth = 1.0
        let p = CGPointMake(getBezierQuadXY(pathPercent, startPoint.x, controlPoint.x, endPoint.x),
                            getBezierQuadXY(pathPercent, startPoint.y, controlPoint.y, endPoint.y))
        node.position = p
        self.view.layer.addSublayer(node)
    }

addNode(0.5)
addNode(0.7)
addNode(0.8)
addNode(0.89)
addNode(1.0)

2/24/15

iOS Autolayout Constraints on Stacked Views



The containing view (gray border) vertically resizes to fit child views.
Containing view is hidden when child view heights total 0.

5 screenshots
1. View layout before applying constraints
2. 3 child views, heights 100
3. Green view height 0
4. Green and blue view height 0
5. Red view height 20, green view height 40


@implementation ViewController
{
    UIView * cv;  //container view, clear w/gray border
    UIView * tv;  //top view, red
    UIView * mv;  //middle view, green
    UIView * bv;  //bottom view, blue

    NSDictionary * bindings;
    NSMutableArray * verticalConstraints;
}

-(void)viewDidLoad
{
    [super viewDidLoad];

    [self createControls];
    [self setupConstraints];
}

-(void)setupConstraints
{
    self.view.translatesAutoresizingMaskIntoConstraints = NO;
    cv.translatesAutoresizingMaskIntoConstraints = NO;
    tv.translatesAutoresizingMaskIntoConstraints = NO;
    mv.translatesAutoresizingMaskIntoConstraints = NO;
    bv.translatesAutoresizingMaskIntoConstraints = NO;

    bindings = NSDictionaryOfVariableBindings(cv, tv, mv, bv);
    verticalConstraints = [NSMutableArray array];

    [self setupHorizontalConstraints];
    [self setupVerticalConstraints];
}

-(void)setupHorizontalConstraints
{
    NSMutableArray * hConstraints = [NSMutableArray array];
    
    //container view
    [hConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-20-[cv(200)]"
                                                                              options:0
                                                                              metrics:nil
                                                                                views:bindings]];
    //top view
    [hConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[tv]-|"
                                                                              options:0
                                                                              metrics:nil
                                                                                views:bindings]];
    //match middle view w/top view
    [hConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[mv(tv)]"
                                                                              options:0
                                                                              metrics:nil
                                                                                views:bindings]];
    //match bottom view w/top view
    [hConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[bv(tv)]"
                                                                              options:0
                                                                              metrics:nil
                                                                                views:bindings]];
    [self.view addConstraints:hConstraints];
}

-(void)setupVerticalConstraints
{
    [self.view removeConstraints:verticalConstraints];
    [verticalConstraints removeAllObjects];

    [verticalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[cv]-20-|"
                                                                                     options:0
                                                                                     metrics:nil
                                                                                       views:bindings]];
    //view heights, container vertical margin
    NSDictionary * metrics = @{ @"th" : @(tv.frame.size.height),
                                @"mh" : @(mv.frame.size.height),
                                @"bh" : @(bv.frame.size.height),
                                @"cm" : @(tv.frame.size.height + mv.frame.size.height + bv.frame.size.height > 0 ? 8 : 0) };

    [verticalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-cm-[tv(th)][mv(mh)][bv(bh)]-cm-|"
                                                                                     options:NSLayoutFormatAlignAllCenterX
                                                                                     metrics:metrics
                                                                                       views:bindings]];
    [self.view addConstraints:verticalConstraints];
}

//init controls w/temp width and position for general layout, before implementing constraints
-(void)createControls
{
    int vWidth  = 100;
    int vHeight = 100;
    int cHeight = vHeight * 3;

    cv = [[UIView alloc] initWithFrame:CGRectMake(20,  20, vWidth, cHeight)];
    tv = [[UIView alloc] initWithFrame:CGRectMake(10,   0, vWidth, vHeight)];
    mv = [[UIView alloc] initWithFrame:CGRectMake(10, 100, vWidth, vHeight)];
    bv = [[UIView alloc] initWithFrame:CGRectMake(10, 200, vWidth, vHeight)];
    
    cv.layer.borderColor = [UIColor lightGrayColor].CGColor;
    cv.layer.borderWidth = 1;

    tv.backgroundColor = [UIColor redColor];
    mv.backgroundColor = [UIColor greenColor];
    bv.backgroundColor = [UIColor blueColor];

    [self.view addSubview:cv];
    [cv addSubview:tv];
    [cv addSubview:mv];
    [cv addSubview:bv];
}


-(void)hideView:(UIView *)aView
{
    aView.frame = CGRectMake(0, 0, aView.frame.size.width, 0);
    [self setupVerticalConstraints];
}

-(void)resizeView:(UIView *)aView height:(int)aHeight
{
    aView.frame = CGRectMake(0, 0, aView.frame.size.width, aHeight);
    [self setupVerticalConstraints];
}

-(void)showView:(UIView *)aView
{
    aView.frame = CGRectMake(0, 0, aView.frame.size.width, 100);
    [self setupVerticalConstraints];
}

2/21/15

iOS Bee Flight Simulator

Bees animate from left to right along bezier path
Tap to toggle visible flight paths
iPad landscape screenshots



Key Tech Features
Core Graphics Layers
Scalable Bezier Paths
Core Animation

CAShapeLayer * visibleFlightPath1;  //ivars
CAShapeLayer * visibleFlightPath2;


-(void)setup
{
    CALayer * b1 = getBumblebee();
    CALayer * b2 = getBumblebee();

    CGPathRef fp1 = getFlightPath(0, 1.0f, 1.0f);
    CGPathRef fp2 = getFlightPath(80, 3.0f, 0.8f);

    CAKeyframeAnimation * a1 = getAnimationWithPath(fp1, 3.3, 0);
    [b1 addAnimation:a1 forKey:nil];
    [self.view.layer addSublayer:b1];

    CAKeyframeAnimation * a2 = getAnimationWithPath(fp2, 3.9, 0);
    [b2 addAnimation:a2 forKey:nil];
    [self.view.layer addSublayer:b2];

    visibleFlightPath1 = getVisibleFlightPath(fp1);
    visibleFlightPath2 = getVisibleFlightPath(fp2);
    [self.view.layer addSublayer:visibleFlightPath1];
    [self.view.layer addSublayer:visibleFlightPath2];
}

CAGradientLayer * getBumblebee()
{
    CAGradientLayer * b = [CAGradientLayer layer];
    b.frame = CGRectMake(0, 0, 25, 25);
    b.borderColor = [UIColor darkGrayColor].CGColor;
    b.borderWidth = 0.5f;
    b.cornerRadius = 10.0f;
    b.masksToBounds = YES;

    CGColorRef black = [UIColor blackColor].CGColor;
    CGColorRef yellow = [UIColor yellowColor].CGColor;
    b.colors = @[ (__bridge id)black,
                  (__bridge id)yellow,
                  (__bridge id)black,
                  (__bridge id)yellow ];

    //vertical gradient
    b.startPoint = CGPointMake(0.0f, 0.5f);
    b.endPoint   = CGPointMake(1.0f, 0.5f);

    b.locations = @[ [NSNumber numberWithFloat:0.0], 
                     [NSNumber numberWithFloat:0.33], 
                     [NSNumber numberWithFloat:0.5], 
                     [NSNumber numberWithFloat:0.9],
                     [NSNumber numberWithFloat:1.0] ];

    return b;
}

CGPathRef getFlightPath(const int yOffset, const CGFloat factor1, const CGFloat factor2)
{
    const CGPoint startPoint = CGPointMake(20, (200 + (yOffset * factor1)));
    const CGFloat normalFlightPoints[] = { 220, 200,
                                           420, 200,
                                           620, 200,
                                           820, 200 };


    UIBezierPath * fp = [UIBezierPath bezierPath];
    [fp moveToPoint:startPoint];


    //add normal flight points
    const int count = sizeof(normalFlightPoints)/sizeof(CGFloat);
    const int cpOffset = 100;
    const CGFloat cpYOffsetFactor = (cpOffset * factor1);
    for (int i = 0, s = 0; i < count; i++, s++)
    {
        const CGFloat x = normalFlightPoints[i];
        const CGFloat y = normalFlightPoints[i + 1] + (yOffset * factor1);
        const CGPoint p = CGPointMake(x, y);

        const CGFloat cpYHighLowOffset = ((s % 2) ? cpYOffsetFactor : -cpYOffsetFactor);
        const CGPoint cp = CGPointMake((p.x - cpOffset), (p.y + cpYHighLowOffset));

        [fp addQuadCurveToPoint:p controlPoint:cp];  //connect flight points
        i += 1;
    }


    //add pseudo random flight points
    const CGFloat w = (yOffset * factor1) * factor2;
    [fp addQuadCurveToPoint:CGPointMake(700, 175 + w) controlPoint:CGPointMake(850, 150 + w)];
    [fp addQuadCurveToPoint:CGPointMake(900, 200 + w) controlPoint:CGPointMake(550, 200 + w)];

    const CGFloat randomYOffset = (arc4random_uniform(2) == 0 ? -cpOffset : cpOffset);
    [fp addCurveToPoint:CGPointMake(1200, fp.currentPoint.y + randomYOffset)
          controlPoint1:CGPointMake(1000, fp.currentPoint.y)
          controlPoint2:CGPointMake(1100, fp.currentPoint.y)];


    return fp.CGPath;
}

CAKeyframeAnimation * getAnimationWithPath(CGPathRef aPath, CFTimeInterval aDuration, CFTimeInterval aTimeOffset)
{
    CAKeyframeAnimation * a = [CAKeyframeAnimation animation];
    a.keyPath = @"position";
    a.path = aPath;
    a.duration = aDuration;
    a.timeOffset = aTimeOffset;
    a.repeatCount = HUGE_VALF;
    a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

    return a;
}

CAShapeLayer * getVisibleFlightPath(CGPathRef aPath)
{
    CAShapeLayer * fp = [CAShapeLayer layer];
    fp.path = aPath;
    fp.fillColor = [UIColor clearColor].CGColor;
    fp.strokeColor = [UIColor darkGrayColor].CGColor;
    fp.lineWidth = 1.0f;
    fp.zPosition = -1.0f;

    return fp;
}

//tap to toggle visible flight paths
-(void)tap:(UIGestureRecognizer*)sender
{
    visibleFlightPath1.hidden = !visibleFlightPath1.hidden;
    visibleFlightPath2.hidden = !visibleFlightPath2.hidden;
}

-(void)viewDidLoad
{
    [super viewDidLoad];
    
    UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [self.view addGestureRecognizer:tap];
    [self setup];
}

1/27/15

Scene Kit 3D Cube Map

NSArray * images = @[[UIImage imageNamed:@"art.scnassets/zp.png"],
                     [UIImage imageNamed:@"art.scnassets/xp.png"],
                     [UIImage imageNamed:@"art.scnassets/zn.png"],
                     [UIImage imageNamed:@"art.scnassets/xn.png"],
                     [UIImage imageNamed:@"art.scnassets/yp.png"],
                     [UIImage imageNamed:@"art.scnassets/yn.png"]];

NSMutableArray * materials = [NSMutableArray array];
for (int i = 0; i < images.count; i++)
{
   SCNMaterial * m = [SCNMaterial material];
   m.diffuse.contents = images[i];
   [materials addObject:m];
}

SCNBox * boxGeo = [SCNBox boxWithWidth:8 height:8 length:8 chamferRadius:0];
boxGeo.materials = materials;
SCNNode * boxNode = [SCNNode nodeWithGeometry:boxGeo];