Skip to content

Instantly share code, notes, and snippets.

@ahayman
Last active October 5, 2015 15:48
Show Gist options
  • Save ahayman/2830483 to your computer and use it in GitHub Desktop.
Save ahayman/2830483 to your computer and use it in GitHub Desktop.
Core Graphics Beveling Routine
//
// CGPathBevel.h
//
// Created by Aaron Hayman on 6/21/12.
// Copyright (c) 2012 FlexileSoft, LLC. All rights reserved.
//
#import <Foundation/Foundation.h>
void bevelPath(CGPathRef path, CGContextRef context, CGFloat bevelDepth, CGColorRef highlight, CGColorRef shadow, CGFloat lightSourceAngle, BOOL evenOddShadows, BOOL eofFill);
//
// CGPathBevel.m
// iDB
//
// Created by Aaron Hayman on 6/21/12.
// Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//
#import "CGPathBevel.h"
#import <QuartzCore/QuartzCore.h>
#define BevelGradSpread .2f
typedef struct{
CGFloat a;
CGFloat b;
CGFloat c;
CGPoint pointA;
CGPoint pointB;
} LineDef;
typedef struct{
CGPoint point;
CGPoint ctrlPoint1;
CGPoint ctrlPoint2;
CGPathElementType type;
CGPoint bevPoint;
CGFloat incidentTheta;
CGFloat bisectingTheta;
BOOL prevShadow;
BOOL shadow;
BOOL nextShadow;
CGPoint shadowBisector;
} PathElement;
typedef struct{
PathElement **elements;
NSUInteger count;
CGPoint lastMove;
CGPoint lastPoint;
NSUInteger size;
} PathElements;
typedef struct{
void **objects;
NSUInteger count;
} PointerArray;
typedef struct{
CGPoint *points;
NSUInteger count;
} PointArray;
CGColorSpaceRef defaultColorSpace(void);
CGFloat PointTheta(CGPoint);
PathElement * NewPathElement(CGPathElementType type);
PathElements * NewPathElementArray(void);
PointerArray * NewPointerArray(void);
void AddToArray(PointerArray *array, void * object);
void AddPathElement(PathElement *element, PathElements *elements);
void GetPathElements(void *info, const CGPathElement *element);
void FindBoundingRect (void *info, const CGPathElement *element);
void SetBisectorsAndFocalPointsForPathElements(PathElements *elements, CGPathRef path, CGFloat lightSourceAngle, BOOL evenOddShadows, BOOL eofFill);
void BevelSubpath(PathElements *subPathElements, CGContextRef context, CGFloat bevelSize, CGColorRef highlight, CGColorRef shadow);
CGColorSpaceRef defaultColorSpace(){
static CGColorSpaceRef space = NULL;
if (space == NULL){
space = CGColorSpaceCreateDeviceRGB();
}
return space;
}
CGFloat PointTheta(CGPoint point){
//This assumes an origin of {0, 0} and returns a theta for the given point
return atan2f(point.y, point.x);
}
PathElement * NewPathElement(CGPathElementType type){
PathElement *pathElement = malloc(sizeof(PathElement));
pathElement->type = type;
pathElement->point = pathElement->bevPoint = pathElement->ctrlPoint1 = pathElement->ctrlPoint2 = pathElement->shadowBisector = CGPointZero;
pathElement->incidentTheta = pathElement->bisectingTheta = CGFLOAT_MAX;
pathElement->prevShadow = pathElement->nextShadow = pathElement->shadow = NO;
return pathElement;
}
PathElements * NewPathElementArray(){
PathElements *elements = malloc(sizeof(PathElements));
elements->elements = NULL;
elements->count = elements->size = 0;
elements->lastMove = elements->lastPoint = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
return elements;
}
PointerArray * NewPointerArray(){
PointerArray *array = malloc(sizeof(PointerArray));
array->count = 0;
array->objects = NULL;
return array;
}
void AddToArray(PointerArray *array, void *object){
array->count++;
array->objects = realloc(array->objects, array->count * sizeof(void *));
array->objects[array->count - 1] = object;
}
void AddPathElement(PathElement *element, PathElements *elements){
elements->count++;
if (elements->count > elements->size){
elements->elements = realloc(elements->elements, elements->count * sizeof(PathElement *));
elements->size = elements->count;
}
elements->elements[elements->count - 1] = element;
}
void GetPathElements(void *info, const CGPathElement *element){
PathElements *elements = (PathElements *)info;
CGPoint *points = element->points;
PathElement *newElement = NewPathElement(element->type);
BOOL (^CGPointsEqual)(CGPoint, CGPoint) = ^BOOL(CGPoint p1, CGPoint p2){
//Using a tolerance to determine point equality. Some default CG shapes include supurflous path elements that I don't care to replicate
CGFloat tolerance = 100;
NSUInteger p1X = p1.x * tolerance;
NSUInteger p1Y = p1.y * tolerance;
NSUInteger p2X = p2.x * tolerance;
NSUInteger p2Y = p2.y * tolerance;
return (p1X > p2X - tolerance && p1X < p2X + tolerance && p1Y > p2Y - tolerance && p1Y < p2Y + tolerance);
};
switch (newElement->type) {
case kCGPathElementMoveToPoint:
newElement->point = points[0];
elements->lastMove = points[0];
break;
case kCGPathElementAddLineToPoint:
newElement->point = points[0];
break;
case kCGPathElementAddCurveToPoint:
newElement->point = points[2];
newElement->ctrlPoint1 = points[0];
newElement->ctrlPoint2 = points[1];
break;
case kCGPathElementAddQuadCurveToPoint:
newElement->point = points[1];
newElement->ctrlPoint1 = points[0];
break;
case kCGPathElementCloseSubpath:
newElement->point = elements->lastMove;
break;
}
//Remove superfluous elements
if (elements->count > 0 && CGPointsEqual(newElement->point, elements->lastPoint)){
free(newElement);
return;
}
//If a path doesn't begin with moveToPoint, it begins at {0,0}, so we create a 'moveToPoint' element representing this
if (elements->count == 0 && element->type != kCGPathElementMoveToPoint){
PathElement *firstElement = NewPathElement(kCGPathElementMoveToPoint);
firstElement->point = CGPointZero;
elements->lastMove = CGPointZero;
AddPathElement(firstElement, elements);
AddPathElement(newElement, elements);
elements->lastPoint = newElement->point;
} else {
elements->lastPoint = newElement->point;
AddPathElement(newElement, elements);
}
}
void SetBisectorsAndFocalPointsForPathElements(PathElements *elements, CGPathRef path, CGFloat lightSourceAngle, BOOL evenOddShadows, BOOL eofFill){
if (elements->count < 3) return;
PointArray *focalPoints = malloc(sizeof(PointArray));
focalPoints->count = 0;
focalPoints->points = NULL;
LineDef (^LineDefForPoints)(CGPoint, CGPoint) = ^LineDef(CGPoint p1, CGPoint p2){
LineDef line = {0,0,0, p1, p2};
line.a = p2.y - p1.y;
line.b = p1.x - p2.x;
line.c = line.a*p1.x + line.b*p1.y;
return line;
};
CGFloat (^CalculateTheta) (CGPoint, CGPoint, CGPoint, CGFloat *) = ^CGFloat (CGPoint endPoint1, CGPoint centerPoint, CGPoint endPoint2, CGFloat *biTheta){
//normalize end points
endPoint1 = CGPointMake(endPoint1.x - centerPoint.x, endPoint1.y - centerPoint.y);
endPoint2 = CGPointMake(endPoint2.x - centerPoint.x, endPoint2.y - centerPoint.y);
//grab our line thetas
CGFloat theta1 = PointTheta(endPoint1);
CGFloat theta2 = PointTheta(endPoint2);
//grab bisecting angle
*biTheta = (fmaxf(theta1, theta2) - fminf(theta1, theta2)) / 2 + fminf(theta1, theta2);
//Determine if angle is pointing into the shape, it not, rotate is 180°
centerPoint.x += cosf(*biTheta);
centerPoint.y += sinf(*biTheta);
if (!CGPathContainsPoint(path, NULL, centerPoint, eofFill))
*biTheta += (*biTheta < M_PI) ? M_PI : -M_PI;
//take the least difference in thetas for the angle of incidence
theta1 = (fmaxf(theta1, theta2) - fminf(theta1, theta2));
if (theta1 > M_PI) theta1 = fabsf((M_PI * 2) - theta1);
return theta1;
};
LineDef (^CalculateBisector)(CGPoint, CGFloat) = ^(CGPoint centerPoint, CGFloat bisectingTheta){
//Calculate a another point on the line
CGPoint biPoint = CGPointMake(cosf(bisectingTheta) * 10, sinf(bisectingTheta) * 10);
//de-normalize biPoint
biPoint.x += centerPoint.x;
biPoint.y += centerPoint.y;
return LineDefForPoints(biPoint, centerPoint);
};
//Takes two LineDef's and returns the intersection of the lines
CGPoint (^CalculateFocalPoint)(LineDef, LineDef) = ^(LineDef l1, LineDef l2){
//m is essentially the diff in line slopes
CGFloat m = (l1.a * l2.b) - (l2.a * l1.b);
if (m == 0)
return CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
else {
CGPoint rPoint = CGPointMake(((l2.b * l1.c) - (l1.b * l2.c)) / m, ((l1.a * l2.c) - (l2.a * l1.c)) / m);
//Round to the nearest tenth.
rPoint.x *= 10;
rPoint.y *= 10;
rPoint.x = roundf(rPoint.x);
rPoint.y = roundf(rPoint.y);
rPoint.x /= 10;
rPoint.y /= 10;
return rPoint;
}
};
CGPoint (^LineIntersect)(LineDef, LineDef) = ^(LineDef l1, LineDef l2){
CGFloat m = (l1.a * l2.b) - (l2.a * l1.b);
if (m == 0)
return CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
else {
return CGPointMake(((l2.b * l1.c) - (l1.b * l2.c)) / m, ((l1.a * l2.c) - (l2.a * l1.c)) / m);
}
};
BOOL (^CGPointsEqual)(CGPoint, CGPoint) = ^BOOL(CGPoint p1, CGPoint p2){
CGFloat tolerance = 100;
NSUInteger p1X = p1.x * tolerance;
NSUInteger p1Y = p1.y * tolerance;
NSUInteger p2X = p2.x * tolerance;
NSUInteger p2Y = p2.y * tolerance;
return (p1X > p2X - tolerance && p1X < p2X + tolerance && p1Y > p2Y - tolerance && p1Y < p2Y + tolerance);
};
void (^AddFocalPoint)(CGPoint) = ^(CGPoint focalPoint){
if (focalPoint.x == CGFLOAT_MAX) return;
if (!CGPathContainsPoint(path, NULL, focalPoint, eofFill)) return;
focalPoints->count++;
focalPoints->points = realloc(focalPoints->points, focalPoints->count * sizeof(CGPoint));
focalPoints->points[focalPoints->count - 1] = focalPoint;
};
PathElement *cElement = nil;
PathElement *pElement = nil;
PathElement *p2Element = nil;
PathElement *poppedElement = nil;
LineDef prevBisector = {CGFLOAT_MAX, CGFLOAT_MAX, CGFLOAT_MAX};
LineDef curBisector = prevBisector;
LineDef firstBisector = prevBisector;
PathElements *updateQueue = NewPathElementArray();
CGPoint cPoint, pPoint, p2Point;
cPoint = pPoint = p2Point = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
CGFloat theta = CGFLOAT_MAX;
int totalCount, escapeCount, cIndex;
cIndex = totalCount = 0;
escapeCount = elements->count * 2;
while (theta == CGFLOAT_MAX) {
totalCount++;
if (pElement)
p2Element = pElement;
if (cElement)
pElement = cElement;
cElement = elements->elements[cIndex];
theta = cElement->bisectingTheta;
if (cElement && pElement && p2Element){
//While cPoint == cElement.point, there is no path progression, queue all elements at the same point to update when we do move
if (!CGPointsEqual(cElement->point, cPoint)){
cPoint = cElement->point;
if (updateQueue->count < 1){
pPoint = pElement->point;
p2Point = p2Element->point;
}
pElement->incidentTheta = CalculateTheta(p2Point, pPoint, cPoint, &(pElement->bisectingTheta));
//If there are elements in queue, update them with the current theta
for (int i = 0; i < updateQueue->count; i++){
poppedElement = updateQueue->elements[i];
poppedElement->incidentTheta = pElement->incidentTheta;
poppedElement->bisectingTheta = pElement->bisectingTheta;
}
updateQueue->count = 0;
//Check previous element and calculate bisector/focalpoint
curBisector = CalculateBisector(pPoint, pElement->bisectingTheta);
if (prevBisector.a != CGFLOAT_MAX)
AddFocalPoint(CalculateFocalPoint(prevBisector, curBisector));
prevBisector = curBisector;
if (firstBisector.a == CGFLOAT_MAX) firstBisector = curBisector;
} else {
//On the first queued item, we need to progress the point cache so that it's accurate
if (theta == CGFLOAT_MAX){
if (updateQueue->count < 1){
p2Point = pPoint;
pPoint = cPoint;
}
AddPathElement(pElement, updateQueue);
} else {
for (int i = 0; i < updateQueue->count; i++){
poppedElement = updateQueue->elements[i];
poppedElement->incidentTheta = pElement->incidentTheta;
poppedElement->bisectingTheta = pElement->bisectingTheta;
}
}
}
}
cIndex++;
if (cIndex >= elements->count)
cIndex = 0;
//If the path is too small (points too close together) this loop could run forever
if (totalCount > escapeCount)
break;
}
//The above will miss checking the intersection of the first and last bisectors
//....yes, I could find a way to include it in the loop but why do that when it's so easy to do this:
AddFocalPoint(CalculateFocalPoint(curBisector, firstBisector));
//Setup the LineDefs to create shadows
LineDef lines[elements->count - 1];
CGPoint prev, current;
current = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
for (int i = 0; i < elements->count; i++){
prev = current;
current = elements->elements[i]->point;
if (i != 0){
lines[i - 1] = LineDefForPoints(prev, current);
}
}
//Define Point of Light Source
CGRect boundingRect = CGPathGetBoundingBox(path);
CGPoint lightSource = CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect));
//A random number designed to avoid scenarios where a light ray shines perfectly through the ends of a line...this seems to indicate I've made an error somewhere but damn...it works so I'm keeping it for now
CGFloat lightDistance = fmaxf(boundingRect.size.width, boundingRect.size.height) * 1.879896f;
lightSource.y += sinf(lightSourceAngle) * lightDistance;
lightSource.x += cosf(lightSourceAngle) * lightDistance;
BOOL (^PointOnLine)(CGPoint, LineDef) = ^BOOL (CGPoint linePoint, LineDef line){
return (linePoint.y >= fminf(line.pointA.y, line.pointB.y) &&
linePoint.y <= fmaxf(line.pointA.y, line.pointB.y) &&
linePoint.x >= fminf(line.pointA.x, line.pointB.x) &&
linePoint.x <= fmaxf(line.pointA.x, line.pointB.x));
};
for (int i = 0; i < elements->count - 1; i++){
cElement = elements->elements[i];
//Each element will test shadows for 3 points: The normal point (used for curves) and two points, one to either side of the normal point (used for line shadows)
CGPoint pointA = cElement->point;
LineDef prevLightRay, nextLightRay, lightRay;
lightRay = LineDefForPoints(lightSource, pointA);
CGPoint pointB = (i == 0) ? elements->elements[elements->count - 2]->point : elements->elements[i - 1]->point;
theta = PointTheta(CGPointMake(pointB.x - pointA.x, pointB.y - pointA.y));
pointA.y += sinf(theta);
pointA.x += cosf(theta);
prevLightRay = LineDefForPoints(lightSource, pointA);
//Move point slightly closer to next point
pointA = cElement->point;
pointB = elements->elements[(i == elements->count - 1) ? 1 : i + 1]->point;
theta = PointTheta(CGPointMake(pointB.x - pointA.x, pointB.y - pointA.y));
pointA.y += sinf(theta);
pointA.x += cosf(theta);
nextLightRay = LineDefForPoints(lightSource, pointA);
LineDef currentLine;
CGPoint currentIntersect;
int prevOcclusionCount = 0, nextOcclusionCount = 0, occlusionCount = 0;
for (int si = 0; si < elements->count - 1; si++){
currentLine = lines[si];
currentIntersect = LineIntersect(prevLightRay, currentLine);
//Check if the intersection occurs within the line bounds
if (si != (i == 0 ? elements->count < 4 ? 2 : elements->count - 2 : i - 1) &&
currentIntersect.x != CGFLOAT_MAX &&
PointOnLine(currentIntersect, currentLine))
{
//If the intersection occurs within the bounds of the lightRay, the line occludes the point
if (PointOnLine(currentIntersect, prevLightRay)){
prevOcclusionCount++;
}
}
currentIntersect = LineIntersect(lightRay, currentLine);
if (si != (i == 0 ? elements->count < 4 ? 2 : elements->count - 2 : i - 1) &&
si != i &&
currentIntersect.x != CGFLOAT_MAX &&
PointOnLine(currentIntersect, currentLine))
{
//If the intersection occurs within the bounds of the lightRay, the line occludes the point
if (PointOnLine(currentIntersect, lightRay)){
occlusionCount++;
}
//Else the point casts a shadow on the line
else {
elements->elements[si]->shadowBisector = currentIntersect; //Note: this will replace previous shadows, maybe I'll change that later
}
}
currentIntersect = LineIntersect(nextLightRay, currentLine);
//Check if the intersection occurs within the line bounds
if (si != i &&
currentIntersect.x != CGFLOAT_MAX &&
PointOnLine(currentIntersect, currentLine))
{
//If the intersection occurs within the bounds of the lightRay, the line occludes the point
if (PointOnLine(currentIntersect, nextLightRay)){
nextOcclusionCount++;
}
}
}
//If there are Even occlusions, there's no shadow
cElement->prevShadow = evenOddShadows ? (prevOcclusionCount % 2 == 1) : (prevOcclusionCount > 0);
cElement->nextShadow = evenOddShadows ? (nextOcclusionCount % 2 == 1) : (nextOcclusionCount > 0);
cElement->shadow = (occlusionCount > 0);
}
pElement = elements->elements[0];
cElement = elements->elements[elements->count - 1];
cElement->prevShadow = pElement->prevShadow;
cElement->shadow = pElement->shadow;
cElement->nextShadow = pElement -> nextShadow;
//Free up memory
free(updateQueue->elements);
free(updateQueue);
free(focalPoints->points);
free(focalPoints);
}
void BevelSubpath(PathElements *subPathElements, CGContextRef context, CGFloat bevelSize, CGColorRef highlight, CGColorRef shadow){
CGPoint (^ScaledCtrlPoint)(CGPoint, CGPoint, CGPoint, CGPoint, CGPoint) = ^CGPoint (CGPoint refPoint1, CGPoint refPoint2, CGPoint bevPoint1, CGPoint bevPoint2, CGPoint ctrlPoint){
CGPoint translation = CGPointMake(bevPoint1.x - refPoint1.x, bevPoint1.y - refPoint1.y);
//Normalize control point to refPoint1
ctrlPoint.x -= refPoint1.x; ctrlPoint.y -= refPoint1.y;
//Get the distances between reference points and bevelled points and calculate the scale
CGFloat refHyp = hypotf(refPoint2.x - refPoint1.x, refPoint2.y - refPoint1.y);
CGFloat bevHyp = hypotf(bevPoint2.x - bevPoint1.x, bevPoint2.y - bevPoint1.y);
CGFloat scale = (bevHyp / refHyp);
//Create transform and apply to control point
CGAffineTransform txfm = CGAffineTransformMakeScale(scale, scale);
txfm = CGAffineTransformTranslate(txfm, translation.x, translation.y);
ctrlPoint = CGPointApplyAffineTransform(ctrlPoint, txfm);
//De normalize the new control point
ctrlPoint.x += refPoint1.x;
ctrlPoint.y += refPoint1.y;
return ctrlPoint;
};
//This will calculate the hypotenuse size based on the incident angle, and then generate the bevelled point
CGPoint (^BevelPoint)(CGPoint, CGFloat, CGFloat) = ^CGPoint (CGPoint refPoint, CGFloat bisectingTheta, CGFloat incidentTheta){
CGFloat hypBevel = bevelSize / sinf(incidentTheta / 2);
return CGPointMake((refPoint.x + (cosf(bisectingTheta) * hypBevel)), (refPoint.y + (sinf(bisectingTheta) * hypBevel)));
};
void (^CalculateOffsets)(CGPoint *fromOffset, CGPoint *toOffset, CGPoint from, CGPoint to) = ^(CGPoint *fromOffset, CGPoint *toOffset, CGPoint from, CGPoint to){
//This calculates an offset to remove seams that occur when two shapes are diagonally butted against each other
//This isn't perfect, and on larger bevel depths with semi-transparent fills a seam may still occur due to the overlap
CGFloat offset = .015f;
if (to.x < from.x){
toOffset->x = -offset;
fromOffset->x = offset;
}
if (to.x > from.x){
toOffset->x = offset;
fromOffset->x = -offset;
}
if (to.y < from.y){
toOffset->y = -offset;
fromOffset->y = offset;
}
if (to.y > from.y){
toOffset->y = offset;
fromOffset->y = -offset;
}
};
void (^FillPath)(CGPathRef, CGPoint, BOOL, CGPoint, BOOL, CGPoint) = ^(CGPathRef fillPath, CGPoint point1, BOOL shadow1, CGPoint point2, BOOL shadow2, CGPoint shadowBisector){
//Calculate the exactly where the transition boundary occurs on the shape
//Yes, this was a PITA to figure out and write
CGContextSaveGState(context);
CGContextAddPath(context, fillPath);
if (shadow1 != shadow2){
CGContextClip(context);
CGFloat boundary = .5f;
if (!CGPointEqualToPoint(shadowBisector, CGPointZero)){
CGFloat tDist = hypotf(point2.x - point1.x, point2.y - point1.y);
CGFloat sDist = hypotf(shadowBisector.x - point1.x, shadowBisector.y - point1.y);
boundary = 1 - (sDist / tDist);
if (boundary < 0 || boundary > 1) boundary = .5f;
}
//Use Boundary to figure out where the gradient starts/ends
CGFloat gStart = fmaxf(0.0f, boundary - BevelGradSpread);
CGFloat gEnd = fminf(1.0f, boundary + BevelGradSpread);
//Draw the gradient
CGFloat locations[4] = {0.0f, gStart, gEnd, 1.0f};
CGColorRef p1Color = (!shadow1) ? highlight : shadow;
CGColorRef p2Color = (!shadow2) ? highlight : shadow;
CGColorRef colorRefs[4] = {p1Color, p1Color, p2Color, p2Color};
CFArrayRef colors = CFArrayCreate(NULL, (const void**)colorRefs, 4, &kCFTypeArrayCallBacks);
CGGradientRef grad = CGGradientCreateWithColors(defaultColorSpace(), colors, locations);
CGContextDrawLinearGradient(context, grad, point1, point2, kCGGradientDrawsAfterEndLocation | kCGGradientDrawsBeforeStartLocation);
CGGradientRelease(grad);
CFRelease(colors);
} else {
CGContextSetFillColorWithColor(context, (!shadow1) ? highlight : shadow);
CGContextFillPath(context);
}
CGContextRestoreGState(context);
};
PathElement *prevElement, *originalElement, *pathElement;
prevElement = originalElement = pathElement = subPathElements->elements[0];
CGPoint curPoint, bevPoint, curCtrlPoint, curBevCtrlPoint, curCtrlPoint2, curBevCtrlPoint2, prevPoint, prevBevPoint, fOff, tOff;
CGMutablePathRef path;
for (int i = 0; i < subPathElements->count; i++){
pathElement = subPathElements->elements[i];
switch (pathElement->type) {
case kCGPathElementMoveToPoint:{
pathElement->bevPoint = BevelPoint(pathElement->point, pathElement->bisectingTheta, pathElement->incidentTheta);
originalElement = pathElement;
break;
}
case kCGPathElementAddLineToPoint:
//calculate the bevel
curPoint = pathElement->point;
prevPoint = prevElement->point;
prevBevPoint = prevElement->bevPoint;
pathElement->bevPoint = bevPoint = BevelPoint(pathElement->point, pathElement->bisectingTheta, pathElement->incidentTheta);
//determine offset to Cover seams
fOff = CGPointZero;
tOff = CGPointZero;
CalculateOffsets(&fOff, &tOff, prevElement->point, pathElement->point);
//create path for bevelled side
path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, prevPoint.x + fOff.x, prevPoint.y + fOff.y);
CGPathAddLineToPoint(path, NULL, prevBevPoint.x + fOff.x, prevBevPoint.y + fOff.y);
CGPathAddLineToPoint(path, NULL, bevPoint.x + tOff.x, bevPoint.y + tOff.y);
CGPathAddLineToPoint(path, NULL, curPoint.x + tOff.x, curPoint.y + tOff.y);
CGPathCloseSubpath(path);
FillPath(path, curPoint, pathElement->prevShadow, prevPoint, prevElement->nextShadow, pathElement->shadowBisector);
CGPathRelease(path);
break;
case kCGPathElementAddCurveToPoint:{
//get prev vars
prevPoint = prevElement->point;
prevBevPoint = prevElement->bevPoint;
//calculate the bevel
curPoint = pathElement->point;
pathElement->bevPoint = bevPoint = BevelPoint(curPoint, pathElement->bisectingTheta, pathElement->incidentTheta);
//scale control points
curCtrlPoint = pathElement->ctrlPoint1;
curCtrlPoint2 = pathElement->ctrlPoint2;
//Note: ctrlPoints are reversed, cause we're going in the other direction
curBevCtrlPoint = ScaledCtrlPoint(curPoint, prevPoint, bevPoint, prevBevPoint, curCtrlPoint2);
curBevCtrlPoint2 = ScaledCtrlPoint(curPoint, prevPoint, bevPoint, prevBevPoint, curCtrlPoint);
//create the path
fOff = CGPointZero;
tOff = CGPointZero;
CalculateOffsets(&fOff, &tOff, prevPoint, curPoint);
path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, prevPoint.x + fOff.x, prevPoint.y + fOff.y);
CGPathAddCurveToPoint(path, NULL, curCtrlPoint.x, curCtrlPoint.y, curCtrlPoint2.x, curCtrlPoint2.y, curPoint.x + tOff.x, curPoint.y + tOff.y);
CGPathAddLineToPoint(path, NULL, bevPoint.x + tOff.x, bevPoint.y + tOff.y);
CGPathAddCurveToPoint(path, NULL, curBevCtrlPoint.x + fOff.x, curBevCtrlPoint.y + fOff.y, curBevCtrlPoint2.x + tOff.x, curBevCtrlPoint2.y + tOff.y, prevBevPoint.x +fOff.x, prevBevPoint.y + fOff.y);
CGPathCloseSubpath(path);
//fill the path
FillPath(path, curPoint, pathElement->shadow, prevPoint, prevElement->shadow, pathElement->shadowBisector);
CGPathRelease(path);
break;
}
case kCGPathElementAddQuadCurveToPoint:
//get prev vars
prevPoint = prevElement->point;
prevBevPoint = prevElement->bevPoint;
//calculate the bevel
curPoint = pathElement->point;
pathElement->bevPoint = bevPoint = BevelPoint(curPoint, pathElement->bisectingTheta, pathElement->incidentTheta);
//scale control point
curCtrlPoint = pathElement->ctrlPoint1;
curBevCtrlPoint = ScaledCtrlPoint(curPoint, prevPoint, bevPoint, prevBevPoint, curCtrlPoint);
//create path
fOff = CGPointZero;
tOff = CGPointZero;
CalculateOffsets(&fOff, &tOff, prevPoint, curPoint);
path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, prevPoint.x + fOff.x, prevPoint.y + fOff.y);
CGPathAddQuadCurveToPoint(path, NULL, curCtrlPoint.x, curCtrlPoint.y, curPoint.x + tOff.x, curPoint.y + tOff.y);
CGPathAddLineToPoint(path, NULL, bevPoint.x + tOff.x, bevPoint.y + tOff.y);
CGPathAddQuadCurveToPoint(path, NULL, curBevCtrlPoint.x, curBevCtrlPoint.y, prevBevPoint.x +fOff.x, prevBevPoint.y + fOff.y);
CGPathCloseSubpath(path);
//fill the path
FillPath(path, curPoint, pathElement->shadow, prevPoint, prevElement->shadow, pathElement->shadowBisector);
CGPathRelease(path);
break;
case kCGPathElementCloseSubpath:
//Get theta
curPoint = originalElement->point;
bevPoint = originalElement->bevPoint;
prevPoint = prevElement->point;
prevBevPoint = prevElement->bevPoint;
//create path for bevelled side
fOff = CGPointZero;
tOff = CGPointZero;
CalculateOffsets(&fOff, &tOff, prevPoint, curPoint);
path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, prevPoint.x + fOff.x, prevPoint.y + fOff.y);
CGPathAddLineToPoint(path, NULL, prevBevPoint.x + fOff.x, prevBevPoint.y + fOff.y);
CGPathAddLineToPoint(path, NULL, bevPoint.x + tOff.x, bevPoint.y + tOff.y);
CGPathAddLineToPoint(path, NULL, curPoint.x + tOff.x, curPoint.y + tOff.y);
CGPathCloseSubpath(path);
FillPath(path, curPoint, pathElement->prevShadow, prevPoint, prevElement->nextShadow, pathElement->shadowBisector);
CGPathRelease(path);
break;
}
prevElement = pathElement;
}
}
void bevelPath(CGPathRef path, CGContextRef context, CGFloat bevelDepth, CGColorRef highlight, CGColorRef shadow, CGFloat lightSourceAngle, BOOL evenOddShadows, BOOL eofFill){
if (bevelDepth <= 0 || !highlight || !shadow) return;
//Grab the all the elements in the path
PathElements *pathElements = NewPathElementArray();
CGPathApply(path, pathElements, GetPathElements);
//Split out the subpaths into separate linked lists, stored in an array
PathElement *element;
PathElements *subPath;
PointerArray *subPaths = NewPointerArray();
for (int i = 0; i < pathElements->count; i++){
element = pathElements->elements[i];
//Sepearete out subpaths into individual stacks
if (i == 0 || element->type == kCGPathElementMoveToPoint){
subPath = NewPathElementArray();
AddToArray(subPaths, subPath);
}
AddPathElement(element, subPath);
}
//Perform the actual bevelling
for (int i = 0; i < subPaths->count; i++){
subPath = subPaths->objects[i];
SetBisectorsAndFocalPointsForPathElements(subPath, path, lightSourceAngle, evenOddShadows, eofFill);
BevelSubpath(subPath, context, bevelDepth, highlight, shadow);
}
//Free memory
for (int i = 0; i < subPaths->count; i++){
subPath = subPaths->objects[i];
free(subPath->elements);
free(subPath);
}
free(subPaths->objects);
free(subPaths);
for (int i = 0; i < pathElements->size; i++){
free(pathElements->elements[i]);
}
free(pathElements->elements);
free(pathElements);
}
@gpdawson
Copy link

This has great potential but there seems to be a problem with the light source (you already left a comment in the code suggesting you suspect it).

I tried beveling a rounded rect path with rects of various dimensions. The light rendering isn't consistent - there are artefacts typically on the vertical left hand side of the rect, which vary according to the exact size of the rect, and it looks as though the light ray is somehow shining almost into the end of this line. I also tried adjusting the "random number" you set to make the light distance almost infinite, but this doesn't fix it, so I suspect a bug in the light ray calculations. Unfortunately I don't know enough about ray tracing to find the cause - but if you could find and fix it I'd be really happy.

@ahayman
Copy link
Author

ahayman commented Aug 19, 2013

I'll take a look at at when I get some time. Oddly, all that I know about ray tracing is entirely what I made up. Essentially, I count the intersection of lines. There's a lot more than could be done, and I'm sure there's better ways to do it. Unfortunately, I'm in a bit of a crunch time right now with my own development, so this'll probably wait a little while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment