Labeling Bar Chart in CorePlot - objective-c

I have a horizontal bar chart using Coreplot in which I have labels within the bar itself with white text
like this
However, when the bar is to short to contain the label I would like the text Black and not offset like this.
Is there anything available to check if the label is clipped or compare the wide of the label to the bar length? So far I have this
-(CPTTextLayer *)dataLabelForPlot:(CPTPlot *)plot recordIndex:(NSUInteger)index
{
NSNumberFormatter *numberFormatter = [NSNumberFormatter percentFormatter];
NSDictionary *bar = [self.dataSource objectAtIndex:index];
NSDecimalNumber *xValue = [bar valueForKey:#"Value"];
NSString *stringLabel = [numberFormatter stringFromNumber:xValue];
CPTMutableTextStyle *whiteStyle = [[CPTMutableTextStyle alloc] init];
whiteStyle.color = [CPTColor whiteColor];
whiteStyle.fontSize = CPTFloat(12.0);
whiteStyle.fontName = #"HelveticaNeue-Medium";
CPTMutableTextStyle *darkStyle = [[CPTMutableTextStyle alloc] init];
darkStyle.color = [CPTColor blackColor];
darkStyle.fontSize = CPTFloat(12.0);
darkStyle.fontName = #"HelveticaNeue-Medium";
CPTTextLayer *textLayer = [[CPTTextLayer alloc] initWithText:stringLabel];
if (**isLabelClipped**) {
plot.labelOffset = 2.0f;
textLayer.textStyle = darkStyle;
} else {
plot.labelOffset = -2.0f;
textLayer.textStyle = whiteStyle;
}
return textLayer;
}
EDIT
As Eric suggested I attempted to to get the screen size from the plotspace, my thinking was I would use the difference between the xValue and the barsBase, so I added the following
NSDecimal plotPoint[2];
plotPoint[CPTCoordinateX] = xValue.decimalValue;
NSNumber *barBase = [self numberForPlot:plot
field:CPTBarPlotFieldBarBase
recordIndex:index];
plotPoint[CPTCoordinateY] = barBase.decimalValue;
CGPoint dataPoint = [plot.plotSpace plotAreaViewPointForPlotPoint:plotPoint numberOfCoordinates:2];
NSLog(#"Data Point:%#", NSStringFromPoint(dataPoint));
But this gives values like
{531.46951532736807, 57.166666666666664}
The barBase value (57..) looks to be the far left of the plot area and not the 0 axis as expected.

Knowing the bar length (from the xValue), you can use the plot space to determine its size on screen. After creating the textLayer, check its bounds size. Compare the two and decide whether to position the label inside or outside the bar.

Related

How do you animate different Core Animation layers simultaneously?

I'm currently trying to implement an animation using Core Animation on iOS. Right now I can apply multiple animations to a single layer using CAAnimationGroup, but if I want to repeat that across various different layers --- simultaneously --- things get hazy.
Currently it looks like CAAnimationGroup is a singleton class (or at least, I only seem to get one instance, and I allocate using [CAAnimationGroup animation].
Any ideas?
NSMutableDictionary* elementInstanceData = [self.elementInstanceValues objectForKey:elementInstance];
NSArray* animationValues = [elementInstanceData objectForKey:#"animationValues"];
NSArray* keyTimeValues = [elementInstanceData objectForKey:#"keyTimeValues"];
NSNumber* elementDurationTime = elementInstanceData[#"duration"];
if(animationValues != nil)
{
NSMutableArray* transValues = [[NSMutableArray alloc]init];
NSMutableArray* scaleValues = [[NSMutableArray alloc]init];
NSMutableArray* rotateValues = [[NSMutableArray alloc]init];
for(NSDictionary* transforms in animationValues)
{
NSValue* trans = transforms[#"translate"];
[transValues addObject:trans];
NSValue* scale = transforms[#"scale"];
[scaleValues addObject:scale];
NSValue* rotate = transforms[#"rotate"];
[rotateValues addObject:rotate];
}
NSMutableArray* ratioTimeValues = [[NSMutableArray alloc]init];
for(NSNumber* time in keyTimeValues)
{
float percentage = ([time floatValue] / [elementDurationTime floatValue]);
[ratioTimeValues addObject:[NSNumber numberWithFloat:percentage ]];
}
NSCAssert( [ratioTimeValues count] == [transValues count], #"Keytime/value mismatch (times:%d, values: %d)", [ratioTimeValues count], [transValues count]) ;
CAKeyframeAnimation* transAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
[transAnim setValues:transValues];
[transAnim setKeyTimes:ratioTimeValues];
transAnim.beginTime = CACurrentMediaTime() + startTime;
transAnim.duration = durationTime;
[allAnimations addObject:transAnim];
CAKeyframeAnimation* scaleAnim = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
[scaleAnim setValues:scaleValues];
[scaleAnim setKeyTimes:ratioTimeValues];
scaleAnim.beginTime = CACurrentMediaTime() + startTime;
scaleAnim.duration = durationTime;
[allAnimations addObject:scaleAnim];
CAKeyframeAnimation* rotAnim = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
[rotAnim setValues:rotateValues];
[rotAnim setKeyTimes:ratioTimeValues];
rotAnim.beginTime = CACurrentMediaTime() + startTime;
rotAnim.duration = durationTime;
[allAnimations addObject:rotAnim];
}
CAAnimationGroup* elementInstanceAnimationGroup = [CAAnimationGroup animation];
elementInstanceAnimationGroup.animations = allAnimations;
elementInstanceAnimationGroup.beginTime = CACurrentMediaTime();
elementInstanceAnimationGroup.duration = [elementDurationTime floatValue];
elementInstanceAnimationGroup.delegate = self;
[elementInstanceData setObject:elementInstanceAnimationGroup forKey:#"animationGroup"];
UIView* targetView = [elementInstanceData objectForKey:#"target"];
[targetView.layer addAnimation:elementInstanceAnimationGroup forKey:nil];
I can apply multiple animations to a single layer using CAAnimationGroup, but if I want to repeat that across various different layers --- simultaneously --- things get hazy.
A single animation group can animate properties of different named sublayers.
For example, suppose I have a layer with two sublayers. The name of the first sublayer is "manny" and the name of the second sublayer is "moe". And let's say I want to animate the position of the two sublayers individually.
Then I use a keyPath of "sublayers.manny.position" for one animation, and a keyPath of "sublayers.moe.position", group them, and attach the group to the superlayer.
You can create an animation or animation group and add it to multiple layers.
CABasicAnimation* fadeIn = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeIn.duration = 0.35;
fadeIn.fromValue = [NSNumber numberWithDouble:0];
fadeIn.toValue = [NSNumber numberWithDouble:1];
for (int i = 0; i < self.indicatorBars.count; i++) {
[self.indicatorBars[i] addAnimation:fadeIn forKey:#"opacity"];
}

multiple function curves in core-plot

I'm currently trying to add more than one mathematical function curves. Below is part of my code.
// draw all expression curves (in viewDidLoad)
for ( j=0; j<self.expNum; j++ ) {
[self getDataForPlot1:self.plotIndex]; // get DataForPlot of one selected expression
[self addCurve:self.plotIndex];
}
I'm trying to add all curves after drawing the axes, but it is only the last input curve that is drawn. How can I separate the dataForPlot for each function?
-(void)getDataForPlot1:(int)index
{
int i;
NSString *exp;
int bound, plotNum;
double interval;
bound = 3 * scaleX;
plotNum = 3 * 60;
interval = (double)bound / plotNum;
NSMutableArray *contentArray = [NSMutableArray arrayWithCapacity:2*plotNum];
for ( i = -plotNum; i < plotNum; i++ ) {
id x;
x = [NSNumber numberWithFloat: i * interval];
// get one expression
.......
id y = [exp numberByEvaluatingString]; // get y values from the expression
[contentArray addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:x, #"x", y, #"y", nil]];
}
dataForPlot = contentArray;
}
-(void)addCurve:(int)index {
CPTScatterPlot *boundLinePlot = [[CPTScatterPlot alloc] init];
CPTMutableLineStyle *lineStyle = [CPTMutableLineStyle lineStyle];
lineStyle.miterLimit = 1.0f;
lineStyle.lineWidth = 3.0f;
lineStyle.lineColor = [CPTColor blueColor];
boundLinePlot.dataLineStyle = lineStyle;
boundLinePlot.identifier = #"Blue Plot";
boundLinePlot.dataSource = self;
[graph addPlot:boundLinePlot toPlotSpace:plotSpace];
boundLinePlot.interpolation = CPTScatterPlotInterpolationCurved;
}
(Answering the question based on comments above)
Save a copy of each dataset separately. It looks like the calculated plot data gets overwritten each time you call -getDataForPlot1:.
Also, set a unique identifier for each plot so the datasource can know which one is requesting data.

Core plot: annotation with text and image

I would like to add an annotation to my graph composed by a text and an image like this:
I can already display the text ("13" in the picture) but I'm not able to add the image below the text.
I've tried with CPTLayer, CPTBorderedLayer,..., but not of them work as expected.
Here is the code I'm using to display the text:
NSNumber *valueToDisplay = [NSNumber numberWithInt:13];
NSString *valueToDisplayString = [formatter stringFromNumber:valueToDisplay];
CPTTextLayer *textLayer = [[CPTTextLayer alloc] initWithText:valueToDisplayString style:style];
self.priceAnnotation.contentLayer = textLayer;
self.priceAnnotation.anchorPlotPoint = [NSArray arrayWithObjects:[NSNumber numberWithFloat:7.0], [NSNumber numberWithFloat:14.0], nil];
[self.graph.plotAreaFrame.plotArea addAnnotation:self.priceAnnotation];
How can I add the image below the text value ?
This is one of the pieces of code I've tried:
CPTBorderedLayer *immagine = [[CPTBorderedLayer alloc] initWithFrame:CGRectMake(0, 0, 77, 36)];
CPTFill *fillImage = [CPTFill fillWithImage:[CPTImage imageWithCGImage:[[UIImage imageNamed:#"sfondoStima.png"] CGImage]]];
immagine.fill = fillImage;
self.imageAnnotation.contentLayer = immagine;
self.imageAnnotation.anchorPlotPoint = [NSArray arrayWithObjects:[NSNumber numberWithFloat:7.0], [NSNumber numberWithFloat:5.0], nil];
[self.graph.plotAreaFrame.plotArea addAnnotation:self.imageAnnotation];
But this is the result: the bitmap (77x36) is for some reason much bigger than what it should be:
Please give me some help ... I've already tried different tutorial/example I've found but none of them seems to work.
Thanks,
Corrado
CPTTextLayer is a subclass of CPTBorderedLayer. For a simple background like this, I wouldn't bother with an image at all. I'd try something like this (untested):
CPTMutableLineStyle lineStyle = [CPTMutableLineStyle lineStyle];
lineStyle.lineWidth = 2.0;
lineStyle.lineColor = [CPTColor whiteColor];
CPTTextLayer *textLayer = [[CPTTextLayer alloc] initWithText:valueToDisplayString style:style];
textLayer.fill = [CPTFill fillWithColor:[CPTColor blueColor]];
textLayer.cornerRadius = 10.0;
textLayer.borderLineStyle = lineStyle;
Set the padding on the textLayer to control the space between the border line and the text.
If you have more complex needs that requires an image, be sure to set the image scale correctly. [CPTImage imageNamed:] does this for you.

Insert line break using SKLabelNode in SpriteKit

Simple question on how to insert a line break using SKLabelNode class in SpriteKit. I have the following code but it does not work -
SKLabelNode *nerdText = [SKLabelNode labelNodeWithFontNamed:#"Times"];
NSString *st1 = #"Test break";
NSString *st2 = #"I want it to break";
NSString *test = [NSString stringWithFormat:#"%#,\r%#",st1,st2]; //Even tried \n
nerdText.text = test;
nerdText.fontSize = 11;
nerdText.fontColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
nerdText.position = CGPointMake(150.0, 250.0);
[self addChild:nerdText];
Please help me out!
I dont think you can, here is a "hack" way to do it
SKNode *nerdText = [SKNode node];
SKLabelNode *a = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
a.fontSize = 16;
a.fontColor = [SKColor yellowColor];
SKLabelNode *b = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
b.fontSize = 16;
b.fontColor = [SKColor yellowColor];
NSString *st1 = #"Line 1";
NSString *st2 = #"Line 2";
b.position = CGPointMake(b.position.x, b.position.y - 20);
a.text = st1;
b.text = st2;
[nerdText addChild:a];
[nerdText addChild:b];
nerdText.position = CGPointMake(150.0, 250.0);
[self addChild:nerdText];
I had the same problem. I created a drop-in replacement for SKLabelNode called DSMultilineLabelNode that supports word wrap, line breaks, etc. The underlying implementation draws the string into a graphics context and then applies that to a texture on an SKSpriteNode.
It's available on GitHub at:
https://github.com/downrightsimple/DSMultilineLabelNode
static func multipleLineText(labelInPut: SKLabelNode) -> SKLabelNode {
let subStrings:[String] = labelInPut.text!.componentsSeparatedByString("\n")
var labelOutPut = SKLabelNode()
var subStringNumber:Int = 0
for subString in subStrings {
let labelTemp = SKLabelNode(fontNamed: labelInPut.fontName)
labelTemp.text = subString
labelTemp.fontColor = labelInPut.fontColor
labelTemp.fontSize = labelInPut.fontSize
labelTemp.position = labelInPut.position
labelTemp.horizontalAlignmentMode = labelInPut.horizontalAlignmentMode
labelTemp.verticalAlignmentMode = labelInPut.verticalAlignmentMode
let y:CGFloat = CGFloat(subStringNumber) * labelInPut.fontSize
print("y is \(y)")
if subStringNumber == 0 {
labelOutPut = labelTemp
subStringNumber++
} else {
labelTemp.position = CGPoint(x: 0, y: -y)
labelOutPut.addChild(labelTemp)
subStringNumber++
}
}
return labelOutPut
}
As of iOS 11/ macOS 10.13, SKLabelNode has a numberOfLines property that behaves in a similar way to the one that UILabel has. By default it's set to 1. If you set it to zero, you can have an unlimited number of lines. See also lineBreakMode and preferredMaxLayoutWidth. I thought it was worth pointing this out here in case anyone arrives at this page before they see the Apple documentation. If your minimum build target is iOS 11/ macOS 10.13, you don't need the helper methods posted above.
Here is another five minute hack by yours truly. It's not too bad.
+(SKSpriteNode*)spritenodecontaininglabelsFromStringcontainingnewlines:(NSString*)text fontname:(NSString*)fontname fontcolor:(NSColor*)colorFont fontsize:(const CGFloat)SIZEFONT verticalMargin:(const CGFloat)VERTICALMARGIN emptylineheight:(const CGFloat)EMPTYLINEHEIGHT {
NSArray* strings = [text componentsSeparatedByString:#"\n"];
//DLog(#"string count: %lu", (unsigned long)strings.count);
NSColor* color = NSColor.clearColor;
#ifdef DEBUG
color = [NSColor colorWithCalibratedRed:1 green:0 blue:0 alpha:0.5];
#endif
SKSpriteNode* spritenode = [SKSpriteNode spriteNodeWithColor:color size:CGSizeMake(0, 0)];
CGFloat totalheight = 0;
CGFloat maxwidth = 0;
NSMutableArray* labels = [NSMutableArray array];
for (NSUInteger i = 0; i < strings.count; i++) {
NSString* str = [strings objectAtIndex:i];
const BOOL ISEMPTYLINE = [str isEqualToString:#""];
if (!ISEMPTYLINE) {
SKLabelNode* label = [SKLabelNode labelNodeWithFontNamed:fontname];
label.text = str;
label.fontColor = colorFont;
label.fontSize = SIZEFONT;
const CGSize SIZEOFLABEL = [label calculateAccumulatedFrame].size;
if (SIZEOFLABEL.width > maxwidth)
maxwidth = SIZEOFLABEL.width;
totalheight += SIZEOFLABEL.height;
[labels addObject:label];
}
else {
totalheight += EMPTYLINEHEIGHT;
[labels addObject:[NSNull null]];
}
if (i + 1 < strings.count)
totalheight += VERTICALMARGIN;
}
spritenode.size = CGSizeMake(maxwidth, totalheight);
//DLog(#"spritenode total size: %#", NSStringFromSize(spritenode.size));
CGFloat y = spritenode.size.height * 0.5;
const CGFloat X = 0;
for (NSUInteger i = 0; i < strings.count; i++) {
id obj = [labels objectAtIndex:i];
if ([obj isKindOfClass:SKLabelNode.class]) {
SKLabelNode* label = obj;
label.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
label.position = ccp(X, y);
[spritenode addChild:label];
const CGSize SIZEOFLABEL = [label calculateAccumulatedFrame].size;
y -= SIZEOFLABEL.height;
}
else {
y -= EMPTYLINEHEIGHT;
}
if (i + 1 < labels.count)
y -= VERTICALMARGIN;
}
return spritenode;
}
Btw you will need
static inline CGPoint ccp( CGFloat x, CGFloat y )
{
return CGPointMake(x, y);
}
So after doing a bit of research I learned that SkLabelNode was not intended to have multiline strings involved. Since functionality is limited with SKLabelNode it makes more sense to simply use a UILabel to hold the place of your text. Learning how to smoothly implement UI elements into sprite kit has made life a whole lot easier. UI elements are created programmatically, and added to your scene by using
[self.view addsubview:(your UIelement)];
So all you have to do
1.Initialize an instance of the UIelement in this case a UIlabel
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
label.backgroundColor = [UIColor whiteColor];
label.textColor = [UIColor blackColor];
label.text = #"helllllllllo";
2. After you have created your UIelement just add it TO THE VIEW Using the method described above
3.I have found it important to note that UI elements and SK elements do not interact the same when it comes to positioning. There are some simple methods provided such as convertPointToView:
-(CGPoint)convertPointToView(CGPoint);
To help when it comes to converting points. I hope that helped Good Luck!
I wrote a solution for Swift 3.
An Xcode demo project is available on the open source GitHub project: https://github.com/benmorrow/Multilined-SKLabelNode
Here's the SKLabelNode extension:
extension SKLabelNode {
func multilined() -> SKLabelNode {
let substrings: [String] = self.text!.components(separatedBy: "\n")
return substrings.enumerated().reduce(SKLabelNode()) {
let label = SKLabelNode(fontNamed: self.fontName)
label.text = $1.element
label.fontColor = self.fontColor
label.fontSize = self.fontSize
label.position = self.position
label.horizontalAlignmentMode = self.horizontalAlignmentMode
label.verticalAlignmentMode = self.verticalAlignmentMode
let y = CGFloat($1.offset - substrings.count / 2) * self.fontSize
label.position = CGPoint(x: 0, y: -y)
$0.addChild(label)
return $0
}
}
}
Here's how you use it:
let text = "hot dogs\ncold beer\nteam jerseys"
let singleLineMessage = SKLabelNode()
singleLineMessage.fontSize = min(size.width, size.height) /
CGFloat(text.components(separatedBy: "\n").count) // Fill the screen
singleLineMessage.verticalAlignmentMode = .center // Keep the origin in the center
singleLineMessage.text = text
let message = singleLineMessage.multilined()
message.position = CGPoint(x: frame.midX, y: frame.midY)
message.zPosition = 1001 // On top of all other nodes
addChild(message)
Here's what the app looks like:
The alternative is to create a bitmap version of the text, then use the resulting image with a SKSpriteNode.
It's easier than it sounds.
An example, assume we have a string or an attributed string and a CGSize variable with the size of the resulting text area.
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
// Assuming size is in actual pixels. Multiply size by the retina scaling
// factor if not.
CGContextRef context = CGBitmapContextCreate(NULL, (size_t)round(size.width), (size_t)round(size.height), 8, (size_t)round(size.width) * 4, rgbColorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(rgbColorSpace);
// Draw text, potentially flipping the coordinate system before
// (depending on methods you use).
// Make sure that you draw the font twice as big for retina.
// E.g. [#"My text" drawInRect:rect withAttributes:attr];
// Once we have drawn the text, simply extract the image and
// Make a texture from it.
CGImageRef image = CGBitmapContextCreateImage(context);
SKTexture *texture = [SKTexture textureWithCGImage:image];
CGImageRelease(image);
CGContextRelease(context);
// Texture created, so make a sprite node to use it.
SKSpriteNode *node = [self node];
node.texture = texture;
// Set the node size to the size in non-retina pixels, so if size was with
// scale factor already multiplied in, then we would need to divide by the scale
// factor.
node.size = size;
Here just to contribute my solution. I find myself wanting the same thing - to make multilines of SKLabelNode from a long string. Creating it one by one and manually positioning them is non practical. So I made an easier way to make multiline SKLabelNode. This method uses SKLabelNodes (and not capturing text into image).
Please see my solution if you are interested:
http://xcodenoobies.blogspot.com/2014/12/multiline-sklabelnode-hell-yes-please-xd.html
The result:
Lots of nice solutions here, but I didn't see any written in swift, so here we go. this function will take in one long string, and break it up where you place \n characters.
func createMultiLineText(textToPrint:String, color:UIColor, fontSize:CGFloat, fontName:String, fontPosition:CGPoint, fontLineSpace:CGFloat)->SKNode{
// create node to hold the text block
var textBlock = SKNode()
//create array to hold each line
let textArr = textToPrint.componentsSeparatedByString("\n")
// loop through each line and place it in an SKNode
var lineNode: SKLabelNode
for line: String in textArr {
lineNode = SKLabelNode(fontNamed: fontName)
lineNode.text = line
lineNode.fontSize = fontSize
lineNode.fontColor = color
lineNode.fontName = fontName
lineNode.position = CGPointMake(fontPosition.x,fontPosition.y - CGFloat(textBlock.children.count ) * fontSize + fontLineSpace)
textBlock.addChild(lineNode)
}
// return the sknode with all of the text in it
return textBlock
}
So I know this question is a little older, but just incase any comes back to it like I have, there's now a property preferredMaxLayoutWidth that you can use in conjunction with lineBreakMode and numberOfLines:
Example:
let longMessage = "Super super super super super super super super super long text"
let label = SKLabelNode(fontNamed: "Thonburi")
label.text = longMessage
label.fontSize = 24
label.fontColor = SKColor.black
// set preferredMaxLayoutWidth to the width of the SKScene
label.preferredMaxLayoutWidth = size.width
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
addChild(label)
Like several others I have implemented a solution to this problem myself. It's a simple SKLabelNode subclass which can be used as a replacement for the regular SKLabelNode. I find subclassing the best approach for this functionality as I use it "everywhere" "all" the time...
The whole thing is available at github (for anyone interested) but the main gist is as follows: It separates the string and creates regular SKLabelNode instances and ads these as children of the node. This is done whenever setText: is invoked:
- (void)setText:(NSString *)text{
self.subNodes = [self labelNodesFromText:text];
[self removeAllChildren];
for (SKLabelNode *childNode in self.subNodes) {
[self addChild:childNode];
}
_text = #""; // (synthesized in the implementation)
}
The label subnodes are created here:
- (NSArray *)labelNodesFromText:(NSString *)text{
NSArray *substrings = [text componentsSeparatedByString:#"\n"];
NSMutableArray *labelNodes = [[NSMutableArray alloc] initWithCapacity:[substrings count]];
NSUInteger labelNumber = 0;
for (NSString *substring in substrings) {
SKLabelNode *labelNode = [SKLabelNode labelNodeWithFontNamed:self.fontName];
labelNode.text = substring;
labelNode.fontColor = self.fontColor;
labelNode.fontSize = self.fontSize;
labelNode.horizontalAlignmentMode = self.horizontalAlignmentMode;
labelNode.verticalAlignmentMode = self.verticalAlignmentMode;
CGFloat y = self.position.y - (labelNumber * self.fontSize * kLineSpaceMultiplier); // kLineSpaceMultiplier is a float constant. 1.5 is the value I have chosen
labelNode.position = CGPointMake(self.position.x, y);
labelNumber++;
[labelNodes addObject:labelNode];
}
return [labelNodes copy];
}
As you might have noticed I also have a property subNodes (array). This comes in handy elsewhere as the full implementation also allows for changing any of properties with the regular SKLabelNode syntax. (Text, fontName, fontSize, alignment etc.)
If anyone is interested, I've created a better SKLabelNode called SKLabelNodePlus that has multi-line support like Chris Allwein's but also has other features I find pretty useful.
Check it out on GitHub:
https://github.com/MKargin0/SKLabelNodePlus
Using https://github.com/downrightsimple/DSMultilineLabelNode and How to write text on image in Objective-C (iOS)? for reference this is what I did for a quick and dirty way to get a text-wrapping SKNode (Xcode 7.1.1):
-(SKNode*)getWrappingTextNode:(NSString*)text maxWidth:(CGFloat)width {
UIImage *img = [self drawText:text widthDimension:width];
return [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:img]];
}
-(UIImage*)drawText:(NSString*)text widthDimension:(CGFloat)width {
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft; //or whatever alignment you want
UIFont *font = [UIFont fontWithName:#"Verdana" size:22]; //or whatever font you want
NSDictionary *att = #{NSFontAttributeName:font, NSParagraphStyleAttributeName: paragraphStyle};
//using 800 here but make sure this height is greater than the potential height of the text (unless you want a max-height I guess but I did not test max-height)
CGRect rect = [text boundingRectWithSize:CGSizeMake(width, 800) options:NSStringDrawingUsesLineFragmentOrigin attributes:att context:nil];
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0f);
[text drawInRect:rect withAttributes:att];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Here is a quick and easy function I wrote to just make life easier.
Step 1) Pass in a string, get a SKSpriteNode.
Step 2) Add sprite node to scene.
/******************************************************************************/
- (SKSpriteNode*) ConvertString: (NSString*) str
WithFontSize: (NSInteger) font_size
ToParagraphWithSize: (CGSize) para_size
{
SKSpriteNode* paragraph = [[SKSpriteNode alloc] initWithColor: [SKColor clearColor]
size: para_size];
// Set the anchor point to the top left corner. This is where English
// paragraphs usually start
paragraph.anchorPoint = CGPointMake(0,1);
// Create an array to hold multilple sub strings. These sub strings will
// become multiple SKLabels that will be added to the paragraph sprite node
// created above
NSMutableArray* str_arr = [[NSMutableArray alloc] init];
// Lets separate words by a single space.
NSArray* word_arr = [str componentsSeparatedByString:#" "];
// 50% is an approximate character height to width ratio. Change this
// number to adjust the number of characters per line you would like.
// Increase it if you have a lot of capitol W's
float est_char_width = font_size * 0.50;
NSInteger num_char_per_line = para_size.width / est_char_width;
// For every word in the original string, make sure it fits on the line
// then add it to the string array.
NSString* temp_str = #"";
for (NSString* word in word_arr)
{
if ((NSInteger)word.length <= num_char_per_line - (NSInteger)temp_str.length)
{
temp_str = [NSString stringWithFormat:#"%# %#", temp_str, word];
}
else
{
[str_arr addObject: temp_str];
temp_str = word;
}
}
[str_arr addObject: temp_str];
// For every sub string, create a label node and add it to the paragraph
for (int i = 0; i < str_arr.count; i++)
{
NSString* sub_str = [str_arr objectAtIndex: i];
SKLabelNode* label = [self CreateLabelWithText: sub_str];
label.fontSize = 14;
label.position = CGPointMake(0, -(i+1) * font_size);
[paragraph addChild: label];
}
return paragraph;
}
/******************************************************************************/
- (SKLabelNode*) CreateLabelWithText: (NSString*) str
{
enum alignment
{
CENTER,
LEFT,
RIGHT
};
SKLabelNode* label;
label = [SKLabelNode labelNodeWithFontNamed:#"ChalkboardSE-Light"];
label.name = #"label_name";
label.text = str;
label.zPosition = 1;
label.horizontalAlignmentMode = LEFT;
label.fontColor = [SKColor whiteColor];
return label;
}
I have written a utility method to take a string and divide it up into an array of strings with a given maximum length. It automatically ends each line with a whole word and removes leading whitespace. Hope it helps somebody!
- (NSArray*)linesFromString:(NSString*)string withMaxLineLength:(int)maxLineLength;
{
NSMutableArray *lines = [NSMutableArray arrayWithCapacity:1];
BOOL gotLine = NO;
BOOL doneFormat = NO;
BOOL endOfString = NO;
int innerLoops = 0;
int outerLoops = 0;
int lineIndex = 0;
int currentStringIndex = 0;
int stringLength = (int)[string length];
int rangeLength = maxLineLength;
NSString *line;
NSString *testChar;
NSString *testChar2;
while (!doneFormat) {
outerLoops++;
while (!gotLine) {
endOfString = NO;
innerLoops++;
line = [string substringWithRange:NSMakeRange(currentStringIndex, rangeLength)];
testChar = [line substringWithRange:NSMakeRange(0, 1)];
if (currentStringIndex + rangeLength > [string length] - 1) {
endOfString = YES;
} else {
testChar2 = [string substringWithRange:NSMakeRange(currentStringIndex + rangeLength, 1)];
}
//If the line starts with a space then advance 1 char and try again.
if ([testChar isEqualToString:#" "]) {
currentStringIndex++;
// If we were at the end of the string then reduce the rangeLength as well.
if (endOfString) {
rangeLength--;
}
// else, if this line ends at the end of a word (or the string) then it's good. ie next char in the string is a space.
} else if ([testChar2 isEqualToString:#" "] || endOfString) {
gotLine = YES;
currentStringIndex += [line length];
// else, make the line shorter by one character and try again
} else if (rangeLength > 1){
rangeLength--;
// Otherwise the word takes up more than 1 line so use it all.
} else {
line = [string substringWithRange:NSMakeRange(currentStringIndex, maxLineLength)];
currentStringIndex += [line length];
gotLine = YES;
}
// Make sure we're not stuck in an endless loop
if (innerLoops > 1000) {
NSLog(#"Error: looped too long");
break;
}
}
// If we processed a line, and the line is not nil, add it to our array.
if (gotLine && line) {
[lines insertObject:line atIndex:lineIndex];
lineIndex++;
}
// Reset variables
rangeLength = maxLineLength;
gotLine = NO;
// If the current index is at the end of the string, then we're done.
if (currentStringIndex >= stringLength) {
doneFormat = YES;
// If we have less than a full line left, then reduce the rangeLength to avoid throwing an exception
} else if (stringLength - (currentStringIndex + rangeLength) < 0) {
rangeLength = stringLength - currentStringIndex;
}
// Make sure we're not stuck in an endless loop
if (outerLoops > 1000) {
NSLog(#"Error: Outer-looped too long");
break;
}
}
return lines;
}
And then I just call it and create some label nodes to add to my layer node as follows. I'm aligning my line labels underneath and with the left edge of button2, so it all lines up left justified.
CGFloat fontSize = 30.0f;
int lineCount;
NSString *description = [product localizedDescription];
NSString *line;
NSArray *lines = [self linesFromString:description withMaxLineLength:43];
if (lines) {
lineCount = (int)[lines count];
for (int i = 0; i < lineCount; i++) {
line = [lines objectAtIndex:i];
// Create a new label for each line and add it to my SKSpriteNode layer
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:#"Superclarendon-Black"];
label.text = line;
label.fontSize = fontSize;
label.scale = 1.0f;
label.name = #"lineLabel";
label.fontColor = [UIColor blackColor];
label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
label.position = CGPointMake(button2.position.x - button2.size.width * 0.5f, button2.position.y - button2.size.height - i * fontSize * 1.1);
[layer addChild:label];
}
}
In the scene editor, change the SKLabelNode's text to attributed in the attributes inspector in the right pane, as shown below.
Doing so will give you very much freedom to customize the text that is displayed without having to create multiple SKLabelNode instances or a UIImage. For instance, you can create a paragraph as shown below.
For programmatic interaction, use the attributedString property of the label node to add custom attributes.
label.numberOfLines = 0 //equates to multiple lines
label.numberOfLines.preferredMaxLayoutWidth = screenWidth

Chroma Key using Core Image filters leaves a halo around text

I have been trying to come up with a simple way to use Core Image filters to key out the background of a slide and to key out text of a certain color. My current project works, but with one caveat: the text that I am trying to remove is gone, but a halo remains.
Here is a screenshot showing the problem:
From what I can tell, there is a blue halo around the "Main Slide Content", but this is probably okay since the main content is still on the screen. I want to remove the halo around the "Test Speaker Notes", though, because that's intended to be entirely keyed out.
In my head, I feel like the solution would be to get a mask of the green text, and then expand that mask a little bit, and then use that in a multiply filter to key out the halos, but I don't know how to approach this with Core Image. I would appreciate any guidance on which path to take here. Here is my applicationDidFinishLaunching method and the keying and blending methods, which are the two methods that do the work. All code is in the project file on GitHub, found [here]: https://github.com/nspaul/chromakeytest
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSImage *img = [NSImage imageNamed:#"testSlide.png"];
NSImage *actsBG = [NSImage imageNamed:#"background.png"];
NSImage *keyed = img;
//Key out the green
keyed = [self keyImage:keyed minHue:0.25 maxHue:0.4];
//Key out the blue background
keyed = [self keyImage:keyed minHue:0.6 maxHue:0.7];
//Put the test slide over the background slide
keyed = [self blendImage:keyed withBackgroundImage:actsBG];
[_topImageView setImage:img];
[_backgroundImageView setImage:actsBG];
[_keyedImageView setImage:keyed];
}
-(NSImage *)blendImage:(NSImage *)img withBackgroundImage:(NSImage *)bgimg {
CIImage *inputImage = [[CIImage alloc] initWithData:[img TIFFRepresentation]];
CIImage *bgInputImage = [[CIImage alloc] initWithData:[bgimg TIFFRepresentation]];
CIFilter *blendFilter = [CIFilter filterWithName:#"CISourceOverCompositing"];
[blendFilter setValue:inputImage forKey:#"inputImage"];
[blendFilter setValue:bgInputImage forKey:#"inputBackgroundImage"];
CIImage *outputImage = [blendFilter valueForKey:#"outputImage"];
NSImage *resultImage = [[NSImage alloc] initWithSize:[outputImage extent].size];
NSCIImageRep *rep = [NSCIImageRep imageRepWithCIImage:outputImage];
[resultImage addRepresentation:rep];
return resultImage;
}
-(NSImage *) keyImage:(NSImage *)imageToKey minHue:(float)minHueAngle maxHue:(float)maxHueAngle {
CIImage *ciImage = [[CIImage alloc] initWithData:[imageToKey TIFFRepresentation]];
// Allocate memory
const unsigned int size = 64;
float *cubeData = (float *)malloc (size * size * size * sizeof (float) * 4);
float rgb[3], hsv[3], *c = cubeData;
// Populate cube with a simple gradient going from 0 to 1
for (int z = 0; z < size; z++){
rgb[2] = ((double)z)/(size-1); // Blue value
for (int y = 0; y < size; y++){
rgb[1] = ((double)y)/(size-1); // Green value
for (int x = 0; x < size; x ++){
rgb[0] = ((double)x)/(size-1); // Red value
// Convert RGB to HSV
// You can find publicly available rgbToHSV functions on the Internet
rgbToHSV(rgb, hsv);
// Use the hue value to determine which to make transparent
// The minimum and maximum hue angle depends on
// the color you want to remove
float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f: 1.0f;
// Calculate premultiplied alpha values for the cube
c[0] = rgb[0] * alpha;
c[1] = rgb[1] * alpha;
c[2] = rgb[2] * alpha;
c[3] = alpha;
c += 4; // advance our pointer into memory for the next color value
}
}
}
NSLog(#"cubeData: %p",cubeData);
// Create memory with the cube data
NSData *data = [NSData dataWithBytesNoCopy:cubeData
length:(64 * 64 * 64 * sizeof (float) * 4)
freeWhenDone:YES];
CIFilter *colorCube = [CIFilter filterWithName:#"CIColorCube"];
[colorCube setValue:[NSNumber numberWithInt:size] forKey:#"inputCubeDimension"];
// Set data for cube
[colorCube setValue:data forKey:#"inputCubeData"];
[colorCube setValue:ciImage forKey:kCIInputImageKey];
CIImage *outImage = [colorCube valueForKey:#"outputImage"];
NSImage *resultImage = [[NSImage alloc] initWithSize:[outImage extent].size];
NSCIImageRep *rep = [NSCIImageRep imageRepWithCIImage:outImage];
[resultImage addRepresentation:rep];
return resultImage;
}