I need to build a realtime plot sin(x)
So I update it 25 times per second. I try to use gradient for the plot line to make it transparent in the end:
Everything looks good - but the performance is critically low.. When I not creating gradient - everything works good.
The needed plot looks like this:
Is it possible to improve my code to solve the performance issue?
CGContextSaveGState(context);
CGFloat value;
UIBezierPath *graph = [UIBezierPath new];
for (NSUInteger counter = 0; counter < historyArray.count; counter++) {
value = [historyArray[counter] floatValue];
if (counter == 0) {
[graph moveToPoint:CGPointMake((CGFloat) (bounds.origin.x + bounds.size.width * 0.75 / 40), (CGFloat) (bounds.origin.y + bounds.size.height / 2 - value * bounds.size.height / 2.1))];
} else {
[graph addLineToPoint:CGPointMake((CGFloat) (bounds.origin.x + (float) counter / (float) (40 - 1) * bounds.size.width * 0.75), (CGFloat) (bounds.origin.y + bounds.size.height / 2 - value * bounds.size.height / 2.1))];
}
}
if (historyArray.count > 0) {
CGFloat colors[] = {
0.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0,
0.0, 1.0, 1.0, 1.0
};
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 3);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;
CGContextSetLineWidth(context, 2.f);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextAddPath(context, graph.CGPath);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
// Define the start and end points for the gradient
// This determines the direction in which the gradient is drawn
CGPoint startPoint = bounds.origin;
CGPoint endPoint = CGPointMake(bounds.origin.x + bounds.size.width, bounds.origin.y);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(context);
}
Creating the gradient isn't likely to be the performance problem. It's more likely clipping it to a complex path. It's not totally clear from your code how many line segments your path has, but I suspect it's in the hundreds, right?
As for more-performant ways to do this:
Reduce the number of points in the path - if you're plotting points every X pixels horizontally, then you're plotting more than you need when the curve isn't changing much (near the peaks and troughs).
Do away with the gradient altogether, and just draw your path with multiple differently-colored line segments. This might actually end up looking better, anyway, since you can adjust the transparency by distance traveled, rather than by horizontal coordinate.
If you really do want the horizontal gradient, try applying it to subsets of the path, which might be faster than doing it all at once.
Related
I need a radial gradient in the shape of an oval or ellipse and it seems like it CGContextDrawRadialGradient can only draw a perfect circle. I've been drawing to a square context then copying/drawing into a rectangular context.
Any better way to do this?
Thanks!
The only way I've found to do this is as Mark F suggested, but I think the answer needs an example to be easier to understand.
Draw an elliptical gradient in a view in iOS (and using ARC):
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// Create gradient
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = {0.0, 1.0};
UIColor *centerColor = [UIColor orangeColor];
UIColor *edgeColor = [UIColor purpleColor];
NSArray *colors = [NSArray arrayWithObjects:(__bridge id)centerColor.CGColor, (__bridge id)edgeColor.CGColor, nil];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
// Scaling transformation and keeping track of the inverse
CGAffineTransform scaleT = CGAffineTransformMakeScale(2, 1.0);
CGAffineTransform invScaleT = CGAffineTransformInvert(scaleT);
// Extract the Sx and Sy elements from the inverse matrix
// (See the Quartz documentation for the math behind the matrices)
CGPoint invS = CGPointMake(invScaleT.a, invScaleT.d);
// Transform center and radius of gradient with the inverse
CGPoint center = CGPointMake((self.bounds.size.width / 2) * invS.x, (self.bounds.size.height / 2) * invS.y);
CGFloat radius = (self.bounds.size.width / 2) * invS.x;
// Draw the gradient with the scale transform on the context
CGContextScaleCTM(ctx, scaleT.a, scaleT.d);
CGContextDrawRadialGradient(ctx, gradient, center, 0, center, radius, kCGGradientDrawsBeforeStartLocation);
// Reset the context
CGContextScaleCTM(ctx, invS.x, invS.y);
// Continue to draw whatever else ...
// Clean up the memory used by Quartz
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
}
Put in a view with a black background you get:
You can change the transform of the context to draw an ellipse (for example, apply CGContextScaleCTM(context, 2.0, 1.0) just before calling CGContextDrawRadialGradient () to draw an elliptical gradient that's twice as wide as it is high). Just remember to apply the inverse transform to your start and end points, though.
I'm ultimately trying to create a gradient overlay to fade from 60% black to clear, to 40% black. The result has a lot of banding, so I'm just trying to get two colours working properly first.
In this first case, I'm trying a gradient from solid black to solid white which draws pretty much perfectly.
In the second case, I'm placing a gradient from 100% black to clear, against a solid gray background. The banding is really awful.
The code to make this gradient looks like this:
// Draw the overlay gradient
CGContextTranslateCTM(context, 0.0, size.height);
CGContextScaleCTM(context, 1.0, -1.0);
UIColor *firstColor = [UIColor colorWithWhite:0.0 alpha:1.0];
UIColor *secondColor = [UIColor clearColor];
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSArray *coreGraphicsColors = #[(id)firstColor.CGColor, (id)secondColor.CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)coreGraphicsColors, 0);
static const CGFloat standardDegree = 270;
const CGFloat degree = (standardDegree * M_PI / 180);
const CGPoint center = CGPointMake(size.width / 2, size.height / 2);
const CGPoint startPoint = CGPointMake(center.x - cos(degree) * size.width / 2, center.y - sin(degree) * size.height / 2);
const CGPoint endPoint = CGPointMake(center.x + cos(degree) * size.width / 2, center.y + sin(degree) * size.height / 2);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
Does anyone have some experience/tips with creating these gradients which are partly transparent? I've tried altering a few things like the positions and color space, all to no avail.
This might help you AlphaGradientView
I am trying to draw an annotation for map , my view is a subclass of MKAnnotationView
I need a shape something like shown below
What I am getting is like this :
Here is the code which I am using :
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
You can achieve the desired effect if you replace your lines with quad curves:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddQuadCurveToPoint(ctx, topLeft.x, height * 2 / 3, midBottom.x, midBottom.y);
CGContextAddQuadCurveToPoint(ctx, topRight.x, height * 2 / 3, topRight.x, topRight.y + height/2);
// CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
// CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
This employs a quadratic bezier curve, which is a good curve when you don't want inflection points. You can achieve a similar curve with the cubic bezier, but if you're not careful with your control points, you can get undesired inflection points. The quadratic curve is just a little easier with only one control point per curve.
By choosing control points with x values the same as the start and end of the semicircle, it ensures a smooth transition from the circle to the curve leading down to the point. By choosing y values for those control points which are relatively close to the start and end of the semicircle, it ensures that the curve will transition quickly from the semicircle to the point. You can adjust these control points, but hopefully this illustrates the idea.
For illustration of the difference between these two types of curves, see the Curves section of the Paths chapter of the Quartz 2D Programming Guide.
I am able to draw a square pixel by pixel as below
for(int i=0 ;i<drawbox.size.width/2;i++)
{
for (int j=0; j<drawbox.size.height/2; j++)
{
Point.y++;
NSLog(#"point:%f,%f",Point.x,Point.y);
}
Point.x++;
}
Here drawrect is CGRect and Point is the CGPoint I am using to draw pixel by pixel
I iterate over this and find a square to be made. This square is filled with each pixel so it just not draw a square with border but it includes all pixels within the square.
I want the same thing but for circle (filled circle's pixels).
How can I achieve this?
Override your drawRect with following code :
You need to take care of 5 things :
SIDE_WEITH = Width of Circle,
Color constants :
_r = Red
_g = Green
_b = Blue
_a = Alpha
And set Progress as per your need : _progress
That it.
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
//// [image drawInRect:rect];
// find the radius and position for the largest circle that fits in the UIView's frame.
int radius, x, y;
int offset = SIDE_WEITH;
// in case the given frame is not square (oblong) we need to check and use the shortest side as our radius.
CGRect frame = self.frame;
if (frame.size.width > frame.size.height) {
radius = frame.size.height;
// we want our circle to be in the center of the frame.
int delta = frame.size.width - radius;
x = delta/2 - 1;
y = 0;
} else {
radius = frame.size.width;
int delta = frame.size.height - radius;
y = delta/2 - 1;
x = 0;
}
// store the largest circle's position and radius in class variable.
_outerCircleRect = CGRectMake(x, y, radius, radius);
// store the inner circles rect, this inner circle will have a radius 10pixels smaller than the outer circle.
// we want to the inner circle to be in the middle of the outer circle.
//_innerCircleRect = CGRectMake(x+offset, y+offset, radius-2*offset , radius-2*offset);
_innerCircleRect = CGRectMake(x+offset, y+offset, radius-2*offset , radius-2*offset);
// get the drawing canvas (CGContext):
CGContextRef context = UIGraphicsGetCurrentContext();
// save the context's previous state:
CGContextSaveGState(context);
// our custom drawing code will go here:
// Draw the gray background for our progress view:
// gradient properties:
CGGradientRef myGradient;
// You need tell Quartz your colour space (how you define colours), there are many colour spaces: RGBA, black&white...
CGColorSpaceRef myColorspace;
// the number of different colours
size_t num_locations = 3;
// the location of each colour change, these are between 0 and 1, zero is the first circle and 1 is the end circle, so 0.5 is in the middle.
CGFloat locations[3] = { 0.0, 0.5 ,1.0 };
// this is the colour components array, because we are using an RGBA system each colour has four components (four numbers associated with it).
CGFloat components[12] = {
0.4, 0.4, 0.4, 0.9, // Start colour
0.9, 0.9, 0.9, 1.0, // middle colour
0.4, 0.4, 0.4, 0.9
}; // End colour
myColorspace = CGColorSpaceCreateDeviceRGB();
myGradient = CGGradientCreateWithColorComponents (myColorspace, components,locations, num_locations);
// gradient start and end points
CGPoint myStartPoint, myEndPoint;
CGFloat myStartRadius, myEndRadius;
myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2;
myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2;
myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2;
myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2;
myStartRadius = _innerCircleRect.size.width/2 ;
myEndRadius = _outerCircleRect.size.width/2;
// draw the gradient.
/*CGContextDrawRadialGradient(context,
myGradient,
myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0);
CGGradientRelease(myGradient);*/
// draw outline so that the edges are smooth:
// set line width
//CGContextSetLineWidth(context, 1);
// set the colour when drawing lines R,G,B,A. (we will set it to the same colour we used as the start and end point of our gradient )
/*CGContextSetRGBStrokeColor(context, 0.4,0.4,0.4,0.9);
// draw an ellipse in the provided rectangle
CGContextAddEllipseInRect(context, _outerCircleRect);
CGContextStrokePath(context);*/
/*CGContextAddEllipseInRect(context, _innerCircleRect);
CGContextStrokePath(context);*/
// Draw the progress:
// First clip the drawing area:
// save the context before clipping
CGContextSaveGState(context);
CGContextMoveToPoint(context,
_outerCircleRect.origin.x + _outerCircleRect.size.width/2, // move to the top center of the outer circle
_outerCircleRect.origin.y +1); // the Y is one more because we want to draw inside the bigger circles.
// add an arc relative to _progress
CGContextAddArc(context,
_outerCircleRect.origin.x + _outerCircleRect.size.width/2,
_outerCircleRect.origin.y + _outerCircleRect.size.width/2,
_outerCircleRect.size.width/2-1,
-M_PI/2,
(-M_PI/2 + _progress*2*M_PI), 0);
CGContextAddArc(context,
_outerCircleRect.origin.x + _outerCircleRect.size.width/2,
_outerCircleRect.origin.y + _outerCircleRect.size.width/2,
_outerCircleRect.size.width/2 - 9,
(-M_PI/2 + _progress*2*M_PI),
-M_PI/2, 1);
// use clode path to connect the last point in the path with the first point (to create a closed path)
CGContextClosePath(context);
// clip to the path stored in context
CGContextClip(context);
// Progress drawing code comes here:
// set the gradient colours based on class variables.
CGFloat components2[12] = { _r, _g, _b, _a, // Start color
((_r + 0.5 > 1) ? 1 : (_r+0.5) ) , ((_g + 0.5 > 1) ? 1 : (_g+0.5) ), ((_b + 0.5 > 1) ? 1 : (_b+0.5) ), ((_a + 0.5 > 1) ? 1 : (_a+0.5)),
_r, _g, _b, _a }; // End color
myGradient = CGGradientCreateWithColorComponents (myColorspace, components2,locations, num_locations);
myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2;
myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2;
myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2;
myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2;
// set the radias for start and endpoints a bit smaller, because we want to draw inside the outer circles.
myStartRadius = _innerCircleRect.size.width/2;
myEndRadius = _outerCircleRect.size.width/2;
CGContextDrawRadialGradient(context,
myGradient,
myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0);
// release myGradient and myColorSpace
CGGradientRelease(myGradient);
CGColorSpaceRelease(myColorspace);
// draw circle on the outline to smooth it out.
CGContextSetRGBStrokeColor(context, _r,_g,_b,_a);
// draw an ellipse in the provided rectangle
CGContextAddEllipseInRect(context, _outerCircleRect);
CGContextStrokePath(context);
CGContextAddEllipseInRect(context, _innerCircleRect);
CGContextStrokePath(context);
//restore the context and remove the clipping area.
CGContextRestoreGState(context);
// restore the context's state when we are done with it:
CGContextRestoreGState(context);
/*CGPathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath , NULL , rect);
CAShapeLayer *circle = [[CAShapeLayer alloc] init];
circle.path = circlePath;
circle.opacity = 0.5;
[self.imageView.layer addSublayer:circle];
CGPathRelease( circlePath );
[circle release];*/
}
for (int i=0; i<drawbox.size.width/2; i++) {
for (int j=(int)(-drawbox.size.height/4.0 * sqrt(1 - pow(4.0*i/drawbox.size.width - 1, 2)) + drawbox.size.height/4.0);
j<=(int)(drawbox.size.height/4.0 * sqrt(1 - pow(4.0*i/drawbox.size.width - 1, 2)) + drawbox.size.height/4.0);
j++)
{
Point.y++;
NSLog(#"point:%f,%f",Point.x,Point.y);
}
tStartPoint.x++;
}
This will draw an ellipse with the same center the rectangle has you drow in the question.
Currently in my drawRect: method I am drawing a line between every point in my set of points, each represented as a CGPoint; however, now I would like to fill in the area that is within the region of these set points. I cannot figure how to do this using the quartz api, is there a way?
Right now, the points are ordered. So it is possible to recognize what point represents the first point of the polygon and so on.
Add your points to an UIBezierPath and then use it's fill method.
This code sample from Apple shows what you need to do:
-(void)drawInContext:(CGContextRef)context
{
// Drawing with a white stroke color
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
// Drawing with a blue fill color
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGPoint center;
// Add a star to the current path
center = CGPointMake(90.0, 90.0);
CGContextMoveToPoint(context, center.x, center.y + 60.0);
for(int i = 1; i < 5; ++i)
{
CGFloat x = 60.0 * sinf(i * 4.0 * M_PI / 5.0);
CGFloat y = 60.0 * cosf(i * 4.0 * M_PI / 5.0);
CGContextAddLineToPoint(context, center.x + x, center.y + y);
}
// And close the subpath.
CGContextClosePath(context);
// Now add the hexagon to the current path
center = CGPointMake(210.0, 90.0);
CGContextMoveToPoint(context, center.x, center.y + 60.0);
for(int i = 1; i < 6; ++i)
{
CGFloat x = 60.0 * sinf(i * 2.0 * M_PI / 6.0);
CGFloat y = 60.0 * cosf(i * 2.0 * M_PI / 6.0);
CGContextAddLineToPoint(context, center.x + x, center.y + y);
}
// And close the subpath.
CGContextClosePath(context);
// Now draw the star & hexagon with the current drawing mode.
CGContextDrawPath(context, drawingMode);
}
Note, this was mentioned in an answer to a similar question.