Layer masking based on positioning iOS - ios

I've never worked with animations on iOS specifically and was hoping to be pointed in the right direction. I have so far animated a bunch of pill shaped objects in a fashion similar to what I have below, however I have yet to implement the correct masking based on the y positioning of the shape.
The pill shapes are just UIViews. I'm looking for a way to get their backgrounds to change based on their positioning; the background itself does not move, just the pills.
EDIT:
Based on Rob's answer below, I am now tying the animation to the device's movement like so:
- (void)setAnimationToDeviceMotion {
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.1f;
NSOperationQueue *motionQueue = [[NSOperationQueue alloc] init];
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical toQueue:motionQueue withHandler:^(CMDeviceMotion *motion, NSError *error) {
if (!error) {
double pitch = motion.attitude.pitch;
NSLog(#"%f", pitch);
[self addMaskToView:self.imageView withAdjustment:pitch];
}
}];
}

Assuming this is eight pill shapes that will reveal a single image underneath, you
create a UIBezierPath for the eight pill shapes
create a CAShapeLayer layer that uses that UIBezierPath (or more accurately, its CGPath)
apply this CAShapeLayer as the mask for the layer for your UIImageView
If you want to animate this over time, just replace the mask (e.g. in response to a accelerometer event, a CADisplayLink or NSTimer, or a UIPanGesture), or just update the path of the CAShapeLayer you have applied as the mask to the UIImageView's layer.
Thus, you might have something like:
- (void)addMaskToView:(UIView *)view withAdjustment:(CGFloat)adjustment
{
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = [[self pillsPathWithAdjustment:adjustment forView:view] CGPath];
self.imageView.layer.mask = mask;
}
- (UIBezierPath *)pillsPathWithAdjustment:(CGFloat)adjustment forView:(UIView *)view
{
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSInteger i = 0; i < kNumberOfPills; i++)
[self addPillPathNumber:i forView:view toPath:path adjustment:adjustment];
return path;
}
- (void)addPillPathNumber:(NSInteger)index forView:(UIView *)view toPath:(UIBezierPath *)path adjustment:(CGFloat)adjustment
{
CGFloat const pillSpacing = 5.0f;
CGFloat const pillWidth = view.bounds.size.width / kNumberOfPills - pillSpacing;
CGFloat const pillHeight = view.bounds.size.height * 0.4;
CGFloat const cornerRounding = 10.0f;
CGPoint point = CGPointMake(index * (pillWidth + pillSpacing) + pillSpacing / 2.0, sinf((float) index * M_PI / kNumberOfPills + adjustment * M_PI * 2.0) * 100.0);
// top
point.x += cornerRounding;
[path moveToPoint:point];
point.x += pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// top right corner
[path addArcWithCenter:CGPointMake(point.x, point.y + cornerRounding) radius:cornerRounding startAngle:3 * M_PI_2 endAngle:2.0 * M_PI clockwise:YES];
point.x += cornerRounding;
point.y += cornerRounding;
// right
point.y += pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower right corner
[path addArcWithCenter:CGPointMake(point.x - cornerRounding, point.y) radius:cornerRounding startAngle:0 endAngle:M_PI_2 clockwise:YES];
// bottom
point.y += cornerRounding;
point.x -= cornerRounding;
point.x -= pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower left corner
[path addArcWithCenter:CGPointMake(point.x, point.y - cornerRounding) radius:cornerRounding startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
// left
point.y -= cornerRounding;
point.x -= cornerRounding;
point.y -= pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// upper left corner
[path addArcWithCenter:CGPointMake(point.x + cornerRounding, point.y) radius:cornerRounding startAngle:M_PI endAngle:3.0 * M_PI_2 clockwise:YES];
[path closePath];
}
By way of illustration, this is how I'd animate this using a CADisplayLink:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CGFloat seconds;
CGFloat fractionsOfSecond = modff(CACurrentMediaTime() - self.startTime, &seconds);
[self addMaskToView:self.imageView withAdjustment:fractionsOfSecond];
}

To do masking, you'll need to work with the view's layers. Instead of moving the views themselves up and down, create views that occupy the whole height of the parent, and then create a CALayer in the pill shape. Set the pill layer as the view's layer's mask, and move it up and down.
Something like this:
UIImageView *myImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redAndBlueBackground"]];
CALayer *pillLayer = [[CALayer alloc] initWithFrame:CGRectMake(0, 0, myImageView.bounds.size.width, myImageView.bounds.size.height / 2];
pillLayer.cornerRadius = 8;
myImageView.layer.maskLayer = pillLayer;
// move the pill layer to height y:
pillLayer.center = CGPointMake(myImageView.bounds.size.width / 2,
(myImageView.bounds.size.height / 2) + y);

Related

[CALayer display]: Ignoring bogus layer size (8681.000000, 3508.000000), contentsScale 3.000000, backing store size (26043.000000, 10524.000000)

What I am doing is, I've an imageview that displays an image and above it I've a overlay view where I draw the annotations. It's working fine on other devices but in iPhone 12 overlays is not displayed!Tried using CATiledLayer with no luck! I am just giving the screenshot of iPhone SE & iPhone 12.
This is iPhone SE output
And this is iPhone 12 output:
I am using UIBezierPath to draw the lines! This is how I am drawing the lines:
+(UIBezierPath*) pathForArrowUsingStartPoint:(CGPoint) startPoint endPoint:(CGPoint) endPoint superRect:(CGRect) superRect lineWidth:(double) LINE_WIDTH
arrowSize:(double) ARROW_SIZE arrowTipSize:(double) ARROW_TIP_DIST
{
UIBezierPath* bp = [UIBezierPath bezierPath];
CGPoint transLatedEndPoint = [Annotation convertBetweenUIViewCoordAndXYCoord:endPoint inRect:superRect];
CGPoint transLatedStartPoint = [Annotation convertBetweenUIViewCoordAndXYCoord:startPoint inRect:superRect];
CGFloat lineAngle = [Annotation angleBetweenPoint1:transLatedStartPoint Point2:transLatedEndPoint];
//calculate the rect to draw
CGPoint p0 ;
p0.x = transLatedEndPoint.x + LINE_WIDTH * cosf(lineAngle + M_PI/2);
p0.y = transLatedEndPoint.y + LINE_WIDTH * sinf(lineAngle + M_PI/2);
p0 = [Annotation convertBetweenUIViewCoordAndXYCoord:p0 inRect:superRect];
CGPoint p1;
p1.x = transLatedEndPoint.x + LINE_WIDTH * cosf(lineAngle - M_PI/2);
p1.y = transLatedEndPoint.y + LINE_WIDTH * sinf(lineAngle - M_PI/2);
p1 = [Annotation convertBetweenUIViewCoordAndXYCoord:p1 inRect:superRect];
CGPoint p2 ;
p2.x = transLatedStartPoint.x - LINE_WIDTH * cosf(lineAngle + M_PI/2);
p2.y = transLatedStartPoint.y - LINE_WIDTH * sinf(lineAngle + M_PI/2);
p2 = [Annotation convertBetweenUIViewCoordAndXYCoord:p2 inRect:superRect];
CGPoint p3;
p3.x = transLatedStartPoint.x - LINE_WIDTH * cosf(lineAngle - M_PI/2);
p3.y = transLatedStartPoint.y - LINE_WIDTH * sinf(lineAngle - M_PI/2);
p3 = [Annotation convertBetweenUIViewCoordAndXYCoord:p3 inRect:superRect];
// now drawing arrow head using triangle
CGPoint arrow0P0;
arrow0P0.x = transLatedEndPoint.x - ARROW_SIZE * cosf(lineAngle - M_PI/4);
arrow0P0.y = transLatedEndPoint.y - ARROW_SIZE * sinf(lineAngle - M_PI/4);
arrow0P0 = [Annotation convertBetweenUIViewCoordAndXYCoord:arrow0P0 inRect:superRect];
CGPoint arrow0P1;
arrow0P1.x = transLatedEndPoint.x - ARROW_SIZE * cosf(lineAngle + M_PI/4);
arrow0P1.y = transLatedEndPoint.y - ARROW_SIZE * sinf(lineAngle + M_PI/4);
arrow0P1 = [Annotation convertBetweenUIViewCoordAndXYCoord:arrow0P1 inRect:superRect];
CGPoint arrow0Tip;
arrow0Tip.x = transLatedEndPoint.x + ARROW_TIP_DIST * cosf(lineAngle);
arrow0Tip.y = transLatedEndPoint.y + ARROW_TIP_DIST * sinf(lineAngle);
arrow0Tip = [Annotation convertBetweenUIViewCoordAndXYCoord:arrow0Tip inRect:superRect];
[bp moveToPoint:p0];
[bp addLineToPoint:p1];
[bp addLineToPoint:p2];
[bp addLineToPoint:p3];
[bp closePath];
//arrows
[bp moveToPoint:arrow0P0];
[bp addLineToPoint:arrow0Tip];
[bp addLineToPoint:arrow0P1];
[bp addLineToPoint:arrow0P0];
[bp closePath];
return bp;
}
Just a comment really, put here for the ease of formatting. You should have some kind of a zoom scale somewhere, say a property
#property (nonatomic) CGFloat zoom;
and then set it when the scrollview zoom, something like
- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
{
[self setZoom:scale * self.zoom inScrollView:scrollView];
}
and then in setZoom apply your limits. YMMV. Just what worked for me when getting similar problem.
Newer device has scale factor of 3. For large image a canvas that big was throwing the error and my canvas was not visible. Just changing the scale factor of my drawing canvas does the trick.
self.drawingCanvas.contentScaleFactor = 2.0;

Drop `points` along a UIBezierPath such that `center` of points lies ON the path

The problem statement:
Create a semi-circular dial that has dots along it's circumference. User can pan and rotate this dial.
Approach:
Created a UIView
Added a circle with radius that's double the height of the UIView (using [UIBezierPath bezierPathWithOvalInRect] and CAShapeLayer)
Add dots (circular CALayer objects) along the circumference of this circle, such that the center of these dots lies ON the circumference.
The obstacle:
The dots are plotted along a path, but the center of these dots does not lie on the circumference.
The code is as follows:
#property (nonatomic, retain) CAShapeLayer* dialLineLayer;
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
self.dialLineLayer = [CAShapeLayer layer];
CGRect path = CGRectMake(SCREEN_WIDTH /2 - self.radius, -self.radius, 2 * self.radius, 2 * self.radius);
[self.dialLineLayer setPath:[[UIBezierPath bezierPathWithOvalInRect:path] CGPath]];
[self.dialLineLayer setStrokeColor:[[UIColor la_white10Color] CGColor]];
[self.dialLineLayer setFillColor:[[UIColor clearColor] CGColor]];
[self.dialLineLayer setLineWidth:3.0f];
[self.layer addSublayer:self.dialLineLayer];
[self addDotsOnLineLayer];
}
return self;
}
- (CGFloat) radius {
// View has an aspect ratio constraint of 75:34 (W:H)
return SCREEN_WIDTH * 34 / 75 - 10; // Circle radius is 10px less that View's height
}
- (CGFloat) circumference {
return M_2_PI * self.radius; // 2Pi * radius
}
- (void) addDotsOnLineLayer {
CGPoint dotPoint = CGPointMake(SCREEN_WIDTH /2 - self.radius - self.radius/2, 0);
for (double t = 0; t < 2*M_PI; t += 0.2) {
CGFloat dotRadius = arc4random_uniform(10) - arc4random_uniform(3); // Random radius between 3px and 10px
CALayer* dotLayer = [CALayer layer];
dotLayer.frame = CGRectMake(dotPoint.x, dotPoint.y, dotRadius, dotRadius);
dotLayer.cornerRadius = dotRadius / 2;
dotLayer.masksToBounds = YES;
dotLayer.backgroundColor = [UIColor greenColor].CGColor;
dotPoint.x = self.radius * cos(t) + (SCREEN_WIDTH / 2);
dotPoint.y = self.radius * sin(t) + 0;
[self.dialLineLayer addSublayer:dotLayer];
}
}
Try this: (Brace for major 'doh' + facepalm ;) )
dotLayer.frame = CGRectMake(0, 0, dotRadius, dotRadius);
dotLayer.position = dotPoint;
Explanation
You are currently positioning the origin of your layer on the path. (Remove the corner radius and you'll see what I mean.) However, since you want the center of the layer on the path, you have to assign to .position.
Note that this is only true, as long as you leave the .anchorPoint property of the layer at {0.5, 0.5}.
See Core Animation Basics for more info.

Draw dynamic shapes iOS at very high rate

I'm trying to create a sort of "radar" that would look like this :
The idea is that the blue part will act like a compass to indicate proximity of stuff to the user. The shape would be narrow, long and blue when the user is far from the objective, and become shorter, thicker and red when he gets closer, as shown below:
To achieve this, I drew a few circles (actually I tried IBDesignable and IBInspectable but I keep getting "Build failed" but that is another subject)
My question is : Is this possible to draw the "cone" part that is supposed to change its dimensions at a high rate without risking lag ?
Can I use UIBezierPath / AddArc or Addline methods to achieve this or I am heading to a dead end ?
There are two approaches:
Define complex bezier shapes and animate use CABasicAnimation:
- (UIBezierPath *)pathWithCenter:(CGPoint)center angle:(CGFloat)angle radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point1 = CGPointMake(center.x - radius * sinf(angle), center.y - radius * cosf(angle));
[path moveToPoint:center];
[path addLineToPoint:point1];
NSInteger curves = 8;
CGFloat currentAngle = M_PI_2 * 3.0 - angle;
for (NSInteger i = 0; i < curves; i++) {
CGFloat nextAngle = currentAngle + angle * 2.0 / curves;
CGPoint point2 = CGPointMake(center.x + radius * cosf(nextAngle), center.y + radius * sinf(nextAngle));
CGFloat controlPointRadius = radius / cosf(angle / curves);
CGPoint controlPoint = CGPointMake(center.x + controlPointRadius * cosf(currentAngle + angle / curves), center.y + controlPointRadius * sinf(currentAngle + angle / curves));
[path addQuadCurveToPoint:point2 controlPoint:controlPoint];
currentAngle = nextAngle;
point1 = point2;
}
[path closePath];
return path;
}
I just split the arc into a series of quad curves. I find cubic curves can get closer, but with just a few quad curves, we get a very good approximation.
And then animate it with CABasicAnimation:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.fillColor = self.startColor.CGColor;
layer.path = self.startPath.CGPath;
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
}
- (IBAction)didTapButton:(id)sender {
self.radarLayer.path = self.finalPath.CGPath;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (id)self.startPath.CGPath;
pathAnimation.toValue = (id)self.finalPath.CGPath;
pathAnimation.removedOnCompletion = false;
self.radarLayer.fillColor = self.finalColor.CGColor;
CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:#"fillColor"];
fillAnimation.fromValue = (id)self.startColor.CGColor;
fillAnimation.toValue = (id)self.finalColor.CGColor;
fillAnimation.removedOnCompletion = false;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = #[pathAnimation, fillAnimation];
group.duration = 2.0;
[self.radarLayer addAnimation:group forKey:#"bothOfMyAnimations"];
}
That yields:
The other approach is to use simple bezier, but animate with display link:
If you wanted to use a CAShapeLayer and a CADisplayLink (which is like a timer that's optimized for screen updates), you could do something like:
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFAbsoluteTime startTime;
#property (nonatomic) CFAbsoluteTime duration;
#property (nonatomic, weak) CAShapeLayer *radarLayer;
#end
#implementation ViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
[self updateRadarLayer:layer percent:0.0];
}
- (IBAction)didTapButton:(id)sender {
[self startDisplayLink];
}
- (void)updateRadarLayer:(CAShapeLayer *)radarLayer percent:(CGFloat)percent {
CGFloat angle = M_PI_4 * (1.0 - percent / 2.0);
CGFloat distance = 200 * (0.5 + percent / 2.0);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.view.center];
[path addArcWithCenter:self.view.center radius:distance startAngle:M_PI_2 * 3.0 - angle endAngle:M_PI_2 * 3.0 + angle clockwise:true];
[path closePath];
radarLayer.path = path.CGPath;
radarLayer.fillColor = [self colorForPercent:percent].CGColor;
}
- (UIColor *)colorForPercent:(CGFloat)percent {
return [UIColor colorWithRed:percent green:0 blue:1.0 - percent alpha:1];
}
- (void)startDisplayLink {
self.startTime = CFAbsoluteTimeGetCurrent();
self.duration = 2.0;
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDisplayLink:(CADisplayLink *)link {
CGFloat percent = (CFAbsoluteTimeGetCurrent() - self.startTime) / self.duration;
if (percent < 1.0) {
[self updateRadarLayer:self.radarLayer percent:percent];
} else {
[self stopDisplayLink];
[self updateRadarLayer:self.radarLayer percent:1.0];
}
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
That yields:
You should probably use one or more CAShapeLayer objects and Core Animation.
You can install the CGPath from a UIBezierPath into a shape layer, and you can animate the changes to the shape layers, although to get the animation to work right you want to have the same number and type of control points in the shape at the beginning and ending of your animation.

How to draw a circle according to the offset when drop down the tableView

I know how to draw the circle according to the offset.
If i drop down the tableView too fast to see the process of the draw,so i want to make it slower.How to make it? Thanks.
This is my circle.m
property progress is the offset (0.0~1.0).
- (void)drawRect:(CGRect)rect {
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
step = 33 * M_PI/18 * self.progress;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:self.bounds.size.width / 2 - 3 startAngle:startAngle endAngle:startAngle + step clockwise:YES];
path.lineWidth = 1.5;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path stroke];
}
If you dig into the layer level, you can subclass CAShapeLayer and use -[CAShapeLayer strokeStart] and -[CAShapeLayer strokeEnd]. Then, you can simply move your CG code into -drawInContext:
Example:
#implementation MyView
- (Class)layerClass
{
return [MyCircleLayer class];
}
- (void)setProgress:(CGFloat)progress
{
MyCircleLayer *layer = (id)self.layer;
[layer setProgress:progress];
}
#end
#interface MyCircleLayer : CAShapeLayer
#property (nonatomic, assign) CGFloat progress;
#end
#implementation MyCircleLayer
// Vary strokeStart/strokeEnd based on where or how you want to animate the drawing
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
/**
Constantly updating strokeStart/strokeEnd should queue up redraws and
should draw fluidly, but this could be delayed with an interval or via layer animation
*/
[self setStrokeEnd:progress];
}
- (void)drawInContext:(CGContextRef)ctx
{
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
// Draw the full circle
...
[path stroke];
}

Changing the speed of a CAKeyframeAnimation mid-animation in iOS

I am trying to allow the user to change the speed of my animation. I am making a CAKeyframeAnimation with a bezier path, and I can get it to display and run correctly.
I try to change the speed by creating a new animation path with a different duration. The planes go back to the beginning (which I haven't tried to fix yet) and do speed up. The path that they are being drawn on goes away at the time it would have had the animation never changed speeds. When the planes finish, another appears at the point at which the animation was paused in the first place. I have no idea what I'm doing wrong. My question is similar to this one modifying dynamically the duration of CAKeyframeAnimation, but I don't understand what the OP said about finally using blocks.
//The first two methods are in a class subclassing UIView
/** Pause each plane's animation */
- (void)pauseAnimation
{
CFTimeInterval pausedTime = [[self layer] convertTime:CACurrentMediaTime() fromLayer:nil];
[self layer].speed = 0.0;
[self layer].timeOffset = pausedTime;
}
/** Resume each plane's animation */
- (void)resumeAnimation
{
CFTimeInterval pausedTime = [[self layer] timeOffset];
[self layer].speed = 1.0;
[self layer].timeOffset = 0.0;
CFTimeInterval timeSincePause = [[self layer] convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
for(SEPlane *plane in planes){
plane.planeAnimationPath.speedMultiplier = 5;
[plane.planeAnimationPath beginAnimation:self];
}
//[self layer].beginTime = timeSincePause;
}
//This method is in the class of planeAnimationPath
/** Begin animating plane along given path */
- (void)beginAnimation:(UIView *) view
{
planeAnimation = nil;
// Create animation layer for animating plane
planeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
planeAnimation.path = [bezierPath CGPath];
planeAnimation.duration = approximateLength/(ANIMATION_SPEED * self.speedMultiplier);
planeAnimation.calculationMode = kCAAnimationPaced;
planeAnimation.fillMode = kCAFillModeForwards;
planeAnimation.rotationMode = kCAAnimationRotateAuto;
planeAnimation.removedOnCompletion = YES;
[planeAnimation setDelegate:self];
// Add animation to image-layer
[imageLayer addAnimation:planeAnimation forKey:animationKey];
// Add image-layer to view
[[view layer] addSublayer:imageLayer];
}
Unlike default animations, which animate from the current position to the target position, CAKeyframeAnimations don't (as far as I can tell). Besides how would you interpret an animation where the current position is not on the path?
The simplest option that I can think of is to do the following in the setter of speedMultiplier:
Create a new animation with the desired path.
Set the duration as if speedMultiplier was 1
Set the speed to speedMultiplier
Set the timeOffset to duration * "percent of new animation already complete"
Add the animation to the layer.
As you might have guessed, the tricky part is step 4. For simple paths this is easy but for arbitrary paths, it will get a bit more complicated. As a starting point, you will need the formula for bezier quadratic and cubic curves. Search for "distance parametrization of bezier curves" and you will find tons of stuff.
Here is a code sample for a simple rectangular path. The window simply has a MPView and a slider:
#implementation MPView {
IBOutlet NSSlider *_slider; // Min=0.0, Max=5.0
CALayer *_hostLayer;
CALayer *_ballLayer;
CAKeyframeAnimation *_ballPositionAnimation;
double _speed;
}
- (void) awakeFromNib
{
CGRect bounds = self.bounds;
[CATransaction begin];
[CATransaction setDisableActions:YES];
_speed = 1.0;
_hostLayer = [CALayer layer];
_hostLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
self.layer = _hostLayer;
self.wantsLayer = YES;
_ballLayer = [CALayer layer];
_ballLayer.bounds = CGRectMake(0, 0, 32, 32);
_ballLayer.position = CGPointMake(40, 40);
_ballLayer.backgroundColor = CGColorGetConstantColor(kCGColorWhite);
_ballLayer.cornerRadius = 16;
_hostLayer.sublayers = #[_ballLayer];
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, _ballLayer.position.x, _ballLayer.position.y);
CGPathAddRect(path, NULL, CGRectInset(bounds, 40, 40));
CGPathCloseSubpath(path);
_ballPositionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
_ballPositionAnimation.path = path;
_ballPositionAnimation.duration = 6;
_ballPositionAnimation.repeatCount = HUGE_VALF;
CGPathRelease(path);
[_ballLayer addAnimation:_ballPositionAnimation forKey:_ballPositionAnimation.keyPath];
[CATransaction commit];
[_slider bind:NSValueBinding toObject:self withKeyPath:#"speed" options:#{NSContinuouslyUpdatesValueBindingOption:#YES}];
}
- (double) speed
{
return _speed;
}
- (void) setSpeed:(double)speed
{
_speed = speed;
CGPoint pos = [(CALayer*)_ballLayer.presentationLayer position];
[CATransaction begin];
[CATransaction setDisableActions:YES];
_ballPositionAnimation.speed = _speed;
_ballPositionAnimation.duration = 5.0;
_ballPositionAnimation.timeOffset = _ballPositionAnimation.duration * [self percentOfPathCompleted:pos];
[_ballLayer addAnimation:_ballPositionAnimation forKey:_ballPositionAnimation.keyPath];
[CATransaction commit];
}
- (double) percentOfPathCompleted:(CGPoint)p
{
CGRect rect = CGRectInset(self.bounds, 40, 40);
double minX = NSMinX(rect);
double minY = NSMinY(rect);
double maxX = NSMaxX(rect);
double maxY = NSMaxY(rect);
double offset = 0.0;
if (p.x == minX && p.y == minY)
return 0.0;
else if (p.x > minX && p.y == minY)
offset = (p.x - minX) / rect.size.width * 0.25;
else if (p.x == maxX && p.y < maxY)
offset = (p.y - minY) / rect.size.height * 0.25 + 0.25;
else if (p.x > minX && p.y == maxY)
offset = (1.0 - (p.x - minX) / rect.size.width) * 0.25 + 0.50;
else
offset = (1.0 - (p.y - minY) / rect.size.height) * 0.25 + 0.75;
NSLog(#"Offset = %f",offset);
return offset;
}
#end

Resources