CATransformLayer and 3D rotation, venetian store or accordion like, keeping tiles adjacent?

168 views Asked by At

I have a set of tiles vertically arranged. They are meant to rotate around the X axis. Think of an accordion. I want each tile n+1 to always hang off tile n.

The cyan tile, in the last two screen shots, ought to be attached to the yellow tile. It is not. I do not understand why. Even less so, why when I rotate row0 I get all next 5 tiles rotated (which is what I want) but when I want to "correct" starting from some later tile by rotating in the other direction, I end up with a disconnect between the 3rd and 4th tile.

For reference, here's the setup:

 CGRect  tileRect  = (CGRect)  {CGPointZero, {self.bounds.size.width, 30.0f}} ;
 CGRect  pageRect  = tileRect ;    pageRect.size.height *= 3 ;
 CGPoint baseCenter= (CGPoint) {self.bounds.size.width / 2.0f, 0} ;

 CGPoint anchorMidTop=   (CGPoint) {0.5f, 0.0f} ;
 CGPoint anchorTopLeft=  (CGPoint) {0.0f, 0.0f} ;

 CGPoint positionNW  =   (CGPoint) {0.0f, 0.0f} ;

 CALayer * (^setupLayerGeometry)(CALayer *, CGRect, CGPoint, CGPoint) =
 ^(CALayer * layer, CGRect b, CGPoint a, CGPoint p) {
     layer.bounds = b ;
     layer.anchorPoint = a ;
     layer.position = p ;
     return layer ;
 } ;

 CALayer * (^setupColor)(CALayer *, UIColor *) = ^(CALayer * layer, UIColor * color) {
     layer.backgroundColor = color.CGColor ;
     layer.opacity = 0.850f ;
     return layer ;
 } ;

 CALayer * (^stdLayer)(UIColor *, CGPoint, CGPoint) =
 ^CALayer * (UIColor * color, CGPoint anchor, CGPoint pos) {
     return setupLayerGeometry(
         setupColor([CALayer layer], color)
         ,   tileRect
         ,   anchor
         ,   pos) ;
 } ; 

 CATransformLayer * (^transformLayer)(CGRect, CGPoint, CGPoint) =
 ^CATransformLayer * (CGRect bounds, CGPoint anchor, CGPoint pos) {
     return (CATransformLayer *) setupLayerGeometry([CATransformLayer layer]
     ,   bounds
     ,   anchor
     ,   pos) ;
 } ;

 self.baseLayer = transformLayer(tileRect,    anchorTopLeft, baseCenter) ;

 CATransform3D initialTransform = self.baseLayer.sublayerTransform ;
 initialTransform.m34 = 1.0f / -200.0f ;
 self.baseLayer.sublayerTransform = initialTransform ;
 [self.layer addSublayer:self.baseLayer] ;

 CALayer * (^wrap0) (CALayer *) = ^CALayer * (CALayer * layer) {
     CALayer * wrap = transformLayer(tileRect,    anchorTopLeft, positionNW) ;
     [wrap addSublayer:layer] ;
     return wrap ;
 } ;

 CALayer * (^wrap)(CALayer *) = wrap0 ;

Now I'm creating six tiles, as plain CALayer's.

 CALayer * row0  = stdLayer([UIColor redColor],      anchorMidTop, (CGPoint) {0, 0}) ;
 CALayer * row1  = stdLayer([UIColor blueColor],     anchorMidTop, (CGPoint) {0, 30}) ;
 CALayer * row2  = stdLayer([UIColor yellowColor],   anchorMidTop, (CGPoint) {0, 60}) ;
 CALayer * row3  = stdLayer([UIColor cyanColor],     anchorMidTop, (CGPoint) {0, 90}) ;
 CALayer * row4  = stdLayer([UIColor purpleColor],   anchorMidTop, (CGPoint) {0, 120}) ;
 CALayer * row5  = stdLayer([UIColor magentaColor],  anchorMidTop, (CGPoint) {0, 150}) ;

Now, I'm wrapping each such tile into a parent CATransformLayer, which I am then adding as sublayer of the previous row, with the intention of having each CATransformLayer affect each of its sublayers.

 [self.baseLayer  addSublayer:wrap(row0)] ;
 [row0.superlayer addSublayer:wrap(row1)] ;
 [row1.superlayer addSublayer:wrap(row2)] ;
 [row2.superlayer addSublayer:wrap(row3)] ;
 [row3.superlayer addSublayer:wrap(row4)] ;
 [row4.superlayer addSublayer:wrap(row5)] ;

 CGFloat angle = M_PI / 10.0f ;

 row0.superlayer.sublayerTransform = CATransform3DMakeRotation(-angle, 1, 0, 0);

At this point I get this:

enter image description here

And I thought I had nailed it until I replaced the last line with:

 row3.superlayer.sublayerTransform = CATransform3DMakeRotation(-angle, 1, 0, 0);

And here's what I get:

enter image description here

If, instead, I replace the above line with:

 row0.superlayer.sublayerTransform = CATransform3DMakeRotation(-angle, 1, 0, 0);
 row3.superlayer.sublayerTransform = CATransform3DMakeRotation( angle, 1, 0, 0);

I get this:

enter image description here

I have read and re-read Apple's documentation, paying close attention to this:

Anchor Points Affect Geometric Manipulations

Geometry related manipulations of a layer occur relative to that layer’s anchor point, which you can access using the layer’s anchorPoint property. The impact of the anchor point is most noticeable when manipulating the position or transform properties of the layer. The position property is always specified relative to the layer’s anchor point, and any transformations you apply to the layer occur relative to the anchor point as well.

(emphasis mine)

It is fair to say that I am now utterly confused. Anyone can see/explain what I am doing wrong?

How can I arrange those layers such that my tiles are always adjacent, top to bottom?

Here's a dump of the layer hierarchy:

 <CATransformLayer> frame: {{100, 0}, {200, 30}} pos: {100, 0} anchor: {0, 0}
   <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 0}
     <CALayer> frame: {{-100, 0}, {200, 30}} pos: {0, 0} anchor: {0.5, 0}
     <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 0}
       <CALayer> frame: {{-100, 30}, {200, 30}} pos: {0, 30} anchor: {0.5, 0}
       <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 0}
         <CALayer> frame: {{-100, 60}, {200, 30}} pos: {0, 60} anchor: {0.5, 0}
         <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 0}
           <CALayer> frame: {{-100, 90}, {200, 30}} pos: {0, 90} anchor: {0.5, 0}
           <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 0}
             <CALayer> frame: {{-100, 120}, {200, 30}} pos: {0, 120} anchor: {0.5, 
             <CATransformLayer> frame: {{0, 0}, {200, 30}} pos: {0, 0} anchor: {0, 
               <CALayer> frame: {{-100, 150}, {200, 30}} pos: {0, 150} anchor: {0.5, 0}
1

There are 1 answers

0
verec On

I got it. Finally!

enter image description here

Here's the code:

- (void) reloadData {
    for (CALayer * layer in [self.baseLayer.sublayers copy]) {
        [layer removeFromSuperlayer] ;
    }

    CGRect  tileRect  = (CGRect)  {CGPointZero, {self.bounds.size.width, 30.0f}} ;
    CGFloat halfWidth = self.bounds.size.width / 2.0f ;

The helper blocks:

    CALayer * (^setupLayerGeometry)(CALayer *, CGRect, CGPoint, CGPoint, NSString *) =
    ^(CALayer * layer, CGRect b, CGPoint a, CGPoint p, NSString * n) {
        layer.bounds = b ;
        layer.anchorPoint = a ;
        layer.position = p ;
        layer.name = n ;
        return layer ;
    } ;

    CALayer * (^setupColor)(CALayer *, UIColor *) = ^(CALayer * layer, UIColor * color) {
        layer.backgroundColor = color.CGColor ;
        layer.opacity = 0.850f ;
        return layer ;
    } ;

    CALayer * (^stdLayer)(UIColor *, CGPoint, CGPoint, NSString *) =
    ^CALayer * (UIColor * color, CGPoint anchor, CGPoint pos, NSString * n) {
        return setupLayerGeometry(
            setupColor([CALayer layer], color)
        ,   tileRect
        ,   anchor
        ,   pos
        ,   n) ;
    } ;

    CATransformLayer * (^transformLayer)(CGRect, CGPoint, CGPoint, NSString *) =
    ^CATransformLayer * (CGRect bounds, CGPoint anchor, CGPoint pos, NSString * n) {
        return (CATransformLayer *) setupLayerGeometry(
            [CATransformLayer layer]
            ,   bounds
            ,   anchor
            ,   pos
            ,   n) ;
    } ;

Now for the baseLayer that is the root CATransformLayer

#define ANC(x,y)    (CGPoint) {x, y}
#define POS(x,y)    (CGPoint) {x, y}

    [self.baseLayer removeFromSuperlayer] ;
    self.baseLayer = transformLayer(tileRect,    ANC(0, 0), POS(halfWidth, 0), @"baseLayer") ;

Creating each row with a CATransformLayer as its root that also acts as the super layer of the next row.

    CALayer * row0  = stdLayer([UIColor redColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row0") ;
    CATransformLayer * trn0 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 0), @"trn0") ;
    [self.baseLayer addSublayer:trn0] ;
    [trn0 addSublayer:row0] ;


    CALayer * row1  = stdLayer([UIColor blueColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row1") ;
    CATransformLayer * trn1 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn1") ;
    [trn1 addSublayer:row1] ;
    [trn0 addSublayer:trn1] ;

    CALayer * row2  = stdLayer([UIColor yellowColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row2") ;
    CATransformLayer * trn2 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn2") ;
    [trn2 addSublayer:row2] ;
    [trn1 addSublayer:trn2] ;

    CALayer * row3  = stdLayer([UIColor cyanColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row3") ;
    CATransformLayer * trn3 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn3") ;
    [trn3 addSublayer:row3] ;
    [trn2 addSublayer:trn3] ;

    CALayer * row4  = stdLayer([UIColor purpleColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row4") ;
    CATransformLayer * trn4 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn4") ;
    [trn4 addSublayer:row4] ;
    [trn3 addSublayer:trn4] ;

    CALayer * row5  = stdLayer([UIColor magentaColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row5") ;
    CATransformLayer * trn5 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn5") ;
    [trn5 addSublayer:row5] ;
    [trn4 addSublayer:trn5] ;

    CALayer * row6  = stdLayer([UIColor lightGrayColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row6") ;
    CATransformLayer * trn6 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn6") ;
    [trn6 addSublayer:row6] ;
    [trn5 addSublayer:trn6] ;

    CALayer * row7  = stdLayer([UIColor darkGrayColor],      ANC(0.0, 0.0), POS(-halfWidth, 0), @"row7") ;
    CATransformLayer * trn7 = (CATransformLayer *) transformLayer(tileRect,    ANC(0, 0), POS(0, 30), @"trn7") ;
    [trn7 addSublayer:row7] ;
    [trn6 addSublayer:trn7] ;

Finally setting the transforms. Since each row is the parent of the next row (thanks to the embedded CATransformLayer) the angle needed is relative to the immediate parent, thus is independent of the row number a particular tile is displayed at.

    CGFloat angle = M_PI / 10.0f ;

    trn0.sublayerTransform =  CATransform3DMakeRotation(1.0*angle, 1, 0, 0);
    trn1.sublayerTransform =  CATransform3DMakeRotation(-1.0*angle, 1, 0, 0);
    trn2.sublayerTransform =  CATransform3DMakeRotation(-1.0*angle, 1, 0, 0);
    trn3.sublayerTransform =  CATransform3DMakeRotation(1.0*angle, 1, 0, 0);
    trn4.sublayerTransform =  CATransform3DMakeRotation(1.0*angle, 1, 0, 0);
    trn5.sublayerTransform =  CATransform3DMakeRotation(-1.0*angle, 1, 0, 0);
    trn6.sublayerTransform =  CATransform3DMakeRotation(-1.0*angle, 1, 0, 0);
    trn7.sublayerTransform =  CATransform3DMakeRotation(2.0*angle, 1, 0, 0);

    CATransform3D initialTransform = self.baseLayer.sublayerTransform ;
    initialTransform.m34 = 1.0f / -200.0f ;
    self.baseLayer.sublayerTransform = initialTransform ;

    [self.layer addSublayer:self.baseLayer] ;

#undef ANC
#undef POS

}

Now I only have to get rid of those stupid colors, antialias the edges and provide some shadowy gradient layer and watch that perspective which is annoying me (the last row "gray" is too tall) More tweaking with m34 and/or the baseLayer center [anchorPoint/position].

Replacing the angle and coefficients as:

    CGFloat angle = M_PI / 3.0f ;

    trn0.sublayerTransform =  CATransform3DMakeRotation(-1.0*angle, 1, 0, 0);
    trn1.sublayerTransform =  CATransform3DMakeRotation(2.0*angle, 1, 0, 0);
    trn2.sublayerTransform =  CATransform3DMakeRotation(-2.0*angle, 1, 0, 0);
    trn3.sublayerTransform =  CATransform3DMakeRotation(2.0*angle, 1, 0, 0);
    trn4.sublayerTransform =  CATransform3DMakeRotation(-2.0*angle, 1, 0, 0);
    trn5.sublayerTransform =  CATransform3DMakeRotation(2.0*angle, 1, 0, 0);
    trn6.sublayerTransform =  CATransform3DMakeRotation(-2.0*angle, 1, 0, 0);
    trn7.sublayerTransform =  CATransform3DMakeRotation(2.0*angle, 1, 0, 0);

And the perspective to:

    initialTransform.m34 = 1.0f / -400.0f ;

We now get:

enter image description here