diff --git a/macarea/draw.m b/macarea/draw.m index 753ecfc1..2de508d4 100644 --- a/macarea/draw.m +++ b/macarea/draw.m @@ -1,16 +1,99 @@ // 6 september 2015 #include "area.h" -// TODO some pixel thick lines aren't actually pixel thick +struct uiDrawPath { + CGMutablePathRef path; + uiDrawFillMode fillMode; + BOOL ended; +}; + +uiDrawPath *uiDrawNewPath(uiDrawFillMode mode) +{ + uiDrawPath *p; + + // TODO uiNew + p = malloc(sizeof (uiDrawPath)); + p->path = CGPathCreateMutable() + p->fillMode = mode; +p->ended = NO; + return p; +} + +void uiDrawFreePath(uiDrawPath *p) +{ + CGPathRelease((CGPathRef) (p->path)); + // TODO uiFree + free(p); +} + +// TODO +#define complain(...) ; + +void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) +{ + if (p->ended) + complain("attempt to add figure to ended path in uiDrawPathNewFigure()"); + CGPathMoveToPoint(p->path, NULL, x, y); +} + +void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double endAngle) +{ + if (p->ended) + complain("attempt to add figure to ended path in uiDrawPathNewFigureWithArc()"); + // TODO + CGPathMoveToPoint(p->path, NULL, startx, starty); + uiDrawPathArcTo(p, xCenter, yCenter, radius, startAngle, endAngle); +} + +void uiDrawPathLineTo(uiDrawPath *p, double x, double y) +{ + if (p->ended) + complain("attempt to add line to ended path in uiDrawPathLineTo()"); + CGPathAddLineToPoint(p->path, NULL, x, y); +} + +void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double endAngle) +{ + if (p->ended) + complain("attempt to add arc to ended path in uiDrawPathArcTo()"); + CGPathAddArc(p->path, NULL, + xCenter, yCenter, + radius, + startAngle, endAngle, + NO); +} + +void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) +{ + if (p->ended) + complain("attempt to add bezier to ended path in uiDrawPathBezierTo()"); + CGPathAddCurveToPoint(p->path, NULL, + c1x, c1y, + c2x, c2y, + endX, endY); +} + +void uiDrawPathCloseFigure(uiDrawPath *p) +{ + if (p->ended) + complain("attempt to close figure of ended path in uiDrawPathCloseFigure()"); + CGPathCloseSubpath(p->path); +} + +void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) +{ + if (p->ended) + complain("attempt to add rectangle to ended path in uiDrawPathAddRectangle()"); + CGPathAddRect(p->path, NULL, CGRectMake(x, y, width, height)); +} + +void uiDrawPathEnd(uiDrawPath *p) +{ + p->ended = TRUE; +} struct uiDrawContext { CGContextRef c; - - BOOL useRGBA; - CGFloat r; - CGFloat g; - CGFloat b; - CGFloat a; }; uiDrawContext *newContext(CGContextRef ctxt) @@ -23,126 +106,152 @@ uiDrawContext *newContext(CGContextRef ctxt) return c; } -void uiDrawBeginPathRGB(uiDrawContext *c, uint8_t r, uint8_t g, uint8_t b) +// a stroke is identical to a fill of a stroked path +// we need to do this in order to stroke with a gradient; see http://stackoverflow.com/a/25034854/3408572 +// doing this for other brushes works too +void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p) { - c->useRGBA = YES; - c->r = ((CGFloat) r) / 255; - c->g = ((CGFloat) g) / 255; - c->b = ((CGFloat) b) / 255; - c->a = 1.0; - CGContextBeginPath(c->c); -} + CGLineCap cap; + CGLineJoin join; + uiDrawPath p2; -void uiDrawBeginPathRGBA(uiDrawContext *c, uint8_t r, uint8_t g, uint8_t b, uint8_t a) -{ - c->useRGBA = YES; - c->r = ((CGFloat) r) / 255; - c->g = ((CGFloat) g) / 255; - c->b = ((CGFloat) b) / 255; - c->a = ((CGFloat) a) / 255; - CGContextBeginPath(c->c); -} + if (!path->ended) + complain("path not ended in uiDrawStroke()"); -// TODO 0.25 for retina? some say yes, some say no -// TODO same adjustment for cairo -#define topoint(x) (((CGFloat) x) + 0.5) - -void uiDrawMoveTo(uiDrawContext *c, intmax_t x, intmax_t y) -{ - CGContextMoveToPoint(c->c, topoint(x), topoint(y)); -} - -void uiDrawLineTo(uiDrawContext *c, intmax_t x, intmax_t y) -{ - CGContextAddLineToPoint(c->c, topoint(x), topoint(y)); -} - -// TODO width-1/height-1? (also for cairo) -void uiDrawRectangle(uiDrawContext *c, intmax_t x, intmax_t y, intmax_t width, intmax_t height) -{ - CGContextAddRect(c->c, CGRectMake(topoint(x), topoint(y), width, height)); -} - -void uiDrawArcTo(uiDrawContext *c, intmax_t xCenter, intmax_t yCenter, intmax_t radius, double startAngle, double endAngle, int lineFromCurrentPointToStart) -{ - if (!lineFromCurrentPointToStart) { - // see http://stackoverflow.com/questions/31489157/extra-line-when-drawing-an-arc-in-swift - // TODO verify correctness - CGFloat x, y; - - x = topoint(xCenter); - y = topoint(yCenter); - x += radius * cos(startAngle); - y -= radius * sin(startAngle); - CGContextMoveToPoint(c->c, x, y); - } - CGContextAddArc(c->c, - topoint(xCenter), topoint(yCenter), - radius, - startAngle, endAngle, - 0); -} - -void uiDrawBezierTo(uiDrawContext *c, intmax_t c1x, intmax_t c1y, intmax_t c2x, intmax_t c2y, intmax_t endX, intmax_t endY) -{ - CGContextAddCurveToPoint(c->c, - topoint(c1x), topoint(c1y), - topoint(c2x), topoint(c2y), - topoint(endX), topoint(endY)); -} - -void uiDrawCloseFigure(uiDrawContext *c) -{ - CGContextClosePath(c->c); -} - -void uiDrawStroke(uiDrawContext *c, uiDrawStrokeParams *p) -{ switch (p->Cap) { case uiDrawLineCapFlat: - CGContextSetLineCap(c->c, kCGLineCapButt); + cap = kCGLineCapButt; break; case uiDrawLineCapRound: - CGContextSetLineCap(c->c, kCGLineCapRound); + cap = kCGLineCapRound; break; case uiDrawLineCapSquare: - CGContextSetLineCap(c->c, kCGLineCapSquare); + cap = kCGLineCapSquare; break; } switch (p->Join) { case uiDrawLineJoinMiter: - CGContextSetLineJoin(c->c, kCGLineJoinMiter); + join = kCGLineJoinMiter; CGContextSetMiterLimit(c->c, p->MiterLimit); break; case uiDrawLineJoinRound: - CGContextSetLineJoin(c->c, kCGLineJoinRound); + join = kCGLineJoinRound; break; case uiDrawLineJoinBevel: - CGContextSetLineJoin(c->c, kCGLineJoinBevel); + join = kCGLineJoinBevel; break; } - CGContextSetLineWidth(c->c, p->Thickness); - if (c->useRGBA) - CGContextSetRGBStrokeColor(c->c, c->r, c->g, c->b, c->a); - else { - // TODO - } - CGContextStrokePath(c->c); + + // create a temporary path identical to the previous one + // the cast is safe; we never modify the CGPathRef and always cast it back to a CGPathRef anyway + p2.path = (CGMutablePathRef) CGPathCreateCopyByStrokingPath(path->path, + NULL, + p->Thickness, + cap, + join, + p->MiterLimit); + p2.fillMode = path->fillMode; + p2.ended = path->ended; + uiDrawFill(c, &p2, b); + // and clean up + CGPathRelease((CGPathRef) (p2.path)); } -void uiDrawFill(uiDrawContext *c, uiDrawFillMode mode) +// for a solid fill, we can merely have Core Graphics fill directly +static void fillSolid(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b) { - if (c->useRGBA) - CGContextSetRGBFillColor(c->c, c->r, c->g, c->b, c->a); - else { - // TODO - } - switch (mode) { + CGContextSetRGBFillColor(ctxt, b->R, b->G, b->B, b->A); + switch (p->fillMode) { case uiDrawFillModeWinding: - CGContextFillPath(c->c); + CGContextFillPath(ctxt); break; case uiDrawFillModeAlternate: - CGContextEOFillPath(c->c); + CGContextEOFillPath(ctxt); break; } } + +// for a gradient fill, we need to clip to the path and then draw the gradient +// see http://stackoverflow.com/a/25034854/3408572 +static void fillGradient(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b) +{ + CGGradientRef gradient; + CGColorSpaceRef colorspace; + CGFloat *colors; + CGFloat *locations; + + // gradients need a color space + // for consistency with windows, use sRGB + colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + + // make the gradient + // TODO uiAlloc + colors = malloc(b->NumStops * 4 * sizeof (CGFloat)); + locations = malloc(b->NumStops * sizeof (CGFloat)); + for (i = 0; i < b->NumStops; i++) { + colors[i * 4 + 0] = b->Stops[i].R; + colors[i * 4 + 1] = b->Stops[i].G; + colors[i * 4 + 2] = b->Stops[i].B; + colors[i * 4 + 3] = b->Stops[i].A; + locations[i] = b->Stops[i].Pos; + } + gradient = CGGradientCreateWithColorComponents(colorspace, colors, locations, b->NumStops); + // TODO uiFree + free(locations); + free(colors); + + // clip + switch (p->fillMode) { + case uiDrawFillModeWinding: + CGContextClip(ctxt); + break; + case uiDrawFillModeAlternate: + CGContextEOClip(ctxt); + break; + } + + // draw the gradient + switch (b->Type) { + case uiDrawBrushTypeLinearGradient: + CGContextDrawLinearGradient(ctxt, + gradient, + CGPointMake(b->X0, b->Y0), + CGPointMake(b->X1, b->Y1), + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + break; + case uiDrawBrushTypeRadialGradient: + CGContextDrawRadialGradient(ctxt, + gradient, + CGPointMake(b->X0, b->Y0), + // make the start circle radius 0 to make it a point + 0, + CGPointMake(b->X1, b->Y1), + b->OuterRadius, + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + break; + } + + // and clean up + CGGradientRelease(gradient); + CGColorSpaceRelease(colorspace); +} + +void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b) +{ + if (!path->ended) + complain("path not ended in uiDrawFill()"); + CGContextAddPath(c->c, (CGPathRef) (p->path)); + switch (b->Type) { + case uiDrawBrushTypeSolid: + fillSolid(c->c, path, b); + return; + case uiDrawBrushTypeLinearGradient: + case uiDrawBrushTypeRadialGradient: + fillGradient(c->c, path, b); + return; +// case uiDrawBrushTypeImage: + // TODO + return; + } + complain("unknown brush type %d in uiDrawFill()", b->Type); +}