iOS: преобразование пути к слою CAShape

Я использовал CAShape Layer перед базовым преобразованием путей - из меньшего круга в больший круг. Достаточно хорошо, но затем я попытался превратить треугольник в круг; это сработало, но трансформация странная. Другими словами, от одной формы к другой он «переворачивается», «закручивается» до того, как сформируется окончательная форма. С одинаковыми формами проблем нет — от круга к кругу, — но с разными формами трансформация странная.

Интересно, это ожидаемый путь? Или есть другие приемы или обходные пути, с помощью которых мы можем плавно и пропорционально преобразовать одну форму в другую, используя CAShape Layer (или есть другие способы сделать это; это не обязательно должно быть CAShape Layer)? Заранее спасибо.


person Unheilig    schedule 25.07.2013    source источник


Ответы (3)


Тщательно выбирая контрольные точки и т. д., вы, вероятно, сможете приготовить путь, который рисуется в виде треугольника, но имеет то же количество сегментов и контрольных точек, что и окружность, которую вы хотите нарисовать. Вот так (все числовые значения предполагают экран iPhone, но суть общая):

@implementation ViewController
{
    CAShapeLayer* shapeLayer;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    shapeLayer = [[CAShapeLayer alloc] init];

    CGRect bounds = self.view.bounds;
    bounds.origin.x += 0.25 * bounds.size.width;
    bounds.size.width *= 0.5;
    bounds.origin.y += 0.25 * bounds.size.height;
    bounds.size.height *= 0.5;

    shapeLayer.frame = bounds;
    shapeLayer.backgroundColor = [[UIColor redColor] CGColor];

    [self.view.layer addSublayer: shapeLayer];
    [self toCircle: nil];
}

CGPoint AveragePoints(CGPoint a, CGPoint b)
{
    return CGPointMake((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
}

- (IBAction)toCircle:(id)sender
{
    UIBezierPath* p = [[UIBezierPath alloc] init];

    [p moveToPoint: CGPointMake(80, 56)];
    [p addCurveToPoint:CGPointMake(144, 120) controlPoint1:CGPointMake(115.34622, 56) controlPoint2:CGPointMake(144, 84.653778)];
    [p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:CGPointMake(144, 131.23434) controlPoint2:CGPointMake(141.0428, 142.27077)];
    [p addCurveToPoint:CGPointMake(48, 175.42563) controlPoint1:CGPointMake(117.75252, 182.61073) controlPoint2:CGPointMake(78.610725, 193.09874)];
    [p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:CGPointMake(38.270771, 169.80846) controlPoint2:CGPointMake(30.191547, 161.72923)];
    [p addCurveToPoint:CGPointMake(47.999996, 64.574379) controlPoint1:CGPointMake(6.9012618, 121.38927) controlPoint2:CGPointMake(17.389269, 82.24749)];
    [p addCurveToPoint:CGPointMake(80, 56) controlPoint1:CGPointMake(57.729225, 58.957207) controlPoint2:CGPointMake(68.765656, 56)];
    [p closePath];

    [CATransaction begin];
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    pathAnimation.duration = 3.f;
    pathAnimation.fromValue = (id)shapeLayer.path;
    pathAnimation.toValue = (id)p.CGPath;
    [shapeLayer addAnimation:pathAnimation forKey:@"path"];
    [CATransaction setCompletionBlock:^{
        shapeLayer.path = p.CGPath;
    }];
    [CATransaction commit];

    double delayInSeconds = 4.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self toTriangle: nil];
    });

}

- (IBAction)toTriangle: (id)sender
{
    UIBezierPath* p = [[UIBezierPath alloc] init];

    // Triangle using the same number and kind of points...
    [p moveToPoint: CGPointMake(80, 56)];
    [p addCurveToPoint: AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152))  controlPoint1:CGPointMake(80, 56) controlPoint2:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152))];
    [p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152)) controlPoint2:CGPointMake(135.42563, 152)];
    [p addCurveToPoint:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint1:CGPointMake(135.42563, 152) controlPoint2:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152))];
    [p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint2:CGPointMake(24.574375, 152)];
    [p addCurveToPoint: AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint1:CGPointMake(24.574375, 152) controlPoint2:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) ];
    [p addCurveToPoint:CGPointMake(80, 56) controlPoint1:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint2:CGPointMake(80, 56)];
    [p closePath];

    [CATransaction begin];
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    pathAnimation.duration = 3.f;
    pathAnimation.fromValue = (id)shapeLayer.path;
    pathAnimation.toValue = (id)p.CGPath;
    [shapeLayer addAnimation:pathAnimation forKey:@"path"];
    [CATransaction setCompletionBlock:^{
        shapeLayer.path = p.CGPath;
    }];
    [CATransaction commit];

    double delayInSeconds = 4.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self toCircle: nil];
    });

}

Даже с числом сегментов и контрольных точек то же самое, анимация все еще выглядит немного шаткой и, вероятно, не такой, какой вы хотели. Причина этого в том, что CA, кажется, сопоставляет все сегменты и контрольные точки один к одному, а затем линейно интерполирует между ними для всех точек. Поскольку взаимосвязь между контрольными точками и результирующим путем не является линейной (в данном случае кубической), линейная интерполяция положений контрольных точек не приведет к линейному перемещению пути. Если вы запустите этот код, вы увидите, что по мере его перехода появляются странные выступы на стороне треугольника, где у пути окружности есть другие точки дуги.

В более общем плане просто неразумно ожидать, что CA будет волшебным образом трансформироваться между двумя произвольными путями каким-то особым образом, который на первый взгляд окажется желательным. Даже приложив усилия, чтобы вручную сконструировать эти пути так, чтобы они могли молиться о трансформации, они все равно не выглядели так, как я думал, что должны.

Возможно, было бы разумнее добиться желаемого эффекта, работая с плоскими путями (т. е. путями, состоящими из множества небольших прямых линий, а не изогнутых элементов пути). Даже это кажется нетривиальным, поскольку вам снова потребуется, чтобы оба пути имели одинаковое количество сегментов, и вам пришлось бы построить эти сегменты так, чтобы общие точки были правильным количеством сегментов вдоль всего пути. дорожка.

В итоге: это довольно сложная проблема, и наивное/бесплатное решение, которое предоставил CoreAnimation, вряд ли соответствует тому, что вы хотите.

person ipmcc    schedule 25.07.2013
comment
+1 Хорошо, ты проделал работу, о которой я думал, но у меня никогда не было сил разобраться :) - person David Rönnqvist; 25.07.2013
comment
@ipmcc: Очень мило. Я тоже о том же подумал после. Я приму ваш ответ. - person Unheilig; 26.07.2013
comment
Если кто-то из вас может как-то улучшить это, пожалуйста, вернитесь и поделитесь. - person Unheilig; 26.07.2013

Из документации свойства path на CAShapeLayer:

Если два пути имеют разное количество контрольных точек или сегментов, результаты не определены.

Так что да, я бы сказал, что это ожидаемо, поскольку круг и треугольник — разные формы.

В этой статье описывается индивидуальная анимация пути. очень подробно.

person David Rönnqvist    schedule 25.07.2013

Я тут угадал! Вы можете определить круг, используя 3 точки. Таким образом, у вас будет столько же точек, сколько у треугольника. Чтобы определить круг, это будет ваш лучший друг

+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
person hfossli    schedule 25.07.2013
comment
@DavidRönnqvist Ах .. Вы говорите, что метод bezierPathWithArcCenter может использовать контрольные точки под капотом? Не думал об этом! - person hfossli; 25.07.2013
comment
Я больше имел в виду то, что говорится в документации: если два пути имеют разное количество контрольных точек или сегментов, результаты не определены. 3 линии представляют собой то же количество сегментов, что и 3 дуги, но они могут иметь разное количество контрольных точек. - person David Rönnqvist; 25.07.2013
comment
@DavidRönnqvist Да, вы правы. Мне любопытны выводы Унхейлига. Может быть, это работает? Хотя документация в этом случае ничего не гарантирует :) - person hfossli; 25.07.2013
comment
@Unheilig Конечно. У них есть бесплатный мост между собой. Вы уже пробовали? Мне интересен результат :) - person hfossli; 26.07.2013