Scene Kit Session WWDC 2013/Sources/ASCSlideTextManager.m
/* |
File: ASCSlideTextManager.m |
Abstract: ASCSlideTextManager manages the layout of the different types of text presented in the slides. |
Version: 1.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import "ASCSlideTextManager.h" |
#import "ASCSlide.h" |
static CGFloat const TEXT_SCALE = 0.02; |
static CGFloat const TEXT_CHAMFER = 1; |
static CGFloat const TEXT_DEPTH = 0.0; |
static CGFloat const TEXT_FLATNESS = 0.4; |
@implementation ASCSlideTextManager { |
// The containers for each type of text |
SCNNode *_subGroups[ASCTextTypeCount]; |
ASCTextType _previousType; |
CGFloat _currentBaseline; |
float _baselinePerType[ASCTextTypeCount]; |
} |
- (id)init { |
if (self = [super init]) { |
self.textNode = [SCNNode node]; |
_currentBaseline = 16; |
} |
return self; |
} |
- (NSColor *)colorForTextType:(ASCTextType)type level:(int)level { |
switch (type) { |
case ASCTextTypeSubtitle: |
return [NSColor colorWithDeviceRed:160/255.0 green:182/255.0 blue:203/255.0 alpha:1]; |
case ASCTextTypeCode: |
return level == 0 ? [NSColor whiteColor] : [NSColor colorWithDeviceRed:242/255.0 green:173/255.0 blue:24/255.0 alpha:1]; |
case ASCTextTypeBody: |
if (level == 2) |
return [NSColor colorWithDeviceRed:115/255.0 green:170/255.0 blue:230/255.0 alpha:1]; |
default: |
return [NSColor whiteColor]; |
} |
} |
- (CGFloat)extrusionDepthForTextType:(ASCTextType)type { |
return type == ASCTextTypeChapter ? 10.0 : TEXT_DEPTH; |
} |
- (CGFloat)fontSizeForTextType:(ASCTextType)type level:(NSUInteger)level { |
switch (type) { |
case ASCTextTypeTitle: |
return 88; |
case ASCTextTypeChapter: |
return 94; |
case ASCTextTypeCode: |
return 36; |
case ASCTextTypeSubtitle: |
return 64; |
case ASCTextTypeBody: |
return level == 0 ? 50 : 40; |
default: |
return 56; |
} |
} |
- (NSFont *)fontForTextType:(ASCTextType)type level:(NSUInteger)level { |
CGFloat fontSize = [self fontSizeForTextType:type level:level]; |
switch (type) { |
case ASCTextTypeCode: |
return [NSFont fontWithName:@"Menlo" size:fontSize]; |
case ASCTextTypeBullet: |
return [NSFont fontWithName:@"Myriad Set" size:fontSize] ?: [NSFont fontWithName:@"Avenir Medium" size:fontSize]; |
case ASCTextTypeBody: |
if (level != 0) |
return [NSFont fontWithName:@"Myriad Set" size:fontSize] ?: [NSFont fontWithName:@"Avenir Medium" size:fontSize]; |
default: |
return [NSFont fontWithName:@"Myriad Set Semibold" size:fontSize] ?: [NSFont fontWithName:@"Avenir Medium" size:fontSize]; |
} |
} |
- (CGFloat)lineHeightForTextType:(ASCTextType)type level:(NSUInteger)level { |
switch (type) { |
case ASCTextTypeTitle: |
return 2.26; |
case ASCTextTypeChapter: |
return 3; |
case ASCTextTypeCode: |
return 1.22; |
case ASCTextTypeSubtitle: |
return 1.78; |
case ASCTextTypeBody: |
return level == 0 ? 1.2 : 1.0; |
default: |
return 1.65; |
} |
} |
- (SCNNode *)textContainerForType:(ASCTextType)type { |
if (type == ASCTextTypeChapter) |
return self.textNode.parentNode; |
if (_subGroups[type]) |
return _subGroups[type]; |
SCNNode *container = [SCNNode node]; |
[self.textNode addChildNode:container]; |
_subGroups[type] = container; |
_baselinePerType[type] = _currentBaseline; |
return container; |
} |
- (void)addEmptyLine { |
_currentBaseline -= 1.2; |
} |
- (SCNNode *)nodeWithText:(NSString *)string withType:(ASCTextType)type level:(NSUInteger)level { |
SCNNode *textNode = [SCNNode node]; |
// Bullet |
if (type == ASCTextTypeBullet) { |
if (level == 0) { |
string = [NSString stringWithFormat:@"• %@", string]; |
} |
else { |
SCNNode *bullet = [SCNNode node]; |
bullet.geometry = [SCNPlane planeWithWidth:10.0 height:10.0]; |
bullet.geometry.firstMaterial.diffuse.contents = [NSColor colorWithDeviceRed:160.0/255 green:182.0/255 blue:203.0/255 alpha:1.0]; |
bullet.position = SCNVector3Make(80, 30, 0); |
bullet.geometry.firstMaterial.lightingModelName = SCNLightingModelConstant; |
bullet.geometry.firstMaterial.writesToDepthBuffer = NO; |
bullet.renderingOrder = 1; |
[textNode addChildNode:bullet]; |
string = [NSString stringWithFormat:@"\t\t\t\t%@", string]; |
} |
} |
// Text attributes |
float extrusion = [self extrusionDepthForTextType:type]; |
SCNText *text = [SCNText textWithString:string extrusionDepth:extrusion]; |
textNode.geometry = text; |
text.flatness = TEXT_FLATNESS; |
text.chamferRadius = extrusion == 0 ? 0 : TEXT_CHAMFER; |
text.font = [self fontForTextType:type level:level]; |
// Layout |
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; |
CGFloat leading = [layoutManager defaultLineHeightForFont:text.font]; |
CGFloat descender = text.font.descender; |
NSUInteger newlineCount = [[text.string componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] count]; |
textNode.pivot = CATransform3DMakeTranslation(0, -descender + newlineCount * leading, 0); |
if (type == ASCTextTypeChapter) { |
SCNVector3 min, max; |
[textNode getBoundingBoxMin:&min max:&max]; |
textNode.position = SCNVector3Make(-11, (-min.y + textNode.pivot.m42) * TEXT_SCALE, 7); |
textNode.scale = SCNVector3Make(TEXT_SCALE, TEXT_SCALE, TEXT_SCALE); |
textNode.rotation = SCNVector4Make(0, 1, 0, M_PI/270.0); |
} |
else { |
textNode.position = SCNVector3Make(-16, _currentBaseline, 0); |
textNode.scale = SCNVector3Make(TEXT_SCALE, TEXT_SCALE, TEXT_SCALE); |
} |
// Material |
if (type == ASCTextTypeChapter) { |
SCNMaterial *frontMaterial = [SCNMaterial material]; |
SCNMaterial *sideMaterial = [SCNMaterial material]; |
frontMaterial.emission.contents = [NSColor darkGrayColor]; |
frontMaterial.diffuse.contents = [self colorForTextType:type level:level]; |
sideMaterial.diffuse.contents = [NSColor lightGrayColor]; |
textNode.geometry.materials = @[frontMaterial, frontMaterial, sideMaterial, frontMaterial, frontMaterial]; |
} |
else { |
// Full white emissive material (visible even when there is no light) |
textNode.geometry.firstMaterial = [SCNMaterial material]; |
textNode.geometry.firstMaterial.diffuse.contents = [NSColor blackColor]; |
textNode.geometry.firstMaterial.emission.contents = [self colorForTextType:type level:level]; |
// Don't write to the depth buffer because we don't want the text to be reflected |
textNode.geometry.firstMaterial.writesToDepthBuffer = NO; |
// Render last |
textNode.renderingOrder = 1; |
} |
return textNode; |
} |
- (SCNNode *)nodeWithCode:(NSString *)string { |
// Node hierarchy: |
// codeNode |
// |__ regularCodeNode |
// |__ emphasis-0 (can be highlighted separately) |
// |__ emphasis-1 (can be highlighted separately) |
// |__ emphasis-2 (can be highlighted separately) |
// |__ ... |
SCNNode *codeNode = [SCNNode node]; |
NSUInteger chunk = 0; |
NSString *regularCode = @""; |
NSString *whitespacesCode = @""; |
// Automatically highlight the parts of the code that are delimited by '#' |
NSArray *components = [string componentsSeparatedByString:@"#"]; |
for (NSUInteger i = 0; i < components.count; i++) { |
NSString *component = components[i]; |
NSString *whitespaces = @""; |
for (NSUInteger j = 0; j < component.length; j++) { |
NSString *character = [component substringWithRange:NSMakeRange(j, 1)]; |
if ([character isEqualToString:@"\n"]) { |
whitespaces = [whitespaces stringByAppendingString:@"\n"]; |
} else { |
whitespaces = [whitespaces stringByAppendingString:@" "]; |
} |
} |
if (i % 2) { |
SCNNode *emphasisedCodeNode = [self nodeWithText:[whitespacesCode stringByAppendingString:component] withType:ASCTextTypeCode level:1]; |
emphasisedCodeNode.name = [NSString stringWithFormat:@"emphasis-%ld", chunk++]; |
[codeNode addChildNode:emphasisedCodeNode]; |
regularCode = [regularCode stringByAppendingString:whitespaces]; |
} else { |
regularCode = [regularCode stringByAppendingString:component]; |
} |
whitespacesCode = [whitespacesCode stringByAppendingString:whitespaces]; |
} |
SCNNode *regularCodeNode = [self nodeWithText:regularCode withType:ASCTextTypeCode level:0]; |
regularCodeNode.name = @"regular"; |
[codeNode addChildNode:regularCodeNode]; |
return codeNode; |
} |
- (SCNNode *)addText:(NSString *)string withType:(ASCTextType)type level:(NSUInteger)level { |
SCNNode *parentNode = [self textContainerForType:type]; |
_currentBaseline -= [self lineHeightForTextType:type level:level]; |
if (type > ASCTextTypeSubtitle) { |
if (_previousType <= ASCTextTypeTitle) { |
_currentBaseline -= 1.0; |
} |
if (_previousType <= ASCTextTypeSubtitle && type > ASCTextTypeSubtitle) { |
_currentBaseline -= 1.3; |
} |
else if (_previousType != type) { |
_currentBaseline -= 1.0; |
} |
} |
SCNNode *textNode = (type == ASCTextTypeCode) ? [self nodeWithCode:string] : [self nodeWithText:string withType:type level:level]; |
[parentNode addChildNode:textNode]; |
if (self.fadesIn) { |
textNode.opacity = 0; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
{ |
textNode.opacity = 1; |
} |
[SCNTransaction commit]; |
} |
_previousType = type; |
return textNode; |
} |
#pragma mark - Public API |
- (SCNNode *)setTitle:(NSString *)title { |
return [self addText:title withType:ASCTextTypeTitle level:0]; |
} |
- (SCNNode *)setSubtitle:(NSString *)title { |
return [self addText:title withType:ASCTextTypeSubtitle level:0]; |
} |
- (SCNNode *)setChapterTitle:(NSString *)title { |
return [self addText:title withType:ASCTextTypeChapter level:0]; |
} |
- (SCNNode *)addText:(NSString *)text atLevel:(NSUInteger)level { |
return [self addText:text withType:ASCTextTypeBody level:level]; |
} |
- (SCNNode *)addBullet:(NSString *)text atLevel:(NSUInteger)level { |
return [self addText:text withType:ASCTextTypeBullet level:level]; |
} |
- (SCNNode *)addCode:(NSString *)string { |
return [self addText:string withType:ASCTextTypeCode level:0]; |
} |
#pragma mark - Animations |
static CGFloat const PIVOT_X = 16; |
static CGFloat const FLIP_ANGLE = M_PI_2; |
static CGFloat const FLIP_DURATION = 1.0; |
// Animate (fade out) to remove the text of specified type |
- (void)fadeOutTextOfType:(ASCTextType)type { |
SCNNode *node = _subGroups[type]; |
_subGroups[type] = nil; |
if (node) { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:FLIP_DURATION]; |
[SCNTransaction setCompletionBlock:^{ |
[node removeFromParentNode]; |
}]; |
{ |
node.opacity = 0; |
} |
[SCNTransaction commit]; |
// Reset the baseline to what it was before adding this text |
_currentBaseline = MAX(_currentBaseline, _baselinePerType[type]); |
} |
} |
// Animate (flip) to remove the text of specified type |
- (void)flipOutTextOfType:(ASCTextType)type { |
SCNNode *node = _subGroups[type]; |
_subGroups[type] = nil; |
if (node) { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0]; |
{ |
node.position = SCNVector3Make(-PIVOT_X, 0, 0); |
node.pivot = CATransform3DMakeTranslation(-PIVOT_X, 0, 0); |
} |
[SCNTransaction commit]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:FLIP_DURATION]; |
[SCNTransaction setCompletionBlock:^{ |
[node removeFromParentNode]; |
}]; |
{ |
node.rotation = SCNVector4Make(0, 1, 0, FLIP_ANGLE); |
node.opacity = 0; |
} |
[SCNTransaction commit]; |
// Reset the baseline to what it was before adding this text |
_currentBaseline = MAX(_currentBaseline, _baselinePerType[type]); |
} |
} |
// Animate to reveal the text of specified type |
- (void)flipInTextOfType:(ASCTextType)type { |
SCNNode *node = _subGroups[type]; |
if (node) { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0]; |
{ |
node.position = SCNVector3Make(-PIVOT_X, 0, 0); |
node.pivot = CATransform3DMakeTranslation(-PIVOT_X, 0, 0); |
node.rotation = SCNVector4Make(0, 1, 0, -FLIP_ANGLE); |
node.opacity = 0; |
} |
[SCNTransaction commit]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:FLIP_DURATION]; |
{ |
node.rotation = SCNVector4Make(0, 1, 0, 0); |
node.opacity = 1; |
} |
[SCNTransaction commit]; |
} |
} |
#pragma mark - Highlighting text |
- (void)highlightBulletAtIndex:(NSUInteger)index { |
// Highlight is done by changing the emission color |
SCNNode *node = _subGroups[ASCTextTypeBullet]; |
if (node) { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.75]; |
{ |
// Reset all |
for (SCNNode *child in node.childNodes) { |
child.geometry.firstMaterial.emission.contents = [NSColor whiteColor]; |
} |
// Unhighlight everything but index |
if (index != NSNotFound) { |
NSUInteger i = 0; |
for (SCNNode *child in node.childNodes) { |
if (i != index) |
child.geometry.firstMaterial.emission.contents = [NSColor darkGrayColor]; |
i++; |
} |
} |
} |
[SCNTransaction commit]; |
} |
} |
- (void)highlightCodeChunks:(NSArray *)chunks { |
SCNNode *node = _subGroups[ASCTextTypeCode]; |
// Unhighlight everything |
[node childNodesPassingTest:^BOOL(SCNNode *child, BOOL *stop) { |
child.geometry.firstMaterial.emission.contents = [self colorForTextType:ASCTextTypeCode level:0]; |
return NO; |
}]; |
// Highlight text inside range |
for (NSNumber *i in chunks) { |
SCNNode *chunkNode = [node childNodeWithName:[NSString stringWithFormat:@"emphasis-%ld", [i unsignedIntegerValue]] recursively:YES]; |
chunkNode.geometry.firstMaterial.emission.contents = [self colorForTextType:ASCTextTypeCode level:1]; |
} |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-01-07