Gold Solution


#1

Just finished the Gold Challenge. It was fun. I got the speed (magnitude) of the vector involved, and then set that to the width of the line segment.

I redid the strokeLine method. I also implemented a new method strokeCompletedLine, to stroke only the lines that have been finished.

[code]- (void) strokeLine: (BNRLine *) line
{
CGPoint velocity = [self.moveRecognizer velocityInView:self];
NSLog(@" This is the velocity %@", NSStringFromCGPoint(velocity));
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
NSLog(@" This is the magnitute %f", magnitude);
UIBezierPath *bp = [UIBezierPath bezierPath];
bp.lineWidth = magnitude/100;
bp.lineCapStyle = kCGLineCapRound;

[bp moveToPoint:line.begin];
[bp addLineToPoint:line.end];
[bp stroke];

}

  • (void) strokeCompletedLine: (BNRLine *) line
    {
    UIBezierPath *bp = [UIBezierPath bezierPath];
    bp.lineWidth = 10;
    bp.lineCapStyle = kCGLineCapRound;

    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
    }
    [/code]

The strokeCompleteLine method goes in the drawRect method:

[code]- (void) drawRect:(CGRect)rect
{
//[self setSelectedLine:nil];

// Draw finished lines in black
[[UIColor blackColor] set];
for (BNRLine *line in self.finishedLines) {
   .......
    
    [self strokeCompletedLine:line];
    
    
}

........

}
[/code]


#2

At first my code updated all line’s widths to the velocity. I figured this was incorrect because the challenge says, “Adjust the thickness of the line being drawn”. So I simply gave each instance of a line its own width property which is set when the line is drawn and before being added to _finishedLines. This makes it so that each time a line is redrawn it will be redrawn with its original width. This could have been implemented a little better but it works great as is!

First I gave BNRLine a new width property:

[code]#import <Foundation/Foundation.h>

@interface BNRLine : NSObject

@property (nonatomic) CGPoint begin;
@property (nonatomic) CGPoint end;
@property (nonatomic) float width; //… here

@end[/code]

and assigned that width property in touchesMoved:

[code]- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@", NSStringFromSelector(_cmd));

for (UITouch *t in touches) {
    NSValue *key = [NSValue valueWithNonretainedObject:t];
    BNRLine *line = [self.linesInProgress objectForKey:key];
    
    CGPoint velocity = [self.moveRecognizer velocityInView:self]; //<---------------- grab the velocity
    line.width = (abs(velocity.x) + abs(velocity.y)) / 50;               //<------------------ and assign a width
    
    
    line.end = [t locationInView:self];
}

[self setNeedsDisplay];

}
[/code]

Then updated strokeLine: to take another argument:

[code]- (void)strokeLine:(BNRLine *)line withWidth:(float)width
{

UIBezierPath *bp = [UIBezierPath bezierPath];
bp.lineWidth = width; // <------------ which will be passed in here. 
bp.lineCapStyle = kCGLineCapRound;

[bp moveToPoint:line.begin];
[bp addLineToPoint:line.end];
[bp stroke];

}[/code]

and simply updated the drawRect: method with the new strokeLine:withWidth:

[code]- (void)drawRect:(CGRect)rect
{

for (BNRLine *line in self.finishedLines) {
    
    CGFloat hue = atan2f(line.end.y - line.begin.y, line.end.x - line.begin.x);
    hue += M_PI;
    hue /= M_PI * 2.0;
    [[UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0] set];
    [self strokeLine:line withWidth:line.width]; //<------------------------------------- HERE
}


[[UIColor redColor] set];
for (NSValue *key in self.linesInProgress) {
    BNRLine *line = self.linesInProgress[key];
    CGFloat hue = atan2f(line.end.y - line.begin.y, line.end.x - line.begin.x);
    hue += M_PI;
    hue /= M_PI * 2.0;
    [[UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0] set];
    
    [self strokeLine:self.linesInProgress[key] withWidth:line.width]; //<-------------- HERE
}

if (self.selectedLine) {
    [[UIColor whiteColor] set];
    [self strokeLine:self.selectedLine withWidth:self.selectedLine.width]; //<------------ AND HERE
}

}[/code]


#3

Below is my approach and version:

First, I added a property to BNRLine to take in a width. My feeling, and I also saw it expressed above, is that each line should retain its set width. So finished lines should not revert to a standard size or take on the size of the last line. This property makes sure that each line is redrawn with the same width as it was drawn in.

BNRLine.h

...
@property (nonatomic) float width;
...

Next, as I am not very good at math I decided to take a simple approach with regards to mapping velocity to line width. I first decided to capture the screen width and height of the device being used and then used that number as the means to decide the width of the line. As far as the screen dimensions I added two local variables and some code in initWithFrame to populate based on the user’s device.

@interface BNRDrawView() <UIGestureRecognizerDelegate>
{
    float screenHeight;
    float screenWidth;
}
...

From initWithFrame:

...
 CGRect screenBounds = [[UIScreen mainScreen]bounds];
 screenHeight = screenBounds.size.height;
 screenWidth = screenBounds.size.width;
...

Small change made to strokeLine that uses the width property of BNRLine versus the static 10 points as in previous exercises:

-(void)strokeLine:(BNRLine *)line
{
    UIBezierPath *bp = [UIBezierPath bezierPath];
    bp.lineWidth = line.width;
    bp.lineCapStyle = kCGLineCapRound;
    
    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
}

I created a new method whose purpose it is to return the width based on my simplistic (read a bit lame) velocity to width calculations. I did make a decision to allow a line width of no greater than 20 so this serves as the max width for the line.

-(float)widthFromVelocity
{
    CGPoint v = [self.moveRecognizer velocityInView:self];
    float fx = fabs(v.x);
    float fy = fabs(v.y);
    float maxLineWidth = 20;
    float lineWidth;
    
    NSLog(@"Velo x = %f and y = %f",fx,fy);
    
    if (fx > screenWidth || fy> screenHeight) lineWidth = maxLineWidth;
    else if (fx > fy) lineWidth = (fx/screenWidth)*maxLineWidth;
    else lineWidth = (fy/screenHeight)*maxLineWidth;
    
    return lineWidth;
}

Finally I modified the touchesMoved method to capture the computed line width using my new method above.

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    BNRLine *line;
    
    NSLog(@"%@",NSStringFromSelector(_cmd));
    
    if ([touches count] != 2)
    {
       for (UITouch *t in touches)
       {
           NSValue *key = [NSValue valueWithNonretainedObject:t];
           line = self.linesInProgress[key];
           line.end = [t locationInView:self];
           line.width = [self widthFromVelocity];
       }
    }
    
    [self setNeedsDisplay];
}

#4

My solution is similar. I set minimum thickness to 3 and max to 30.

@interface BNRLine : NSObject
@property (nonatomic) NSInteger thickness;

@interface BNRDrawView () <UIGestureRecognizerDelegate>
@property (nonatomic) NSInteger velocity;

-(void)moveLine:(UIPanGestureRecognizer *)gr
    self.velocity = hypotf(abs([gr velocityInView:self].x),
                           abs([gr velocityInView:self].y));

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
        line.thickness = MIN((int)(self.velocity/10)+3, 30);

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
        line.thickness = 10;

-(void)strokeLine:(BNRLine *)line
    bp.lineWidth = line.thickness;

#5

I didn’t read the other solutions, but the way I did it was quite simple once my brain turned on. I made it so that lineWidth was the absolute value of the max of x & y of the velocity and that there was no upper or lower limits.

I added a size property to BNRLine.h in order to store the velocity of each line drawn.

In touchesBegan: I added an initial line.size.

In touchesMoved: I added my formula.

Finally in strokeLine:

Hope it helps to anyone confused.


#6

My doubt might sound lame or dumb,but it is haunting me a lot. I was able to log the the velocity of the pan, but my doubt is how to adjust the thickness of the line. I see lot of solutions but I couldn’t understand the formulas used. There are many methods, I just want to know the exact logic, I’m not so good with the math. Can any one kindly help me out.


#7

@snaveen Here is my line…

fabsf() = absolute value of float in ()
MAX() = picks the largest number either number, in my case x or y

So in my case whichever velocity is faster, MAX() chooses that one. Then fabsf() makes it a positive number (if it isn’t already.) Then that gets set to the line.size.
Absolute value, “fabsf()”, is a math concept that makes any number positive. (Ex. absolute value of -5 is 5, absolute value of 3 is 3, absolute value of -10 is 10)

You can make it so that the line is reduced to a smaller size. I didn’t test the code but I assume you can just say “line.size = line.size / 2;” or whatever you want it to be.


#8

Added an int lineWidth property then added code to touchesBegan.
Velocity can be negative so it’s good to fabs before max.
Been getting line widths from 1 to 30.

CGPoint v = [self.moveRecognizer velocityInView:self]; int velocity = MAX_ABS(v.x, v.y); int lineWidth = velocity / 100; NSLog(@"velocity = %d, lineWidth = %d", velocity, lineWidth); line.lineWidth = lineWidth > 0 ? lineWidth : 1;


#9

You should use hypot instead of fabs and max, because hypot gives the true velocity.

However, does anyone know if the PanGestureRecognizer’s velocityInView method can be used for multiple touches? I get zero velocity when using the simulator and pressing option, which makes sense as the two touches seem to be symmetrical around the center of the screen and ostensibly cancel out. Is it possible to get the velocities of each individual touch?


#10

I just updated my drawRect method and updated strokeLine for variable lineWidth, it would be slightly different to the book example because I didn’t like how we were drawing all finishedLines, then the selectedLine on top of it, wasting draw time. So I was lucky it worked out here for the better. All I did was to edit the part where selectedLine is draw to extract the velocity of the pan, like everyone else and find the magnitude

- (void)drawRect:(CGRect)rect {
    for (BNRLine *finishedLine in self.finishedLines) {
        if (CGPointEqualToPoint(finishedLine.begin, self.selectedLine.begin) && CGPointEqualToPoint(finishedLine.end, self.selectedLine.end)) {
            CGPoint velocity = [self.panRecogniser velocityInView:self];
            NSLog(@"This is the velocity of pan: %@", NSStringFromCGPoint(velocity));
            CGFloat magnitude = hypotf(velocity.x, velocity.y);
            NSLog(@"This is the magnitude: %f", magnitude);
            [[UIColor greenColor] set];
            [self strokeLine:finishedLine ForWidth:magnitude/50];
        }
        else {
            [self setColorForLineAngle:atan2(finishedLine.end.x - finishedLine.begin.x, finishedLine.end.y - finishedLine.begin.y) * 180/M_PI];
            [self strokeLine:finishedLine ForWidth:10];
        }
    }
    for (BNRCircle *finishedCircle in self.finishedCircles) {
        [[UIColor orangeColor] set];
        [self strokeCircle:finishedCircle];
    }
    
    [[UIColor redColor] set];
    for (NSValue *key in self.linesInProgress) {
        [self strokeLine:self.linesInProgress[key] ForWidth:10];
    }
    for (NSValue *key in self.circleInProgress) {
        [self strokeCircle:self.circleInProgress[key]];
    }
}
- (void)strokeLine:(BNRLine *)line ForWidth:(float)lineWidth {
    UIBezierPath *bp = [UIBezierPath bezierPath];
    if (!lineWidth) {
        lineWidth = 10;
    }
    bp.lineWidth = lineWidth;
    bp.lineCapStyle = kCGLineCapRound;
    
    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
}