Efficient Drawing Storage Method? - iphone

I'm developing a basic drawing app that has a few classes. One of these is a "Line" class that stores some info on a line the user can draw. When the user drags round the screen, a line is created for each movement of their finger, so a line is drawn following their finger. I store all the line objects in an NSArray and redraw each time something changes. However, since the array just keeps getting bigger, and eventually starts to slow down. Is there any way to just draw the new lines, or a better storage mechanism for the lines?
EDIT:
Read the answer below, but setNeedsDisplayInRect: doesn't seem to be working. I am calling it like so:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *t in touches) {
//add line to array w/ x and y values from touch
[self setNeedsDisplayInRect:[self rectForLine:line]];
}
}
-(CGRect)rectForLine:(Line*)line {
CGFloat x1 = [line begin].x;
CGFloat y1 = [line begin].y;
CGFloat x2 = [line end].x;
CGFloat y2 = [line end].y;
CGFloat originX = (x1>x2) ? x1 : x2;
CGFloat originY = (y1<y2) ? y1 : y2;
CGFloat diffX = ((x1>x2) ? x1 : x2) - originX;
CGFloat diffY = ((y1>x2) ? y1 : y2) - originY;
return CGRectMake(originX, originY, diffX, diffY);
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 10.0);
CGContextSetLineCap(context, kCGLineCapRound);
//[[UIColor blackColor] set];
for (Line *line in completeLines) {
if (CGRectContainsPoint(rect, [line begin]) && CGRectContainsPoint(rect, [line end])) {
//draw line onto the screen
}
}

If a "line" is just a pair of points in your data model, that's not the part slowing you down.
Your perf problem is that you're redrawing the whole thing every line that gets added, which is O(n^2) or very specifically a Schlemiel-the-Painter's algorithm.
The simplest improvement, assuming you're drawing inside the -drawRect: of a UIView, is probably to only invalidate and redraw the rect that contains the new line. Based on the start and end points, you can create the invalid rect to send to -setNeedsDisplayInRect:, and then in your drawRect, look at all your lines, and only draw the ones that fall inside the rect. This bounds your draw performance to the size of the lines and to a much lesser degree, the drawing that's "already there".

Related

how to fill color in white spaces using touch event?

want to fill all white spaces with different color using touch events
Right now i able to fill circles picking colors from picker but how to fill the intigrated part with different color......
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UIColor *cl=[UIColor clearColor];
UITouch *tuch=[touches anyObject];
if ([clr isEqualToString:#"Red"]) {
cl=[UIColor redColor];
}
else if ([clr isEqualToString:#"Blue"]) {
cl=[UIColor blueColor] ;
}
else if ([clr isEqualToString:#"Green"]) {
cl=[UIColor greenColor];
}
CGPoint p = [tuch locationInView:self];
float xsq1=p.x -50;
xsq1=xsq1*xsq1;
float ysq1=p.y-110;
ysq1=ysq1*ysq1;
float h1 = ABS(sqrt(xsq1 + ysq1));
float xsq2=p.x -100;
xsq2=xsq2*xsq2;
float ysq2=p.y-110;
ysq2=ysq2*ysq2;
float h2 = ABS(sqrt(xsq2 + ysq2));
float xsq3=p.x -50;
xsq3=xsq3*xsq3;
float ysq3=p.y-190;
ysq3=ysq3*ysq3;
float h3 = ABS(sqrt(xsq3 + ysq3));
if (h1<=40) {
NSLog(#"touches inside of first circle");
CGContextSetFillColorWithColor(context, cl.CGColor);
CGRect cir1 = CGRectMake(10,266,80,80);
CGContextFillEllipseInRect(context, cir1);
[self setNeedsDisplayInRect:cir1];
}
else if (h2<=40) {
NSLog(#"touches inside of second circle");
CGContextSetFillColorWithColor(context, cl.CGColor);
CGRect cir2 = CGRectMake(60,266,80,80);
CGContextFillEllipseInRect(context, cir2);
[self setNeedsDisplayInRect:cir2];
}
}
There are two tasks here, the first is to detect which region has been touched, the second is to fill that region. Both require you to calculate the circle intersection points of the above image using trigonometry, and to know their positions.
A simple solution to the touch region detection would be to check if the touch is contained in any of the circles, this is easily calculated by calculating the distance to the touch point from the circles centre, if it is less than the radius it is inside the circle. If it is inside more than one circle, you know it belongs in that intersection region. It if is inside no circles, but the x component is in between the centre of a left circle and a right circle it must be in the region in between all the circles. Otherwise the touch point must be outside all of the circles.
In order to fill the various sections of the above image, you can create paths containing the regions you need to fill and fill them with CGContextFillPath. Something like this:
// draw a path to contain the fill region
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, startx, starty);
CGContextAddArcToPoint(ctx, ...);
// lots of other CGContextAddArcToPoint or AddLineToPoint method calls here to define the clip region
// close the clip path
CGContextClosePath(ctx);
// now you can fill the region
CGContextFillPath(ctx);
You can repeat this for as many paths as you like. You can calculate the path arcs to use from the circle intersection points and radii.

Draw line or rectangle on cocos2d Layer

Can you let me know what is the best way to draw a line or rectangle on a scene layer using Cocos2d ios4 iphone.
So far have tried Texture2d, but it is more like a paint brush and is not so good. Tried drawing a line using draw method, but previous line disappears on drawing another line.
Basically want to draw multiple horizontal ,vertical, oblique beams. Please suggest. Any code would help a lot .
The code to draw using texture is below:
CGPoint start = edge.start;
CGPoint end = edge.end;
// begin drawing to the render texture
[target begin];
// for extra points, we'll draw this smoothly from the last position and vary the sprite's
// scale/rotation/offset
float distance = ccpDistance(start, end);
if (distance > 1)
{
int d = (int)distance;
for (int i = 0; i < d; i++)
{
float difx = end.x - start.x;
float dify = end.y - start.y;
float delta = (float)i / distance;
[brush setPosition:ccp(start.x + (difx * delta), start.y + (dify * delta))];
[brush setScale:0.3];
// Call visit to draw the brush, don't call draw..
[brush visit];
}
}
// finish drawing and return context back to the screen
[target end];
The rendering is not good esp. with oblique lines as the scaling affects the quality.
Cheers
You could create a separate layer and call the draw method like this:
-(void) draw
{
CGSize s = [[Director sharedDirector] winSize];
drawCircle( ccp(s.width/2, s.height/2), circleSize, 0, 50, NO);
It's for a circle but the principle is the same. This is from a project I made a while back and it worked then. Don't know if anything has changed since.
You need to add draw method to your layer:
-(void) draw {
// ...
}
Inside it you can use some openGL like functions and cocos2d wrapper methods for openGL.
Hint: other methods can be called inside draw method.
But keep in mind that using other name for method
containing openGL instructions, that's not called inside draw mentioned above simply won't work.
Even when called from update method or other method used by scheduleUpdate selector.
So you will end up with something like this:
-(void) draw {
glEnable(GL_LINE_SMOOTH);
glColor4ub(255, 0, 100, 255);
glLineWidth(4);
CGPoint verts[] = { ccp(0,200), ccp(300,200) };
ccDrawLine(verts[0], verts[1]);
[self drawSomething];
[self drawSomeOtherStuffFrom:ccp(a,b) to:ccp(c,d)];
[someObject doSomeDrawingAsWell];
}
For more information check out cocos2d-iphone programming guide :
http://www.cocos2d-iphone.org/wiki/doku.php/prog_guide:draw_update?s[]=schedule#draw

Drawing app on iPad using OpenGL

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:
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.
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.

Scaling custom draw code for different iOS resolutions

I am struggling to get my custom drawing code to render at the proper scale for all iOS devices, i.e., older iPhones, those with retina displays and the iPad.
I have a subclass of UIView that has a custom class that displays a vector graphic. It has a scale property that I can set. I do the scaling in initWithCoder when the UIView loads and I first instantiate the vector graphic. This UIView is shown when the user taps a button on the home screen.
At first I tried this:
screenScaleFactor = 1.0;
if ([UIScreen instancesRespondToSelector:#selector(scale)]) {
screenScaleFactor = [[UIScreen mainScreen] scale];
}
// and then I multiply stuff by screenScale
... which worked for going between normal iPhones and retina iPhones, but chokes on the iPad. As I said, you can get to the UIView at issue by tapping a button on the home screen. When run on the iPad, if you display the UIView when at 1X, it works, but at 2X I get a vector graphic that twice as big as it should be.
So I tried this instead:
UPDATE: This block is the one that's right. (with the corrected spelling, of course!)
screenScaleFactor = 1.0;
if ([self respondsToSelector:#selector(contentScaleFactor)]) { //EDIT: corrected misspellng.
screenScaleFactor = (float)self.contentScaleFactor;
}
// again multiplying stuff by screenScale
Which works at both 1X and 2X on the iPad and on the older iPhones, but on a retina display, the vector graphic is half the size it should be.
In the first case, I query the UIScreen for its scale property and in the second case, I'm asking the parent view of the vector graphic for its contentsScaleFactor. Neither of these seem to get me where I want for all cases.
Any suggestions?
UPDATE:
Here's the method in my subclassed UIView (it's called a GaugeView):
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGAffineTransform t0 = CGContextGetCTM(context);
t0 = CGAffineTransformInvert(t0);
CGContextConcatCTM(context, t0);
[needle updateBox];
[needle draw: context];
}
needle is of class VectorSprite which is a subclass of Sprite which is subclassed from NSObject. These are from a programming book I'm working through. needle has the scale property that I set.
updateBox comes from Sprite and looks like this:
- (void) updateBox {
CGFloat w = width*scale;
CGFloat h = height*scale;
CGFloat w2 = w*0.5;
CGFloat h2 = h*0.5;
CGPoint origin = box.origin;
CGSize bsize = box.size;
CGFloat left = -kScreenHeight*0.5;
CGFloat right = -left;
CGFloat top = kScreenWidth*0.5;
CGFloat bottom = -top;
offScreen = NO;
if (wrap) {
if ((x+w2) < left) x = right + w2;
else if ((x-w2) > right) x = left - w2;
else if ((y+h2) < bottom) y = top + h2;
else if ((y-h2) > top) y = bottom - h2;
}
else {
offScreen =
((x+w2) < left) ||
((x-w2) > right) ||
((y+h2) < bottom) ||
((y-h2) > top);
}
origin.x = x-w2*scale;
origin.y = y-h2*scale;
bsize.width = w;
bsize.height = h;
box.origin = origin;
box.size = bsize;
}
Sprite also has the draw and drawBody methods which are:
- (void) draw: (CGContextRef) context {
CGContextSaveGState(context);
// Position the sprite
CGAffineTransform t = CGAffineTransformIdentity;
t = CGAffineTransformTranslate(t,x,y);
t = CGAffineTransformRotate(t,rotation);
t = CGAffineTransformScale(t,scale,scale);
CGContextConcatCTM(context, t);
// draw sprite body
[self drawBody: context];
CGContextRestoreGState(context);
}
- (void) drawBody: (CGContextRef) context {
// Draw your sprite here, centered
// on (x,y)
// As an example, we draw a filled white circle
if (alpha < 0.05) return;
CGContextBeginPath(context);
CGContextSetRGBFillColor(context, r,g,b,alpha);
CGContextAddEllipseInRect(context, CGRectMake(-width/2,-height/2,width,height));
CGContextClosePath(context);
CGContextDrawPath(context,kCGPathFill);
}
How, exactly, are you rendering the graphic?
This should be handled automatically in drawRect: (the context you get should be already 2x). This should also be handled automatically with UIGraphicsBeginImageContextWithOptions(size,NO,0); if available (if you need to fall back to UIGraphicsBeginImageContext(), assume a scale of 1). You shouldn't need to worry about it unless you're drawing the bitmap yourself somehow.
You could try something like self.contentScaleFactor = [[UIScreen mainScreen] scale], with appropriate checks first (this might mean if you display it in an iPad at 2x, you'll get high-res graphics).
Fundamentally, there's not much difference between an iPad in 2x mode and a "retina display", except that the iPad can switch between 1x and 2x.
Finally, there's a typo: #selector(contentsScaleFactor) has an extra s.

Iphone SDK touches event

I am trying to better understand touches by writing a few liner which which would track touches:
- (void)drawRect:(CGRect)rect {
NSLog (#"My draw");
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextMoveToPoint(context, prev.x, prev.y);
CGContextAddLineToPoint(context, cur.x, cur.y);
CGContextStrokePath(context);
return;
}
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
prev = [touch previousLocationInView:self];
cur = [touch locationInView:self];
[self setNeedsDisplayInRect:CGRectMake (prev.x, prev.y, fabs (cur.x-prev.x), fabs (cur.y - prev.y) )];
}
It clear that my setNeedsDisplay is not correct, as it only works for movement from positive to negative coordinates (from upper left to lower right). With this questions:
It appears that I have to individually write setNeedsDisplay for 4 different cases of potential movement directions (from pos X, Y to neg X,Y, from pos X to neg X, etc). Is this a correct approach, or I am missing some fundamental piece?
Even for the correct movement the line is not solid, but rather broken (depends on the speed of finger tracking). Why is not is solid line to track movement?
Thanks!
setNeedsDisplay does not immediately call drawRect. In fact, touchesMoved may be called several times before the next time the view is drawn, which is why you are seeing broken lines.
Your call to CGRectMake should be:
CGRectMake (MIN(cur.x, prev.x), MIN (cur.y, prev.y), fabs (cur.x-prev.x), fabs (cur.y - prev.y))
In my code, I generalize this with a function to make a rect from any two points, e.g.:
CGRect rectWithPoints(CGPoint a, CGPoint b)
{
return CGRectMake(
MIN (a.x, b.x),
MIN (a.y, b.y),
fabs (a.x - b.x),
fabs (a.y - b.y));
}