I've got code that allows me to draw on top of a UIImageView
- but I want to be able to limit the drawing area.
I've been able to limit the size, but now I'm unable to position it: if i change (0, 0) to anything else my image disappeared completely and the ability to draw stops working
drawImage - is a UIImageView
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
mouseSwiped = YES;
UITouch *touch = [touches anyObject];
currentPoint = [touch locationInView:self];
UIGraphicsBeginImageContext(CGSizeMake(560, 660));
[drawImage.image drawInRect:CGRectMake(0, 0, 560, 660)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 5.0);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0, 1, 0, 1);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
[drawImage setFrame:CGRectMake(0, 0, 560, 660)];
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
[self addSubview:drawImage];
}
any help is very much appreciated
I'm not sure I fully understand what you're trying to do, but I'll take a swing:
[drawImage.image drawInRect:CGRectMake(0, 0, 560, 660)];
This tells the image assigned to the drawImage to draw itself in the current context at 0,0. Since the context is a bitmap image context, its extent will be from 0,0 to 560,660. It seems to me that this is probably what you want to do, otherwise you would have blank areas in your bitmap. If you were to change that 0,0 to 10,0, I would expect there to be a 10pt wide blank area in the resulting bitmap (which seems like a waste).
Then later you do this:
[drawImage setFrame:CGRectMake(0, 0, 560, 660)];
This is setting the UIImageView's coordinates in its superview's coordinate system. If you change that 0,0 to, say, 5,5 I would expect your UIImageView to appear 5pt down and 5pt to the right in its superview.
You say you want to "limit the drawing area." I assume you mean the part where you're drawing over the image. The easiest way to do that would be to set a clip rect on the bitmap context before you draw. For instance, if you wanted to prevent drawing in a 10pt band around the edge of the image, you could do that like this:
UIGraphicsBeginImageContext(CGSizeMake(560, 660));
[drawImage.image drawInRect:CGRectMake(0, 0, 560, 660)];
CGContextClipToRect(UIGraphicsGetCurrentContext(), CGRectMake(10,10,540,640));
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
// ... and so on
Not 100% sure if that's what you're looking for, but hopefully it helps.
try by restricting the currentPoint
if (currentPoint.x <= 20 || currentPoint.x >= 280 ||
currentPoint.y <= 20 || currentPoint.y >= 400)
{
lastPoint = currentPoint;
return;
}
Related
The Problem: I have a small game where u can scratch a image above another image. The upper image becomes invisible where you are scratching. The whole thing is placed into a SpriteKit view. The problem is, that on weaker devices (iPhone4) the scratching image is only updated when the user stops scratching.
I assume that the image is only updated when the user does not scratch until the image is completely rendered and displayed.
Can someone suggest a better way to see the scratching immediately. I am aware that the scratching consumes a lot of performance but I dont mind if it lags a little.
Here the code:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[_scratchImage drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _lastPoint.x, _lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 25.0f );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextSetBlendMode (UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_scratchImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
_lastPoint = currentPoint;
SKTexture* scratchTexture = [SKTexture textureWithImage:_scratchImage];
_scratchSprite.texture = scratchTexture;
}
UPDATE
I ended up recording the points as suggested in the correct answer. But other than updating the image in a CADisplayLink callback I updated the screen in a
-(void)update(float currentTime)
callback from SpriteKit (Which is kind of the same for SpriteKit use cases)
My code in the update method looks like the following:
-(void)displayUpdated
{
UIGraphicsBeginImageContext(self.view.frame.size);
[_scratchImage drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGMutablePathRef linePath = nil;
linePath = CGPathCreateMutable();
CGPathMoveToPoint(linePath, NULL, _lastPoint.x, _lastPoint.y);
for (NSValue* val in _recordedPoints) {
CGPoint p = [val CGPointValue];
CGPathAddLineToPoint(linePath, NULL, p.x, p.y);
NSLog(#"%f, %f", p.x, p.y);
_lastPoint = p;
}
//flush array
[_recordedPoints removeAllObjects];
CGContextAddPath(UIGraphicsGetCurrentContext(), linePath);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 25.0f );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextSetBlendMode (UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_scratchImage = UIGraphicsGetImageFromCurrentImageContext();
CGPathRelease(linePath);
UIGraphicsEndImageContext();
SKTexture* scratchTexture = [SKTexture textureWithImage:_scratchImage];
_scratchSprite.texture = scratchTexture;
}
A more performant code would also hold a reference to the context but I could not manage to get that working. If someone can suggest a solution I would be happy. Nevertheless my Framerate on a iPhone 4 went up to 25-30 fps from about 5 fps.
I would try the following:
Measure in Instruments.app Time Profiler.
Speed up drawing by creating a CGBitmapContext as an instance variable. Then you can skip the [_scratchImage drawInRect:...] in each step.
Separate the drawing and the event handling. Right now, you will always create a new UIImage for each touch event. It could be better to coalesce the stroked lines so the creation of a UIImage does not happen as often. E.g. put the line segments in a queue and draw in a callback from CADisplayLink.
I don't know much about SpriteKit, In UIView, you should put your drawing code in drawRect method. Is it the same as SpriteKit ?
I recently made my app compatible with the newest retina iPad, but my drawings get all blurry. I then tried changing the contextWithOptions, but it became really laggy and jagged when I drew. Everything i've tried doesn't work. Any ideas?
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
mouseSwiped = YES;
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
if (UIGraphicsBeginImageContextWithOptions != NULL) {
UIGraphicsBeginImageContextWithOptions((self.view.frame.size), NO, 1);
//becomes laggy if I set it to 0 or 2. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
} else {
UIGraphicsBeginImageContext(self.view.frame.size);
}
[self.tempDrawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
CGContextStrokePath(UIGraphicsGetCurrentContext());
self.tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext();
[self.tempDrawImage setAlpha:opacity];
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(!mouseSwiped) {
UIGraphicsBeginImageContext(self.view.frame.size);
[self.tempDrawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, opacity);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
CGContextFlush(UIGraphicsGetCurrentContext());
self.tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
UIGraphicsBeginImageContextWithOptions((self.view.frame.size), NO, 0);
[self.mainImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:1.0];
[self.tempDrawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:opacity];
self.mainImage.image = UIGraphicsGetImageFromCurrentImageContext();
self.tempDrawImage.image = nil;
UIGraphicsEndImageContext();
}
Maybe I should change the image when the user ends touches? Any help please?
Correct, you do not want to try to create images for every touch in your view. You simply want to draw your path. Or, more accurately, you want your touch gesture to (a) update your model which consists of a bunch of arrays of paths (each path being, itself, an array of points); and (b) invoke the necessary call to update the UI for the updated model.
To render the drawings, you have two basic approaches:
Create a UIView subclass that has a drawRect method that iterates through your series of paths and draws them; or
Create a CAShapeLayer which uses your path and then do [self.view.layer addSublayer:layer].
Either technique is fine. But note, in neither of these techniques do you do any UIGraphicsBeginImageContext, UIGraphicsGetImageFromCurrentImageContext, and UIGraphicsEndImageContext during the gesture. You only do that if and when you want to save the drawing as an image (presumably well after the user's drawing gesture is done).
But if you do it this way, you'll have absolutely no problem keeping up with the user's gesture, even on retina screens. Manipulating images is a slow and memory-inefficient process, so you only want to do that sparingly.
By the way, if you made this series of paths as the central part of your drawing's model, it opens up other opportunities, too. For example, it will be easy to "undo" a path simply by removing that path object from your model and re-rendering your view. But if you saved it as an image, removing a couple of stokes becomes impractical.
In the end, you have to decide whether your app will be a vector drawing app (where your saved files are arrays of paths, possibly also with the option to save an image, too) or whether it's more of a bitmap editor (where, at the end, you discard the vector information and only save the bitmap). Regardless of which approach you adopt, employing path objects during the gesture will likely make the UI more responsive.
I am creating a paint app and I want to know how to implement the eraser tool. I don't want to have my eraser tool to paint white color because I want to allow users to change the background color. And also, is it possible to set the hardness of the brush? If yes, please tell me how.
Thank you
Here's what I've done so far:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
lastPoint = [touch locationInView:self.view];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[drawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[drawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
- (IBAction)clear:(id)sender {
drawImage.image = nil;
}
Ok here's what I did for the eraser tool:
I add this line of code:
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
So the code will be something something like this:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[drawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
// I add this
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), sizeSlider.value);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
i have make paint application and it is available on iTunes "Easy Doodle" App. There is no special logic for eraser. just get RGB value of background image and pass it in
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(),R value,G value,B value, 1.0);
according to background image selected.
Ok so I have been looking for a solution on how to create an eraser for a week now, I had to change the way I looked at it cause no matter how many times I tried to erase something out it would erase the background image too.
However, in the end I did find a solution that suited me (where I could erase the paint and still have the perception of the background image being untouched or erased) and hopefully it will help others.
So here goes...
1) Create 2 imageviews in the UIView
2) Set both imageviews with the background image that you want it to be...
3) I created a segmented control to determine if the user wanted to erase
4) Added the following function when segmented control is being selected..
func drawLineForRubber(fromPoint: CGPoint, toPoint: CGPoint) {
// 1
UIGraphicsBeginImageContext(view.frame.size)
let context = UIGraphicsGetCurrentContext()
mainImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
// 2
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
CGContextAddLineToPoint(context, toPoint.x, toPoint.y)
// 3
CGContextSetBlendMode(context, CGBlendMode.Clear)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, 10.0)
//this line is very important as it paints the screen clear
CGContextSetRGBStrokeColor(context, red, green, blue, 0.0)
CGContextSetBlendMode(context, CGBlendMode.Clear)
// 4
CGContextStrokePath(context)
// 5
mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
mainImageView.alpha = opacity
UIGraphicsEndImageContext()
}
As you will see there is a line in there that sets the CGContextSetRGBStrokeColor with the value of 0.0, this is important cause when the user is erasing the paint, he is erasing the paint from the top imageview along with the paint with a transperent color. However, since you have another imageview underneath it, the perception is it looks like its being rubbed out without affecting the background image.
On the contrary it is rubbed out but the imageview behind, makes it look like it hasnt. When you want to export the image (You can do it in a number of ways but I went with the following) you have painted with the background image just combine both image views as the
1st imageview will have the paint you painted and the
2nd imageview will have the original background image you wanted.
func share() {
let layer = UIApplication.sharedApplication().keyWindow!.layer
let scale = UIScreen.mainScreen().scale
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale); // reconsider size property for your screenshot
layer.renderInContext(UIGraphicsGetCurrentContext()!)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
UIGraphicsBeginImageContext(mainImageView.bounds.size)
mainImageView.image?.drawInRect(CGRect(x: 0, y: 0,
width: mainImageView.frame.size.width, height: mainImageView.frame.size.height))
UIGraphicsEndImageContext()
let activity = UIActivityViewController(activityItems: [screenshot], applicationActivities: nil)
presentViewController(activity, animated: true, completion: nil)
}
Hope this helps others along the way.. man it took me ages to get my head around it =)
I have this code to colour with a simply line:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
mouseSwiped = YES;
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:drawImage];
UIGraphicsBeginImageContext(drawImage.frame.size);
[drawImage.image drawInRect:CGRectMake(0, 0, drawImage.frame.size.width, drawImage.frame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 15.0);
//eraser
/*
if (eraser){
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0, 0, 0, 0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeCopy);
}*/
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0, 0, 0, 1.0); //black
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
with this code I'm able to color in a view or imageview; I can choose color and size; but now I want to use a specific pattern to color this view; a specific png to use when touchmoved is called; can you help me?
Check out https://stackoverflow.com/a/707329/108574 if you want to draw pattern (tiled images).
However, you are doing it the wrong way in your code. You aren't supposed to draw to the graphics context during UI events. Drawing routines should be inside - (void)drawRect:(CGRect)rect of a UIView instance. When a UI event refreshes graphics, you record the new state somewhere, and then issue - (void)setNeedsDisplay message to the UIView instance to redraw.
Try colorWithPatternImage:
Creates and returns a color object using the specified image.
+ (UIColor *)colorWithPatternImage:(UIImage *)image
(reference)
In my code:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
mouseSwiped = YES;
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:drawImage];
UIGraphicsBeginImageContext(drawImage.frame.size);
[drawImage.image drawInRect:CGRectMake(0, 0, drawImage.frame.size.width, drawImage.frame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10.0);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), r, g, b, a);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
mouseMoved++;
if (mouseMoved == 10) {
mouseMoved = 0;
}
}
I'm able to draw a line in drawImage, but if I want draw a line that remove the other line? as a eraser that cancel the others line drawn. is it possible?
If you want to "erase" pixels by drawing a line that replaces what's underneath with transparency, then you need to make your stroke colour fully transparent but also change the blend mode to kCGBlendModeCopy (with the default blend mode, drawing with a fully transparent stroke colour has of course no effect).
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0, 0, 0, 0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeCopy);
After that, you can draw the "eraser" line just like you are doing in your code snippet (CGContextMoveToPoint, CGContextAddLineToPoint, CGContextStrokePath)