Как поворачивать CALayer вокруг диагональной линии?

Я пытаюсь реализовать флип-анимацию, которая будет использоваться в настольной игре, например iPhone-приложении. Ожидается, что анимация будет похожа на фрагмент игры, который вращается и изменяется на цвет его спины (вроде фрагмент Реверси). Мне удалось создать анимацию, которая переворачивает фигуру вокруг своей ортогональной оси, но когда я пытаюсь перевернуть ее вокруг диагональной оси, изменяя вращение вокруг оси z, фактическое изображение также поворачивается (не удивительно). Вместо этого я хотел бы повернуть изображение "как есть" вокруг диагональной оси.

Я попытался изменить layer.sublayerTransform, но без успеха.

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

Окончательная версия: На основе предложения Лоренцоса создать дискретную ключевую анимацию и вычислить матрицу преобразования для каждого кадра. Эта версия вместо этого пытается оценить количество "направляющих" кадров, необходимых на основе размера слоя, а затем использует анимацию с линейной привязкой. Эта версия вращается с произвольным углом, поэтому для поворота вокруг диагональной линии используйте угол 45 градусов.

Пример использования:

[someclass flipLayer:layer image:image angle:M_PI/4]

Реализация:

- (void)animationDidStop:(CAAnimationGroup *)animation
                finished:(BOOL)finished {
  CALayer *layer = [animation valueForKey:@"layer"];

  if([[animation valueForKey:@"name"] isEqual:@"fadeAnimation"]) {
    /* code for another animation */
  } else if([[animation valueForKey:@"name"] isEqual:@"flipAnimation"]) {
    layer.contents = [animation valueForKey:@"image"];
  }

  [layer removeAllAnimations];
}

- (void)flipLayer:(CALayer *)layer
            image:(CGImageRef)image
            angle:(float)angle {
  const float duration = 0.5f;

  CAKeyframeAnimation *rotate = [CAKeyframeAnimation
                                 animationWithKeyPath:@"transform"];
  NSMutableArray *values = [[[NSMutableArray alloc] init] autorelease];
  NSMutableArray *times = [[[NSMutableArray alloc] init] autorelease];
  /* bigger layers need more "guiding" values */
  int frames = MAX(layer.bounds.size.width, layer.bounds.size.height) / 2;
  int i;
  for (i = 0; i < frames; i++) {
    /* create a scale value going from 1.0 to 0.1 to 1.0 */
    float scale = MAX(fabs((float)(frames-i*2)/(frames - 1)), 0.1);

    CGAffineTransform t1, t2, t3;
    t1 = CGAffineTransformMakeRotation(angle);
    t2 = CGAffineTransformScale(t1, scale, 1.0f);
    t3 = CGAffineTransformRotate(t2, -angle);
    CATransform3D trans = CATransform3DMakeAffineTransform(t3);

    [values addObject:[NSValue valueWithCATransform3D:trans]];
    [times addObject:[NSNumber numberWithFloat:(float)i/(frames - 1)]];
  }
  rotate.values = values;
  rotate.keyTimes = times;
  rotate.duration = duration;
  rotate.calculationMode = kCAAnimationLinear;

  CAKeyframeAnimation *replace = [CAKeyframeAnimation
                                  animationWithKeyPath:@"contents"];
  replace.duration = duration / 2;
  replace.beginTime = duration / 2;
  replace.values = [NSArray arrayWithObjects:(id)image, nil];
  replace.keyTimes = [NSArray arrayWithObjects:
                      [NSNumber numberWithDouble:0.0f], nil];
  replace.calculationMode = kCAAnimationDiscrete;

  CAAnimationGroup *group = [CAAnimationGroup animation];
  group.duration = duration;
  group.timingFunction = [CAMediaTimingFunction
                          functionWithName:kCAMediaTimingFunctionLinear];
  group.animations = [NSArray arrayWithObjects:rotate, replace, nil];
  group.delegate = self;
  group.removedOnCompletion = NO;
  group.fillMode = kCAFillModeForwards;
  [group setValue:@"flipAnimation" forKey:@"name"];
  [group setValue:layer forKey:@"layer"];
  [group setValue:(id)image forKey:@"image"];

  [layer addAnimation:group forKey:nil];
}

Исходный код:

+ (void)flipLayer:(CALayer *)layer
          toImage:(CGImageRef)image
        withAngle:(double)angle {
  const float duration = 0.5f;

  CAKeyframeAnimation *diag = [CAKeyframeAnimation
                               animationWithKeyPath:@"transform.rotation.z"];
  diag.duration = duration;
  diag.values = [NSArray arrayWithObjects:
                 [NSNumber numberWithDouble:angle],
                 [NSNumber numberWithDouble:0.0f],
                 nil];
  diag.keyTimes = [NSArray arrayWithObjects:
                   [NSNumber numberWithDouble:0.0f],
                   [NSNumber numberWithDouble:1.0f],
                   nil];
  diag.calculationMode = kCAAnimationDiscrete;

  CAKeyframeAnimation *flip = [CAKeyframeAnimation
                               animationWithKeyPath:@"transform.rotation.y"];
  flip.duration = duration;
  flip.values = [NSArray arrayWithObjects:
                 [NSNumber numberWithDouble:0.0f],
                 [NSNumber numberWithDouble:M_PI / 2],
                 [NSNumber numberWithDouble:0.0f],
                 nil];
  flip.keyTimes = [NSArray arrayWithObjects:
                   [NSNumber numberWithDouble:0.0f],
                   [NSNumber numberWithDouble:0.5f],
                   [NSNumber numberWithDouble:1.0f],
                   nil];
  flip.calculationMode = kCAAnimationLinear;

  CAKeyframeAnimation *replace = [CAKeyframeAnimation
                                  animationWithKeyPath:@"contents"];
  replace.duration = duration / 2;
  replace.beginTime = duration / 2;
  replace.values = [NSArray arrayWithObjects:(id)image, nil];
  replace.keyTimes = [NSArray arrayWithObjects:
                      [NSNumber numberWithDouble:0.0f], nil];
  replace.calculationMode = kCAAnimationDiscrete;

  CAAnimationGroup *group = [CAAnimationGroup animation];
  group.removedOnCompletion = NO;
  group.duration = duration;
  group.timingFunction = [CAMediaTimingFunction
                          functionWithName:kCAMediaTimingFunctionLinear];
  group.animations = [NSArray arrayWithObjects:diag, flip, replace, nil];
  group.fillMode = kCAFillModeForwards;

  [layer addAnimation:group forKey:nil];
}

Ответы

Ответ 1

вы можете подделать его так: создать аффинное преобразование, которое разрушает слой по диагонали:

A-----B           B
|     |          /
|     |   ->   A&D
|     |        /
C-----D       C

измените изображение и преобразуйте CALayer обратно в другую анимацию. Это создаст иллюзию слоя, вращающегося вокруг его диагонали.

Матрица для этого должна быть, если я правильно помню математику:

0.5 0.5 0
0.5 0.5 0
0   0   1

Обновление: ok, CA действительно не любит использовать вырожденные преобразования, но вы можете его аппроксимировать следующим образом:

CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, 0.001f, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);  

в моих тестах на симуляторе все еще была проблема, потому что вращение происходит быстрее, чем te перевод, поэтому с сплошным черным квадратом эффект был немного странным. Я полагаю, что если у вас есть центрированный спрайт с прозрачной областью вокруг него, эффект будет близок к ожидаемому. Затем вы можете настроить значение матрицы t3, чтобы узнать, есть ли у вас более привлекательный результат.

после дальнейшего исследования выяснилось, что он должен анимировать собственный переход с помощью ключевых кадров, чтобы обеспечить максимальный контроль самого перехода. скажем, вы должны были отобразить эту анимацию за секунду, вы должны сделать десять матриц, которые будут отображаться на каждой десятой доли секунды с помощью интерполяции с использованием kCAAnimationDiscrete; эта матрица может быть сгенерирована с помощью приведенного ниже кода:

CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, animationStepValue, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);  

где animationStepValue для ech ключевого кадра берется из этой прогрессии:

{1 0.7 0.5 0.3 0.1 0.3 0.5 0.7 1}

то есть: вы генерируете десять различных матриц преобразования (фактически 9), нажимая их как ключевые кадры, которые должны отображаться на каждую десятую секунды, а затем используя параметр "не интерполировать". вы можете настроить номер анимации для обеспечения плавности и производительности *

* Извините за возможные ошибки, эта последняя часть была написана без проверки орфографии.

Ответ 2

Я решил. У вас, вероятно, уже есть решение, но вот что я нашел. Это очень просто...

Вы можете использовать CABasicAnimation для вращения по диагонали, но это должна быть конкатенация двух матриц, а именно существующей матрицы слоя, плюс CATransform3DRotate. "Трюк" в 3DRotate вам нужно указать координаты для поворота вокруг.

Код выглядит примерно так:


CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));

Это приведет к вращению, которое выглядит так, как если бы верхний левый угол квадрата вращался вокруг оси Y = X и перемещался в нижний правый угол.

Код для анимации выглядит следующим образом:


CABasicAnimation *ani1 = [CABasicAnimation animationWithKeyPath:@"transform"];

// set self as the delegate so you can implement (void)animationDidStop:finished: to handle anything you might want to do upon completion
[ani1 setDelegate:self];

// set the duration of the animation - a float
[ani1 setDuration:dur];

// set the animation "toValue" which MUST be wrapped in an NSValue instance (except special cases such as colors)
ani1.toValue = [NSValue valueWithCATransform3D:CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0))];

// give the animation a name so you can check it in the animationDidStop:finished: method
[ani1 setValue:@"shrink" forKey:@"name"];

// finally, apply the animation
[theLayer addAnimation:ani1 [email protected]"arbitraryKey"];

Что это! Этот код будет вращать квадрат (theLayer) до невидимости, когда он перемещается на 90 градусов и представляет собой ортогонально на экран. Затем вы можете изменить цвет и сделать ту же самую анимацию, чтобы вернуть ее обратно. Такая же анимация работает, потому что мы конкатенируем матрицы, поэтому каждый раз, когда вы хотите повернуть, просто сделайте это дважды, или измените M_PI/2 на M_PI.

Наконец, следует отметить, и это заставило меня с ума сойти, что по завершении слой вернется в исходное состояние, если вы явно не установите его в состояние конечной анимации. Это означает, что перед строкой [theLayer addAnimation:ani1 [email protected]"arbitraryKey"]; вы захотите добавить


theLayer.transform = CATransform3DConcat(v.theSquare.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));

чтобы установить его значение после завершения анимации. Это предотвратит возврат привязки в исходное состояние.

Надеюсь, это поможет. Если не ты, то, возможно, кто-то другой, который ударил головой о стену, как мы!:)

Приветствия,

Крис

Ответ 3

Вот пример Xamarin iOS, который я использую, чтобы откинуть угол квадратной кнопки, как ухо собаки (легко портировано на obj-c):

введите описание изображения здесь

Метод 1: использует анимацию вращения с 1 для осей x и y (примеры в Xamarin.iOS, но легко переносимые для obj-c):

// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(0.10, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
    // note the final 3 params indicate "rotate around x&y axes, but not z"
    var transf = CATransform3D.MakeRotation(-1 * (nfloat)Math.PI / 4, 1, 1, 0);
    transf.m34 = 1.0f / -500;
    Layer.Transform = transf;
}, null);

Метод 2: просто добавьте поворот x-axis и y-axis вращения в CAAnimationGroup, чтобы они запускались одновременно:

// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(1.0, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
    nfloat angleTo = -1 * (nfloat)Math.PI / 4;
    nfloat angleFrom = 0.0f ;
    string animKey = "rotate";

    // y-axis rotation
    var anim = new CABasicAnimation();
    anim.KeyPath = "transform.rotation.y";
    anim.AutoReverses = false;
    anim.Duration = 0.1f;
    anim.From = new NSNumber(angleFrom);
    anim.To = new NSNumber(angleTo);

    // x-axis rotation
    var animX = new CABasicAnimation();
    animX.KeyPath = "transform.rotation.x";
    animX.AutoReverses = false;
    animX.Duration = 0.1f;
    animX.From = new NSNumber(angleFrom);
    animX.To = new NSNumber(angleTo);

    // add both rotations to a group, to run simultaneously
    var animGroup = new CAAnimationGroup();
    animGroup.Duration = 0.1f;
    animGroup.AutoReverses = false;
    animGroup.Animations = new CAAnimation[] {anim, animX};
    Layer.AddAnimation(animGroup, animKey);

    // add perspective
    var transf = CATransform3D.Identity;
    transf.m34 = 1.0f / 500;
    Layer.Transform = transf;

}, null);