Project download link
I don't believe it's necessary to leave Quartz to achieve this reproduction. Everything you've described as well as everything I've gathered from messing with Impress.js seems to be replicable through applying transforms (mostly 2D, some 3D) to a set of UILabels added to a container view that can be moved freely within the main view.
To do this, the project I created uses a subclass of UILabel
titled "ImpressLabel" with an extra init function where instead of passing the label a frame, you pass it a size, a center point, and a CGFloat
for the label's rotation on the Z axis. This transform is applied to the label upon instantiation so that when you set up the labels they will appear on screen already in the position and transformation you specify.
Then as far as configuring the text goes, you can pass the label an NSAttributedString
instead of a NSString
. This allows you to modify different parts of the string independently, so different words in the label can be different sizes, fonts, colors, background colors, etc. Here's an example of the above two paragraphs:
ImpressLabel *label1 = [[ImpressLabel alloc] initWithSize:CGSizeMake(260.0f, 80.0f) andCenterPointInSuperview:CGPointMake(500.0f, 500.0f) andRotationInSuperview:0.0f andEndingScaleFactor:1.3];
NSMutableAttributedString *firstLabelAttributes = [[NSMutableAttributedString alloc] initWithString:@"then you should try\nimpress.js*\n* no rhyme intended"];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 2]
range:NSMakeRange(0, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 8]
range:NSMakeRange(firstLabelAttributes.string.length - 19, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont boldSystemFontOfSize:label1.font.pointSize + 14]
range:NSMakeRange(23, 11)];
[label1 setNumberOfLines:3];
[label1 setAttributedText:firstLabelAttributes];
[label1 setTextAlignment:NSTextAlignmentCenter];
[containmentView addSubview:label1];
Now, on to more of the guts of the whole operation. As I mentioned above, this subclass adds a tap gesture to each label. When a tap is recognized a few things happen. The view containing the labels will pan/back/away by adjusting it's scale. It will also begin rotate and adjust its anchor point in the main view so that when the animation stops, the selected label will be centered on screen in the correct orientation. Then of course, while this is all going on the alpha of the selected label will be brought up to 1.0f while the alpha of the rest will be lowered to 0.25f.
- (void)tapForRotationDetected:(UITapGestureRecognizer *)sender {
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
[scale setToValue:[NSNumber numberWithFloat:0.8]];
[scale setAutoreverses:YES];
[scale setDuration:0.3];
//Create animation to adjust the container views anchorpoint.
CABasicAnimation *adjustAnchor = [CABasicAnimation animationWithKeyPath:@"anchorPoint"];
[adjustAnchor setFromValue:[NSValue valueWithCGPoint:self.superview.layer.anchorPoint]];
[adjustAnchor setToValue:[NSValue valueWithCGPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)]];
[adjustAnchor setRemovedOnCompletion:NO];
[adjustAnchor setFillMode:kCAFillModeForwards];
//Create animation to rotate the container view within its superview.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
//Create the animation group to apply these transforms
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
[animationGroup setAnimations:@[adjustAnchor,rotation]];
[animationGroup setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[animationGroup setDuration:0.6];
//Apply the end results of the animations directly to the container views layer.
[self.superview.layer setTransform:CATransform3DRotate(CATransform3DIdentity, DEGREES_TO_RADIANS(-self.rotationInSuperview), 0.0f, 0.0f, 1.0f)];
[self.superview.layer setAnchorPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)];
[self.superview.layer addAnimation:animationGroup forKey:@"animationGroup"];
//Animate the alpha property of all ImpressLabels in the container view.
[self.superview bringSubviewToFront:self];
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
for (ImpressLabel *label in sender.view.superview.subviews) {
if ([label isKindOfClass:[ImpressLabel class]]) {
if (label != self) {
[label setAlpha:0.25f];
}else{
[label setAlpha:1.0f];
}
}
}
} completion:nil];
}
Now, to address some of the concerns listed in your question.
I've profiled this project in Instruments' allocations tool and it
only consumes about 3.2 MB overall, so I'd say this approach is
efficient enough.
The sample I've provided animates most objects in 2D space, with the exception of the scaling animation which is an illusion at best. What I've done here is meant to serve as an example of how this can be done and isn't a 100% complete demonstration because as I stated above, animation really isn't my area of expertise. However, from looking over the docs, it seems like the key to having a label rotated in the third dimension and adjusting its superview to rotate all the labels and leave the selected label flat would be the use of CATransform3DInvert()
. Although I haven't yet had time to fully figure out it works, it looks like it might be exactly what is needed in this scenario.
As far as mirroring goes, I don't believe there will be any problems with properly scaling everything. Looking over Apple's Multiple Display Programming Guide, it looks like the object passed to the NSNotification
UIScreenDidConnectNotification
is a UIScreen
object. Since this is the case, you can easily ask for this displays bounds, and adjust the frames of the labels and the container view accordingly.
Note: In this example, only 0, 90, 180, and -90 degree transforms animate 100% correctly, due to the anchor point's coordinates being incorrectly generated. It looks like the solution lies with CGPointApplyAffineTransform(<#CGPoint point#>, <#CGAffineTransform t#>)
, but again I haven't had as much time to play with it as I would have liked. Either way, this should be plenty to get you started with your reproduction.
This has definitely sparked my interest though and when I get a chance to work on this again, I'll gladly update this post with any new information. Hope this helps!