In the app I'm building, a square image is imported, it is broken into 16 tiles, and those tiles are placed in a square on the screen, starting from the top. However, I'm adding an extra bar of info at the top above the tiles, so I need to move the set of tiles further down the screen. I've played around with the for loop and the coordinates that the code makes, but nothing is giving me the desired respect.
-(void) initPuzzle:(NSString *) imagePath{
UIImage *orgImage = [UIImage imageNamed:imagePath];
if( orgImage == nil ){
return;
}
[self.tiles removeAllObjects];
tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES;
tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES;
blankPosition = CGPointMake( NUM_HORIZONTAL_PIECES-1, NUM_VERTICAL_PIECES-1 );
for( int x=0; x<NUM_HORIZONTAL_PIECES; x++ ){
for( int y=0; y<NUM_VERTICAL_PIECES; y++ ){
// *** The coordinates need to be altered - ?
CGPoint orgPosition = CGPointMake(x,y);
if( blankPosition.x == orgPosition.x && blankPosition.y == orgPosition.y ){
continue;
}
CGRect frame = CGRectMake(tileWidth*x, tileHeight*y,
tileWidth, tileHeight );
CGImageRef tileImageRef = CGImageCreateWithImageInRect( orgImage.CGImage, frame );
UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef];
CGRect tileFrame = CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y,
tileWidth, tileHeight );
tileImageView = [[Tile alloc] initWithImage:tileImage];
tileImageView.frame = tileFrame;
tileImageView.originalPosition = orgPosition;
tileImageView.currentPosition = orgPosition;
CGImageRelease( tileImageRef );
[tiles addObject:tileImageView];
// now add to view
[self.view insertSubview:tileImageView atIndex:0];
}
}
[self shuffle];
}
int topBarWidth = 100; //100 based on the example in your comment
Then at the point in your code where you're setting the frames of each tile, add topBarWidth to whatever y value you were previously setting.
Another option is to create a subview just for your tiles and then reposition that instead. If you then wanted to animate all the tiles moving up and down you'd only have to animate the tiles view, instead of recalculating the position of all the images.
Related
I am trying to magnify an NSScrollView which contains NSTextView and keep it centered to its content at all times. The NSTextView has left/right insets to keep the word wrapping consistent and to keep the paragraphs nicely at the center of the view.
Both [NSScrollView scaleUnitSquareToSize:...] and setMagnification:... have their own quirks and problems, but for now setMagnification seems a better option, as it is not relative.
Here's what happens (among other strange stuff):
On resizing, I update the insets:
CGFloat inset = self.textScrollView.frame.size.width / 2 - _documentWidth / 2;
self.textView.textContainerInset = NSMakeSize(inset, TEXT_INSET_TOP);
self.textView.textContainer.size = NSMakeSize(_documentWidth, self.textView.textContainer.size.height);
Zooming in:
CGFloat magnification = [self.textScrollView magnification];
NSPoint center = NSMakePoint(self.textScrollView.frame.size.width / 2, self.textScrollView.frame.size.height / 2);
if (zoomIn) magnification += .05; else magnification -= .05;
[self.textScrollView setMagnification:magnification centeredAtPoint:center];
Everything kind of works for a while. Sometimes, depending on from which window corner the window is resized, the ScrollView loses its center, and I haven't found a solution for re-centering the view of a magnified NSScrollView.
After magnification, layout constraints can get broken too when resizing the window, especially when the textContainer is clipped out of view, and the app crashes with the following error:
*** Assertion failure in -[NSISLinearExpression addVariable:coefficient:], /Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1349.91/Layout.subproj/IncrementalSimplex/NSISLinearExpression.m:716
One problem might be that I am setting the insets according to UIScrollView frame size, because the contained NSTextView's coordinates don't seem to be relative but absolute after magnification.
Is there any safe way to magnifying this sort of view and keeping it centered to its content at all times? And why are my constraints breaking?
I've run into similar problems, and unfortunately I ended up doing the centering myself. Here are some of the highlights of my solution.
needs recursion prevention! (otherwise stackoverflow :)
create a non-drawable NSView as the documentView, and then add your drawable view as a subview which is centered manually, and manually set the frame to the visibleRect of the parent.
override visibleRect, call it a second time if its invalid, and debug to make sure it is valid!
zooming layered backed views sux. You could try using an NSTiledLayer, but I've tried and abandoned that solution multiple times.
Code below:
#interface FlippedParentView : NSView
#end
#implementation FlippedParentView
- (BOOL) isFlipped { return YES; }
#end
- (void)awakeFromNib
{
[self resetMouseInfo];
[[self window] setAcceptsMouseMovedEvents:YES];
needsFullRedraw = YES;
[self setAcceptsTouchEvents:YES];
// problem: when zoomed-in, CALayer backed NSOpenGLView becomes too large
// and hurts performance.
// solution: create a fullsizeView for the NSScrollView to resize,
// and make NSOpenGLView a subview. Keep NSOpenGLView size the same as visibleRect,
// positioning it as needed on the fullsizeView.
NSScrollView *scrollvw = [self enclosingScrollView];
[scrollvw setBackgroundColor:[NSColor darkStrokeColor]];
fullsizeView = [[FlippedParentView alloc] initWithFrame: [self frame]];
[scrollvw setDocumentView:fullsizeView];
[fullsizeView setAutoresizesSubviews:NO];
//printf("mask %d\n", [self autoresizingMask]);
[fullsizeView setAutoresizingMask: NSViewHeightSizable | NSViewWidthSizable | NSViewMinYMargin | NSViewMaxYMargin | NSViewMaxXMargin | NSViewMinXMargin];
[self setAutoresizingMask: NSViewNotSizable];
[fullsizeView addSubview:self];
}
- (NSRect) visibleRect
{
NSRect visRect = [super visibleRect];
if ( visRect.size.width == 0 )
{
visRect = [[self superview] visibleRect];
if ( visRect.size.width == 0 )
{
// this jacks up everything
DUMP( #"bad visibleRect" );
}
visRect.origin = NSZeroPoint;
}
return visRect;
}
- (void) _my_zoom: (double)newZoom
{
mouseFocusPt = [self focusPt];
NSRect oldVisRect = [[self superview] visibleRect];
if ( newZoom < 1.0 )
newZoom = 1.0;
if ( newZoom > kZoomFactorMax ) newZoom = kZoomFactorMax;
float xpct = (mouseFocusPt.x - oldVisRect.origin.x) /
( NSMaxX(oldVisRect) - oldVisRect.origin.x );
float ypct = (mouseFocusPt.y - oldVisRect.origin.y) /
( NSMaxY(oldVisRect) - oldVisRect.origin.y );
float oldZoom = zoomFactor;
zoomFactor = newZoom;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Stay locked on users' relative mouse location, so user can zoom in and back out without
// the view scrolling out from under the mouse location.
NSPoint newFocusPt = NSMakePoint (mouseFocusPt.x * newZoom/oldZoom,
mouseFocusPt.y * newZoom/oldZoom) ;
NSRect myFrame = fullsizeFrame; // [self frame];
float marginPercent = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
[self updateContext];
NSRect newVisRect;
newVisRect.size = [self visibleRect].size;
newVisRect.origin.x = (newFocusPt.x) - (xpct * newVisRect.size.width);
//DLog( #"xpct %0.2f, zoomFactor %0.2f, newVisRect.origin.x %0.2f", xpct, zoomFactor, newVisRect.origin.x);
myFrame = fullsizeFrame; // [self frame];
float marginPercent2 = (myFrame.size.height - drawableSizeWithMargins.height) / drawableSizeWithMargins.height;
float marginDiff = (marginPercent - marginPercent2) * drawableSizeWithMargins.height;
newVisRect.origin.y = (newFocusPt.y ) - (ypct * newVisRect.size.height) - marginDiff;
//DLog( #"ypct %0.2f, zoomFactor %0.2f, newVisRect.origin.y %0.2f", ypct, zoomFactor, newVisRect.origin.y);
//DLog( #"marginPercent %0.2f newVisRect %#", marginPercent, NSStringFromRect(newVisRect) );
if ( newVisRect.origin.x < 1 ) newVisRect.origin.x = 1;
if ( newVisRect.origin.y < 1 ) newVisRect.origin.y = 1;
// NSLog( #"zoom scrollRectToVisible %# bounds %#", NSStringFromRect(newVisRect), NSStringFromRect([[self superview] bounds]) );
// if ( iUseMousePt || isSlider )
[[self superview] scrollRectToVisible:newVisRect];
}
// - zoomFactor of 1.0 is defined as the zoomFactor needed to show entire selected context within visibleRect,
// including margins of 5% of the context size
// - zoomFactor > 1.0 will make pixels look bigger (view a subsection of a larger total drawableSize)
// - zoomFactor < 1.0 will make pixels look smaller (selectedContext size will be less than drawableSize)
-(void)updateContext
{
static BOOL sRecursing = NO;
if ( sRecursing ) return; // prevent recursion
sRecursing = YES;
//NSRect scrollRect = [[self superview] frame];
NSRect clipViewRect = [[[self enclosingScrollView] contentView] frame];
NSRect visRect = [[self superview] visibleRect]; // careful... visibleRect is sometimes NSZeroRect
float layoutWidth = clipViewRect.size.width;
float layoutHeight = clipViewRect.size.height;
marginPct = layoutHeight / (layoutHeight - (overlayViewMargin*2) );
// Satisfy the constraints fully-zoomed-out case:
// 1) the drawable rect is centered in the view with at margins.
// Allow for 5% margins (1.025 = 2.5% left, right, top, bottom)
// 2) guarantee the drawable rect does not overlap the mini-map in upper right corner.
NSRect baseRect = NSZeroRect;
baseRect.size = visRect.size;
NSRect drawableBaseRect = getCenteredRectFloat(baseRect, metaUnionRect.size );
//drawableSizeWithMargins = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor ) );
drawableSizeWithMargins = nsScaleSize( drawableBaseRect.size, zoomFactor );
// drawableSize will NOT include the margins. We loop until we've satisfied
// the constraints above.
drawableSize = drawableSizeWithMargins;
do
{
NSSize shrunkSize;
shrunkSize.width = layoutWidth / marginPct;
shrunkSize.height = layoutHeight / marginPct;
//drawableSize = nsIntegralSize( nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct ));
drawableSize = nsScaleSize( drawableBaseRect.size, zoomFactor / marginPct );
[self calculateMiniMapRect]; // get approx. size. Will calculate once more below.
NSRect shrunkRect = getCenteredRectNoScaling(baseRect, shrunkSize );
// DLog( #"rough miniMapRect %# shrunk %#", NSStringFromRect(miniMapRect), NSStringFromRect(shrunkRect));
// make sure minimap doesn't overlap drawable when you scroll to top-left
NSRect topMiniMapRect = miniMapRect;
topMiniMapRect.origin.x -= visRect.origin.x;
topMiniMapRect.origin.y = 0;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
float topMarginOffset = shrunkRect.size.height + (baseRect.size.height * 0.025);
shrunkRect.origin.y = NSMaxY(baseRect) - topMarginOffset;
if ( !NSIntersectsRect( topMiniMapRect, shrunkRect ) )
{
topMarginPercent = fabs(shrunkRect.origin.y - drawableBaseRect.origin.y) / baseRect.size.height;
break;
}
marginPct *= 1.025;
} while (1);
fullsizeFrame.origin = NSZeroPoint;
fullsizeFrame.size.width = fmax(drawableSizeWithMargins.width, layoutWidth);
fullsizeFrame.size.height = fmax(drawableSizeWithMargins.height, layoutHeight);
[fullsizeView setFrame:fullsizeFrame];
NSRect myNewFrame = [fullsizeView visibleRect];
if (myNewFrame.size.width > 0)
[self setFrame: myNewFrame]; //NSView
sRecursing = NO;
}
I am trying to set a circular avatar of a player of a game with a piechart representation on the avatar's circular border.
Player 1 -
Wins 25%
Lost 70%
Drawn 5%
cell.selectedPhoto.frame = CGRectMake(cell.selectedPhoto.frame.origin.x, cell.selectedPhoto.frame.origin.y, 75, 75);
cell.selectedPhoto.clipsToBounds = YES;
cell.selectedPhoto.layer.cornerRadius = 75/2.0f;
cell.selectedPhoto.layer.borderColor=[UIColor orangeColor].CGColor;
cell.selectedPhoto.layer.borderWidth=2.5f;
cell.selectedBadge.layer.cornerRadius = 15;
I have the UIImageView as a circle already with a single border colour.
At first guess perhaps I will need to clear the border of my UIImageView and have instead a UIView sitting behind my UIImageView that is a standard piechart, but is there a smarter way of doing this?
Thank you in advance.
I would recommend you create a custom UIView subclass for this, that manages various CALayer objects to create this effect. I was going to set about doing this in Core Graphics, but if you ever want to add some nice animations to this, you'll want to stick with Core Animation.
So let's first define our interface.
/// Provides a simple interface for creating an avatar icon, with a pie-chart style border.
#interface AvatarView : UIView
/// The avatar image, to be displayed in the center.
#property (nonatomic) UIImage* avatarImage;
/// An array of float values to define the values of each portion of the border.
#property (nonatomic) NSArray* borderValues;
/// An array of UIColors to define the colors of the border portions.
#property (nonatomic) NSArray* borderColors;
/// The width of the outer border.
#property (nonatomic) CGFloat borderWidth;
/// Animates the border values from their current values to a new set of values.
-(void) animateToBorderValues:(NSArray*)borderValues duration:(CGFloat)duration;
#end
Here we can set the avatar image, border width, and provide an array of colors and values. Next, lets work on implementing this. First we'll want to define some variables that we'll want to keep track of.
#implementation AvatarView {
CALayer* avatarImageLayer; // the avatar image layer
NSMutableArray* borderLayers; // the array containing the portion border layers
UIBezierPath* borderLayerPath; // the path used to stroke the border layers
CGFloat radius; // the radius of the view
}
Next, lets setup our avatarImageLayer, as well as a couple other variables in the initWithFrame method:
-(instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
radius = frame.size.width*0.5;
// create border layer array
borderLayers = [NSMutableArray array];
// create avatar image layer
avatarImageLayer = [CALayer layer];
avatarImageLayer.frame = frame;
avatarImageLayer.contentsScale = [UIScreen mainScreen].nativeScale; // scales the layer to the screen scale
[self.layer addSublayer:avatarImageLayer];
}
return self;
}
Next let's define our method that will populate the border layers when the borderValues property updates, allowing the view to have a dynamic number of border layers.
-(void) populateBorderLayers {
while (borderLayers.count > _borderValues.count) { // remove layers if the number of border layers got reduced
[(CAShapeLayer*)[borderLayers lastObject] removeFromSuperlayer];
[borderLayers removeLastObject];
}
NSUInteger colorCount = _borderColors.count;
NSUInteger borderLayerCount = borderLayers.count;
while (borderLayerCount < _borderValues.count) { // add layers if the number of border layers got increased
CAShapeLayer* borderLayer = [CAShapeLayer layer];
borderLayer.path = borderLayerPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = _borderWidth;
borderLayer.strokeColor = (borderLayerCount < colorCount)? ((UIColor*)_borderColors[borderLayerCount]).CGColor : [UIColor clearColor].CGColor;
if (borderLayerCount != 0) { // set pre-animation border stroke positions.
CAShapeLayer* previousLayer = borderLayers[borderLayerCount-1];
borderLayer.strokeStart = previousLayer.strokeEnd;
borderLayer.strokeEnd = previousLayer.strokeEnd;
} else borderLayer.strokeEnd = 0.0; // default value for first layer.
[self.layer insertSublayer:borderLayer atIndex:0]; // not strictly necessary, should work fine with `addSublayer`, but nice to have to ensure the layers don't unexpectedly overlap.
[borderLayers addObject:borderLayer];
borderLayerCount++;
}
}
Next, we want to make a method that can update the layer's stroke start and end values when borderValues gets updated. This could be merged into previous method, but if you want to setup animation you'll want to keep it separate.
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
Next, we just need to override the setters in order to update certain aspects of the border and avatar image when the values change:
-(void) setAvatarImage:(UIImage *)avatarImage {
_avatarImage = avatarImage;
avatarImageLayer.contents = (id)avatarImage.CGImage; // update contents if image changed
}
-(void) setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
CGFloat halfBorderWidth = borderWidth*0.5; // we're gonna use this a bunch, so might as well pre-calculate
// set the new border layer path
borderLayerPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius, radius} radius:radius-halfBorderWidth startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
for (CAShapeLayer* s in borderLayers) { // apply the new border layer path
s.path = borderLayerPath.CGPath;
s.lineWidth = borderWidth;
}
// update avatar masking
CAShapeLayer* s = [CAShapeLayer layer];
avatarImageLayer.frame = CGRectMake(halfBorderWidth, halfBorderWidth, self.frame.size.width-borderWidth, self.frame.size.height-borderWidth); // update avatar image frame
s.path = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius-halfBorderWidth, radius-halfBorderWidth} radius:radius-borderWidth startAngle:0 endAngle:M_PI*2.0 clockwise:YES].CGPath;
avatarImageLayer.mask = s;
}
-(void) setBorderColors:(NSArray *)borderColors {
_borderColors = borderColors;
NSUInteger i = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeColor = ((UIColor*)borderColors[i]).CGColor;
i++;
}
}
-(void) setBorderValues:(NSArray *)borderValues {
_borderValues = borderValues;
[self populateBorderLayers];
[self updateBorderStrokeValues];
}
Finally, we can even take one step further by animating the layers! Let's just add a single of method that can handle this for us.
-(void) animateToBorderValues:(NSArray *)borderValues duration:(CGFloat)duration {
_borderValues = borderValues; // update border values
[self populateBorderLayers]; // do a 'soft' layer update, making sure that the correct number of layers are generated pre-animation. Pre-sets stroke positions to a pre-animation state.
// define stroke animation
CABasicAnimation* strokeAnim = [CABasicAnimation animation];
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokeAnim.duration = duration;
CGFloat cumulativeValue = 0;
for (int i = 0; i < borderLayers.count; i++) {
cumulativeValue += [borderValues[i] floatValue];
CAShapeLayer* s = borderLayers[i];
if (i != 0) [s addAnimation:strokeAnim forKey:#"startStrokeAnim"];
// define stroke end animation
strokeAnim.keyPath = #"strokeEnd";
strokeAnim.fromValue = #(s.strokeEnd);
strokeAnim.toValue = #(cumulativeValue);
[s addAnimation:strokeAnim forKey:#"endStrokeAnim"];
strokeAnim.keyPath = #"strokeStart"; // re-use the previous animation, as the values are the same (in the next iteration).
}
// update presentation layer values
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self updateBorderStrokeValues]; // sets stroke positions.
[CATransaction commit];
}
And that's it! Here's an example of the usage:
AvatarView* v = [[AvatarView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
v.avatarImage = [UIImage imageNamed:#"photo.png"];
v.borderWidth = 10;
v.borderColors = #[[UIColor colorWithRed:122.0/255.0 green:108.0/255.0 blue:255.0/255.0 alpha:1],
[UIColor colorWithRed:100.0/255.0 green:241.0/255.0 blue:183.0/255.0 alpha:1],
[UIColor colorWithRed:0 green:222.0/255.0 blue:255.0/255.0 alpha:1]];
// because the border values default to 0, you can add this without even setting the border values initially!
[v animateToBorderValues:#[#(0.4), #(0.35), #(0.25)] duration:2];
Results
Full project: https://github.com/hamishknight/Pie-Chart-Avatar
Actually you can directly create your own layer from CALayer. here is a sample Animation layer from my own project.
AnimationLayer.h
#import <QuartzCore/QuartzCore.h>
#interface AnimationLayer : CALayer
#property (nonatomic,assign ) float percent;
#property (nonatomic, strong) NSArray *percentValues;
#property (nonatomic, strong) NSArray *percentColours;
#end
percentValues are your values for which part is gotten.
it should be #[#(35),#(75),#(100)] for win ratio:%35, loose:%40 and draw:%25.
percentColors are UIColor objects for win, loose and draw.
in `AnimationLayer.m`
#import "AnimationLayer.h"
#import <UIKit/UIKit.h>
#implementation AnimationLayer
#dynamic percent,percentValues,percentColours;
+ (BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:#"percent"]){
return YES;
}else
return [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)ctx
{
CGFloat arcStep = (M_PI *2) / 100 * (1.0-self.percent); // M_PI*2 is equivalent of full cirle
BOOL clockwise = NO;
CGFloat x = CGRectGetWidth(self.bounds) / 2; // circle's center
CGFloat y = CGRectGetHeight(self.bounds) / 2; // circle's center
CGFloat radius = MIN(x, y);
UIGraphicsPushContext(ctx);
// draw colorful circle
CGContextSetLineWidth(ctx, 12);//12 is the width of circle.
CGFloat toDraw = (1-self.percent)*100.0f;
for (CGFloat i = 0; i < toDraw; i++)
{
UIColor *c;
for (int j = 0; j<[self.percentValues count]; j++)
{
if (i <= [self.percentValues[j] intValue]) {
c = self.percentColours[j];
break;
}
}
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep+0.02;
CGContextAddArc(ctx, x, y, radius-6, startAngle, endAngle, clockwise);//set the radius as radius-(half of your line width.)
CGContextStrokePath(ctx);
}
UIGraphicsPopContext();
}
#end
and in some place where you will use this effect, you should call this like
+(void)addAnimationLayerToView:(UIView *)imageOfPlayer withColors:(NSArray *)colors andValues:(NSArray *)values
{
AnimationLayer *animLayer = [AnimationLayer layer];
animLayer.frame = imageOfPlayer.bounds;
animLayer.percentColours = colors;
animLayer.percentValues = values;
[imageOfPlayer.layer insertSublayer:animLayer atIndex:0];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percent"];
[animation setFromValue:#1];
[animation setToValue:#0];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setDuration:6];
[animLayer addAnimation:animation forKey:#"imageAnimation"];
}
This has been driving me crazy.. I have a large image, and need to have a view that is both zoomable, and scrollable (ideally it should also be able to rotate, but I've given up on that part). Since the image is very large, I plan on using CATiledLayer, but I simply can't get it to work.
My requirements are:
I need to be able to zoom (on mouse center) and pan
The image should not change its width:height ratio (shouldn't resize, only zoom).
This should run on Mac OS 10.9 (NOT iOS!)
Memory use shouldn't be huge (although up to like 100 MB should be ok).
I have the necessary image both complete in one file, and also tiled into many (even have it for different zoom levels). I prefer using the tiles, as that should be easier on memory, but both options are available.
Most of the examples online refer to iOS, and thus use UIScrollView for the zoom/pan, but I can't get to copy that behaviour for NSScrollView. The only example for Mac OS X I found is this, but his zoom always goes to the lower left corner, not the middle, and when I adapt the code to use png files instead of pdf, the memory use gets around 400 MB...
This is my best try so far:
#implementation MyView{
CATiledLayer *tiledLayer;
}
-(void)awakeFromNib{
NSLog(#"Es geht los");
tiledLayer = [CATiledLayer layer];
// set up this view & its layer
self.wantsLayer = YES;
self.layer = [CALayer layer];
self.layer.masksToBounds = YES;
self.layer.backgroundColor = CGColorGetConstantColor(kCGColorWhite);
// set up the tiled layer
tiledLayer.delegate = self;
tiledLayer.levelsOfDetail = 4;
tiledLayer.levelsOfDetailBias = 5;
tiledLayer.anchorPoint = CGPointZero;
tiledLayer.bounds = CGRectMake(0.0f, 0.0f, 41*256, 22*256);
tiledLayer.autoresizingMask = kCALayerNotSizable;
tiledLayer.tileSize = CGSizeMake(256, 256);
self.frame = CGRectMake(0.0f, 0.0f, 41*256, 22*256);
self.layer = tiledLayer;
//[self.layer addSublayer:tiledLayer];
[tiledLayer setNeedsDisplay];
}
-(void)drawRect:(NSRect)dirtyRect{
CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
CGFloat scale = CGContextGetCTM(context).a;
CGSize tileSize = tiledLayer.tileSize;
tileSize.width /= scale;
tileSize.height /= scale;
// calculate the rows and columns of tiles that intersect the rect we have been asked to draw
int firstCol = floorf(CGRectGetMinX(dirtyRect) / tileSize.width);
int lastCol = floorf((CGRectGetMaxX(dirtyRect)-1) / tileSize.width);
int firstRow = floorf(CGRectGetMinY(dirtyRect) / tileSize.height);
int lastRow = floorf((CGRectGetMaxY(dirtyRect)-1) / tileSize.height);
for (int row = firstRow; row <= lastRow; row++) {
for (int col = firstCol; col <= lastCol; col++) {
NSImage *tile = [self tileForScale:scale row:row col:col];
CGRect tileRect = CGRectMake(tileSize.width * col, tileSize.height * row,
tileSize.width, tileSize.height);
// if the tile would stick outside of our bounds, we need to truncate it so as
// to avoid stretching out the partial tiles at the right and bottom edges
tileRect = CGRectIntersection(self.bounds, tileRect);
[tile drawInRect:tileRect];
}
}
}
-(BOOL)isFlipped{
return YES;
}
But this deforms the image, and doesn't zoom or pan correctly (but at least the tile selection works)...
I can't believe this is so hard, any help would be greatly appreciated. Thanks :)
After a lot of research and tries, I finally managed to get this to work using this example. Decided to post it for future reference. Open the ZIP > CoreAnimationLayers> TiledLayers, there's a good example there. That's how CATiledLayer works with OS X, and since the example there doesn't handle zoom very well, I leave here my zoom code
-(void)magnifyWithEvent:(NSEvent *)event{
[super magnifyWithEvent:event];
if (!isZooming) {
isZooming = YES;
BOOL zoomOut = (event.magnification > 0) ? NO : YES;
if (zoomOut) {
[self zoomOutFromPoint:event.locationInWindow];
} else {
[self zoomInFromPoint:event.locationInWindow];;
}
}
}
-(void)zoomInFromPoint:(CGPoint)mouseLocationInWindow{
if(zoomLevel < pow(2, tiledLayer.levelsOfDetailBias)) {
zoomLevel *= 2.0f;
tiledLayer.transform = CATransform3DMakeScale(zoomLevel, zoomLevel, 1.0f);
tiledLayer.position = CGPointMake((tiledLayer.position.x*2) - mouseLocationInWindow.x, (tiledLayer.position.y*2) - mouseLocationInWindow.y);
}
}
-(void)zoomOutFromPoint:(CGPoint)mouseLocationInWindow{
NSInteger power = tiledLayer.levelsOfDetail - tiledLayer.levelsOfDetailBias;
if(zoomLevel > pow(2, -power)) {
zoomLevel *= 0.5f;
tiledLayer.transform = CATransform3DMakeScale(zoomLevel, zoomLevel, 1.0f);
tiledLayer.position = CGPointMake((tiledLayer.position.x + mouseLocationInWindow.x)/2, (tiledLayer.position.y + mouseLocationInWindow.y)/2);
}
}
Pretty new to cocoa development and really stuck with probably a fundamental problem.
So in short, my app UI looks like a simple window with a nsslider at the bottom. What I need is to generate N images and place them, onto N nsviews in my app window.
What it does so far:
I'm clicking on the slider (holding it) and dragging it. While I'm dragging it nothing happens to my views (pictures are not generated). When I release the slider the pictures got generated and my view get filled with them.
What I want:
- I need the views to be filled with pictures as I'm moving the slider.
I figured out the little check box on the NSSlider properties, which is continuous, and I'm using it, but my image generator still doesn't do anything until I release the slider.
Here is my code:
// slider move action
- (IBAction)sliderMove:(id)sender
{
[self generateProcess:[_slider floatValue];
}
// generation process
- (void) generateProcess:(Float64) startPoint
{
// create an array of times for frames to display
NSMutableArray *stops = [[NSMutableArray alloc] init];
for (int j = 0; j < _numOfFramesToDisplay; j++)
{
CMTime time = CMTimeMakeWithSeconds(startPoint, 60000);
[stops addObject:[NSValue valueWithCMTime:time]];
_currentPosition = initialTime; // set the current position to the last frame displayed
startPoint+=0.04; // the step between frames is 0.04sec
}
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 500, 100, 100);
NSImageView *iView = [[NSImageView alloc] initWithFrame:rect];
[iView setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[iView setImage:myImage];
[self.windowForSheet.contentView addSubview: iView];
[_viewsToRemove addObject:iView];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
}
}
If you have any thoughts or ideas, please share with me, I will really appreciate it!
Thank you
In order to make your NSSlider continuous, open your window controller's XIB file in Interface Builder and click on the NSSlider. Then, open the Utilities area
select the Attributes Inspector
and check the "Continuous" checkbox
under the Control header. Once you've done this, your IBAction sliderMove: will be called as the slider is moved rather than once the mouse is released.
Note: Alternatively, with an
NSSlider *slider = //...
one can simply call
[slider setContinuous:YES];
I am working on a game and I would like to add a proper slicing feature in it.. so when a sprite sliced, 2 new sprites should be created.. please check here
At the moment, I am just reducing the size and duplicating the sprites.. Something like this.. Thanks in advance..
- (BOOL) sliceSprite: (Sprite *) sprite withPath: (UIBezierPath *) slicePath
{
CGSize size = sprite.size;
size.width /= 2;
size.height /=2;
sprite.size = size;
sprite.sliced = YES;
Sprite *newSprite = [[Sprite alloc] initWithImage: sprite.image];
newSprite.position = sprite.position;
newSprite.size = size;
newSprite.sliced = YES;
newSprite.inView = YES;
newSprite.xVelocity = SLICE_SPEEDUP * sprite.yVelocity;
newSprite.yVelocity = SLICE_SPEEDUP * sprite.xVelocity;
newSprite.angularVelocity = -SLICE_REVUP * sprite.angularVelocity;
[sprites addObject: newSprite];
[newSprite release];
sprite.angularVelocity = SLICE_REVUP * sprite.angularVelocity;
sprite.xVelocity = -SLICE_SPEEDUP * sprite.xVelocity;
sprite.yVelocity = -SLICE_SPEEDUP * sprite.yVelocity;
return YES;
}
- (void) sliceSpritesInSwipePath
{
CGRect swipeRect = [swipePath bounds];
for (NSUInteger i = 0; i < [sprites count]; i++)
{
Sprite *sprite = [sprites objectAtIndex: i];
if ([sprite intersectsWithPathInArray: swipePoints
inRect: swipeRect])
if ([self sliceSprite: sprite withPath: swipePath])
{
[self resetSwipe];
if (![sliceSound isPlaying])
[sliceSound play];
break;
}
}
}
Is the specific line of splitting required? Fruit Ninja just spawns two halves of the fruit, as if it was split down the middle, this would be quite easy to do:
Create two sprites which are half the width of the original sprite
Position them 1/4 and 3/4 of the way along the original sprite's horizontal centre line
Add rotation/acceleration etc.
Modify texture coordinates so that the left sprite has the left half of the texture and the right sprite has the right half of the texture.
Since you're using CoreGraphics here, why not simply use a clipping path when drawing the sprite(s)?
Duplicate the sprite to be sliced, then apply simple polygons masking the two halves as their respective clipping paths. The function you need is called CGContextClip and a short tutorial can be found here.
Edit: The tutorial lists this example:
CGContextBeginPath (context);
CGContextAddArc (context, w/2, h/2, ((w>h) ? h : w)/2, 0, 2*PI, 0);
CGContextClosePath (context);
CGContextClip (context);
This sets the current path to a circle, then applies the current path as the clipping region.