I am programmatically creating a CALayer
subclass which applies a bit of pixel noise to itself. The code works in that it renders noise in the layer, but there is a strange artifact on the image that I am unable to determine the root cause.
Here is a sample image with the noiseOpacity
turned up to make the problem more visible.
The pink box is a UANoisyGradientLayer
, a CAGradientLayer
subclass with the following bits:
@interface UANoisyGradientLayer ()
@property (nonatomic, retain) CIContext *noiseContext;
@property (nonatomic, retain) CIFilter *noiseGenerator;
@property (nonatomic, retain) CIImage *noiseImage;
@end
@implementation UANoisyGradientLayer
@synthesize noiseOpacity = _noiseOpacity, noiseImage;
- (id)init {
self = [super init];
if (self) {
self.noiseOpacity = 0.10;
self.noiseContext = [CIContext contextWithOptions:nil];
self.noiseGenerator = [CIFilter filterWithName:@"CIColorMonochrome"];
[self.noiseGenerator setValue:[[CIFilter filterWithName:@"CIRandomGenerator"] valueForKey:@"outputImage"] forKey:@"inputImage"];
[self.noiseGenerator setDefaults];
self.noiseImage = [self.noiseGenerator outputImage];
}
return self;
}
- (void)drawInContext:(CGContextRef)ctx {
[super drawInContext:ctx];
CGRect extentRect = [self.noiseImage extent];
if (CGRectIsInfinite(extentRect) || CGRectIsEmpty(extentRect)) {
extentRect = self.bounds;
}
CGImageRef cgimg = [self.noiseContext createCGImage:self.noiseImage fromRect:extentRect];
CGContextSetBlendMode(ctx, kCGBlendModeOverlay);
CGContextSetAlpha(ctx, self.noiseOpacity);
CGContextDrawImage(ctx, self.bounds, cgimg);
CGImageRelease(cgimg);
}
Basically, I create the CIImage
in init using a CIRandomGenerator
as input to a CIColorMonochrome
filter. Then, when it comes time to draw it, I create a CGImageRef
out of it using self.bounds
(the extent is always infinite
or 0), and draw it to the context.
The result is mostly fine, but as you can see in the image, there seems to be some stretching going on. What is happening here?
Although not fixing the original problem, I approached this from a different angle and have duplicated the output. Instead of trying to generate a single image the size of the self.bounds
, I am now generating an image that is only 64x64, then tiling it using CGContextDrawTiledImage
. Because I am now sizing it at a fixed size, I could pull some code out of the drawInContext:
method. And finally, because there was no more image generation done in the draw methods, I was able to make it a static var so it is only ever generated once! Here is the complete class:
static CGImageRef __noiseImage = nil;
static CGFloat __noiseImageWidth = 0.0;
static CGFloat __noiseImageHeight = 0.0;
@implementation UANoisyGradientLayer
@synthesize noiseOpacity = _noiseOpacity;
- (id)init {
self = [super init];
if (self) {
self.noiseOpacity = 0.1f;
self.needsDisplayOnBoundsChange = YES;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CIContext *noiseContext = [CIContext contextWithOptions:nil];
CIFilter *noiseGenerator = [CIFilter filterWithName:@"CIColorMonochrome"];
[noiseGenerator setValue:[[CIFilter filterWithName:@"CIRandomGenerator"] valueForKey:@"outputImage"] forKey:@"inputImage"];
[noiseGenerator setDefaults];
CIImage *ciImage = [noiseGenerator outputImage];
CGRect extentRect = [ciImage extent];
if (CGRectIsInfinite(extentRect) || CGRectIsEmpty(extentRect)) {
extentRect = CGRectMake(0, 0, 64, 64);
}
__noiseImage = [noiseContext createCGImage:ciImage fromRect:extentRect];
__noiseImageWidth = CGImageGetWidth(__noiseImage);
__noiseImageHeight = CGImageGetHeight(__noiseImage);
});
}
return self;
}
- (void)drawInContext:(CGContextRef)ctx {
[super drawInContext:ctx];
if (self.noiseOpacity > 0) {
CGContextSaveGState(ctx);
CGPathRef path = [[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius] CGPath];
CGContextAddPath(ctx, path);
CGContextClip(ctx);
CGContextSetBlendMode(ctx, kCGBlendModeOverlay);
CGContextSetAlpha(ctx, self.noiseOpacity);
CGContextDrawTiledImage(ctx, CGRectMake(0, 0, __noiseImageWidth, __noiseImageHeight), __noiseImage);
CGContextRestoreGState(ctx);
}
}
@end
NOTE: CIColorMonochrome
and CIRandomGenerator
require iOS 6 (or newer). Make sure to include the required frameworks (CoreImage.framework
and QuartzCore.framework
).