How to draw stars using Quartz Core?
Asked Answered
R

3

9

I'm trying to adapt an example provided by Apple in order to programmatically draw stars in line, the code is the following:

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, aSize);

    for (NSUInteger i=0; i<stars; i++) 
    {

       CGContextSetFillColorWithColor(context, aColor);
       CGContextSetStrokeColorWithColor(context, aColor);

       float w = item.size.width;
       double r = w / 2;
       double theta = 2 * M_PI * (2.0 / 5.0); // 144 degrees

       CGContextMoveToPoint(context, 0, r);

       for (NSUInteger k=1; k<5; k++) 
       {
          float x = r * sin(k * theta);
          float y = r * cos(k * theta);
          CGContextAddLineToPoint(context, x, y);
       }

       CGContextClosePath(context);
       CGContextFillPath(context);
    }

The code above draws a perfect star, but is 1. displayed upside down 2. is black and without border. What I want to achive is to draw many stars on the same line and with the given style. I understand that I'm actually drawing the same path 5 times in the same position and that I have somehow to flip the context vertically, but after several tests I gave up! (I lack the necessary math and geometry skills :P)... could you please help me?

UPDATE:

Ok, thanks to CocoaFu, this is my refactored and working draw utility:

- (void)drawStars:(NSUInteger)count inContext:(CGContextRef)context;
{
    // constants
    const float w = self.itemSize.width;
    const float r = w/2;
    const double theta = 2 * M_PI * (2.0 / 5.0);
    const float flip = -1.0f; // flip vertically (default star representation)

    // drawing center for the star
    float xCenter = r;

    for (NSUInteger i=0; i<count; i++) 
    {
        // get star style based on the index
        CGContextSetFillColorWithColor(context, [self fillColorForItemAtIndex:i]);
        CGContextSetStrokeColorWithColor(context, [self strokeColorForItemAtIndex:i]);

        // update position
        CGContextMoveToPoint(context, xCenter, r * flip + r);

        // draw the necessary star lines
        for (NSUInteger k=1; k<5; k++) 
        {
            float x = r * sin(k * theta);
            float y = r * cos(k * theta);
            CGContextAddLineToPoint(context, x + xCenter, y * flip + r);
        }

        // update horizontal center for the next star
        xCenter += w + self.itemMargin; 

        // draw current star
        CGContextClosePath(context);
        CGContextFillPath(context);
        CGContextStrokePath(context);
    }
}
Rum answered 9/12, 2011 at 12:55 Comment(3)
What do you mean upside down? A star does not have a 'bottom' as such, am I wrong? Maybe you could post a couple of screenshots, of what you are currently getting and what you are expecting.Pomeranian
A star has usually one point at the top an 2 on the bottom. Normal star: allstarbaseballcamp.com/star_clipart.gif, upside down (vertical flipped) star: sunandshield.files.wordpress.com/2010/03/eastern-star.jpgRum
See calayer.com/core-animation/2016/05/22/…Mortmain
S
29

Here is code that will draw 3 stars in a horizontal line, it's is not pretty but it may help:

-(void)drawRect:(CGRect)rect
{
    int aSize = 100.0;
    const CGFloat color[4] = { 0.0, 0.0, 1.0, 1.0 }; // Blue
    CGColorRef aColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), color);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, aSize);
    CGFloat xCenter = 100.0;
    CGFloat yCenter = 100.0;

    float  w = 100.0;
    double r = w / 2.0;
    float flip = -1.0;

    for (NSUInteger i=0; i<3; i++) 
    {
        CGContextSetFillColorWithColor(context, aColor);
        CGContextSetStrokeColorWithColor(context, aColor);

        double theta = 2.0 * M_PI * (2.0 / 5.0); // 144 degrees

        CGContextMoveToPoint(context, xCenter, r*flip+yCenter);

        for (NSUInteger k=1; k<5; k++) 
        {
            float x = r * sin(k * theta);
            float y = r * cos(k * theta);
            CGContextAddLineToPoint(context, x+xCenter, y*flip+yCenter);
        }
        xCenter += 150.0;
    }
    CGContextClosePath(context);
    CGContextFillPath(context);
}

enter image description here

Soph answered 9/12, 2011 at 14:8 Comment(7)
thanks a lot! Anyway the stars are still in the wrong position... how can I flip them vertically? (I'm feeling an idiot :P)Rum
I solved using: CGAffineTransform verticalFlip = CGAffineTransformMake(1, 0, 0, -1, 0, w); CGContextConcatCTM(context, verticalFlip); ...is there a better way?Rum
There probably is, because by doing that everything you draw after the call will be upside down. So you should revert the transform after drawing the stars, in case you want to draw something else as well.Pomeranian
I shared the final implementation ;)Rum
@Rum OK, but the code is horrible, it was a quick proof in the time before I left home for work.Soph
Is it possible to add rounded corners to the star easily or this would a big pain?Cholecyst
@ishhhh drawRect: is a method is the UIView class that is called by iOS so it needs to be in a UIView instance. See the docomentation.Soph
H
7

Here's an algorithm to implement what buddhabrot implied:

- (void)drawStarInContext:(CGContextRef)context withNumberOfPoints:(NSInteger)points center:(CGPoint)center innerRadius:(CGFloat)innerRadius outerRadius:(CGFloat)outerRadius fillColor:(UIColor *)fill strokeColor:(UIColor *)stroke strokeWidth:(CGFloat)strokeWidth {
    CGFloat arcPerPoint = 2.0f * M_PI / points;
    CGFloat theta = M_PI / 2.0f;

    // Move to starting point (tip at 90 degrees on outside of star)
    CGPoint pt = CGPointMake(center.x - (outerRadius * cosf(theta)), center.y - (outerRadius * sinf(theta)));
    CGContextMoveToPoint(context, pt.x, pt.y);

    for (int i = 0; i < points; i = i + 1) {
        // Calculate next inner point (moving clockwise), accounting for crossing of 0 degrees
        theta = theta - (arcPerPoint / 2.0f);
        if (theta < 0.0f) {
            theta = theta + (2 * M_PI);
        }
        pt = CGPointMake(center.x - (innerRadius * cosf(theta)), center.y - (innerRadius * sinf(theta)));
        CGContextAddLineToPoint(context, pt.x, pt.y);

        // Calculate next outer point (moving clockwise), accounting for crossing of 0 degrees
        theta = theta - (arcPerPoint / 2.0f);
        if (theta < 0.0f) {
            theta = theta + (2 * M_PI);
        }
        pt = CGPointMake(center.x - (outerRadius * cosf(theta)), center.y - (outerRadius * sinf(theta)));
        CGContextAddLineToPoint(context, pt.x, pt.y);
    }
    CGContextClosePath(context);
    CGContextSetLineWidth(context, strokeWidth);
    [fill setFill];
    [stroke setStroke];
    CGContextDrawPath(context, kCGPathFillStroke);
}

Works for me for most basic stars. Tested from 2 points (makes a good diamond!) up to 9 stars.

If you want a star with point down, change the subtraction to addition.

To draw multiples, create a loop and call this method multiple times, passing a new center each time. That should line them up nicely!

Harte answered 27/8, 2013 at 2:57 Comment(2)
What is the ratio innerRadius / outerRadius for a 5-star to be "perfect" (like the one drawn in the other answer)?Cholecyst
I don't know if I would call the star in the other answer "perfect", but I believe the ratio is 2 / 5 (innerRadius / outerRadius)Misery
B
4

I prefer using a CAShaperLayer to implementing drawRect, as it can then be animated. Here's a function that will create a path in the shape of a 5 point star:

func createStarPath(size: CGSize) -> CGPath {
    let numberOfPoints: CGFloat = 5

    let starRatio: CGFloat = 0.5

    let steps: CGFloat = numberOfPoints * 2

    let outerRadius: CGFloat = min(size.height, size.width) / 2
    let innerRadius: CGFloat = outerRadius * starRatio

    let stepAngle = CGFloat(2) * CGFloat(M_PI) / CGFloat(steps)
    let center = CGPoint(x: size.width / 2, y: size.height / 2)

    let path = CGPathCreateMutable()

    for i in 0..<Int(steps) {
        let radius = i % 2 == 0 ? outerRadius : innerRadius

        let angle = CGFloat(i) * stepAngle - CGFloat(M_PI_2)

        let x = radius * cos(angle) + center.x
        let y = radius * sin(angle) + center.y

        if i == 0 {
            CGPathMoveToPoint(path, nil, x, y)
        }
        else {
            CGPathAddLineToPoint(path, nil, x, y)
        }
    }

    CGPathCloseSubpath(path)
    return path
}

It can then be used with a CAShapeLayer like so:

let layer = CAShapeLayer()
layer.path = createStarPath(CGSize(width: 100, height: 100))
layer.lineWidth = 1
layer.strokeColor = UIColor.blackColor().CGColor
layer.fillColor = UIColor.yellowColor().CGColor
layer.fillRule = kCAFillRuleNonZero
Borax answered 20/2, 2015 at 17:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.