Archive for March, 2013

Art Helper: Batch Retina Pairs

Screen Shot 2013-03-29 at 2.23.44 PM

At Lyle Andrew’s request, I’ve added a batch processor utility into Art Helper. It’s still in testing (ping me by email if you’d like to help), but it works like this:

You pick a folder, it searches that folder for art files. They have to use an even pixel size in both dimensions. Art Helper then creates an output folder for the new pairs and builds them there. This way, he doesn’t have to drag in each item to process it.

Want new features? Just let me know and I’ll see what I can do. This is basically a crowd-sourced tool.

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);

iOS Drawing book beta readers

F0114

UPDATE: The drawing book is now getting close to complete. Thank you to everyone who wrote in. I’ll post about the next projects and more beta reading opportunities shortly.

I’m still looking for beta readers for the new iOS Drawing book. Think you might be interested? Please send me a note by email and tell me about yourself and your background.

Here are a few points I include in my standard welcome, that should give you an idea of what I’m looking for:

WHAT TO LOOK FOR
I expect you to read through the chapter and find things that are (1) wrong, (2) unclear, (3) missing, or (4) badly expressed. Your focus is technical review. Please don’t worry about misspellings, phrasing, semicolons, etc. There are people to help with that during the copy edit phase. Your role is technical clarity and accuracy. Your help there is greatly appreciated.

BE BRUTAL
I do not expect you to be kind in your feedback. Please be brutal and honest. I’d rather fix things now before the material goes out to the general public than after. If something is wrong, it’s wrong. If you point this out, I will not wallow in hurt feelings, I will be grateful to you.

TIMELINESS MATTERS
This book is being written on a very short deadline. I need your feedback turned around in just a week or so after I post each chapter. I know that’s a really short turnaround and thank you in advance for promptness!

DO NOT DISTRIBUTE THIS MATERIAL
Selling books is how I support my family. Please do not distribute any of these documents. Thank you!

STICK AROUND
Books are a marathon, not a sprint. I need readers as much towards the end of the book as the start. Please, please, please stick around

Art Helper 1.0.3 is live, 1.0.4 submitted

Screen Shot 2013-03-21 at 6.36.38 PM

 

Art Helper 1.0.3 just went live, offers some Retina fixes, thanks to Matt Stevens and Lyle Andrews.

I took advantage of the quick review cycle to submit another version just a few minutes later. This includes improved multi-resolution TIFF that I added for Rudy Richter (he was my beta tester and feature requester) and a new icon.

I also, cautiously, am adding 10.7 support — we’ll see how that goes. Thanks to David Green, Mahipal Raythattha, and Gwynne Raskind for testing.

As always, this is written primarily for my own use and for friends. If you have any bug reports or feature requests, just let me know.

Context State

Screen Shot 2013-03-21 at 4.57.32 PM

I spent far too much time today documenting the way contexts can store state. To cheer myself up, I decided to represent this all as bunnies. Clicking on the image should take you to a full-size version, where you can wallow in the CGContextRef possibilities.

 

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.