need some help with quartz 2d, it is completely new for me.
Basically my app needs to follow the touch, draw that line starting from center multiple times. The issue is that it has to be dynamic and the lines have to be on equally spread( kind of like octopus starting from center). The way I have it on android is that I remember the shape paths in array, than draw it multiple times with rotating the coordinate system, but I cannot figure out how to do it on iOS.
My rotate function
- (void) rotateContext:(int)angle
{
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), self.center.x, self.center.y);
CGContextRotateCTM(UIGraphicsGetCurrentContext(), radians(angle));
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -self.center.x, -self.center.y);
}
It only works if I try do do it in drawRect(), and it rotates all the paths with it.
Can you please suggest me a good way to solve the problem?
Thanks
This can lead you to a solution:
(maybe it even compiles)
/* setup the context */
UIBezierPath *bpath = [UIBezierPath bezierPath];
UIBezierPath *subpath =
[UIBezierPath bezierPathWithOvalInRect:<#some rect#>];
[bpath appendPath:subpath];
/* add more stuff to the path as you wish */
bezierPath.lineWidth = 2;
/* draw the same path rotated multiple times */
for(NSInteger i = 0; i < 4; i++) {
[bpath applyTransform:CGAffineTransformMakeRotation(M_PI_2 * i)];
[bpath stroke];
}
/* teardown the context */
Rotating a bezier is tricky, you'll need to apply a more complex transformation depending on the results you expect.
Those bezier paths objects can be stored in an array or whatever you need.
Related
I've got a "SLATE2" tablet that allows me to write on a tablet with a special pen and interact with my own app. I'm having some trouble though, and I don't think it's a problem with hardware.
- (void)touchesBegan: (CGPoint)point
{
[path moveToPoint:point];
}
- (void)touchesMoved: (CGPoint)point
{
[path addLineToPoint: point]; // (4)
[Newpath appendPath: path];
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[self setNeedsDisplay];
}
- (void)touchesEnded: (CGPoint)point
{
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[path removeAllPoints];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] setStroke];
[pathTwo stroke];
return;
}
The drawing to the UIView screen is far too slow. I'm looking at the console and the events being raised by the actual tablet are at almost lightspeed. If there is a better way to draw on this UIView faster please show me.
I've dscovered the issue. fitInto is called so frequently and pathTwo is re-drawn every time. I need fitInto to be called everytime theres a new part of the pad that's drawn on to scale how much is shown.
I tried turning off anti-aliasing but that didn't work.
fitInto works like so...
imagine the screen of the iphone
and if you were to draw on a tablet, a square tablet, at the bottom, say a circle, if you were to draw a circle, it would just zoom in on the iphone screen as its the only drawing on the tablet. if you draw at the top of the tablet, the circle at the bottom is now shown to scale of the entire tablet because of the two opposing ends. i don't think this function could be made any faster... but let's try!
func fit(into:CGRect) -> Self {
let bounds = self.cgPath.boundingBox
let sw = into.size.width/bounds.width
let sh = into.size.height/bounds.height
let factor = min (5, min(sw, max(sh, 0.0)))
return scale(x: factor, y: factor, into: into)
}
and here is scale
func scale(x:CGFloat, y:CGFloat, into: CGRect? = nil) -> Self{
var transform = CGAffineTransform(scaleX: x, y: y)
if into != nil {
transform = transform.concatenating(CGAffineTransform(translationX: into!.midX - self.cgPath.boundingBox.midX, y: into!.midY - self.cgPath.boundingBox.midY))
}
let _ = applyCentered(transform: transform)
return self
}
Have you checked how many points are actually in your path? I don't think you want to draw your CGPath pixel by pixel. Instead you should only add a segment when the distance exceeds a certain threshold. You can also simplify the path by drawing curves through the points instead of drawing line segments. See this primer on bezier curves.
EDIT: The Ray Wenderlich sample app looks like its actually doing the same thing, adding points in TouchedMoved. However in touchesEnded they rasterize the curve into a bitmap. I'm not so sure that that will help your case though because it looks like you are deleting all of the points in touchesEnded. I would still look at their code and see if you have any other points that could be a bottle neck.
What effect are you after? Are you trying to let the user trace lines and curves with the pen, or dots if they tap?
The simple fact is that iOS is not fast enough to track the user's pen/finger strokes point-by-point. Instead, you want to collect a series of points and create a UIBezierPath that connects those points with line segments. That will give you something pretty close to what you want. However, if the user traces fast, that will lag behind as well.
I am able to generate the UIBezierPath of characters with whatever selected font and size. Now I want to make an etched line in between the bezier path. Can I get the center points of the bezier path along? Or any other way that I can make the center dotted line and follow that path?
Here is the code how I do so
Reference Link. I want something like this. :
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 568, 568) cornerRadius:0];
UIBezierPath *circlePath = [self createArcPath];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
shapeView = [CAShapeLayer layer];
shapeView.geometryFlipped = false;
shapeView.path = path.CGPath;
shapeView.fillRule = kCAFillRuleEvenOdd;
shapeView.fillColor = [UIColor grayColor].CGColor;
shapeView.opacity = 1.0;
shapeView.lineDashPattern = #[#2, #3];
[self.view.layer addSublayer:shapeView];
CGFloat dashes[] = {2, 3};
[path setLineDash:dashes count:2 phase:0];
- (UIBezierPath *)createArcPath
{
// Create path from text
// See: http://www.codeproject.com/KB/iPhone/Glyph.aspx
// License: The Code Project Open License (CPOL) 1.02 http://www.codeproject.com/info/cpol10.aspx
letters = CGPathCreateMutable();
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica-Bold"),80, NULL);
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)font, kCTFontAttributeName,//د
nil];//ج
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:#"H"
attributes:attrs];
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attrString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// for each RUN
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get FONT for this run
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// for each GLYPH in run
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// get Glyph & Glyph-data
CFRange thisGlyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, thisGlyphRange, &glyph);
CTRunGetPositions(run, thisGlyphRange, &position);
// Get PATH of outline
{
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x+200, position.y+80);
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
}
CFRelease(line);
self.path = [UIBezierPath bezierPath];
[self.path appendPath:[UIBezierPath bezierPathWithCGPath:letters]];
return self.path;
}
Can I get the center points of the bezier path along? Or any other way
that I can make the center dotted line and follow that path?
To know that user's finger is going along the path or not you have to Hit-Detection on a Path.
To determine whether a touch event occurred on the filled portion of a
path, you can use the containsPoint: method of UIBezierPath. This
method tests the specified point against all closed subpaths in the
path object and returns YES if it lies on or inside any of those
subpaths.
If you want to do hit-testing on the stroked portion of the path
(instead of the fill area), you must use Core Graphics. The
CGContextPathContainsPoint function lets you test points on either the
fill or stroke portion of the path currently assigned to the graphics
context.
Below method tests to see whether the specified point intersects the specified path. The inFill parameter lets the caller specify whether the point should be tested against the filled or stroked portion of the path. The path passed in by the caller must contain one or more closed subpaths for the hit detection to succeed.
Testing points against a path object.
- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath *)path inFillArea:(BOOL)inFill
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef cgPath = path.CGPath;
BOOL isHit = NO;
// Determine the drawing mode to use. Default to
// detecting hits on the stroked portion of the path.
CGPathDrawingMode mode = kCGPathStroke;
if (inFill)
{
// Look for hits in the fill area of the path instead.
if (path.usesEvenOddFillRule)
mode = kCGPathEOFill;
else
mode = kCGPathFill;
}
// Save the graphics state so that the path can be removed later.
CGContextSaveGState(context);
CGContextAddPath(context, cgPath);
// Do the hit detection.
isHit = CGContextPathContainsPoint(context, point, mode);
CGContextRestoreGState(context);
return isHit;
}
Check this link to know more about Hit-Detection on a Path
Getting center of path
You can get the width of path as myPath.bounds.size.width; and to get the center of the path just divide width by 2.
And to draw dashed line check this answer
To make dashed line on any UIBezierPath as:
CGFloat dashes[] = {2, 3};
//passing an array with the values {2,3} sets a dash pattern that alternates between a 2 space-unit-long painted segment and a 3 space-unit-long unpainted segment.
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineDash:dashes count:2 phase:0];
and to dash on CAShapeLayer use the property lineDashPattern as:
shapeView.lineDashPattern = #[#2, #3];
While bezier paths are a convenient option for simple drawing, they are complex calculations, so it will be prohibitively expensive and complicated to use them for this purpose. That's because you need to be able to calculate arbitrary points along the path and to know the prior point in order to know the current direction.
You need some other description of the characters to allow you to know these details more easily. That means using a simple vector (or stroke) description of the fonts (as mentioned by rob mayoff in the comments). This description breaks the characters down into a number of straight line segments which are easy to work with. They are all governed by a y=mx+c calculation and you always know 2 points on each line segment so it's easy to interpolate between them and to know the direction of movement.
If the descriptions at the link provided by rob (here) aren't 'accurate' enough for the size at which you want to display the characters you can create new versions with more points in order to achieve a closer approximation to bezier curve options.
Now, with this description you have a lot of options...
For dragging a finger along the path you can interpolate between points to find the closest point on the path to the current touch point and determine when the touch point has strayed too far from the path, or has intersected it. This intersection processing is also how you can determine the coverage percentage. You just need to choose the interpolation gap distance (chosen for a suitable resolution without creating too many points that are really close together) and a tolerance for how far touch points can be from the 'next' path point.
This also allows for other things in the video you link, like dropping images at each point along the (interpolated) path and animating those images around.
You can calculate points along a UIBezierPath with the code in this open source repo:
https://github.com/ImJCabus/UIBezierPath-Length
If you have your 'a' character represented as a bezier path, you can calculate the red points in the image above by writing something like:
UIBezierPath *path = /* ... */;
NSUInteger subdivisions = 100;//however precise you want to be
for(NSUInteger i = 0; i < subdivisions; i++) {
CGFloat percent = (CGFloat)i/subdivisions;
CGPoint point = [path pointAtPercentOfLength:percent];
//draw a dot at this point, or trace it, or whatever you want to do
}
Try this code to draw red dot line:
CAShapeLayer* dotLayer = [CAShapeLayer layer];
dotLayer.path = path.CGPath;
dotLayer.strokeColor = [UIColor redColor].CGColor;
dotLayer.lineWidth = 2;//or the diameter of the dots
dotLayer.lineDashPattern = #[#0, #10];//change 10 to the distance between dots
dotLayer.lineJoin = kCALineJoinRound;
dotLayer.lineCap = kCALineCapRound;
What you want is rather complicated, but I believe it can be achieved with the following steps:
1) get the letter outline path using your method
2) create a new path using CGPathCreateCopyByStrokingPath with lineWidth equal 1
3) get all segments of the new path
4) find all intersection points of all segments
5) determine which intersecting lines are adjacent and use their intersection points to form a center line
6) repeat steps 2-6 increasing the lineWidth value in step 2
I change the question details because I don't have enough reputation for more links, thanks for understand.
I've used UIBezierPath to draw rects/lines, but it is slowly for drawing.
It's running on a device(iPhone5, iOS 9.2; iPhone5C, iOS8.4);
The running time is about 30s, I looked in the Time Profiler in the instruments, all blocked at the UIBezierPath's stroke method;
Below is comes from method [- (void)drawSeriesBar:(CGRect)rect cellData:(CerKlineItem *)data]:
Another method:
Does anyone who knows why the UIBezierPath spends long time? And how to fix it? Thanks for kindly help!
Place code here:
//draw line
UIBezierPath * path = [UIBezierPath bezierPath];
[color setStroke];
CGFloat highHeight = heightForPrice(data.high,rect);
CGPoint highPoint = CGPointMake(CGRectGetMinX(rect) + CGRectGetWidth(rect) / 2 , CGRectGetMinY(rect) + highHeight);
[path moveToPoint:highPoint];
CGFloat lowHeight = heightForPrice(data.low,rect);
CGPoint lowPoint = CGPointMake(CGRectGetMinX(rect) + CGRectGetWidth(rect) / 2 , CGRectGetMinY(rect) + lowHeight);
[path addLineToPoint:lowPoint];
[path setLineWidth:1];
[path stroke];
Your two versions are far from being equivalent:
the first one contains: move, line, move, line, line, line, as you don't clear the path before adding the second part. Also, the drawing code does not know it's a rectangle, it's using generic polygon fill code
the second one is just filling a rectangle
You may try replacing the first one with a new path, created using bezierPathWithRect:.
I am making a game for iOS. I inputed two images, one will be used as the player, and the other which is an object. I want to code that if the two objects intersect, then it runs the method EndGame.
-(void)Collision{
if (CGRectIntersectsRect(Ball.frame, Platform1.frame)) {
[self EndGame];
}
}
Although, The images are not a square shape, but the UIImage is a square. Therefore when the two objects intersect it Ends the game, even if the two images look like they came close it still ends the game because of the incorrect collision detection. Do you have suggestions?
Can I change the image shape on XCODE or can I make it so if the image collides with a certain point on the players image?
Thanks.
create a custom view and in draw rect create a bezier path. Something like this
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:ball.center radius:ballRadius startAngle:0.0 endAngle:M_PI*2.0 clockwise:YES];
This will draw a circle view
EDIT: heres a sample - I'm not sure exactly what all the details are, so this is pretty generic
#interface ballView : UIView//create subclass of UIView
...
#implementation ...
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];//create bezier path
[path addArcWithCenter:self.center radius:5.0 startAngle:0.0 endAngle:M_PI*2.0 clockwise:YES];//draw circle. You can change around the parameters to the way you like
[[UIColor orangeColor]setStroke];
[path stroke];//draw line
I'm trying to programmatically recreate the indented button look that can be seen on a UINavigationBarButton. Not the shiny two tone look or the gradient, just the perimeter shading:
It looks like an internal dark shadowing around the entire view perimeter, slightly darker at the top? And then an external highlighting shadow around the lower view perimeter.
I've played a bit with Core Graphics, and experimented with QuartzCore and shadowing with view.layer.shadowRadius and .shadowOffset, but can't even get the lower highlighting to look right. I'm also not sure where to start to achieve both a dark shadowing with internal offset and a light shadowing with external offset.
It seems as though you want a border that looks looks like a shadow. Since the shadow appears to some sort of gradient, setting a border as a gradient won't be possible at first glance. However, it is possible to create a path that represents the border and then fill that with a gradient. Apple provides what seems to be a little known function called CGPathCreateCopyByStrokingPath. This takes a path (say, a rounded rect, for example) and creates a new path that would be the stroke of the old path given the settings you pass into the function (like line width, join/cap setting, miter limit, etc). So lets say you define a path (this isn't exactly what Apple provides, but's it's similar):
+ (UIBezierPath *) bezierPathForBackButtonInRect:(CGRect)rect withRoundingRadius:(CGFloat)radius{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint mPoint = CGPointMake(CGRectGetMaxX(rect) - radius, rect.origin.y);
CGPoint ctrlPoint = mPoint;
[path moveToPoint:mPoint];
ctrlPoint.y += radius;
mPoint.x += radius;
mPoint.y += radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:M_PI + M_PI_2 endAngle:0 clockwise:YES];
mPoint.y = CGRectGetMaxY(rect) - radius;
[path addLineToPoint:mPoint];
ctrlPoint = mPoint;
mPoint.y += radius;
mPoint.x -= radius;
ctrlPoint.x -= radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
mPoint.x = rect.origin.x + (10.0f);
[path addLineToPoint:mPoint];
[path addLineToPoint:CGPointMake(rect.origin.x, CGRectGetMidY(rect))];
mPoint.y = rect.origin.y;
[path addLineToPoint:mPoint];
[path closePath];
return path;
}
This returns a path similar to Apple's back button (I use this in my app). I have added this method (along with dozens more) as a category to UIBezierPath.
Now lets add that inner shadow in a drawing routine:
- (void) drawRect:(CGRect)rect{
UIBezierPath *path = [UIBezierPath bezierPathForBackButtonInRect:rect withRoundingRadius:5.0f];
//Just fill with blue color, do what you want here for the button
[[UIColor blueColor] setFill];
[path fill];
[path addClip]; //Not completely necessary, but borders are actually drawn 'around' the path edge, so that half is inside your path, half is outside adding this will ensure the shadow only fills inside the path
//This strokes the standard path, however you might want to might want to inset the rect, create a new 'back button path' off the inset rect and create the inner shadow path off that.
//The line width of 2.0f will actually show up as 1.0f with the above clip: [path addClip];, due to the fact that borders are drawn around the edge
UIBezierPath *innerShadow = [UIBezierPath bezierPathWithCGPath: CGPathCreateCopyByStrokingPath(path.CGPath, NULL, 2.0f, path.lineCapStyle, path.lineJoinStyle, path.miterLimit)];
//You need this, otherwise the center (inside your path) will also be filled with the gradient, which you don't want
innerShadow.usesEvenOddFillRule = YES;
[innerShadow addClip];
//Now lets fill it with a vertical gradient
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint start = CGPointMake(0, 0);
CGPoint end = CGPointMake(0, CGRectGetMaxY(rect));
CGFloat locations[2] = { 0.0f, 1.0f};
NSArray *colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:.7f alpha:.5f].CGColor, (id)[UIColor colorWithWhite:.3f alpha:.5f].CGColor, nil];
CGGradientRef gradRef = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (__bridge CFArrayRef)colors, locations);
CGContextDrawLinearGradient(context, gradRef, start, end, 0);
CGGradientRelease(gradRef);
}
Now this is just a simple example. I don't save/restore contexts or anything, which you'll probably want to do. There are things you might still want to do to make it better, like maybe inset the 'shadow' path if you want to use a normal border. You might want to use more/different colors and locations. But this should get you started.
UPDATE
There is another method you can use to create this effect. I wrote an algorithm to bevel arbitrary bezier paths in core graphics. This can be used to create the effect you're looking for. This is an example of how I use it in my app:
You pass to the routine the CGContextRef, CGPathRef, size of the bevel and what colors you want it to use for the highlight/shadow.
The code I used for this can be found here:Github - Beveling Algorithm.
I also explain the code and my methodology here: Beveling-Shapes in Core Graphics
Using the layer's shadow won't do it. You need both a light outer shadow and a dark inner shadow to get that effect. A layer can only have one (outer) shadow. (Also, layer shadows are redrawn dynamically, and force CPU-based rendering which kills performance.)
You'll need to do your own drawing with CoreGraphics, either in a view's drawRect: method or a layer's drawInContext: method. (Or you draw into an image context and then reuse the image.) Said drawing will mostly use CGContext functions. (I'll name some below, but this link has documentation for them all.)
For a round rect button, you might find it tedious to create the appropriate CGPath -- instead, you can use +[UIBezierPath bezierPathWithRoundedRect:cornerRadius:] and then the path's CGPath property to set the context's current path with CGContextAddPath.
You can create an inner shadow by setting a clipping path (see CGContextClip and related functions) to the shape of the button, setting up a shadow (see CGContextSetShadowWithColor and related functions), and then drawing around the outside of the shape you want shadowed. For the inner shadow, stroke (CGContextStrokePath) a round-rect that's a bit larger than your button, using a thick stroke width (CGContextSetLineWidth) so there's plenty of "ink" to generate a shadow (remember, this stroke won't be visible due to the clipping path).
You can create an outer shadow in much the same way -- don't use a clipping path this time, because you want the shadow to be outside the shape, and fill (CGContextFillPath) the shape of your button instead of stroking it. Note that drawing a shadow is sort of a "mode": you save the graphics state (CGContextSaveGState), setup a shadow, then draw the shape you want to see a shadow of (the shape itself isn't drawn when you're in this mode), and finally restore state (CGContextRestoreGState) to get out of "shadow mode". Since that mode doesn't draw the shape, only the shadow, you'll need to draw the shape itself separately.
There's an order to do this all in, too. It should be obvious if you think about the order in which you'd paint these things with physical media: First draw the outer shadow, then the button's fill, then the inner shadow. You might add a stroke after that if the inner shadow doesn't give you a pronounced enough outline.
There are a few drawing tools which can output source code for CoreGraphics: Opacity is one that I use. Be careful with these, though, as they code they generate may not be efficient.