How to curve CGMutablePath?

后端 未结 1 973
小蘑菇
小蘑菇 2020-12-24 09:29

With the following shape:

\"enter

I was wondering how do you get it to curve l

相关标签:
1条回答
  • 2020-12-24 10:10

    In your first example, you start with a path that has several closed subpaths. Apparently you want to warp the centers of the subpaths, but leave the individual subpaths unwarped relative to their (new) centers. I'm going to ignore that, because the solution even without that is already terribly complex.

    So, let's consider how to define the “warp field”. We'll use three control points:

    example with control points

    The warp leaves fixedPoint unchanged. It moves startPoint to endPoint by rotation and scaling, not by simply interpolating the coordinates.

    Furthermore, it applies the rotation and scaling based on distance from fixedPoint. And not just based on the simple Euclidean distance. Notice that we don't want apply any rotation or scaling to the top endpoints of the “V” shape in the picture, even though those endpoints are a measurable Euclidean distance from fixedPoint. We want to measure distance along the fixedPoint->startPoint vector, and apply more rotation/scaling as that distance increases.

    This all requires some pretty heavy trigonometry. I'm not going to try to explain the details. I'm just going to dump code on you, as a category on UIBezierPath:

    UIBezierPath+Rob_warp.h

    #import <UIKit/UIKit.h>
    
    @interface UIBezierPath (Rob_warp)
    
    - (UIBezierPath *)Rob_warpedWithFixedPoint:(CGPoint)fixedPoint startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
    
    @end
    

    UIBezierPath+Rob_warp.m

    Note that you'll need the Rob_forEach category from this answer.

    #import "UIBezierPath+Rob_warp.h"
    #import "UIBezierPath+Rob_forEach.h"
    #import <tgmath.h>
    
    static CGPoint minus(CGPoint a, CGPoint b) {
        return CGPointMake(a.x - b.x, a.y - b.y);
    }
    
    static CGFloat length(CGPoint vector) {
        return hypot(vector.x, vector.y);
    }
    
    static CGFloat dotProduct(CGPoint a, CGPoint b) {
        return a.x * b.x + a.y * b.y;
    }
    
    static CGFloat crossProductMagnitude(CGPoint a, CGPoint b) {
        return a.x * b.y - a.y * b.x;
    }
    
    @implementation UIBezierPath (Rob_warp)
    
    - (UIBezierPath *)Rob_warpedWithFixedPoint:(CGPoint)fixedPoint startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
    
        CGPoint startVector = minus(startPoint, fixedPoint);
        CGFloat startLength = length(startVector);
        CGPoint endVector = minus(endPoint, fixedPoint);
        CGFloat endLength = length(minus(endPoint, fixedPoint));
        CGFloat scale = endLength / startLength;
    
        CGFloat dx = dotProduct(startVector, endVector);
        CGFloat dy = crossProductMagnitude(startVector, endVector);
        CGFloat radians = atan2(dy, dx);
    
        CGPoint (^warp)(CGPoint) = ^(CGPoint input){
            CGAffineTransform t = CGAffineTransformMakeTranslation(-fixedPoint.x, -fixedPoint.y);
            CGPoint inputVector = minus(input, fixedPoint);
            CGFloat factor = dotProduct(inputVector, startVector) / (startLength * startLength);
            CGAffineTransform w = CGAffineTransformMakeRotation(radians * factor);
            t = CGAffineTransformConcat(t, w);
            CGFloat factoredScale = pow(scale, factor);
            t = CGAffineTransformConcat(t, CGAffineTransformMakeScale(factoredScale, factoredScale));
            // Note: next line is not the same as CGAffineTransformTranslate!
            t = CGAffineTransformConcat(t, CGAffineTransformMakeTranslation(fixedPoint.x, fixedPoint.y));
            return CGPointApplyAffineTransform(input, t);
        };
    
        UIBezierPath *copy = [self.class bezierPath];
        [self Rob_forEachMove:^(CGPoint destination) {
            [copy moveToPoint:warp(destination)];
        } line:^(CGPoint destination) {
            [copy addLineToPoint:warp(destination)];
        } quad:^(CGPoint control, CGPoint destination) {
            [copy addQuadCurveToPoint:warp(destination) controlPoint:warp(control)];
        } cubic:^(CGPoint control0, CGPoint control1, CGPoint destination) {
            [copy addCurveToPoint:warp(destination) controlPoint1:warp(control0) controlPoint2:warp(control1)];
        } close:^{
            [copy closePath];
        }];
        return copy;
    }
    
    @end
    

    Ok, so how do you use this crazy thing? In the case of a path like the “V” in the example, you could do it like this:

    CGRect rect = path.bounds;
    CGPoint fixedPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPoint startPoint = CGPointMake(fixedPoint.x, CGRectGetMaxY(rect));
    path = [path Rob_warpedWithFixedPoint:fixedPoint startPoint:startPoint endPoint:endAnchor];
    

    I'm computing fixedPoint as the center of the top edge of the path's bounding box, and startPoint as the center of the bottom edge. The endAnchor is under user control in my test program. It looks like this in the simulator:

    demo with V

    A bubble-type path looks like this:

    demo with bubbles

    You can find my test project here: https://github.com/mayoff/path-warp

    0 讨论(0)
提交回复
热议问题