Last active
October 5, 2015 15:48
-
-
Save ahayman/2830483 to your computer and use it in GitHub Desktop.
Core Graphics Beveling Routine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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); | |
} |
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
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.