iOS CAEmitterCell firework effect - ios

I'm wanna create an effect like firework. I know that I can use for it CAEmitterCell. I tried to download few examples, but I still in progress with this feature in iOS 5.0
Anybody know good tutorial how to create firework effect using CAEmitterCell?
I need to create something like below:
All particles are going from the center (red circle) in direction to green circles. Red circle this is initial point that will have some CGPoint value.

I have found this solution:
it's how DWFParticleView.h file looks
#import <UIKit/UIKit.h>
#interface DWFParticleView : UIView
#property (nonatomic) CGFloat time;
-(void)configurateEmmiter;
-(void)setEmitterPositionFromTouch: (UITouch*)t;
-(void)setIsEmitting:(BOOL)isEmitting andPoint:(CGPoint)point;
#end
this is how looks DWFParticleView.m
#import "DWFParticleView.h"
#import <QuartzCore/QuartzCore.h>
#implementation DWFParticleView
{
CAEmitterLayer* fireEmitter; //1
}
-(void)configurateEmmiter
{
//set ref to the layer
fireEmitter = (CAEmitterLayer*)self.layer; //2
//configure the emitter layer
fireEmitter.emitterPosition = CGPointMake(50, 50);
fireEmitter.emitterSize = CGSizeMake(5, 5);
CAEmitterCell* fire = [CAEmitterCell emitterCell];
fire.birthRate = 0;
fire.lifetime = 2.0;
fire.lifetimeRange = 0.5;
//fire.color = [[UIColor colorWithRed:0.8 green:0.4 blue:0.2 alpha:0.6] CGColor];
fire.contents = (id)[[UIImage imageNamed:#"star_icon.png"] CGImage];
[fire setName:#"fire"];
fire.velocity = 80;
fire.velocityRange = 20;
fire.emissionRange = M_PI * 2.0f;
fire.scaleSpeed = 0.1;
fire.spin = 0.5;
//add the cell to the layer and we're done
fireEmitter.emitterCells = [NSArray arrayWithObject:fire];
fireEmitter.renderMode = kCAEmitterLayerAdditive;
}
+ (Class) layerClass {
return [CAEmitterLayer class];
}
-(void)setEmitterPositionFromTouch: (UITouch*)t
{
//change the emitter's position
fireEmitter.emitterPosition = [t locationInView:self];
}
-(void)setIsEmitting:(BOOL)isEmitting andPoint:(CGPoint)point
{
fireEmitter.emitterPosition = point;
//turn on/off the emitting of particles
[fireEmitter setValue:[NSNumber numberWithInt:isEmitting?50:0]
forKeyPath:#"emitterCells.fire.birthRate"];
[self performSelector:#selector(decayStep) withObject:nil afterDelay:self.time];
}
- (void)decayStep {
[fireEmitter setValue:[NSNumber numberWithInt:0]
forKeyPath:#"emitterCells.fire.birthRate"];
}
#end
this is method one tap on screen for example that shows our effect
- (void)tap {
DWFParticleView *fireView = [[DWFParticleView alloc] init];
fireView.time = 0.3;
[fireView setUserInteractionEnabled:NO];
[fireView configurateEmmiter];
[fireView setFrame:CGRectMake(0, 0, 0, 0)];
[fireView setBackgroundColor:[UIColor redColor]];
[self.view addSubview:fireView];
}
the material I have found here: ray

Related

How do I add both shadow and rounded corners to a UIVisualEffectView?

I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.
It works, but not flawlessly. The shadow darkens the effect of the blur. Is there a way to perfect it?
.h
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
.m
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.25;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// Random container frame for testing...
self.containerView.frame =
CGRectMake(20.0,
200.0,
380,
480);
self.containerShadowView.frame = self.containerView.frame;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:self.containerShadowView.bounds cornerRadius:self.containerView.layer.cornerRadius] CGPath];
}
You can do this by masking your containerShadowView with a "cutout" that matches the containerView effect view.
So, this is how it looks - I centered the 380x480 view, and used 0.9 for the .shadowOpacity to emphasize the differences.
Your original on the left, masked version on the right:
Kinda difficult to tell what's really going on, since that could be an opaque layer, we'll add a label behind it:
and, to clarify what we're doing, let's look at it with the containerView effect view hidden:
Here's the source code I used for that - each tap anywhere will cycle through the 8 different layouts:
#import <UIKit/UIKit.h>
#interface OrigShadowView : UIView
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
#end
#implementation OrigShadowView
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.9;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// let's center a 380 x 480 rectangle in self
CGFloat w = 380.0;
CGFloat h = 480.0;
CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
self.containerView.frame = vRect;
self.containerShadowView.frame = self.containerView.frame;
// change origin to 0,0 because the following will be relative to the subviews
vRect.origin = CGPointZero;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
}
#end
#interface MaskShadowView : UIView
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
#end
#implementation MaskShadowView
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.9;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// let's center a 380 x 480 rectangle in self
CGFloat w = 380.0;
CGFloat h = 480.0;
CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
self.containerView.frame = vRect;
self.containerShadowView.frame = self.containerView.frame;
// change origin to 0,0 because the following will be relative to the subviews
vRect.origin = CGPointZero;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
UIBezierPath *bigBez;
UIBezierPath *clipBez;
// we need a rectangle that will encompass the shadow radius
// double the shadowRadius is probably sufficient, but since it won't be seen
// and won't affect anything else, we'll make it 4x
CGRect expandedRect = CGRectInset(vRect, -self.containerShadowView.layer.shadowRadius * 4.0, -self.containerShadowView.layer.shadowRadius * 4.0);
bigBez = [UIBezierPath bezierPathWithRect:expandedRect];
// we want to "clip out" a rounded rect in the center
// which will be the same size as the visual effect view
clipBez = [UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius];
[bigBez appendPath:clipBez];
bigBez.usesEvenOddFillRule = YES;
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = UIColor.whiteColor.CGColor;
maskLayer.path = bigBez.CGPath;
self.containerShadowView.layer.mask = maskLayer;
}
#end
#interface BlurTestViewController : UIViewController
{
OrigShadowView *origView;
MaskShadowView *newView;
UILabel *bkgLabel;
// so we can step through on taps to see the results
NSInteger step;
UILabel *infoLabel;
}
#end
#implementation BlurTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
bkgLabel = [UILabel new];
bkgLabel.textColor = UIColor.blueColor;
bkgLabel.font = [UIFont systemFontOfSize:48.0 weight:UIFontWeightBlack];
bkgLabel.textAlignment = NSTextAlignmentCenter;
bkgLabel.numberOfLines = 0;
bkgLabel.text = #"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.";
bkgLabel.text = #"I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.";
origView = [OrigShadowView new];
newView = [MaskShadowView new];
[self.view addSubview:bkgLabel];
[self.view addSubview:origView];
[self.view addSubview:newView];
infoLabel = [UILabel new];
infoLabel.font = [UIFont systemFontOfSize:20.0 weight:UIFontWeightBold];
infoLabel.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:infoLabel];
step = 0;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// let's inset the "shadow blur" views 40-points
CGRect r = CGRectInset(self.view.frame, 40.0, 40.0);
origView.frame = r;
newView.frame = r;
// let's put the background label midway down the screen
r.origin.y += r.size.height * 0.5;
r.size.height *= 0.5;
bkgLabel.frame = r;
// put the info label near the top
infoLabel.frame = CGRectMake(40.0, 80.0, r.size.width, 40.0);
[self nextStep];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self nextStep];
}
- (void)nextStep {
bkgLabel.hidden = YES;
origView.hidden = YES;
newView.hidden = YES;
origView.containerView.hidden = NO;
newView.containerView.hidden = NO;
step++;
switch (step) {
case 1:
origView.hidden = NO;
infoLabel.text = #"1: Original View";
break;
case 2:
newView.hidden = NO;
infoLabel.text = #"2: Masked View";
break;
case 3:
bkgLabel.hidden = NO;
origView.hidden = NO;
infoLabel.text = #"3: Original View";
break;
case 4:
bkgLabel.hidden = NO;
newView.hidden = NO;
infoLabel.text = #"4: Masked View";
break;
case 5:
origView.hidden = NO;
origView.containerView.hidden = YES;
infoLabel.text = #"5: Original View - effect view hidden";
break;
case 6:
newView.hidden = NO;
newView.containerView.hidden = YES;
infoLabel.text = #"6: Masked View - effect view hidden";
break;
case 7:
bkgLabel.hidden = NO;
origView.hidden = NO;
origView.containerView.hidden = YES;
infoLabel.text = #"7: Original View - effect view hidden";
break;
default:
bkgLabel.hidden = NO;
newView.hidden = NO;
newView.containerView.hidden = YES;
infoLabel.text = #"8: Masked View - effect view hidden";
step = 0;
break;
}
}
#end

Zoom UIView inside UIScrollView with drawing

I have a scroll view (gray) with a zooming view inside (orange). The problem is if I zoom this view the red shape drawn on it gets zoomed too including lines width and blue squares size. What I want is to keep constant lines width and blue squares size (like on first picture) scaling just the area of the shape itself according to zoom level (drawn text is just for reference, I don't care about its size)
before zoom
after zoom
view controller
#import "ViewController.h"
#import "ZoomingView.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
#end
#implementation ViewController
{
ZoomingView *_zoomingView;
}
- (void)viewDidLayoutSubviews
{
[self setup];
}
- (void)setup
{
CGFloat kViewSize = self.scrollView.frame.size.width - 40;
self.scrollView.minimumZoomScale = 1;
self.scrollView.maximumZoomScale = 10;
self.scrollView.delegate = self;
self.scrollView.contentSize = self.scrollView.bounds.size;
_zoomingView = [[ZoomingView alloc] initWithFrame:
CGRectMake((self.scrollView.frame.size.width - kViewSize) / 2,
(self.scrollView.frame.size.height - kViewSize) / 2,
kViewSize,
kViewSize)];
[self.scrollView addSubview:_zoomingView];
}
#pragma mark - UIScrollViewDelegate
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return _zoomingView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
// zooming view position fix
UIView *zoomView = [scrollView.delegate viewForZoomingInScrollView:scrollView];
CGRect zvf = zoomView.frame;
if (zvf.size.width < scrollView.bounds.size.width) {
zvf.origin.x = (scrollView.bounds.size.width - zvf.size.width) / 2.0f;
} else {
zvf.origin.x = 0.0;
}
if (zvf.size.height < scrollView.bounds.size.height) {
zvf.origin.y = (scrollView.bounds.size.height - zvf.size.height) / 2.0f;
} else {
zvf.origin.y = 0.0;
}
zoomView.frame = zvf;
[_zoomingView updateWithZoomScale:scrollView.zoomScale];
}
#end
zooming view
#import "ZoomingView.h"
#implementation ZoomingView
{
CGFloat _zoomScale;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)setup
{
self.backgroundColor = [UIColor orangeColor];
_zoomScale = 1;
}
- (void)drawRect:(CGRect)rect
{
const CGFloat kPointSize = 10;
NSArray *points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1);
// points
[[UIColor blueColor] setStroke];
for (NSValue *value in points) {
CGPoint point = [value CGPointValue];
CGContextStrokeRect(context, CGRectMake(point.x - kPointSize / 2,
point.y - kPointSize / 2,
kPointSize,
kPointSize));
}
// lines
[[UIColor redColor] setStroke];
for (NSUInteger i = 0; i < points.count; i++) {
CGPoint point = [points[i] CGPointValue];
if (i == 0) {
CGContextMoveToPoint(context, point.x, point.y);
} else {
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextClosePath(context);
CGContextStrokePath(context);
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
[self setNeedsDisplay];
}
#end
EDIT
Based on proposed solution (which works for sure) I was interested if I could make it work using my drawRect routine and Core Graphics methods.
So I changed my code this way, applying proposed scaling and contentsScale approach from this answer. As a result, without contentsScale drawing looks very blurry and with it much better, but a light blurriness persists anyway.
So the approach with layers gives the best result, although I don't get why.
- (void)drawRect:(CGRect)rect
{
const CGFloat kPointSize = 10;
NSArray *points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
CGFloat scaledPointSize = kPointSize * (1.0 / _zoomScale);
CGFloat lineWidth = 1.0 / _zoomScale;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
// points
[[UIColor blueColor] setStroke];
for (NSValue *value in points) {
CGPoint point = [value CGPointValue];
CGContextStrokeRect(context, CGRectMake(point.x - scaledPointSize / 2,
point.y - scaledPointSize / 2,
scaledPointSize,
scaledPointSize));
}
// lines
[[UIColor redColor] setStroke];
for (NSUInteger i = 0; i < points.count; i++) {
CGPoint point = [points[i] CGPointValue];
if (i == 0) {
CGContextMoveToPoint(context, point.x, point.y);
} else {
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextClosePath(context);
CGContextStrokePath(context);
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
[self setNeedsDisplay];
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithBool:YES]
forKey:kCATransactionDisableActions];
self.layer.contentsScale = zoomScale;
[CATransaction commit];
}
You may be better off putting your boxes and line-shape on CAShapeLayers, where you can update the line-width based on the zoom scale.
You only need to create and define your line-shape once. For your boxes, though, you'll need to re-create the path when you change the zoom (to keep the width/height of the boxes at a constant non-zoomed point size.
Give this a try. You should be able to simply replace your current ZoomingView.m class - no changes to the view controller necessary.
//
// ZoomingView.m
//
// modified by Don Mag
//
#import "ZoomingView.h"
#interface ZoomingView()
#property (strong, nonatomic) CAShapeLayer *shapeLayer;
#property (strong, nonatomic) CAShapeLayer *boxesLayer;
#property (strong, nonatomic) NSArray *points;
#end
#implementation ZoomingView
{
CGFloat _zoomScale;
CGFloat _kPointSize;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)setup
{
self.backgroundColor = [UIColor orangeColor];
_points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
_zoomScale = 1;
_kPointSize = 10.0;
// create and setup boxes layer
_boxesLayer = [CAShapeLayer new];
[self.layer addSublayer:_boxesLayer];
_boxesLayer.strokeColor = [UIColor redColor].CGColor;
_boxesLayer.fillColor = [UIColor clearColor].CGColor;
_boxesLayer.lineWidth = 1.0;
_boxesLayer.frame = self.bounds;
// create and setup shape layer
_shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:_shapeLayer];
_shapeLayer.strokeColor = [UIColor greenColor].CGColor;
_shapeLayer.fillColor = [UIColor clearColor].CGColor;
_shapeLayer.lineWidth = 1.0;
_shapeLayer.frame = self.bounds;
// new path for shape
UIBezierPath *thePath = [UIBezierPath new];
for (NSValue *value in _points) {
CGPoint point = [value CGPointValue];
if ([value isEqualToValue:_points.firstObject]) {
[thePath moveToPoint:point];
} else {
[thePath addLineToPoint:point];
}
}
[thePath closePath];
[_shapeLayer setPath:thePath.CGPath];
// trigger the boxes update
[self updateWithZoomScale:_zoomScale];
}
- (void)drawRect:(CGRect)rect
{
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
CGFloat scaledPointSize = _kPointSize * (1.0 / zoomScale);
// create a path for the boxes
// needs to be done here, because the width/height of the boxes
// must change with the scale
UIBezierPath *thePath = [UIBezierPath new];
for (NSValue *value in _points) {
CGPoint point = [value CGPointValue];
CGRect r = CGRectMake(point.x - scaledPointSize / 2.0,
point.y - scaledPointSize / 2.0,
scaledPointSize,
scaledPointSize);
[thePath appendPath:[UIBezierPath bezierPathWithRect:r]];
}
[_boxesLayer setPath:thePath.CGPath];
_boxesLayer.lineWidth = 1.0 / zoomScale;
_shapeLayer.lineWidth = 1.0 / zoomScale;
[self setNeedsDisplay];
}
#end
Results:
Note: Should go without saying, but... This is intended to be a starting point for you to work with, not "production code."

should this CustomView be a category or a subclass?

I want to extend an existing custom UIViewclass so it can be used in several places within an app. The view has a method that arranges multiple UIButtonsin a circle. The number of UIButtons will vary depending on where the method is called. So too will the size of the UIButton and the radius of the circle. It would also be useful (but not essential) to be able to call the method several times within the same UIView.
Which is the better way ? to make this a category or a sub-class? It would appear I could use either based on this discussion of pros and cons. But my question is more specific.
I’ve made categories for UIColor and UIFontbut so far I have not been able to make this method work as a category just by following Apple's documentation (I tried it both as a class method and as an instance method). Before I try to make it work as a subclass, can someone who has done it before, either way, please recommend the better approach based on my example below.
Here is the method as it was in the CustomView
circleOfButtons
- (void)circleOfButtons {
screenCentre.x = CGRectGetWidth (self.bounds) / 2;
screenCentre.y = CGRectGetHeight (self.bounds) / 2;
for (int i = 1; i <= buttonCount; i++) {
radians = 2 * M_PI * i / buttonCount;
CGFloat arcStartPoint = - M_PI / 2; // first point clockwise after 12 o'clock
buttonCentre.x = screenCentre.x + radius * cos(radians + arcStartPoint);
buttonCentre.y = screenCentre.y + radius * sin(radians + arcStartPoint);
CGPoint target = CGPointMake(buttonCentre.x, buttonCentre.y);
CGFloat x = screenCentre.x - buttonSize / 2;
CGFloat y = screenCentre.y - buttonSize / 2;
CGFloat wide = buttonSize;
CGFloat high = buttonSize;
UIButton *circleButton = [[UIButton alloc] initWithFrame:CGRectMake(x, y, wide, high)];
[circleButton setTag:i];
circleButton.clipsToBounds = YES;
circleButton.layer.masksToBounds = NO;
circleButton.layer.borderWidth = 0.25f;
circleButton.layer.cornerRadius = buttonSize/2;
circleButton.layer.borderColor = [UIColor blackColor].CGColor;
circleButton.backgroundColor = UIColor.whiteColor;
[circleButton setTitle:[NSString stringWithFormat:#"%i", i] forState:UIControlStateNormal];
[self addSubview:circleButton];
// animation 1
[UIView animateWithDuration:0.5 animations:^{
circleButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
circleButton.center = screenCentre;
}
completion:^(BOOL finished){}];
// animation 2
[UIView animateWithDuration:1.0f animations:^{
circleButton.transform = CGAffineTransformIdentity;
circleButton.center = target;
}
completion:^(BOOL finished){}];
}
and here is the CustomView
CustomView.m
#import <UIKit/UIKit.h>
#import "CustomView.h"
#implementation CustomView : UIView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:[UIScreen mainScreen].bounds];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = true;
self.backgroundColor = [UIColor lightGrayColor];
buttonCount = 5; //16;
buttonSize = 80; //41;
radius = 68; //105;
[self circleOfButtons];
}
return self;
}
CustomView.h
#import <UIKit/UIKit.h>
#interface CustomView : UIView {
CGPoint screenCentre, buttonCentre;
float radius, radians, buttonSize;
int buttonCount;
}
ViewController.m
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGRect rect = [UIScreen mainScreen].bounds;
float statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
CGRect screenFrame = CGRectMake(0, statusBarHeight, rect.size.width, rect.size.height - statusBarHeight);
self.view = [[UIView alloc] initWithFrame: screenFrame];
CustomView *cv = [[CustomView alloc]initWithFrame:screenFrame];
[self.view addSubview:cv];
}

Creating a mask with edge detection by selecting part of an image

The story is that, the user selects a part of an image by drawing lines or curves. Based on the selection, a mask will be created using an edge detector.
How can i achieve this?
Are CoreImage and GPUImage could help to make this possible?
Is edge detection enough to make a mask?
I don't have any advanced knowledge on image processing and manipulation. I am just a starter trying to understand and learn it. By the way, this should be done in iOS.
Just like this one (the masking part).
I wrote a sample for you to show you how to use mask to select part of image, if you want to implement more complex effect, you just need to change the path, then you can get any shape you want.
#interface MainViewController ()
#property UIImageView* imageV;
#property UIView* selectView;
#property UIView *blockView;
#property CGPoint startPoint;
#property CGPoint endPoint;
#end
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIBarButtonItem *recoverItem = [[UIBarButtonItem alloc] initWithTitle:#"Recover" style:UIBarButtonItemStyleDone target:self action:#selector(recover)];
[self.navigationItem setLeftBarButtonItem:recoverItem];
_imageV = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
_imageV.image = [UIImage imageNamed:#"test"];
[self.view addSubview:_imageV];
_blockView = [[UIView alloc] init];
_blockView.frame = _imageV.bounds;
_blockView.backgroundColor = [UIColor whiteColor];
[_imageV addSubview:_blockView];
_blockView.hidden = true;
_selectView = [[UIView alloc] init];
_selectView.layer.borderColor = [UIColor greenColor].CGColor;
_selectView.layer.borderWidth = 2;
[_imageV addSubview:_selectView];
_selectView.hidden = true;
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.view addGestureRecognizer:panGR];
}
- (void) handlePan: (UIPanGestureRecognizer *)pan{
if (UIGestureRecognizerStateBegan == pan.state) {
_startPoint = [pan locationInView:_imageV];
}
else if (UIGestureRecognizerStateChanged == pan.state) {
_selectView.hidden = false;
CGPoint currentP = [pan locationInView:_imageV];
float rectWidth = currentP.x - _startPoint.x;
float rectHeight = currentP.y - _startPoint.y;
_selectView.frame = CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight);
}
else if (UIGestureRecognizerStateEnded == pan.state) {
_endPoint = [pan locationInView:_imageV];
float rectWidth = _endPoint.x - _startPoint.x;
float rectHeight = _endPoint.y - _startPoint.y;
//create path
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:[UIScreen mainScreen].bounds];
UIBezierPath *otherPath = [[UIBezierPath bezierPathWithRect:CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight)] bezierPathByReversingPath];
[maskPath appendPath:otherPath];
//set mask
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
[_blockView.layer setMask:maskLayer];
_blockView.hidden = false;
_selectView.hidden = true;
}
}
-(void) recover{
_blockView.hidden = true;
}
#end
Hope it can help you.

Detecting the touch in CAShapeLayer stroke

I have created a simple audio player which plays a single audio. The views shows CAShapeLayer circular progress and also shows current time using CATextLayer. The figure below shows, the view:
Everything works fine until now, I can play, pause and the CAShapeLayer shows the progress. Now, I want to make it so that, when I touch the stroke (track) portion of the CAShapeLayer path, I would want to seek the player to that time. I tried few approaches but I could not detect touches on all parts of the stroke. It seems like the calculations I have done is not quite appropriate. I would be very happy if any body could help me with this.
Here is my complete code,
#interface ViewController ()
#property (nonatomic, weak) CAShapeLayer *progressLayer;
#property (nonatomic, weak) CAShapeLayer *trackLayer;
#property (nonatomic, weak) CATextLayer *textLayer;
#property (nonatomic, strong) AVAudioPlayer *audioPlayer;
#property (nonatomic, weak) NSTimer *audioPlayerTimer;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self prepareLayers];
[self prepareAudioPlayer];
[self prepareGestureRecognizers];
}
- (void)prepareLayers
{
CGFloat lineWidth = 40;
CGRect shapeRect = CGRectMake(0,
0,
CGRectGetWidth(self.view.bounds),
CGRectGetWidth(self.view.bounds));
CGRect actualRect = CGRectInset(shapeRect, lineWidth / 2.0, lineWidth / 2.0);
CGPoint center = CGPointMake(CGRectGetMidX(actualRect), CGRectGetMidY(actualRect));
CGFloat radius = CGRectGetWidth(actualRect) / 2.0;
UIBezierPath *track = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:0 endAngle:2 * M_PI
clockwise:true];
UIBezierPath *progressLayerPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:-M_PI_2
endAngle:2 * M_PI - M_PI_2
clockwise:true];
progressLayerPath.lineWidth = lineWidth;
CAShapeLayer *trackLayer = [CAShapeLayer layer];
trackLayer.contentsScale = [UIScreen mainScreen].scale;
trackLayer.shouldRasterize = YES;
trackLayer.rasterizationScale = [UIScreen mainScreen].scale;
trackLayer.bounds = actualRect;
trackLayer.strokeColor = [UIColor lightGrayColor].CGColor;
trackLayer.fillColor = [UIColor whiteColor].CGColor;
trackLayer.position = self.view.center;
trackLayer.lineWidth = lineWidth;
trackLayer.path = track.CGPath;
self.trackLayer = trackLayer;
CAShapeLayer *progressLayer = [CAShapeLayer layer];
progressLayer.contentsScale = [UIScreen mainScreen].scale;
progressLayer.shouldRasterize = YES;
progressLayer.rasterizationScale = [UIScreen mainScreen].scale;
progressLayer.masksToBounds = NO;
progressLayer.strokeEnd = 0;
progressLayer.bounds = actualRect;
progressLayer.fillColor = nil;
progressLayer.path = progressLayerPath.CGPath;
progressLayer.position = self.view.center;
progressLayer.lineWidth = lineWidth;
progressLayer.lineJoin = kCALineCapRound;
progressLayer.lineCap = kCALineCapRound;
progressLayer.strokeColor = [UIColor redColor].CGColor;
CATextLayer *textLayer = [CATextLayer layer];
textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.shouldRasterize = YES;
textLayer.rasterizationScale = [UIScreen mainScreen].scale;
textLayer.font = (__bridge CTFontRef)[UIFont systemFontOfSize:30.0];
textLayer.position = self.view.center;
textLayer.bounds = CGRectMake(0, 0, 300, 100);
[self.view.layer addSublayer:trackLayer];
[self.view.layer addSublayer:progressLayer];
[self.view.layer addSublayer:textLayer];
self.trackLayer = trackLayer;
self.progressLayer = progressLayer;
self.textLayer = textLayer;
[self displayText:#"Play"];
}
- (void)prepareAudioPlayer
{
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc]
initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:#"Song" withExtension:#"mp3"]
error:&error];
self.audioPlayer.volume = 0.2;
if (!self.audioPlayer) {
NSLog(#"Error occurred, could not create audio player");
return;
}
[self.audioPlayer prepareToPlay];
}
- (void)prepareGestureRecognizers
{
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(playerViewTapped:)];
[self.view addGestureRecognizer:tap];
}
- (void)playerViewTapped:(UITapGestureRecognizer *)tap
{
CGPoint tappedPoint = [tap locationInView:self.view];
if ([self.view.layer hitTest:tappedPoint] == self.progressLayer) {
CGPoint locationInProgressLayer = [self.view.layer convertPoint:tappedPoint toLayer:self.progressLayer];
NSLog(#"Progress view tapped %#", NSStringFromCGPoint(locationInProgressLayer));
// this is called sometimes but not always when I tap the stroke
} else if ([self.view.layer hitTest:tappedPoint] == self.textLayer) {
if ([self.audioPlayer isPlaying]) {
[self.audioPlayerTimer invalidate];
[self displayText:#"Play"];
[self.audioPlayer pause];
} else {
[self.audioPlayer play];
self.audioPlayerTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(increaseProgress:)
userInfo:nil
repeats:YES];
}
}
}
- (void)increaseProgress:(NSTimer *)timer
{
NSTimeInterval currentTime = self.audioPlayer.currentTime;
NSTimeInterval totalDuration = self.audioPlayer.duration;
float progress = currentTime / totalDuration;
self.progressLayer.strokeEnd = progress;
int minute = ((int)currentTime) / 60;
int second = (int)currentTime % 60;
NSString *progressString = [NSString stringWithFormat:#"%02d : %02d ", minute,second];
[self displayText:progressString];
}
- (void)displayText:(NSString *)text
{
UIColor *redColor = [UIColor redColor];
UIFont *font = [UIFont fontWithName:#"HelveticaNeue" size:70];
NSDictionary *attribtues = #{
NSForegroundColorAttributeName: redColor,
NSFontAttributeName: font,
};
NSAttributedString *progressAttrString = [[NSAttributedString alloc] initWithString:text
attributes:attribtues];
self.textLayer.alignmentMode = kCAAlignmentCenter;
self.textLayer.string = progressAttrString;
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
void(^animationBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect rect = (CGRect){.origin = CGPointZero, .size = size};
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
self.progressLayer.position = center;
self.textLayer.position = center;
self.trackLayer.position = center;
};
[coordinator animateAlongsideTransitionInView:self.view
animation:animationBlock completion:nil];
}
#end
As far as I know the CALAyer hitTest method does a very primitive bounds check. If the point is inside the layer's bounds, it returns YES, otherwise it returns NO.
CAShapeLayer doesn't make any attempt to tell if the point intersects the shape layer's path.
UIBezierPath does have a hitTest method, but I'm pretty sure that detects touches in the interior of a closed path, and would not work with a thick line arc line the one you're using.
If you want to hit test using paths I think you're going to have to roll your own hitTest logic that builds a UIBezierPath that is a closed path defining the shape you are testing for (your thick arc line). Doing that for an animation that's in-flight might be too slow.
Another problem you'll face: You're interrogating a layer that has an in-flight animation. Even when you're simply animating the center property of a layer that doesn't work. Instead you have to interrogate the layer's presentationLayer, which is a layer that approximates the settings of an in-flight animation.
I think your best solution will be to take the starting and ending position of your arc, convert the start-to end value to starting and ending angles, use trig to convert your touch position to an angle and radius, and see if the touch position is in the range of the start-to-end touch and within the inner and outer radius of the indicator line. That would just require some basic trig.
Edit:
Hint: The trig function you want to use to convert x and y position to an angle is arc tangent. arc tangent takes an X and a Y value and gives back an angle. However, to use the arc tangent properly you need to implement a bunch of logic to figure out what quadrant of the circle your point is in.In C, the math library function you want is atan2(). The atan2() function takes care of everything for you. You'll convert your point so 0,0 is in the center, and atan2() will give you the angle along the circle. To calculate the distance from the center of the circle you use the distance formula, a² + b² = c².

Resources