Place images along a bezier path
I guess you want something like this:
You can find my complete demo app project in this github repository.
Anyway, this is an interesting little problem.
You need to generate an array of points along the path, and I assume you want them to be equally spaced. Generating this points isn't trivial.
Fortunately, Core Graphics contains a function that will do it for you, but it's not obvious which one. The function is CGPathCreateCopyByDashingPath
.
First, let's make a UIBezierPath
category that creates a dashed copy:
UIBezierPath+Rob_dash.h
#import
@interface UIBezierPath (Rob_dash)
- (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase;
@end
UIBezierPath+Rob_dash.m
#import "UIBezierPath+Rob_dash.h"
@implementation UIBezierPath (Rob_dash)
- (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase {
CGFloat lengths[pattern.count];
size_t i = 0;
for (NSNumber *number in pattern) {
lengths[i++] = number.doubleValue;
}
CGPathRef dashedCGPath = CGPathCreateCopyByDashingPath(self.CGPath, NULL, phase, lengths, pattern.count);
UIBezierPath *dashedPath = [self.class bezierPathWithCGPath:dashedCGPath];
CGPathRelease(dashedCGPath);
return dashedPath;
}
@end
Once we have a dashed path, we need to enumerate the elements of the path (the individual commands like moveToPoint:
, addLineToPoint:
, and so on). The only way to do that is using another Core Graphics function CGPathApply
. Let's write another UIBezierPath
category that uses blocks to make it easier. This one's a bit longer:
UIBezierPath+Rob_forEach.h
#import
typedef void (^Rob_UIBezierPath_moveBlock)(CGPoint destination);
typedef void (^Rob_UIBezierPath_lineBlock)(CGPoint destination);
typedef void (^Rob_UIBezierPath_quadBlock)(CGPoint control, CGPoint destination);
typedef void (^Rob_UIBezierPath_cubicBlock)(CGPoint control0, CGPoint control1, CGPoint destination);
typedef void (^Rob_UIBezierPath_closeBlock)(void);
@interface UIBezierPath (Rob_forEach)
- (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock;
@end
UIBezierPath+Rob_forEach.m
#import "UIBezierPath+Rob_forEach.h"
struct ForEachBlocks {
__unsafe_unretained Rob_UIBezierPath_moveBlock moveBlock;
__unsafe_unretained Rob_UIBezierPath_lineBlock lineBlock;
__unsafe_unretained Rob_UIBezierPath_quadBlock quadBlock;
__unsafe_unretained Rob_UIBezierPath_cubicBlock cubicBlock;
__unsafe_unretained Rob_UIBezierPath_closeBlock closeBlock;
};
static void applyBlockToPathElement(void *info, const CGPathElement *element) {
struct ForEachBlocks *blocks = info;
switch (element->type) {
case kCGPathElementMoveToPoint:
if (blocks->moveBlock != nil) {
blocks->moveBlock(element->points[0]);
}
break;
case kCGPathElementAddLineToPoint:
if (blocks->lineBlock != nil) {
blocks->lineBlock(element->points[0]);
}
break;
case kCGPathElementAddQuadCurveToPoint:
if (blocks->quadBlock) {
blocks->quadBlock(element->points[0], element->points[1]);
}
break;
case kCGPathElementAddCurveToPoint:
if (blocks->cubicBlock) {
blocks->cubicBlock(element->points[0], element->points[1], element->points[2]);
}
break;
case kCGPathElementCloseSubpath:
if (blocks->closeBlock) {
blocks->closeBlock();
}
break;
}
}
@implementation UIBezierPath (Rob_forEach)
- (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock {
struct ForEachBlocks blocks = {
.moveBlock = moveBlock,
.lineBlock = lineBlock,
.quadBlock = quadBlock,
.cubicBlock = cubicBlock,
.closeBlock = closeBlock
};
CGPathApply(self.CGPath, &blocks, applyBlockToPathElement);
}
@end
OK, now we want to use these two categories together to dash the path, then walk along the dashes and emit the point at the end of each dash. Note that a dash might consist of multiple contiguous line/curve segments. We need to watch for move commands to know when a dash ends. Also, to draw each arrow at the correct angle, we need to know the tangent of the curve at each point, so we'll compute that also, as a unit vector. In the case of a straight line segment, the tangent vector is parallel to the line segment. In the case of curves, the control point immediately prior to the endpoint of the curve determines the tangent at the endpoint.
UIBezierPath+Rob_points.h
#import
@interface UIBezierPath (Rob_points)
- (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint point, CGVector vector))block;
@end
UIBezierPath+Rob_points.m
#import "UIBezierPath+Rob_points.h"
#import "UIBezierPath+Rob_dash.h"
#import "UIBezierPath+Rob_forEach.h"
#import
static CGVector vectorFromPointToPoint(CGPoint tail, CGPoint head) {
CGFloat length = hypot(head.x - tail.x, head.y - tail.y);
return CGVectorMake((head.x - tail.x) / length, (head.y - tail.y) / length);
}
@implementation UIBezierPath (Rob_points)
- (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint, CGVector))block {
UIBezierPath *dashedPath = [self Rob_dashedPathWithPattern:@[ @(interval * 0.5), @(interval * 0.5) ] phase:0];
__block BOOL hasPendingSegment = NO;
__block CGPoint pendingControlPoint;
__block CGPoint pendingPoint;
[dashedPath Rob_forEachMove:^(CGPoint destination) {
if (hasPendingSegment) {
block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint));
hasPendingSegment = NO;
}
pendingPoint = destination;
} line:^(CGPoint destination) {
pendingControlPoint = pendingPoint;
pendingPoint = destination;
hasPendingSegment = YES;
} quad:^(CGPoint control, CGPoint destination) {
pendingControlPoint = control;
pendingPoint = destination;
hasPendingSegment = YES;
} cubic:^(CGPoint control0, CGPoint control1, CGPoint destination) {
pendingControlPoint = control1;
pendingPoint = destination;
hasPendingSegment = YES;
} close:nil];
if (hasPendingSegment) {
block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint));
}
}
@end
Now we can find points along a path, and the unit tangent vector at each point. Let's make a custom view that uses this ability in drawRect:
:
ArrowView.h
#import
@interface ArrowView : UIView
@property (nonatomic) CGFloat interval;
@end
ArrowView.m
#import "ArrowView.h"
#import "UIBezierPath+Rob_figureEight.h"
#import "UIBezierPath+Rob_points.h"
@implementation ArrowView
- (void)setInterval:(CGFloat)interval {
_interval = interval;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
UIImage *arrow = [UIImage imageNamed:@"right233.png"];
UIBezierPath *path = [UIBezierPath Rob_figureEightInRect:CGRectInset(self.bounds, 40, 40)];
// [path stroke];
[path Rob_forEachPointAtInterval:self.interval perform:^(CGPoint point, CGVector vector) {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextTranslateCTM(gc, point.x, point.y);
CGContextConcatCTM(gc, CGAffineTransformMake(vector.dx, vector.dy, -vector.dy, vector.dx, 0, 0));
CGContextTranslateCTM(gc, -0.5 * arrow.size.width, -0.5 * arrow.size.height);
// UIRectFrame((CGRect){ CGPointZero, arrow.size });
[arrow drawAtPoint:CGPointZero];
} CGContextRestoreGState(gc);
}];
}
@end
That's all there is to it, if you want to draw arrow images along a path.
There's a little bonus in my demo app repository. If you go back to the first commit, I implemented a different solution also: a category that takes a path and “arrowizes” it, putting an arrowhead at the end of each subpath. If you combine that with dashing (as I did in that version of the project), you get arrows along the path. But it ended up not looking as nice as using arrow images.
How do I further animate an image that's on a bezier path
You can use a CAShapeLayer
to display the path. A shape layer provides two properties strokeStart
and strokeEnd
that have values between 0
and 1
and determine the fraction of the path that is visible. By animating strokeEnd
you can create the impression that the path is "drawn" on the screen. See http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer for an example that animates a pen that draws a line.
In order to change the image of the bee you can simply add an animation that changes the contents of your bee layer. For example, to change the image of the pen in the CAShapeLayer
project above, we can add the following code to the startAnimation
method.
CAKeyframeAnimation *imageAnimation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
imageAnimation.duration = 1.0;
imageAnimation.repeatCount = HUGE_VALF;
imageAnimation.calculationMode = kCAAnimationDiscrete;
imageAnimation.values = [NSArray arrayWithObjects:
(id)[UIImage imageNamed:@"noun_project_347_1.png"].CGImage,
(id)[UIImage imageNamed:@"noun_project_347_2.png"].CGImage,
nil];
[self.penLayer addAnimation:imageAnimation forKey:@"contents"];
This simply adds a key animation to the contents
property of the layer that displays the pen. Note that the image "noun_project_347_1.png"
is not part of the project so you would have to add an image to see the effect. I have simply horizontally flipped the image that comes with the project. By setting calculationMode
to kCAAnimationDiscrete
the animation does not interpolate between two images by replaces one by the other.
How to draw image inside a circular bezier path
I've read really great posts by rob mayoff, UIBezierPath Subtract Path, iOS UIImage clip to paths, many thanks.
Here is an adoption of his code. Keep your creation code of outerRing
and innerRing
, add them to an array named paths
.
[self.paths addObject:outerRing];
[self.paths addObject:innerRing];
Then use this help method.
- (UIImage *)maskedImage
{
CGRect rect = CGRectZero;
rect.size = self.originalImage.size;
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0);
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:self.paths[1]];
clipPath.usesEvenOddFillRule = YES;
CGContextSaveGState(UIGraphicsGetCurrentContext()); {
[clipPath addClip];
[[UIColor orangeColor] setFill];
[self.paths[0] fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIImage *mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0); {
CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, mask.CGImage);
[self.originalImage drawAtPoint:CGPointZero];
}
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return maskedImage;
}
In drawRect
,
UIImage *maskedImage = [self maskedImage];
[maskedImage drawInRect:self.bounds blendMode:kCGBlendModeNormal alpha:1.0];
creating a bezier path from a simple image ios
Why not do it the other way around?
- let the user draw the path
- create an image from the drawn path using core graphics
- compare both images with a framework like opencv
Canvas, make an image follow a Bezier curve
You can use the same function to get another point slightly before the current one on the curve, then use Math.atan2
to get the angle between the two points.
Then, you'll need to use ctx.translate()
and ctx.rotate()
to mutate the transformation matrix instead of setting the position in the .drawImage()
call. (The .setTransform()
call at the start of the animation method resets the matrix for each frame.)
I also added an "onion skin" effect here, so the motion is better seen.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var statue = new Image();
statue.src = "https://i.ibb.co/3TvCH8n/liberty.png";
function getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent) {
var x =
Math.pow(1 - percent, 2) * startPt.x +
2 * (1 - percent) * percent * controlPt.x +
Math.pow(percent, 2) * endPt.x;
var y =
Math.pow(1 - percent, 2) * startPt.y +
2 * (1 - percent) * percent * controlPt.y +
Math.pow(percent, 2) * endPt.y;
return { x: x, y: y };
}
const startPt = { x: 600, y: 200 };
const controlPt = { x: 300, y: 100 };
const endPt = { x: 0, y: 200 };
var percent = 0;
statue.addEventListener("load", () => {
ctx.clearRect(0, 0, c.width, c.height);
animate();
});
function animate() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
// "Onion skin" effect so the last frame is slightly retained to better show the motion.
ctx.fillStyle = "rgba(255,255,255,0.1)";
ctx.fillRect(0, 0, c.width, c.height);
percent = (percent + 0.003) % 1;
var point = getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent);
var lastPoint = getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent - 0.003);
var angle = Math.atan2(lastPoint.y - point.y, lastPoint.x - point.x);
// Debug pointer line
ctx.beginPath();
ctx.moveTo(point.x, point.y);
ctx.lineTo(point.x + Math.cos(angle) * 50, point.y + Math.sin(angle) * 50);
ctx.stroke();
// Actual drawing
ctx.translate(point.x, point.y);
ctx.rotate(angle);
ctx.drawImage(statue, 0, 0, statue.width, statue.height, -50, -50, 100, 100);
requestAnimationFrame(animate);
}
Related Topics
Swiftui Mvvm Coordinator/Router/Navigationlink
How to Use Bluetooth Classic Instead of Le
Prompt When Trying to Dial a Phone Number Using Tel:// Scheme on iOS 10.3
Decoding JSON Web Tokens in Swift
Adding Thousand Separator to Int in Swift
Check If a Uialertview Is Showing
Working with C APIs from Swift
Changing the Height of the Navigation Bar iOS Swift
Unable to Get the Access Token Through [Fbsdkaccesstoken Currentaccesstoken] in iPhone Sdk
Need Assistance Setting Two Buttons of Equal Width, Side by Side Autolayout
Kcfstreamerrordomainssl, -9802 When Connecting to a Server by Ip Address Through Https in iOS 9
Valid File Path for Archiverootobject and Unarchiverootobject
How to Create an Empty Application in Xcode Without Storyboard
Prevent Segue in Prepareforsegue Method
Sorting Nsarray of Dictionaries by Value of a Key in the Dictionaries