I'm trying to draw a stroke of Bezier Curve with linear gradient going from red to green. What I do at the moment is:
NSBezierPath* path = [NSBezierPath bezierPath];
[path setLineWidth: 1];
NSPoint startPoint = { 10, 100 };
NSPoint endPoint = { 590, 500 };
int r1 = arc4random() % 1000;
int r2 = arc4random() % 1000;
NSPoint cp1 = { 700, -500 + r1 };
NSPoint cp2 = { -500 + r2, 700 };
[path moveToPoint: startPoint];
[path curveToPoint: endPoint
controlPoint1: cp1
controlPoint2: cp2];
if (curves.count == 50) {
[curves removeObjectAtIndex:0];
}
[curves addObject:path];
int i = 0;
for (NSBezierPath * p in curves) {
[[redColors objectAtIndex:i++] set];
[p stroke];
}
And this works great, but when I convert NSBezierPath path to CGPathRef myPath = [path quartzPath] and iterate over 'CGPathRef' instead of 'NSBezierPath':
CGPathRef myPath = [path quartzPath];
if (curves.count == size) {
[paths removeObjectAtIndex:0];
}
[paths addObject:myPath];
CGContextRef c = [[NSGraphicsContext currentContext]graphicsPort];
for (int i = 0; i < paths.count; i++) {
[self drawPath:c :[paths objectAtIndex:i]:i];
}
My performance drops from about 30 FPS to 5 FPS!
Here is my code for drawPath:
-(void) drawPath:(CGContextRef) c: (CGPathRef) myPath: (int) i {
CGContextSaveGState(c);
CGContextAddPath(c, myPath);
CGContextReplacePathWithStrokedPath(c);
CGContextClip(c);
// Draw a linear gradient from top to bottom
CGContextDrawLinearGradient(c, cfGradients[i], start, end, 0);
CGContextRestoreGState(c);
}
redColors and cfGradients are arrays storing elements with alpha from 0-1/0-255, so they don't need to be recreated at each iteration.
This performance is even much worse than in Java. Surely there must be a way to draw a stroke more efficiently without all this transitions from NSBezierPath to CGPathRef, etc.
Please help.
Related
I'm trying to create a dashed line using an SKShapeNode in SpriteKit for Mac OS X in Objective C, not iOS and not in Swift. I've taken a look at many SO articles for this and tried to piece it together. The main issue that a lot of them are in Swift. Here's what I've come up with so far, but it's not showing anything and I'm not sure why:
-(void) drawBarLineDivisions {
...
NSBezierPath *path = [NSBezierPath bezierPath];
CGPoint startPoint = CGPointMake(0, 250);
CGPoint endPoint = CGPointMake(450, 250);
[path moveToPoint:startPoint];
[path lineToPoint:endPoint];
CGFloat pattern[2] = {10.0, 10.0};
CGPathRef dashedLine = CGPathCreateCopyByDashingPath ([self CGPathFromPath:path], nil, 0, pattern, 2);
SKShapeNode *line = [SKShapeNode shapeNodeWithPath:dashedLine];
CGPathRelease(dashedLine);
int x = i/(double)measureDivisions*[self getGridArea]/[self getMeasuresDisplayed];
[line setPosition:CGPointMake(x, 0)];
[line setFillColor:fillColor];
[line setStrokeColor:[NSColor clearColor]];
[self addChild:line];
}
//helper method for converting NSBezierPath into CGPath
- (CGMutablePathRef)CGPathFromPath:(NSBezierPath *)path
{
CGMutablePathRef cgPath = CGPathCreateMutable();
NSInteger n = [path elementCount];
for (NSInteger i = 0; i < n; i++) {
NSPoint ps[3];
switch ([path elementAtIndex:i associatedPoints:ps]) {
case NSMoveToBezierPathElement: {
CGPathMoveToPoint(cgPath, NULL, ps[0].x, ps[0].y);
break;
}
case NSLineToBezierPathElement: {
CGPathAddLineToPoint(cgPath, NULL, ps[0].x, ps[0].y);
break;
}
case NSCurveToBezierPathElement: {
CGPathAddCurveToPoint(cgPath, NULL, ps[0].x, ps[0].y, ps[1].x, ps[1].y, ps[2].x, ps[2].y);
break;
}
case NSClosePathBezierPathElement: {
CGPathCloseSubpath(cgPath);
break;
}
default: NSAssert(0, #"Invalid NSBezierPathElement");
}
}
return cgPath;
}
Recently I was coding in Spritekit
What I need is to find a point of the bezier curve while SKNode is moving follow the UIBezierPath.BTW the bezier curve just has one control point.
I use the equation of bezier curve to find the 't', and use this 't' to get the point,I found that my code is just fit with the "straight line" of the bezier curve.If there is a big curve, I would be wrong, I think the speed of SKNode is not constant.I am very pleasure to know where is not right or get other solution.
Here is my code, it is for testing.
-(void)setupTestCurve
{
float ts =0.56f;
float timeofmove = 5.0f;
SKSpriteNode* arrow = [SKSpriteNode spriteNodeWithImageNamed:#"blue_b_01"];
arrow.scale = 0.8;
arrow.anchorPoint = CGPointMake(0.5, 0.5);
[background addChild:arrow];
CGPoint point1 = CGPointMake(400, 400);
CGPoint point2 = CGPointMake(800, 100);
CGPoint controlP = CGPointMake(100, 100);
[ToolObjective setupTestCurve:(SKSpriteNode *)background startp:point1 endp:point2 anchorPoint:controlP];
CGPoint ps = [ToolObjective get3dPointbezierInterpolation:ts startp:point1 controlp:controlP endp:point2];
float twill = [ToolObjective get3dTofbezierInterpolation:point1 nowPoint:ps endp:point2 anchorPoint:controlP];
SKAction *w = [SKAction waitForDuration:twill * timeofmove];
SKAction *paction = [SKAction runBlock:^{
[ToolObjective setupTestpoint:background startp:ps color:[UIColor redColor]];
}];
SKAction *m = [SKAction sequence:#[w,paction]];
CGPathRef patharrow = [ToolObjective Get3dBezierPath:point1 endPoint:point2 anchorPoint:controlP];
SKAction *moveBow = [SKAction followPath:patharrow asOffset:NO orientToPath:YES duration:timeofmove];
[arrow runAction:[SKAction group:#[m,moveBow]]];
}
here is the tool function
/*
add a bezier curve to the background
*/
+(void)setupTestCurve:(SKSpriteNode *)background startp:(CGPoint)startPoint endp:(CGPoint)endpoint anchorPoint:(CGPoint)anchorPoint
{
UIBezierPath *path=[UIBezierPath bezierPath];
[path moveToPoint:startPoint];
[path addQuadCurveToPoint:endpoint controlPoint:anchorPoint];
SKShapeNode *circle = [SKShapeNode node];
circle.path = path.CGPath;
[background addChild:circle];
}
/*
get point on a bezier curve with 't'
*/
+(CGPoint) get3dPointbezierInterpolation:(CGFloat)t startp:(CGPoint)startp controlp:(CGPoint)controlp endp:(CGPoint)endp
{
CGFloat q0x = (1 - t) * startp.x + t * controlp.x;
CGFloat q1x = (1 - t) * controlp.x + t * endp.x;
CGFloat x = (1 - t) * q0x + t * q1x;
CGFloat q0y = (1 - t) * startp.y + t * controlp.y;
CGFloat q1y = (1 - t) * controlp.y + t * endp.y;
CGFloat y = (1 - t) * q0y + t * q1y;
return CGPointMake(x, y);
}
/*
get 't' of a bezier curve with a point on this bezier curve
*/
+(CGFloat) get3dTofbezierInterpolation:(CGPoint)startp nowPoint:(CGPoint)nowp endp:(CGPoint)endp anchorPoint:(CGPoint)controlp
{
CGFloat ax = (startp.x - 2 * controlp.x + endp.x);
CGFloat bx = (2 * (controlp.x - startp.x));
CGFloat cx = (startp.x - nowp.x);
CGFloat tx1,tx2;
CGFloat px = bx * bx - 4 * ax * cx;
if (px >= 0) {
tx1 = (-bx + sqrt(px)) / (2 * ax);
tx2 = (-bx - sqrt(px)) / (2 * ax);
}
if (tx1 == tx2) {
return tx1;
}
CGFloat ay = (startp.y - 2 * controlp.y + endp.y);
CGFloat by = (2 * (controlp.y - startp.y));
CGFloat cy = (startp.y - nowp.y);
CGFloat ty1,ty2;
CGFloat py = by * by - 4 * ay * cy;
if (py >= 0) {
ty1 = (-by + sqrt(py)) / (2 * ay);
ty2 = (-by - sqrt(py)) / (2 * ay);
}
if (ty1 == ty2) {
return ty1;
}
if (fabsf(tx1 - ty1) < 0.0001)
return tx1;
else if (fabsf(tx1 - ty2) < 0.0001) {
return tx1;
}
else if (fabsf(tx2 - ty1) < 0.0001) {
return tx2;
}
else if (fabsf(tx2 - ty2) < 0.0001) {
return tx2;
}
return 0;
}
/*
get bezier curve path with just one anchorPoint
*/
+(CGPathRef)Get3dBezierPath:(CGPoint)startPoint endPoint:(CGPoint)endPoint anchorPoint:(CGPoint)anchorPoint
{
UIBezierPath *path=[UIBezierPath bezierPath];
[path moveToPoint:startPoint];
[path addQuadCurveToPoint:endPoint controlPoint:anchorPoint];
return path.CGPath;
}
/*
add a point on bezier curve
*/
+(void)setupTestpoint:(SKSpriteNode *)background startp:(CGPoint)point color:(UIColor *)color
{
SKShapeNode *cpoint = [SKShapeNode node];
UIBezierPath *ppath = [UIBezierPath bezierPathWithRect:CGRectMake(point.x,point.y,10,10)];
cpoint.fillColor = color;
cpoint.path = ppath.CGPath;
[background addChild:cpoint];
}
You can use these code in the project to test.
Thank you very much!
A day later I find that sknode is not constant speed moving follow the bezier path. Use this method.
for (float i = 0; i < 1.0001f; i+=0.10f) {
CGPoint ps = [ToolObjective get3dPointbezierInterpolation: i startp:point1 controlp:controlP endp:point2];
[ToolObjective setupTestpoint:background startp:ps color:[UIColor yellowColor]];
}
So the key problem is to find a way to make the sknode has a constant speed.
I will post my solution here.Or you have already known please tell me !
I'm trying to draw a line with a specific width. I searched for examples online, but I only found examples using straight lines. I need curved lines. Also, I need to detect if the user touched within the line. Is it possible to achieve this using Objective C and Sprite Kit? If so can someone provide an example?
You can use UIBezierPath to create bezier curves (nice smooth curves). You can specify this path for a CAShapeLayer and add that as a sublayer to your view:
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 150)];
[path addCurveToPoint:CGPointMake(110, 150) controlPoint1:CGPointMake(40, 100) controlPoint2:CGPointMake(80, 100)];
[path addCurveToPoint:CGPointMake(210, 150) controlPoint1:CGPointMake(140, 200) controlPoint2:CGPointMake(170, 200)];
[path addCurveToPoint:CGPointMake(310, 150) controlPoint1:CGPointMake(250, 100) controlPoint2:CGPointMake(280, 100)];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = 10;
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
layer.path = path.CGPath;
[self.view.layer addSublayer:layer];
If you want to randomize it a little, you can just randomize some of the curves. If you want some fuzziness, add some shadow. If you want the ends to be round, specify a rounded line cap:
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point = CGPointMake(10, 100);
[path moveToPoint:point];
CGPoint controlPoint1;
CGPoint controlPoint2 = CGPointMake(point.x - 5.0 - arc4random_uniform(50), 150.0);
for (NSInteger i = 0; i < 5; i++) {
controlPoint1 = CGPointMake(point.x + (point.x - controlPoint2.x), 50.0);
point.x += 40.0 + arc4random_uniform(20);
controlPoint2 = CGPointMake(point.x - 5.0 - arc4random_uniform(50), 150.0);
[path addCurveToPoint:point controlPoint1:controlPoint1 controlPoint2:controlPoint2];
}
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = 5;
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
layer.path = path.CGPath;
layer.shadowColor = [UIColor redColor].CGColor;
layer.shadowRadius = 2.0;
layer.shadowOpacity = 1.0;
layer.shadowOffset = CGSizeZero;
layer.lineCap = kCALineCapRound;
[self.view.layer addSublayer:layer];
If you want it to be even more irregular, break those beziers into smaller segments, but the idea would be the same. The only trick with conjoined bezier curves is that you want to make sure that the second control point of one curve is in line with the first control point of the next one, or else you end up with sharp discontinuities in the curves.
If you want to detect if and when a user taps on it, that's more complicated. But what you have to do is:
Make a snapshot of the image:
- (UIImage *)captureView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 1.0); // usually I'd use 0.0, but we'll use 1.0 here so that the tap point of the gesture matches the pixel of the snapshot
if ([view respondsToSelector:#selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
BOOL success = [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
NSAssert(success, #"drawViewHierarchyInRect failed");
} else {
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Get the color of the pixel at the coordinate that the user tapped by identifying the color of the pixel the user tapped.
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
CGPoint point = [gesture locationInView:gesture.view];
CGFloat red, green, blue, alpha;
UIColor *color = [self image:self.image colorAtPoint:point];
[color getRed:&red green:&green blue:&blue alpha:&alpha];
if (green < 0.9 && blue < 0.9 && red > 0.9)
NSLog(#"tapped on curve");
else
NSLog(#"didn't tap on curve");
}
Where I adapted Apple's code for getting the pixel buffer in order to determine the color of the pixel the user tapped on was.
// adapted from https://developer.apple.com/library/mac/qa/qa1509/_index.html
- (UIColor *)image:(UIImage *)image colorAtPoint:(CGPoint)point
{
UIColor *color;
CGImageRef imageRef = image.CGImage;
// Create the bitmap context
CGContextRef context = [self createARGBBitmapContextForImage:imageRef];
NSAssert(context, #"error creating context");
// Get image width, height. We'll use the entire image.
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGRect rect = {{0,0},{width,height}};
// Draw the image to the bitmap context. Once we draw, the memory
// allocated for the context for rendering will then contain the
// raw image data in the specified color space.
CGContextDrawImage(context, rect, imageRef);
// Now we can get a pointer to the image data associated with the bitmap
// context.
uint8_t *data = CGBitmapContextGetData (context);
if (data != NULL) {
size_t offset = (NSInteger) point.y * 4 * width + (NSInteger) point.x * 4;
uint8_t alpha = data[offset];
uint8_t red = data[offset+1];
uint8_t green = data[offset+2];
uint8_t blue = data[offset+3];
color = [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha / 255.0];
}
// When finished, release the context
CGContextRelease(context);
// Free image data memory for the context
if (data) {
free(data); // we used malloc in createARGBBitmapContextForImage, so free it
}
return color;
}
- (CGContextRef) createARGBBitmapContextForImage:(CGImageRef) inImage
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
size_t bitmapByteCount;
size_t bitmapBytesPerRow;
// Get image width, height. We'll use the entire image.
size_t pixelsWide = CGImageGetWidth(inImage);
size_t pixelsHigh = CGImageGetHeight(inImage);
// Declare the number of bytes per row. Each pixel in the bitmap in this
// example is represented by 4 bytes; 8 bits each of red, green, blue, and
// alpha.
bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);
// Use the generic RGB color space.
colorSpace = CGColorSpaceCreateDeviceRGB(); // CGColorSpaceCreateDeviceWithName(kCGColorSpaceGenericRGB);
NSAssert(colorSpace, #"Error allocating color space");
// Allocate memory for image data. This is the destination in memory
// where any drawing to the bitmap context will be rendered.
bitmapData = malloc(bitmapByteCount);
NSAssert(bitmapData, #"Unable to allocate bitmap buffer");
// Create the bitmap context. We want pre-multiplied ARGB, 8-bits
// per component. Regardless of what the source image format is
// (CMYK, Grayscale, and so on) it will be converted over to the format
// specified here by CGBitmapContextCreate.
context = CGBitmapContextCreate (bitmapData,
pixelsWide,
pixelsHigh,
8, // bits per component
bitmapBytesPerRow,
colorSpace,
(CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
NSAssert(context, #"Context not created!");
// Make sure and release colorspace before returning
CGColorSpaceRelease( colorSpace );
return context;
}
I implemented a methode that returns a NSBitmapImageRep. Onto that bitmap 10x2 rectangles should be drawn and each rectangle should be filled with the color cyan. But for each rectangle the cyan value should be increased by 12 (value starts at 0).
The result bitmap gets 20 rectangles, like expected. But the color doesn't differ between the rectangles. All rectangles have the same cyan value.
I have no idea what's the problem. Can somebody please give me a hint?
-(NSBitmapImageRep*)drawOntoBitmap
{
NSRect offscreenRect = NSMakeRect(0.0, 0.0, 1000.0, 400.0);
NSBitmapImageRep *image = nil;
image = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:offscreenRect.size.width
pixelsHigh:offscreenRect.size.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSDeviceCMYKColorSpace
bitmapFormat:0
bytesPerRow:(4 * offscreenRect.size.width)
bitsPerPixel:32];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:image]];
NSRect colorRect;
NSBezierPath *thePath;
int cyan = 0;
int x = 0;
int y = 0;
int w = 0;
int h = 0;
for (intj = 0; j<2; j++)
{
y = j * 200;
h = y + 200;
for (int i = 0; i<10; i++)
{
x = i * 100;
w = x + 100;
colorRect = NSMakeRect(x, y, w, h);
thePath = [NSBezierPath bezierPathWithRect: colorRect];
cyan += 12;
[[NSColor colorWithDeviceCyan:cyan magenta:0 yellow:0 black:0 alpha:100] set];
[thePath fill];
}
}
[NSGraphicsContext restoreGraphicsState];
return image;
}
For each rect the same color value is used and it's the last cyan value that's set after the both loops are passed.
OK, found out that the NSColor value has a range of 0.0 - 1.0.
So I have to set my cyan to float like that:
cyan += 12/255;
The value has to be smaller than 1.0.
I am trying to draw a simple diagram. I have squares that are connected by lines. The lines are drawn with NSBezierPath's. I am using a random color for the lines so I can follow them. My problem is that the lines change color.
The output -
Code - I removed the lines that draw the squares so it only draws the lines:
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
[[NSColor whiteColor] setFill];
NSRectFill(dirtyRect);
CGFloat height = 70.0f;
CGFloat yOffset = 20.0f;
NSRect rect = NSMakeRect(50.0f, 50.0f, 200.0f, height);
NSEnumerator *reverseEnumerator = [steps reverseObjectEnumerator];
NSMutableDictionary *rectsByStep = [NSMutableDictionary dictionaryWithCapacity:10];
for( WFGJobStep *step in reverseEnumerator )
{
[[NSColor redColor] setFill];
[rectsByStep setObject:[NSValue valueWithRect:rect] forKey:[NSNumber numberWithInteger:step.step]];
rect.origin.y += height + yOffset;
}
reverseEnumerator = [steps reverseObjectEnumerator];
for( WFGJobStep *step in reverseEnumerator )
{
NSRect stepRect = [[rectsByStep objectForKey:[NSNumber numberWithInteger:step.step]] rectValue];
NSPoint startPoint = NSMakePoint( stepRect.origin.x + stepRect.size.width, stepRect.origin.y);
// draw lines
for( NSNumber *stepNumber in step.nextSteps )
{
//
//
// Line drawing code here
//
//
NSBezierPath * path = [NSBezierPath bezierPath];
[path setLineWidth: 4];
NSRect targetStepRect = [[rectsByStep objectForKey:stepNumber] rectValue];
NSPoint endPoint = NSMakePoint( targetStepRect.origin.x + targetStepRect.size.width, targetStepRect.origin.y + targetStepRect.size.height);
[path moveToPoint:startPoint];
CGFloat controlX = ( startPoint.y - endPoint.y ) * .2 + stepRect.origin.x + stepRect.size.width + 20;
[path curveToPoint:endPoint controlPoint1:NSMakePoint(controlX, startPoint.y) controlPoint2:NSMakePoint(controlX, endPoint.y)];
NSRect square = NSMakeRect( endPoint.x, endPoint.y, 9, 9 );
[path appendBezierPathWithOvalInRect: square];
[[self randomColor] set];
[path stroke];
}
}
}
- (NSColor *)randomColor {
float c[4];
c[0] = (arc4random() / RAND_MAX) * 1;
c[1] = (arc4random() / RAND_MAX) * 1;
c[2] = (arc4random() / RAND_MAX) * 1;
c[3] = 1.0f;
return [NSColor colorWithDeviceRed:c[0] green:c[1] blue:c[2] alpha:c[3]];
}
My issue is that this is in a scoll view and this is my first NSScrollView. I was not aware that as the view scrolls it redraws its subviews. The issue was that I was using a random color for my stroke so that as I scrolled it would randomly change the colors of the lines.