I'm currently working on a UICollectionViewLayout which is more or less similar to the Garageband iPad Appication - featuring reusable decoration/supplementary views.
One of the main features is gridline snapping - which would enable you to snap the X/Y frame positions of the GREEN cells to the nearest horizontal/vertical gridline.
The issue I'm having, is that the base X/Y position doesn't start at 0,0 because I have a floating column and floating row - so my snapping calculation is wrong because it doesn't consider the floating row/floating column width/height.
Here is an image illustrating my problem:
Here is my code for calculating the snapped positions:
CGPoint cellTopLeft = self.frame.origin;
CGFloat gridlineWidth = 30;
CGFloat floatingColumnWidth = 120; // width of the floating left column
CGFloat floatingRowHeight = 100; // height of the floating top row
CGFloat rowHeight = 100; // height of rows on the left (table 0, table 1, table 2, etc)
float snapped_x = [self closest:cellTopLeft.x toValue:gridlineWidth];
float snapped_y = [self closest:cellTopLeft.y toValue:rowHeight];
- (float)closest:(float)input toValue:(float)value {
return value * floorf((input / value) + 0.5);
}
As you can see by the "Grand 0" green cell that I'm actively dragging, the position is wrong.
Thanks #zrzka - sometimes the seemingly complicated bugs can be the easiest ones to solve, and just takes an extra pair of eyes.
The solution was:
float snapped_x = [self closest:cellTopLeft.x - floatingColumnWidth toValue:gridlineWidth] + floatingColumnWidth
float snapped_y = [self closest:cellTopLeft.y - floatingRowHeight toValue:rowHeight] + floatingRowHeight;
Related
I am trying to create a repeating background from top to bottom. I have got the background to actually scroll however there's a black area that appears with each repetition. I'm not entirely sure why this is happening... Below is my code:
- (void)onEnter
{
[super onEnter];
[self initBackground];
}
-(void)initBackground
{
NSString *backgroundImage = #"Stars.png";//[self getThemeBG];
background1 = [CCSprite spriteWithImageNamed:backgroundImage];
background1.position = ccp(self.contentSize.width*0.5f,self.contentSize.height*0.5f);
[self addChild:background1 z:-123];
background2 = [CCSprite spriteWithImageNamed:backgroundImage];
background2.position = ccp(self.contentSize.width*0.5f,self.contentSize.height*0.5f+background1.contentSize.height);
background2.flipY = true;
[self addChild:background2 z:-456];
}
-(void)scrollBackground:(CCTime)dt
{
CGSize s = [[CCDirector sharedDirector] viewSize];
CGPoint pos1 = background1.position;
CGPoint pos2 = background2.position;
pos1.y -= kScrollSpeed;
pos2.y -= kScrollSpeed;
if(pos1.y <=-(s.height*0.5f) )
{
pos1.y = pos2.y + background2.contentSize.height;
}
if(pos2.y <=-(s.height*0.5f) )
{
pos2.y = pos1.y + background1.contentSize.height;
}
background1.position = pos1;
background2.position = pos2;
}
-(void)update:(CCTime)delta
{
[self scrollBackground:delta];
}
I have defined kScrollSpeed as 3 and both backgroundImage1 and 2 are CCSprites. I'm not entirely sure why a black area is appearing with each cycle.
The dimensions of the background image is 640 x 1136px.
The trick to solve the "black lines" issue with tiling graphics, specifically for cocos2d but also applicable to Sprite Kit and other render engines is to ensure that content is drawn on exact pixel boundaries.
The only way to ensure that is to cast positions to integers, or on Retina round to the next nearest "half point" (ie on Retina 123.5 is a valid exact pixel coordinate, but not on non-Retina devices).
Where exactly to perform the rounding depends on your code, but in general instead of doing this:
background1.position = pos1;
You need to do:
pos1.x = round(pos1.x * 2.0) / 2.0;
pos1.y = round(pos1.y * 2.0) / 2.0;
background1.position = pos1;
Casting to int is often recommended but it means that on Retina devices your content will only move on a 2-pixel boundary. The above solution doesn't use int casting but relies on rounding to allow .5 coordinates. It works equally well on both standard and Retina resolution devices.
PS: I strongly advice to create a helper function or overriding the setPosition: method for the above rounding code. You don't want to spread these calculations all over your code.
More points to consider:
only round once, at the very end of your position calculations, before applying the new position to the node's position property
you may need to apply the same round procedure to parent's position, for example if you change position of both tile sprites and their parent (ie a layer or container node)
The red line is UIView that is rotating with anchor point at its center. What I want to achieve is that when I drag my finger around that green circle holding it on the red circle, that red line goes along with the finger. But I can't do that without knowing the angle. How do I count it?
You can use the Pythagorean theorem to calculate the radius. In this case you know that the center of the circle is at x:152, y:0. Your touch location would be at x1, y1, which gives you enough information to calculate the legs.
In code:
CGPoint center = CGPointMake(152.0f, 0.0f);
CGPoint touchPoint = [gestureRecognizer locationInView:yourView];
float xLeg = fabs(center.x - touchPoint.x);
float yLeg = fabs(touchPoint.y);
angle = atan(xLeg / yLeg);
//float radius = sqrt(pow(xLeg, 2) + pow(yLeg, 2));
Hope it helps!
Note: Edited to reflect change of wording. Original question asked for radius.
use the arctangent of 3 points. (center, the point, where touch started/ or a fixed position that you define as angle 0 , the current touch point )
double AngleBetweenThreePoints(CGPoint point1,CGPoint point2, CGPoint point3)
{
CGPoint p1 = CGPointMake(point2.x - point1.x, -1*point2.y - point1.y *-1);
CGPoint p2 = CGPointMake(point3.x -point1.x, -1*point3.y -point1.y*-1);
double angle = atan2(p2.x*p1.y-p1.x*p2.x,p1.y*p2.y);
return angle /(2* M_PI);
}
I am in process of developing a small game where a space-ship travels through a layer (doh!), in some situations the spaceship comes close to an enemy, and the whole layer is zoomed in on the space-ship with the zoom level being dependent on the distance between the ship and the enemy. All of this works fine.
The main question, however, is how do I keep the zoom being centered on the space-ship?
Currently I control the zooming in the GameLayer object through the update method, here is the code:
-(void) prepareLayerZoomBetweenSpaceship{
CGPoint mainSpaceShipPosition = [mainSpaceShip position];
CGPoint enemySpaceShipPosition = [enemySpaceShip position];
float distance = powf(mainSpaceShipPosition.x - enemySpaceShipPosition.x, 2) + powf(mainSpaceShipPosition.y - enemySpaceShipPosition.y,2);
distance = sqrtf(distance);
/*
Distance > 250 --> no zoom
Distance < 100 --> maximum zoom
*/
float myZoomLevel = 0.5f;
if(distance < 100){ //maximum zoom in
myZoomLevel = 1.0f;
}else if(distance > 250){
myZoomLevel = 0.5f;
}else{
myZoomLevel = 1.0f - (distance-100)*0.0033f;
}
[self zoomTo:myZoomLevel];
}
-(void) zoomTo:(float)zoom {
if(zoom > 1){
zoom = 1;
}
// Set the scale.
if(self.scale != zoom){
self.scale = zoom;
}
}
Basically my question is: How do I zoom the layer and center it exactly between the two ships? I guess this is like a pinch zoom with two fingers!
Below is some code that should get it working for you. Basically you want to:
Update your ship positions within the parentNode's coordinate system
Figure out which axis these new positions will cause the screen will be bound by.
Scale and re-position the parentNode
I added some sparse comments, but let me know if you have any more questions/issues. It might be easiest to dump this in a test project first...
ivars to put in your CCLayer:
CCNode *parentNode;
CCSprite *shipA;
CCSprite *shipB;
CGPoint destA, deltaA;
CGPoint destB, deltaB;
CGPoint halfScreenSize;
CGPoint fullScreenSize;
init stuff to put in your CCLayer:
CGSize size = [[CCDirector sharedDirector] winSize];
fullScreenSize = CGPointMake(size.width, size.height);
halfScreenSize = ccpMult(fullScreenSize, .5f);
parentNode = [CCNode node];
[self addChild:parentNode];
shipA = [CCSprite spriteWithFile:#"Icon-Small.png"]; //or whatever sprite
[parentNode addChild:shipA];
shipB = [CCSprite spriteWithFile:#"Icon-Small.png"];
[parentNode addChild:shipB];
//schedules update for every frame... might not run great.
//[self schedule:#selector(updateShips:)];
//schedules update for 25 times a second
[self schedule:#selector(updateShips:) interval:0.04f];
Zoom / Center / Ship update method:
-(void)updateShips:(ccTime)timeDelta {
//SHIP POSITION UPDATE STUFF GOES HERE
...
//1st: calc aspect ratio formed by ship positions to determine bounding axis
float shipDeltaX = fabs(shipA.position.x - shipB.position.x);
float shipDeltaY = fabs(shipA.position.y - shipB.position.y);
float newAspect = shipDeltaX / shipDeltaY;
//Then: scale based off of bounding axis
//if bound by x-axis OR deltaY is negligible
if (newAspect > (fullScreenSize.x / fullScreenSize.y) || shipDeltaY < 1.0) {
parentNode.scale = fullScreenSize.x / (shipDeltaX + shipA.contentSize.width);
}
else { //else: bound by y-axis or deltaX is negligible
parentNode.scale = fullScreenSize.y / (shipDeltaY + shipA.contentSize.height);
}
//calculate new midpoint between ships AND apply new scale to it
CGPoint scaledMidpoint = ccpMult(ccpMidpoint(shipA.position, shipB.position), parentNode.scale);
//update parent node position (move it into view of screen) to scaledMidpoint
parentNode.position = ccpSub(halfScreenSize, scaledMidpoint);
}
Also, I'm not sure how well it'll perform with a bunch of stuff going on -- but thats a separate problem!
Why don't you move the entire view, & position it so the ship is in the centre of the screen? I haven't tried it with your example, but it should be straight forward. Maybe something like this -
CGFloat x = (enemySpaceShipPosition.x - mainSpaceShipPosition.x) / 2.0 - screenCentreX;
CGFloat y = (enemySpaceShipPosition.y - mainSpaceShipPosition.y) / 2.0 - screenCentreY;
CGPoint midPointForContentOffset = CGPointMake(-x, -y);
[self setContentOffset:midPointForContentOffset];
...where you've already set up screenCentreX & Y. I haven't used UISCrollView for quite a while (been working on something in Unity so I'm forgetting all by Obj-C), & I can't remember how the contentOffset is affected by zoom level. Try it & see! (I'm assuming you're using a UIScrollView, maybe you could try that too if you're not)
I'm trying to size the rows of a NSTableView to exactly 9 rows fit. I've tried [menuTableView setRowHeight:floor(menuRect.size.height / 9)]; and I've tried [menuTableView setRowHeight:(menuRect.size.height / 9)]; and [menuTableView setRowHeight:ceil(menuRect.size.height / 9)]; but all of them have the same issue, if I've selected row 0 then row 9 is clipped at the bottom and if I select row 9 then row 0 is clipped at the top.
How can I set up a NSTableView to show 9 full rows so no matter what there is never a partial row visible?
Thanks,
edit: Yes menuRect is the frame for the menuTableView
edit:
Below is the full method I use to position the NSTableView and set the row height I've tried to remove any rounding issues with the use of the rowHeight variable. You can see with the attached screen shot that the last row isn't fully displayed
- (void) updateGUIPositions
{
NSRect screenRect = [self frame];
NSRect menuRect;
// Set the menu location on screen
NSInteger spacer = screenRect.size.width / 9;
menuRect.size.width = floor(screenRect.size.width - (spacer * 3)) / 2;
menuRect.size.height = ceil(screenRect.size.height - (spacer * 2));
NSInteger rowHeight = menuRect.size.height / 9;
menuRect.size.height = rowHeight * 9;
menuRect.origin.x = menuRect.size.width + (spacer * 2);
menuRect.origin.y = spacer;
[menuScrollView setFrame:menuRect];
// Setup the row height
[menuTableView setRowHeight:rowHeight];
}
I am not sure if this will work, but it sounds like a rounding error(I had the same problem with a UITableView). Try making the height of the NSTableView's frame 1px larger. So, menuTableView.frame.height = menuTableView.frame.height+1;
EDIT
Try using this...
`float rowHeight = menuRect.size.height / 9.0f;`
If you can fix the size of the rows and instead set the size of the frame programmatically, this related question suggests this formula works:
numRows * (rowHeight + intercellSpacing.height)
Using that formula, the scroller will draw, but you could just turn it off.
Hoping this is fairly simple. Basically, I'm drawing some text onto an NSView and need to know the "physical" height and width of the glyphs being drawn.
The font is fixed-width so I don't need to worry about kerning, all I need to do is ensure each glyph is centred horizontally and vertically within its "space"
This isn't the actual code but I've re-written it in a way that should make it easier to understand what I'm trying to achieve.
CGFontRef myFontRef = CGFontCreateWithDataProvider(myFontDataProvider);
CGContextSetFont(thisContext, myFontRef);
CGFloat fontSize = 1.0; //will be changeable later
CGContextSetFontSize(thisContext, fontSize);
CGRect charBounds = CGRectMake(x, y, widthPerChar, heightPerChar);
//paint charBounds with background colour
CGFloat textX = x + 2;
CGFloat textY = y + 5;
CGContextShowGlyphsAtPoint(thisContext, textX, textY, (CGGlyph *)displayedString, 1);
If I was able to calculate the width of the displayed glyph it would be easy to work out what textX and textY should be.
Thanks in advance to anybody willing to assist.
Partly worked it out from this code - here's what I came up with:
CGContextSetTextDrawingMode(thisContext, kCGTextInvisible);
CGContextShowGlyphsAtPoint(thisContext, 1, 1, (const CGGlyph *)"x", 1);
CGPoint postPosition = CGContextGetTextPosition(thisContext);
CGFloat textOffsetX = ((postPosition.x - 1) / 2);
CGFloat textOffsetY = (fontSize * 5);
CGContextSetTextDrawingMode(thisContext, kCGTextFillStroke);
Not a huge fan of doing it like this but it's working for now. At least it can be calculated once for each window resize which isn't too bad.
Thanks for the help though!
You can calculate the width of single glyph in this way:
CGRect units;
CGFontGetGlyphBBoxes(font, &glyph, 1, &units);
float width = units.size.width/CGFontGetUnitsPerEm(font)*fontSize;