15

I'm creating a drawing app ( text ) for the iPad using OpenGL. I've already had a look at Apple's example GLPaint, and my app is now based on that code. My app should be just for drawing text, not for painting pictures.

Well, my App works, I can write some text. But the writing isn't really good, it doesn't make fun to write. The drawing path isn't smooth, it's angularly because I'm drawing a line from one point to another. And the path has everywhere the same width. My idea is: when you're writing fast the line is thinner than when you're writing slow. It should be the same experience like writing with a real pen.

How can I make the path look much smoother? How can I vary the width of the line depending on the speed of writing?

Here you can see what I mean:

example

genpfault
  • 51,148
  • 11
  • 85
  • 139
burki
  • 2,946
  • 6
  • 37
  • 51

2 Answers2

27

The best way to smooth the drawing is use a bezeir curve. Here is my code. It is a modified version I found on apple's dev site, but I don't remember the original link:

CGPoint drawBezier(CGPoint origin, CGPoint control, CGPoint destination, int segments)
{
 CGPoint vertices[segments/2];
 CGPoint midPoint;
 glDisable(GL_TEXTURE_2D);
 float x, y;

 float t = 0.0;
 for(int i = 0; i < (segments/2); i++)
 {
  x = pow(1 - t, 2) * origin.x + 2.0 * (1 - t) * t * control.x + t * t * destination.x;
  y = pow(1 - t, 2) * origin.y + 2.0 * (1 - t) * t * control.y + t * t * destination.y;
  vertices[i] = CGPointMake(x, y);
  t += 1.0 / (segments);

 }
 //windowHeight is the height of you drawing canvas.
 midPoint = CGPointMake(x, windowHeight - y);
 glVertexPointer(2, GL_FLOAT, 0, vertices);
 glDrawArrays(GL_POINTS, 0, segments/2);
 return midPoint;
}

That will draw based on three points. The control is the midpoint, which you need to return. The new midpoint will be different than the previous. Also, if you go through the above code, it will only draw half the line. The next stroke will fill it in. This is required. my code for calling this function (the above is in C, this is in Obj-C):

   //Invert the Y axis to conform the iPhone top-down approach
   invertedYBegCoord = self.bounds.size.height - [[currentStroke objectAtIndex:i] CGPointValue].y;
   invertedYEndCoord = self.bounds.size.height - [[currentStroke objectAtIndex:i+1] CGPointValue].y;
   invertedYThirdCoord = self.bounds.size.height - [[currentStroke objectAtIndex:i+2] CGPointValue].y;
   //Figure our how many dots you need
   count = MAX(ceilf(sqrtf(([[currentStroke objectAtIndex:i+2] CGPointValue].x - [[currentStroke objectAtIndex:i] CGPointValue].x) 
         * ([[currentStroke objectAtIndex:i+2] CGPointValue].x - [[currentStroke objectAtIndex:i] CGPointValue].x) 
         + ((invertedYThirdCoord - invertedYBegCoord) * (invertedYThirdCoord - invertedYBegCoord))) / pointCount), 1);

   newMidPoint = drawBezier(CGPointMake([[currentStroke objectAtIndex:i] CGPointValue].x, invertedYBegCoord), CGPointMake([[currentStroke objectAtIndex:i+1] CGPointValue].x, invertedYEndCoord), CGPointMake([[currentStroke objectAtIndex:i+2] CGPointValue].x, invertedYThirdCoord), count);

   int loc = [currentStroke count]-1;
   [currentStroke insertObject:[NSValue valueWithCGPoint:newMidPoint] atIndex:loc];
   [currentStroke removeObjectAtIndex:loc-1];

That code will get the mid point based on inverted iPad points, and set the 'control' as the current point.

That will smooth out the edges. Now regarding the line width, you just need to find the speed of that drawing. It is easiest just to find the length of your line. This is easily done using component mathematics. I don't have any code for it, but here is a primer for component mathmatics from a physics site. Or you can simply divide (above) count by some number to find out how thick you need the line to be (count uses component mathematics).

I store point data in an array called currentStroke, in case it wasn't obvious.

That should be all you need.

EDIT:

To store points, you should use touchesBegin and touchesEnd:

- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
    self.currentStroke = [NSMutableArray array];
    CGPoint point = [ [touches anyObject] locationInView:self];
    [currentStroke addObject:[NSValue valueWithCGPoint:point]];
    [self draw];

}

- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
    CGPoint point = [ [touches anyObject] locationInView:self];
    [currentStroke addObject:[NSValue valueWithCGPoint:point]];
    [self draw];
}


- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [ [touches anyObject] locationInView:self];
    [currentStroke addObject:[NSValue valueWithCGPoint:point]];
    [self draw];
}

That is pretty much an entire drawing application there. If you are using GL_Paint, then you are already using point sprites, which this system is build on.

Beaker
  • 1,633
  • 13
  • 22
  • Thanks very much, this really helps. But I'm not sure where to implement the second part of the code. In the GLPaint example there's a function renderLineFromPoint: toPoint:, which is called everytime the user moves his finger. Need I have to put the second part of your code into that function? – burki Jan 06 '11 at 12:03
  • Or could you add the whole code? I mean the complete methods. That would be very kind. – burki Jan 06 '11 at 20:00
  • Added touchesBegin, touchesMoved, and touchesEnd. That is pretty much an entire openGL drawing application up in the post. – Beaker Jan 07 '11 at 02:24
  • what do the 'i' variables represent in the second section of code where the 'invertedYBegCoord' vars are assigned? – Todd Hopkinson Apr 14 '11 at 23:38
  • @icnivad It is in case you want to loop over your line to redraw. It isn't necessary. I forgot to remove it for this example actually. Anyway just assume i=0. – Beaker Apr 15 '11 at 05:01
  • There still seems to be a lot missing from this, I've been trying to plug it in as best I can and it seems to work to an extent, but my lines are broken up in to dots and sometimes straight lines shoot out from where I'm drawing. Do you have this project uploaded somewhere? At first I was getting a crash because there weren't enough points to satisfy your second snippet at touchesBegan. I'm definitely missing something but I don't know where to go from here... – Patrick T Nelson Feb 01 '14 at 11:32
  • @PatrickTNelson This was written for iOS 3/4. I would be surprised if it still functioned as it was intended. If lines/dots are being drawn in the wrong spot, I would recommend checking that the iOS to OpenGL coordinate mapping is correct. – Beaker May 10 '14 at 19:44
4

With regards to the second part of your question (how to vary the width of the line depending on the speed of writing), you should be able to achieve this by taking advantage of UITouch's timestamp property in your touchesBegan:withEvent: and touchesMoved:withEvent: method.

You can calculate the time difference between two subsequent touch events by storing the timestamp of the most recent UITouch object and comparing it to the new one. Dividing the distance of the swiping motion by the time difference should give you some measurement of the movement speed.

Now all you need to do is to come up with a way to convert speed of writing into line width, which will probably come down to picking an arbitrary value and adjusting it until you're happy with the result.

Leo Cassarani
  • 1,289
  • 10
  • 14
  • Thanks so far. I've already thought of something like this. Would be nice if you knew how to solve my first problem. Hope you're happy enough with upvote. – burki Jan 05 '11 at 20:00