Archive for the ‘iOS’ Category

Adding Visual Depth

Screen Shot 2013-04-08 at 12.12.27 PM

I played a bit with  adding depth to the color sampler app.  You can view the results at the linked video (about 30 seconds). Pretty much all the updates were created using Bezier paths, although I did end up using those to build reusable image components rather than computing the highlights for every view.

A lot of people were giving me feedback that everything on the screen felt like it was missing a dimension. A lot of the changes are quite subtle, like the slight edges around the swatches and the button highlights. The biggest change is the color adjustment wheel in the modal screen. I changed the wheel background entirely, and tried to make the center color look a bit more glassy rather than a solid.

I also added  a few improvements, which are hard to see here, into the algorithm that draws text along paths. You see this on the color adjustment wheel and on the camera sampling view.

Reversing Bezier Curves

bp1

 

bp2

 

It turns out that reversing a path is a lot trickier than it first appears. Sadly, the built in UIBezierPath option bezierPathByReversingPath is broken. Here’s what Xcode reports when that method is called on the Hello path you see above:

HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b6c4: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b784: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b794: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b7e4: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b7f4: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
HelloWorld(62165,0xacc69a28) malloc: *** error for object 0x994b814: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
Apr  8 09:04:30 Esopus-Spitzenburg.local HelloWorld[62165] : void CGPathAddLineToPoint(CGMutablePathRef, const CGAffineTransform *, CGFloat, CGFloat): no current point.

And here’s what the path created by bezierPathByReversingPath looks like:

iOS Simulator Screen shot Apr 8, 2013 9.04.34 AM

It’s not exactly the result you expect.

To create a solution, I ended up writing my own Bezier library, where I decompose each path into first, its subpaths (each subpath begins with a Move operation, and may or may not end with a Close operation), and then into the component elements.

Reversing paths turns out to be a bit trickier than I anticipated. That’s because when you reverse items, you have to take close points into account. That means a reversed path often looks like this:

  • Move to the destination point of 2nd to last element (the one that comes before the close command)
  • Add a line to the first element of the path
  • Reverse the each line and curve, using the destination for each item as the start point, and the start element as the destination
  • If you had a close command in the original path, apply that close to avoid odd line cap artifacts

Here’s the code in question. It’s still got new code smell all over it, so if you find anything I messed up please let me know. Also, it uses quite a few custom subclasses and routines, which I’ve omitted to focus on the approach rather than the details.

Bounds and Accuracy

Screen Shot 2013-03-26 at 7.46.14 PM

Update: Ryan Petrich to the rescue with a much simpler solution: He writes “If you want the precise bounds, use CGPathGetPathBoundingBox (path.CGPath)”. Thanks Ryan!

 

 

So it turns out that the UIBezierPath bounds command isn’t particularly accurate, which is a problem when using custom paths with Auto Layout. You’ll want to provide accurate content insets to the constraints system.

The image at the top of this post represents both the Apple-provided bounds (the outer rectangle) and the true bounds (the inner rectangle). I visualized the control points for my path (blue circles) to confirm that the bounds were being calculated by taking the union of all points — destination and control points. Unfortunately, that’s not a very good strategy for calculating bounds.

Instead, you really need to calculate the edges of the curves, as they will almost never extend as far as the control points for most drawing.

Here’s the solution I’ve been working with, probably in far more detail than needed.

// Workarounds
void updateMinMax(CGPoint point, CGFloat *minX, CGFloat *maxX, CGFloat *minY, CGFloat *maxY)
{
    if (!minX) return;
    if (!maxX) return;
    if (!minY) return;
    if (!maxY) return;

    if (point.x < *minX)
        *minX = point.x;
    if (point.y < *minY)
         *minY = point.y;
    if (point.x > *maxX)
        *maxX = point.x;
    if (point.y > *maxY)
        *maxY = point.y;
}

// Proper bounds
- (CGRect) calculatedBounds
{
    CGFloat minX = FLT_MAX;
    CGFloat minY = FLT_MAX;
    CGFloat maxX = -(FLT_MAX / 2);
    CGFloat maxY = -(FLT_MAX / 2);

    BezierElement *current = nil;

    for (BezierElement *element in self.elements)
    {
        switch (element.elementType)
        {
            case kCGPathElementMoveToPoint:
            {
                current = element;
                updateMinMax(current.point, &minX, &maxX, &minY, &maxY);
                break;
            }
            case kCGPathElementAddLineToPoint:
            {
                current = element;
                updateMinMax(current.point, &minX, &maxX, &minY, &maxY);
                break;
            }
            case kCGPathElementCloseSubpath:
            {
                break;
            }
            case kCGPathElementAddCurveToPoint:
            {
                for (int i = 0; i <= NUMBER_OF_BEZIER_SAMPLES; i++)
                {
                    CGPoint p = CubicBezierPoint((CGFloat) i / (CGFloat) NUMBER_OF_BEZIER_SAMPLES, current.point, element.controlPoint1, element.controlPoint2, element.point);
                    updateMinMax(p, &minX, &maxX, &minY, &maxY);
                }
                current = element;
                break;
            }
            case kCGPathElementAddQuadCurveToPoint:
            {
                for (int i = 0; i <= NUMBER_OF_BEZIER_SAMPLES; i++)
                {
                    CGPoint p = QuadBezierPoint((CGFloat) i / (CGFloat) NUMBER_OF_BEZIER_SAMPLES, current.point, element.controlPoint1, element.point);
                    updateMinMax(p, &minX, &maxX, &minY, &maxY);
                }
                current = element;
                break;
            }
        }
    }

    // This does not take line width into account
    CGRect baseRect = CGRectMake(minX, minY, (maxX - minX), (maxY - minY));
    return baseRect;
}

- (CGRect) calculatedBoundsWithLineWidth;
{
    return CGRectInset(self.calculatedBounds, -self.lineWidth / 2.0f, -self.lineWidth / 2.0f);
}

Calculating Bezier points

Screen Shot 2013-03-24 at 4.08.38 PM

iOS supports three kind of Bezier elements: line segments, quadratic curves, and cubic curves.  Each of these participate in the UIBezierPath class to create complex shapes for UIKit and Quartz drawing routines.

Retrieving the component curves provides a powerful development tool. It enables you to transform paths by scaling, rotation , and translation, and provides a basis for applying drawing functions along the curve.

For example, you may want to place or draw items along the curve  to create fun text layouts or to animate gameplay. Knowing where things fall is key.

Screen Shot 2013-03-24 at 4.51.45 PM

Unfortunately, the CGPathElement structure doesn’t offer you much to work with. It provides an element type, which might be one of: move to point, add line to point, add curve to point, add quad curve to point, or close path.

A variable-sized array of CGPoints, depending on the number of parameters used to define that element. Close and move operations include no parameters. The curve-to-point operation uses three.

So how do you calculate the intermediate points between one point and the next? Well, with linear items, it’s easy. You calculate the vector from one point to the next and scale it to the percent of progress.

- (CGPoint) interpolateFrom: (CGPoint) p1 to: (CGPoint) p2 
    withPercent: (CGFloat) percent withSlope: (CGPoint *) slope
{
    CGFloat dx = p2.x - p1.x;
    CGFloat dy = p2.y - p1.y;

    if (slope)
        *slope = CGPointMake(dx, dy);

    CGFloat px = p1.x + dx * percent;
    CGFloat py = p1.y + dy * percent;

    return CGPointMake(px, py);
}

But given a curve, how do you interpolate? Fortunately, you can just apply the base curve math. Here’s are cubic (2 control points) and quadradic (1 control point) functions to calculate those values. You supply the percent of progress (from 0 to 1), the start value, the end value, and the one or two control values.

float CubicBezier(float t, float start, float c1, float c2, float end)
{
    CGFloat t_ = (1.0 - t);
    CGFloat tt_ = t_ * t_;
    CGFloat ttt_ = t_ * t_ * t_;
    CGFloat tt = t * t;
    CGFloat ttt = t * t * t;

    return start * ttt_
    + 3.0 *  c1 * tt_ * t
    + 3.0 *  c2 * t_ * tt
    + end * ttt;
}

float QuadBezier(float t, float start, float c1, float end)
{
    CGFloat t_ = (1.0 - t);
    CGFloat tt_ = t_ * t_;
    CGFloat tt = t * t;

    return start * tt_
    + 2.0 *  c1 * t_ * t
    + end * tt;
}

For example, lets say you have two points A and B, and a control point C1. You want to calculate the value 30% of the way between the two of them. Call the function twice, once for the x value, once for the y. Combine the two to produce a destination point.

P.x = QuadBezier(0.3, A.x, C1.x, B.x);
P.y = QuadBezier(0.3, A.y, C1.y, B.y);
return CGPointMake(P.x, P.y);

Blending Mode sample images

kCGBlendModeDifferenceThere are times you are called upon to either explain or understand the various miscellany of Quartz blending modes. The simplest way to figure this out is, I find, to generate cheat sheet images to view as you read the documentation.

The following method automatically builds a set of reference images, naming them by the blend mode applied to create each one, and saving them to the application Documents folder.

- (void) generateBlendingImages
{
    NSArray *names = @[@"kCGBlendModeNormal", @"kCGBlendModeMultiply", @"kCGBlendModeScreen", @"kCGBlendModeOverlay", @"kCGBlendModeDarken", @"kCGBlendModeLighten", @"kCGBlendModeColorDodge", @"kCGBlendModeColorBurn", @"kCGBlendModeSoftLight", @"kCGBlendModeHardLight", @"kCGBlendModeDifference", @"kCGBlendModeExclusion", @"kCGBlendModeHue", @"kCGBlendModeSaturation", @"kCGBlendModeColor", @"kCGBlendModeLuminosity", @"kCGBlendModeClear", @"kCGBlendModeCopy", @"kCGBlendModeSourceIn", @"kCGBlendModeSourceOut", @"kCGBlendModeSourceAtop", @"kCGBlendModeDestinationOver", @"kCGBlendModeDestinationIn", @"kCGBlendModeDestinationOut", @"kCGBlendModeDestinationAtop", @"kCGBlendModeXOR", @"kCGBlendModePlusDarker", @"kCGBlendModePlusLighter"];

    CGRect rect = CGRectMake(0, 0, 100, 100);
    UIBezierPath *shape1 = [UIBezierPath bezierPathWithOvalInRect:rect];
    rect.origin.x += 50;
    UIBezierPath *shape2 = [UIBezierPath bezierPathWithOvalInRect:rect];

    for (int i = kCGBlendModeNormal; i <= kCGBlendModePlusLighter; i++)
    {
        NSString *name = [names[i - kCGBlendModeNormal] stringByAppendingPathExtension:@"png"];
        UIGraphicsBeginImageContext(CGSizeMake(150, 100));

        [greenColor set];
        [shape1 fill];

        CGContextSetBlendMode(UIGraphicsGetCurrentContext(), i);

        [purpleColor set];
        [shape2 fill];

        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        NSData *data = UIImagePNGRepresentation(image);
        [data writeToFile:DOCS_PATH(name) atomically:YES];
        UIGraphicsEndImageContext();

    }
}

Latest sample: UIBezierPath + CADisplayLink + Data Tube

and updated:

A few years ago, I developed a Data Tube class. This class models a fixed-latency queue. That is, the queue fills up to a certain length (in this example, that comes to 100 samples). It then auto-pops each time a new item is added.

This behavior forces the collection to store a fixed number of items, creating a moving window across a set of newly generated data points.

Today, I decided to use data tubes along with an AV foundation recording instance. Basically, I just took audio samples over time, and used those average levels to drive a UIBezierCurve.

Everything is connected to a standard CADisplayLink. It creates the heartbeat for the algorithm and tells the view when to sample and re-draw the curve.

You see the results in the video at the top of this post.

In the bigger picture, this is part of my effort to build a motivation section of the iOS Drawing book. Specifically, I want to answer, “Why draw?”. This sample shows the flexibility created by building your own feedback by using simple drawing primitives. I think it’s pretty nifty.

Note: The YouTube compression makes the output look a lot glitchier than it looks on an actual device.

Funpaths repository at Github

iOS Simulator Screen shot Mar 16, 2013 2.36.47 PM

If anyone is interested in a few fun shapes, I put up some UIBezierPaths at github. These are courtesy of PaintCode, through SVG import. I discovered that I could build a shape in Photoshop, export it to Illustrator, and save to SVG.

None of the art is mine, and the shapes seem to be freely licensed. The cute stuff is by ~hikaridrops on Deviant Art.

In related news, this UIBezierPath is not a dogcow:

iOS Simulator Screen shot Mar 15, 2013 9.47.15 PM

but this is:

iOS Simulator Screen shot Mar 15, 2013 9.48.33 PM

For details, see Macintosh Technical Note #31.