SpriteKit collision prevent from pushing out - objective-c

I have this problem:
I have node A and node B.
What i want is when collision between them happens (node A collides with the top of B), node B won't be pushing node A a bit up. Because as it is now, when A collides with B, its being pushed back a bit, and collision instantly ends(didEndContact is called). So when im like colliding with that B, collision instantly ends, then starts again, then ends, then starts again.... What i want is that if A collided with B, A will lay on this B. I noticed i could achieve that by setting A.dynamic = NO, but i need to apply velocity to A, and if its not dynamic, velocity wont work.
This is the code:
typedef NS_OPTIONS(uint32_t, CollisionCategory)
{
CollisionCategoryPlayer = 1 << 0,
CollisionCategoryDiggable = 1 << 1,
};
-(void)didMoveToView:(SKView *)view
{
self.playerNode = [SKSpriteNode spriteNodeWithImageNamed:#"player"];
self.playerNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.playerNode.size];
self.playerNode.physicsBody.categoryBitMask = CollisionCategoryPlayer;
self.playerNode.physicsBody.collisionBitMask = CollisionCategoryDiggable;
self.playerNode.physicsBody.contactTestBitMask = CollisionCategoryDiggable;
self.playerNode.position = CGPointMake(160, 520);
self.playerNode.name = #"player";
self.playerNode.physicsBody.allowsRotation = NO;
self.playerNode.physicsBody.friction = 0;
[self addChild:self.playerNode];
SKSpriteNode* spriteNode = [SKSpriteNode spriteNodeWithTexture:textToUse];
spriteNode.name = keyFromCoordinate(coord);
spriteNode.position = CGPointMake(160, 400);
spriteNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:spriteNode.size];
spriteNode.physicsBody.dynamic = NO;
spriteNode.physicsBody.categoryBitMask = CollisionCategoryDiggable;
spriteNode.physicsBody.collisionBitMask = CollisionCategoryPlayer;
spriteNode.physicsBody.contactTestBitMask = CollisionCategoryPlayer;
spriteNode.physicsBody.resting = YES;
[self addChild:spriteNode];
}
-(void)update:(NSTimeInterval)currentTime
{
self.playerNode.physicsBody.velocity = CGVectorMake(0, -200);
}
And again the problem is that didBeginContact is called, then didEndContact, then didBegin and so on, what i want is - did begin is only called once.

Setting the restitution = 0 for both physicsBodies helped

Related

How to animate SKNodes or SKSpriteNode to collide and bounce?

I have a few questions regarding animating SKNodes and or SKSpriteNodes. First one is how do i get them to move once the view loads? How do i make them collide and bounce off each other and go into the other direction. I am using an SKScene for this. Also is there a way to group SKSriteNodes with SKLabels?
I have:
#import <SpriteKit/SpriteKit.h>
//constants for the collision bitmap
static const uint32_t classicCategory = 1 << 0;
static const uint32_t arcadeCategory = 1 << 1;
static const uint32_t frenzieCategory = 1 << 2;
#interface HomeScene : SKScene <SKPhysicsContactDelegate> {
BOOL isSoundActionCompleted;
}
#end
Here is where I declare the SKSpriteNodes
SKSpriteNode *classicMode = [SKSpriteNode spriteNodeWithImageNamed:#"bubble.png"];
classicMode.size = CGSizeMake(130, 130);
classicMode.position = CGPointMake(self.size.width/2, self.size.height/2+50);
classicMode.name = #"classicMode";
[self addChild:classicMode];
SKLabelNode *classicTitle = [SKLabelNode labelNodeWithFontNamed:#"Noteworthy"];
classicTitle.text = #"Classic";
classicTitle.fontSize = 25;
classicTitle.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
classicTitle.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
classicTitle.fontColor = [SKColor darkGrayColor];
classicTitle.position = CGPointMake(self.size.width/2, self.size.height/2+50);
classicTitle.name = #"classicTitle";
[self addChild:classicTitle];
The other one here
SKSpriteNode *arcadeMode = [SKSpriteNode spriteNodeWithImageNamed:#"bubble.png"];
arcadeMode.size = CGSizeMake(130, 130);
arcadeMode.position = CGPointMake(self.size.width/2+68, self.size.height/2-65);
arcadeMode.name = #"arcadeMode";
[self addChild:arcadeMode];
SKLabelNode *arcadeTitle = [SKLabelNode labelNodeWithFontNamed:#"Noteworthy"];
arcadeTitle.text = #"Arcade";
arcadeTitle.fontSize = 25;
arcadeTitle.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
arcadeTitle.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
arcadeTitle.fontColor = [SKColor darkGrayColor];
arcadeTitle.position = CGPointMake(self.size.width/2+68, self.size.height/2-65);
arcadeTitle.name = #"arcadeTitle";
[self addChild:arcadeTitle];
Collision:
//collision and contact detection
self.physicsBody.categoryBitMask = arcadeCategory;
//physics and collision detection
classicMode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:classicMode.size];
classicMode.physicsBody.dynamic = NO; //no gravity
classicMode.physicsBody.categoryBitMask = arcadeCategory;
arcadeMode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:arcadeMode.size];
arcadeMode.physicsBody.dynamic = NO; //no gravity
arcadeMode.physicsBody.categoryBitMask = classicCategory;
And collision detection:
//collision detection
classicMode.physicsBody.categoryBitMask = classicCategory;
classicMode.physicsBody.collisionBitMask = arcadeCategory;
classicMode.physicsBody.contactTestBitMask = arcadeCategory;
arcadeMode.physicsBody.categoryBitMask = arcadeCategory;
arcadeMode.physicsBody.collisionBitMask = classicCategory;
arcadeMode.physicsBody.contactTestBitMask = classicCategory;
1) first problem in your code is that two static bodies never collide make them dynamic. so make atleast one or both bodies dynamic.
2)secondly for linear movement use body.physicsBody.velocity property with dynamic body bodies.
3)change your bitmask categories
classicMode.physicsBody.categoryBitMask = classicCategory;
classicMode.physicsBody.collisionBitMask = arcadeCategory;
classicMode.physicsBody.contactTestBitMask = arcadeCategory;
arcadeMode.physicsBody.categoryBitMask = arcadeCategory;
remember that contactTestBitMask uses in spritekit when two bodies contact or collide with each other and collisionBitMask is used to prevnet them to overlap or pass through each other
i cut arcadeMode.physicsBody.collisionBitMask = classicCategory;
arcadeMode.physicsBody.contactTestBitMask = classicCategory;
from your code because you alreay define it in
classicMode.physicsBody.categoryBitMask = classicCategory;
classicMode.physicsBody.collisionBitMask = arcadeCategory;
classicMode.physicsBody.contactTestBitMask = arcadeCategory; so no need to redfine it
use contactTestBitMask and collisionBitMask only once when you want two category interact with each other
4) handle all your collision and collision logic inside didbegincontact and didEndContact

SpriteKit only detecting partial collision on physics body

I have a simple jumping game setup where an hero jumps over a ball rolling towards him.
there is no gravity setup on the scene
the hero jump straight up and then straight down. if the hero stays still "didBeginContact" is fired and I can detect the collision. If the front of the hero hits the rolling ball when jumping up or coming down "didBeginContact" is fired. However if the back half of the hero land on the ball as it is going under it, it does not detect the collision.
Does anyone have any idea why it only catches the collision on the front? This seems more like edge detection vs. object detection! is that possible?
static const uint32_t groundCategory = 0x1 << 1;
static const uint32_t obstacleCategory = 0x1 << 2;
static const uint32_t heroCategory = 0x1 << 3;
SKSpriteNode *hero = [[SKSpriteNode alloc] initWithColor:[SKColor redColor] size:CGSizeMake(100, 115)];
[hero setPosition:CGPointMake(HERO_UP_POSITION_X, HEIGHT_GAME / 2 + hero.size.height / 2 + 10)];
hero.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hero.size];
[hero.physicsBody setAffectedByGravity:NO];
hero.physicsBody.usesPreciseCollisionDetection = YES;
hero.physicsBody.allowsRotation = NO;
hero.physicsBody.categoryBitMask = heroCategory;
hero.physicsBody.contactTestBitMask = obstacleCategory | groundCategory;
hero.physicsBody.collisionBitMask = 0;
hero.zPosition = 0;
hero.name = kHeroUpName;
[self addChild:hero];
obstacle = [[SKSpriteNode alloc] initWithImageNamed:kImgObstacleDown];
obstacle.name = #"obstacle";
[obstacle setPosition:CGPointMake(WIDTH_GAME + x, HEIGHT_GAME / 2 + y)];
obstacle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:obstacle.size.width / 2];
obstacle.physicsBody.usesPreciseCollisionDetection = YES;
[obstacle.physicsBody setAffectedByGravity:NO];
obstacle.physicsBody.categoryBitMask = obstacleCategory;
obstacle.physicsBody.contactTestBitMask = heroCategory;
obstacle.physicsBody.collisionBitMask = 0;
obstacle.zPosition = 1;
I figured out the problem.
I was checking if the object was past the hero in order to track score, If it was past the center of the hero I renamed it, so that it wouldn't get checked and scored again.
Thanks #0x141E your snippet made me realize that I was checking for names inDidBeginContact (Which fails) vs. checking for categoryBitMask
There are two test cases that needs to be in your didBeginContact: 1) if contact.bodyA is the hero and contact.bodyB is the obstacle and 2) if contact.bodyA is the obstacle and contact.bodyB is the hero. You are likely handling only one case. Here's an example of how to do that:
-(void)didBeginContact:(SKPhysicsContact *)contact
{
if ((contact.bodyA.categoryBitMask == heroCategory && contact.bodyB.categoryBitMask obstacleCategory)
|| (contact.bodyA.categoryBitMask == obstacleCategory && contact.bodyB.categoryBitMask == heroCategory)) {
// Do something here
}
}

Physics bodies other than my player won't call didBeginContact when colliding

In my game I have 4 bitmasks and everything is setup, yet didBeginContact only gets called when the first bitmask (playerCategory) collides with something. if 3 collides with 4, nothing happens, even though I have the contactTestBitMask set for them to collide.
myscene.h
self.physicsWorld.gravity = CGVectorMake(0.0, -2.45);
self.physicsWorld.contactDelegate = self;
self.player = [SKSpriteNode spriteNodeWithColor:[SKColor blueColor] size: CGSizeMake(10.0, 20.0)];
self.player.position = CGPointMake(20.0, self.frame.size.height);
self.player.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.player.size];
self.player.physicsBody.affectedByGravity = YES;
self.player.physicsBody.dynamic = YES;
self.player.physicsBody.categoryBitMask = playerCategory;
self.player.physicsBody.contactTestBitMask = enemyCategory;
self.player.physicsBody.collisionBitMask = tileCategory;
[self.gameNode addChild:self.player];
[...]
- (void) swipeRightHandler:(UISwipeGestureRecognizer *) recognizer {
NSLog(#"swipe right");
SKSpriteNode *attackRect = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(40, 5)];
attackRect.position = CGPointMake(self.player.position.x + 10, self.player.position.y);
attackRect.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:attackRect.size];
attackRect.physicsBody.categoryBitMask = attackCategory;
attackRect.physicsBody.contactTestBitMask = 255;
attackRect.physicsBody.collisionBitMask = enemyCategory;
attackRect.physicsBody.affectedByGravity = NO;
attackRect.physicsBody.dynamic = NO;
[self.gameNode addChild:attackRect];
[attackRect runAction:[SKAction moveBy:CGVectorMake(250, 0) duration:1.0] completion:^{
[attackRect removeFromParent];
}];
}
RandomLevelGenerator.m:
SKSpriteNode *enemy = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:CGSizeMake(20, 20)];
enemy.position = CGPointMake(visual_x, FLOOR_X + arc4random_uniform(MAX_Y - FLOOR_X));
enemy.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:enemy.size];
enemy.physicsBody.categoryBitMask = enemyCategory;
enemy.physicsBody.contactTestBitMask = playerCategory | attackCategory;
enemy.physicsBody.collisionBitMask = attackCategory;
enemy.physicsBody.affectedByGravity = NO;
enemy.physicsBody.dynamic = NO;
[self.scene.gameNode addChild:enemy];
LevelGenerator.h:
static const uint32_t playerCategory = 0x1 << 0;
static const uint32_t tileCategory = 0x1 << 1;
static const uint32_t enemyCategory = 0x1 << 2;
static const uint32_t attackCategory = 0x1 << 3;
MyScene.m again:
SKPhysicsBody *firstBody, *secondBody;
if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
{
firstBody = contact.bodyA;
secondBody = contact.bodyB;
} else {
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
NSLog(#"%d - %d", firstBody.categoryBitMask, secondBody.categoryBitMask);
That last NSLog, only ever prints 1 - x, never 3 - x. I've also tried allowing other collisions and I can't get any of them to collide.
If the categoryBitMask for two bodies ANDed together is zero, they will not generate contact events. For instance this happens with playerCategory (1) and enemyCategory (4): 1 & 4 = 0 <== no contact events are generated, ever, between players and enemies. In that case it won't even get to check the contactBitMask flags.
If you want two bodies to generate contact events, they both have to have the same category bitmask flag set.
The default for categoryBitMask is 0xFFFFFFFF so that all bodies can contact with each other. Same for collisionBitMask because generally you want all bodies to contact and collide by default.
The only bit mask you really need to modify in most cases is the contactBitMask, whose default is 0, meaning no contact events are generated by default (to improve performance).
Only change the other bitmasks where changing contactBitMask alone does not suffice.
For example when you want contact events, but no collision feedback (= change in velocity/position of colliding bodies when they come in contact), then make sure that those body's collisionBitMask ANDed together is 0. This allows bodies to generate contact events, but pass through each other. Useful to create triggers, ie when the player enters an area you can let him pass but also trigger a game event.
Or when you want to use the same contact bitmasks for two different categories of bodies who should only collide/contact with other bodies sharing the same category - ie regular and "ghost" characters who should have the same contact behavior with everything else in the game, except they shouldn't contact/collide with their corporeal (non-ghost) counterparts. In that case, use the same contactBitMask and collisionBitMask for both bodies. Then set all flags in categoryBitMask for all other categories, but don't set the flags for the two categories representing the corporeal and ghost entities. Ie if corporeal category is 2 and ghost category is 8, the categoryBitMask should be: 0xFFFFFFFF - 2 - 8 = 0xFFFFFFF5 (lower 8 bits would be: 11110101)
Long story short: categoryBitMask flags should be cleared from 0xFFFFFFFF (rather than set to a specific bit) so that individual bodies in specific categories don't contact with each other but still contact with all other bodies.

Box2d: Elastic rope takes time to get back to its initial position

I am creating a rubber band in Box2d. Here is my code.
// init physics
[self initPhysics];
// Create ball body
CCSprite *ball = [CCSprite spriteWithFile:#"rubberband.png"];
ball.position = ccp(100, 100);
ball.tag = 1;
// [self addChild:ball];
//=======Params
// Position and size
b2Vec2 lastPos = b2Vec2(154.0/PTM_RATIO,65.0/PTM_RATIO); //set position first body
float widthBody = 2.0/PTM_RATIO;
float heightBody = 2.0/PTM_RATIO;
// Body params
float density = 0.0;
float restitution = 0.5;
float friction = 0.5;
// Distance joint
float dampingRatio = 0.85;
float frequencyHz = 10;
// Rope joint
float kMaxWidth = 50.0/PTM_RATIO;
// Bodies
int countBodyInChain = 68;
b2Body* prevBody;
//========Create bodies and joints
for (int k = 0; k < countBodyInChain; k++) {
b2BodyDef bodyDef;
if(k==0 || k==countBodyInChain-1) bodyDef.type = b2_staticBody; //first and last bodies are static
else bodyDef.type = b2_dynamicBody;
bodyDef.position = lastPos;
bodyDef.fixedRotation = YES;
b2Body* body = world->CreateBody(&bodyDef);
b2PolygonShape distBodyBox;
distBodyBox.SetAsBox(widthBody, heightBody);
b2FixtureDef fixDef;
fixDef.density = density;
fixDef.restitution = restitution;
fixDef.friction = friction;
fixDef.shape = &distBodyBox;
body->CreateFixture(&fixDef);
if(k>0) {
b2RevoluteJointDef armJointDef;
armJointDef.Initialize(prevBody, body, lastPos);
armJointDef.enableMotor = true;
armJointDef.enableLimit = true;
armJointDef.maxMotorTorque = 1;
world->CreateJoint(&armJointDef);
//Create rope joint
b2RopeJointDef rDef;
rDef.maxLength = (body->GetPosition() - prevBody->GetPosition()).Length() * kMaxWidth;
rDef.localAnchorA = rDef.localAnchorB = b2Vec2_zero;
rDef.bodyA = prevBody;
rDef.bodyB = body;
rDef.collideConnected = false;
world->CreateJoint(&rDef);
} //if k>0
lastPos += b2Vec2(widthBody, 0); //modify b2Vect for next body
prevBody = body;
} //for -loop
[self scheduleUpdate];
}
return self;
}
Problem is that when the app starts, rubber band appears in stretched form in U shape and then it gradually start contracting and coming to become straight horizontally. Can anyone please tell me why it is happening? I want the rubber band to be without being stretched in the beginning.
Best Regards
You don't update lastPos so all bodies occupy the same position initially. Box2D will force them apart and this could lead to the problem.

How to get the on-screen location of an NSStatusItem

I have a question about the NSStatusItem for cocoa in mac osx. If you look at the mac app called snippets (see the movie at http://snippetsapp.com/). you will see that once you clicked your statusbar icon that a perfectly aligned view / panel or maybe even windows appears just below the icon.
My question is ... How to calculate the position to where to place your NSWindow just like this app does?
I have tried the following:
Subclass NSMenu
Set the view popery for the first item of the menu (Worked but enough)
Using addSubview instead of icon to NSStatusItem this worked but could not get higher then 20px
Give the NSStatusItem a view, then get the frame of that view's window. This technically counts as UndocumentedGoodness, so don't be surprised if it breaks someday (e.g., if they start keeping the window offscreen instead).
I don't know what you mean by “could not get heigher then 20px”.
To do this without the hassle of a custom view, I tried the following (that works). In the method that is set as the action for the status item i.e. the method that is called when the user clicks the status item, the frame of the status item can be retrieved by:
[[[NSApp currentEvent] window] frame]
Works a treat for me
Given an NSMenuItem and an NSWindow, you can get the point that centers your window right below the menu item like this:
fileprivate var centerBelowMenuItem: CGPoint {
guard let window = window, let barButton = statusItem.button else { return .zero }
let rectInWindow = barButton.convert(barButton.bounds, to: nil)
let screenRect = barButton.window?.convertToScreen(rectInWindow) ?? .zero
// We now have the menu item rect on the screen.
// Let's do some basic math to center our window to this point.
let centerX = screenRect.origin.x-(window.frame.size.width-barButton.bounds.width)/2
return CGPoint(x: centerX, y: screenRect.origin.y)
}
No need for undocumented API's.
Maybe another solution which works for me (swift 4.1) :
let yourStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let frameOrigin = yourStatusItem.button?.window?.frame.origin
let yourPoint = CGPoint(x: (frameOrigin?.x)!, y: (frameOrigin?.y)! - 22)
yourWindow?.setFrameOrigin(yourPoint)
It seems that this app uses Matt's MAAttachedWindow. There's an sample application with the same layout & position.
NOTE: PLEASE DO NOT USE THIS, at least not for the purpose of locating an NSStatusItem.
Back when I posted this, this crazy image matching technique was the only way to solve this problem without undocumented API. Now, you should use Oskar's solution.
If you're willing to use image analysis to find the status item on a menu bar, here's a category for NSScreen which does exactly that.
It might seem crazy to do it this way, but it's fast, relatively small, and it's the only way of finding a status item without undocumented API.
If you pass in the current image for the status item, this method should find it.
#implementation NSScreen (LTStatusItemLocator)
// Find the location of IMG on the screen's status bar.
// If the image is not found, returns NSZeroPoint
- (NSPoint)originOfStatusItemWithImage:(NSImage *)IMG
{
CGColorSpaceRef csK = CGColorSpaceCreateDeviceGray();
NSPoint ret = NSZeroPoint;
CGDirectDisplayID screenID = 0;
CGImageRef displayImg = NULL;
CGImageRef compareImg = NULL;
CGRect screenRect = CGRectZero;
CGRect barRect = CGRectZero;
uint8_t *bm_bar = NULL;
uint8_t *bm_bar_ptr;
uint8_t *bm_compare = NULL;
uint8_t *bm_compare_ptr;
size_t bm_compare_w, bm_compare_h;
BOOL inverted = NO;
int numberOfScanLines = 0;
CGFloat *meanValues = NULL;
int presumptiveMatchIdx = -1;
CGFloat presumptiveMatchMeanVal = 999;
// If the computer is set to Dark Mode, set the "inverted" flag
NSDictionary *globalPrefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain];
id style = globalPrefs[#"AppleInterfaceStyle"];
if ([style isKindOfClass:[NSString class]]) {
inverted = (NSOrderedSame == [style caseInsensitiveCompare:#"dark"]);
}
screenID = (CGDirectDisplayID)[self.deviceDescription[#"NSScreenNumber"] integerValue];
screenRect = CGDisplayBounds(screenID);
// Get the menubar rect
barRect = CGRectMake(0, 0, screenRect.size.width, 22);
displayImg = CGDisplayCreateImageForRect(screenID, barRect);
if (!displayImg) {
NSLog(#"Unable to create image from display");
CGColorSpaceRelease(csK);
return ret; // I would normally use goto(bail) here, but this is public code so let's not ruffle any feathers
}
size_t bar_w = CGImageGetWidth(displayImg);
size_t bar_h = CGImageGetHeight(displayImg);
// Determine scale factor based on the CGImageRef we got back from the display
CGFloat scaleFactor = (CGFloat)bar_h / (CGFloat)22;
// Greyscale bitmap for menu bar
bm_bar = malloc(1 * bar_w * bar_h);
{
CGContextRef bmCxt = NULL;
bmCxt = CGBitmapContextCreate(bm_bar, bar_w, bar_h, 8, 1 * bar_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
// Draw the menu bar in grey
CGContextDrawImage(bmCxt, CGRectMake(0, 0, bar_w, bar_h), displayImg);
uint8_t minVal = 0xff;
uint8_t maxVal = 0x00;
// Walk the bitmap
uint64_t running = 0;
for (int yi = bar_h / 2; yi == bar_h / 2; yi++)
{
bm_bar_ptr = bm_bar + (bar_w * yi);
for (int xi = 0; xi < bar_w; xi++)
{
uint8_t v = *bm_bar_ptr++;
if (v < minVal) minVal = v;
if (v > maxVal) maxVal = v;
running += v;
}
}
running /= bar_w;
uint8_t threshold = minVal + ((maxVal - minVal) / 2);
//threshold = running;
// Walk the bitmap
bm_bar_ptr = bm_bar;
for (int yi = 0; yi < bar_h; yi++)
{
for (int xi = 0; xi < bar_w; xi++)
{
// Threshold all the pixels. Values > 50% go white, values <= 50% go black
// (opposite if Dark Mode)
// Could unroll this loop as an optimization, but probably not worthwhile
*bm_bar_ptr = (*bm_bar_ptr > threshold) ? (inverted?0x00:0xff) : (inverted?0xff:0x00);
bm_bar_ptr++;
}
}
CGImageRelease(displayImg);
displayImg = CGBitmapContextCreateImage(bmCxt);
CGContextRelease(bmCxt);
}
{
CGContextRef bmCxt = NULL;
CGImageRef img_cg = NULL;
bm_compare_w = scaleFactor * IMG.size.width;
bm_compare_h = scaleFactor * 22;
// Create out comparison bitmap - the image that was passed in
bmCxt = CGBitmapContextCreate(NULL, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
CGContextSetBlendMode(bmCxt, kCGBlendModeNormal);
NSRect imgRect_og = NSMakeRect(0,0,IMG.size.width,IMG.size.height);
NSRect imgRect = imgRect_og;
img_cg = [IMG CGImageForProposedRect:&imgRect context:nil hints:nil];
CGContextClearRect(bmCxt, imgRect);
CGContextSetFillColorWithColor(bmCxt, [NSColor whiteColor].CGColor);
CGContextFillRect(bmCxt, CGRectMake(0,0,9999,9999));
CGContextScaleCTM(bmCxt, scaleFactor, scaleFactor);
CGContextTranslateCTM(bmCxt, 0, (22. - IMG.size.height) / 2.);
// Draw the image in grey
CGContextSetFillColorWithColor(bmCxt, [NSColor blackColor].CGColor);
CGContextDrawImage(bmCxt, imgRect, img_cg);
compareImg = CGBitmapContextCreateImage(bmCxt);
CGContextRelease(bmCxt);
}
{
// We start at the right of the menu bar, and scan left until we find a good match
int numberOfScanLines = barRect.size.width - IMG.size.width;
bm_compare = malloc(1 * bm_compare_w * bm_compare_h);
// We use the meanValues buffer to keep track of how well the image matched for each point in the scan
meanValues = calloc(sizeof(CGFloat), numberOfScanLines);
// Walk the menubar image from right to left, pixel by pixel
for (int scanx = 0; scanx < numberOfScanLines; scanx++)
{
// Optimization, if we recently found a really good match, bail on the loop and return it
if ((presumptiveMatchIdx >= 0) && (scanx > (presumptiveMatchIdx + 5))) {
break;
}
CGFloat xOffset = numberOfScanLines - scanx;
CGRect displayRect = CGRectMake(xOffset * scaleFactor, 0, IMG.size.width * scaleFactor, 22. * scaleFactor);
CGImageRef displayCrop = CGImageCreateWithImageInRect(displayImg, displayRect);
CGContextRef compareCxt = CGBitmapContextCreate(bm_compare, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
CGContextSetBlendMode(compareCxt, kCGBlendModeCopy);
// Draw the image from our menubar
CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), displayCrop);
// Blend mode difference is like an XOR
CGContextSetBlendMode(compareCxt, kCGBlendModeDifference);
// Draw the test image. Because of blend mode, if we end up with a black image we matched perfectly
CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), compareImg);
CGContextFlush(compareCxt);
// Walk through the result image, to determine overall blackness
bm_compare_ptr = bm_compare;
for (int i = 0; i < bm_compare_w * bm_compare_h; i++)
{
meanValues[scanx] += (CGFloat)(*bm_compare_ptr);
bm_compare_ptr++;
}
meanValues[scanx] /= (255. * (CGFloat)(bm_compare_w * bm_compare_h));
// If the image is very dark, it matched well. If the average pixel value is < 0.07, we consider this
// a presumptive match. Mark it as such, but continue looking to see if there's an even better match.
if (meanValues[scanx] < 0.07) {
if (meanValues[scanx] < presumptiveMatchMeanVal) {
presumptiveMatchMeanVal = meanValues[scanx];
presumptiveMatchIdx = scanx;
}
}
CGImageRelease(displayCrop);
CGContextRelease(compareCxt);
}
}
// After we're done scanning the whole menubar (or we bailed because we found a good match),
// return the origin point.
// If we didn't match well enough, return NSZeroPoint
if (presumptiveMatchIdx >= 0) {
ret = CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame));
ret.x -= (IMG.size.width + presumptiveMatchIdx);
ret.y -= 22;
}
CGImageRelease(displayImg);
CGImageRelease(compareImg);
CGColorSpaceRelease(csK);
if (bm_bar) free(bm_bar);
if (bm_compare) free(bm_compare);
if (meanValues) free(meanValues);
return ret;
}
#end
From the Apple NSStatusItem Class Reference:
Setting a custom view overrides all the other appearance and behavior settings defined by NSStatusItem. The custom view is responsible for drawing itself and providing its own behaviors, such as processing mouse clicks and sending action messages.