在一张图片中合并背景颜色和占位符图片

问题描述

我正在构建一个刮刮卡体验,其中我有一个背景颜色和一个透明的 png 在顶部。 在此之下,我有带有刮刮卡内容的实际图像。 我想将背景颜色和透明图像组合为一个 uiimage,这样当我刮擦它时,我就能看到下面的内容

我曾尝试为实际图像添加背景颜色,但是当我刮擦它时,我看不到内容。相反,背景颜色开始清除透明图像。

enter image description here

我编写了以下代码,用于在我触摸后清除图像区域:

- (UIImage *)addtouches:(NSSet *)touches {
    CGSize size = CGSizeMake(self.image.size.width * self.image.scale,self.image.size.height * self.image.scale);
    CGContextRef ctx = _imageContext;
    
    CGContextSetFillColorWithColor(ctx,[UIColor clearColor].CGColor);
    CGContextSetstrokeColorWithColor(ctx,[UIColor colorWithRed:0 green:0 blue:0 alpha:0].CGColor);
    int tempFilled = _tilesFilled;
    
    // process touches
    for (UITouch *touch in touches) {
        CGContextBeginPath(ctx);
        CGPoint touchPoint = [touch locationInView:self];
        touchPoint = fromUItoQuartz(touchPoint,self.bounds.size);
        touchPoint = scalePoint(touchPoint,self.bounds.size,size);
        
        if(UITouchPhaseBegan == touch.phase){
            [self.touchPoints removeAllObjects];
            [self.touchPoints addobject:[NSValue valueWithCGPoint:touchPoint]];
            [self.touchPoints addobject:[NSValue valueWithCGPoint:touchPoint]];
            // on begin,we just draw ellipse
            CGRect rect = CGRectMake(touchPoint.x - _radius,touchPoint.y - _radius,_radius*2,_radius*2);
            CGContextAddEllipseInRect(ctx,rect);
            CGContextFillPath(ctx);
            static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:@selector(fillTileWithPoint:)];
            (*fillTileFunc)(self,@selector(fillTileWithPoint:),rect.origin);
        } else if (UITouchPhaseMoved == touch.phase) {
            [self.touchPoints addobject:[NSValue valueWithCGPoint:touchPoint]];
            // then touch moved,we draw superior-width line
            CGContextSetstrokeColor(ctx,CGColorGetComponents([UIColor clearColor].CGColor));
            CGContextSetLineCap(ctx,kCGLineCapRound);
            CGContextSetlinewidth(ctx,2 * _radius);
            //            CGContextMovetoPoint(ctx,prevPoint.x,prevPoint.y);
            //            CGContextAddLinetoPoint(ctx,rect.origin.x,rect.origin.y);
            
            while(self.touchPoints.count > 3){
                CGPoint bezier[4];
                bezier[0] = ((NSValue*)self.touchPoints[1]).CGPointValue;
                bezier[3] = ((NSValue*)self.touchPoints[2]).CGPointValue;
                
                CGFloat k = 0.3;
                CGFloat len = sqrt(pow(bezier[3].x - bezier[0].x,2) + pow(bezier[3].y - bezier[0].y,2));
                bezier[1] = ((NSValue*)self.touchPoints[0]).CGPointValue;
                bezier[1] = [self normalizeVector:CGPointMake(bezier[0].x - bezier[1].x - (bezier[0].x - bezier[3].x),bezier[0].y - bezier[1].y - (bezier[0].y - bezier[3].y) )];
                bezier[1].x *= len * k;
                bezier[1].y *= len * k;
                bezier[1].x += bezier[0].x;
                bezier[1].y += bezier[0].y;
                
                bezier[2] = ((NSValue*)self.touchPoints[3]).CGPointValue;
                bezier[2] = [self normalizeVector:CGPointMake( (bezier[3].x - bezier[2].x)  - (bezier[3].x - bezier[0].x),(bezier[3].y - bezier[2].y)  - (bezier[3].y - bezier[0].y) )];
                bezier[2].x *= len * k;
                bezier[2].y *= len * k;
                bezier[2].x += bezier[3].x;
                bezier[2].y += bezier[3].y;
                
                CGContextMovetoPoint(ctx,bezier[0].x,bezier[0].y);
                CGContextAddCurvetoPoint(ctx,bezier[1].x,bezier[1].y,bezier[2].x,bezier[2].y,bezier[3].x,bezier[3].y);
                
                [self.touchPoints removeObjectAtIndex:0];
            }
            
            CGContextstrokePath(ctx);
            
            CGPoint prevPoint = [touch prevIoUsLocationInView:self];
            prevPoint = fromUItoQuartz(prevPoint,self.bounds.size);
            prevPoint = scalePoint(prevPoint,size);
            
            static const FillTileWithTwoPointsFunc fillTileFunc = (FillTileWithTwoPointsFunc) [self methodForSelector:@selector(fillTileWithTwoPoints:end:)];
            (*fillTileFunc)(self,@selector(fillTileWithTwoPoints:end:),touchPoint,prevPoint);
        }
    }
    
    // was _tilesFilled changed?
    if(tempFilled != _tilesFilled) {
        [_delegate mdscratchImageView:self didChangeMaskingProgress:self.maskingProgress];
    }
    
    CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    return image;
}

/*
 * filling tile with one ellipse
 */
-(void)fillTileWithPoint:(CGPoint) point{
    size_t x,y;
    point.x = MAX( MIN(point.x,self.image.size.width - 1),0);
    point.y = MAX( MIN(point.y,self.image.size.height - 1),0);
    x = point.x * self.maskedMatrix.max.x / self.image.size.width;
    y = point.y * self.maskedMatrix.max.y / self.image.size.height;
    char value = [self.maskedMatrix valueForCoordinates:x y:y];
    if (!value){
        [self.maskedMatrix setValue:1 forCoordinates:x y:y];
        _tilesFilled++;
    }
}

/*
 * filling tile with line
 */
-(void)fillTileWithTwoPoints:(CGPoint)begin end:(CGPoint)end{
    CGFloat incrementerForx,incrementerFory;
    static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:@selector(fillTileWithPoint:)];
    
    /* incrementers - about size of a tile */
    incrementerForx = (begin.x < end.x ? 1 : -1) * self.image.size.width / _tilesX;
    incrementerFory = (begin.y < end.y ? 1 : -1) * self.image.size.height / _tilesY;
    
    // iterate on points between begin and end
    CGPoint i = begin;
    while(i.x <= MAX(begin.x,end.x) && i.y <= MAX(begin.y,end.y) && i.x >= MIN(begin.x,end.x) && i.y >= MIN(begin.y,end.y)){
        (*fillTileFunc)(self,i);
        i.x += incrementerForx;
        i.y += incrementerFory;
    }
    (*fillTileFunc)(self,end);
}

解决方法

您可能想要做的是使用图层蒙版。

遮罩图层时,来自 Apple 的文档:

图层的 Alpha 通道决定了图层内容和背景的透出程度。完全或部分不透明的像素允许底层内容显示出来,但完全透明的像素会阻挡该内容。

所以,你想用一条路径来掩盖你的黑眼圈。

但是,要获得“刮掉”效果,您需要使用清除笔划绘制路径...这是使用 CAShapeLayer 无法完成的。

因此,我们将使用自定义 CALayer 子类。

MyShapeLayer.h

//
//  MyShapeLayer.h
//

#import <QuartzCore/QuartzCore.h>

@interface MyShapeLayer : CALayer

@property(nonatomic) CGPathRef path;

@end

MyShapeLayer.m

//
//  MyShapeLayer.m
//

#import <UIKit/UIKit.h>
#import "MyShapeLayer.h"

@implementation MyShapeLayer

- (void)drawInContext:(CGContextRef)inContext {
    // fill entire layer with solid color
    CGContextSetGrayFillColor(inContext,0.0,1.0);
    CGContextFillRect(inContext,self.bounds);
    
    // we want to "clear" the stroke
    CGContextSetStrokeColorWithColor(inContext,[UIColor clearColor].CGColor);
    // any color will work,as the mask uses the alpha value
    CGContextSetFillColorWithColor(inContext,[UIColor whiteColor].CGColor);
    // adjust drawing-line-width as desired
    CGContextSetLineWidth(inContext,60.0);

    CGContextSetLineCap(inContext,kCGLineCapRound);
    CGContextSetLineJoin(inContext,kCGLineJoinRound);
    CGContextAddPath(inContext,self.path);
    CGContextSetBlendMode(inContext,kCGBlendModeSourceIn);
    CGContextDrawPath(inContext,kCGPathFillStroke);
}

@end

现在我们可以创建一个 UIView 子类来在 CAShapeLayer 上绘制一条实心圆路径并用我们的 MyShapeLayer 对其进行屏蔽。

ScratchOffView.h

//
//  ScratchOffView.h
//

#import <UIKit/UIKit.h>

@interface ScratchOffView : UIView

@property (assign,readwrite) CGFloat expandedBounds;

@end

ScratchOffView.m

//
//  ScratchOffView.m
//

#import "ScratchOffView.h"

#import "MyShapeLayer.h"

@interface ScratchOffView()

@property (strong,nonatomic) UIBezierPath *maskPath;
@property (strong,nonatomic) MyShapeLayer *maskLayer;
@property (strong,nonatomic) CAShapeLayer *scratchOffShapeLayer;
@property (strong,nonatomic) CALayer *scratchOffLayer;

@end

@implementation ScratchOffView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {
    
    _maskPath = [UIBezierPath new];
    _maskLayer = [MyShapeLayer new];
    _scratchOffLayer = [CALayer new];
    _scratchOffShapeLayer = [CAShapeLayer new];
    
    // Important,otherwise you will get a black rectangle
    _maskLayer.opaque = NO;

    // add the layer holding the shape to "Scratch Off"
    [self.layer addSublayer:_scratchOffShapeLayer];
    
    UIColor *c = [UIColor colorWithRed:50.0 / 255.0 green:150.0 / 255.0 blue:140.0 / 255.0 alpha:1.0];
    [_scratchOffShapeLayer setFillColor:c.CGColor];
    
    // set the mask layer
    [_scratchOffShapeLayer setMask:_maskLayer];
    
    // default 0.0 == no expanded bounds for touch
    _expandedBounds = 0.0;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    [_maskLayer setFrame:[self bounds]];
    [_scratchOffShapeLayer setFrame:[self bounds]];
    
    UIBezierPath *b = [UIBezierPath bezierPathWithOvalInRect:[self bounds]];
    [_scratchOffShapeLayer setPath:b.CGPath];
    
    // triggers drawInContext
    [_maskLayer setNeedsDisplay];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint currentPoint = [touch locationInView:self];
    [_maskPath moveToPoint:currentPoint];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint currentPoint = [touch locationInView:self];
    // add line to our maskPath
    [_maskPath addLineToPoint:currentPoint];
    // update the mask layer path
    [_maskLayer setPath:_maskPath.CGPath];
    // triggers drawInContext
    [_maskLayer setNeedsDisplay];
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // accept touch if within expanded bounds
    //  setting _expandedBounds to a Positive number allows the
    //  touches to start outside the frame
    CGRect r = CGRectInset([self bounds],-_expandedBounds,-_expandedBounds);
    return CGRectContainsPoint(r,point);
}

@end

请注意,我们添加了一个属性:expandedBounds。由于只有在此视图上开始时触摸才会注册,因此我们可以(实际上)扩展视图的边界,以便用户可以触摸并“拖入圆圈中”。

这是一个完整的示例实现。为了尝试匹配您的问题,我使用此图像 (420 x 460 pixels) 作为“背景”图像:

enter image description here

并将此图像 (284 x 284 pixels) 作为“在刮掉的圆圈下显示的图像”(透明区域是我们希望圆圈的大小):

enter image description here

ScratchOffTestViewController.h

//
//  ScratchOffTestViewController.h
//

#import <UIKit/UIKit.h>

@interface ScratchOffTestViewController : UIViewController

@end

ScratchOffTestViewController.m

//
//  ScratchOffTestViewController.m
//

#import "ScratchOffTestViewController.h"
#import "ScratchOffView.h"

@interface ScratchOffTestViewController ()

@property (strong,nonatomic) ScratchOffView *scratchOffView;

@end

@implementation ScratchOffTestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    // create the Scratch Off View
    _scratchOffView = [ScratchOffView new];
    
    // load background and giftBox image
    UIImage *bkgImage = [UIImage imageNamed:@"backgroundImage"];
    UIImage *giftBoxImage = [UIImage imageNamed:@"giftBox"];
    if (!bkgImage || !giftBoxImage) {
        NSLog(@"Could not load images!!!");
        return;
    }
    
    UIImageView *bkgImageView = [UIImageView new];
    UIImageView *giftImageView = [UIImageView new];
    
    bkgImageView.image = bkgImage;
    giftImageView.image = giftBoxImage;
    
    bkgImageView.translatesAutoresizingMaskIntoConstraints = NO;
    giftImageView.translatesAutoresizingMaskIntoConstraints = NO;
    _scratchOffView.translatesAutoresizingMaskIntoConstraints = NO;

    [self.view addSubview:bkgImageView];
    [self.view addSubview:giftImageView];
    [self.view addSubview:_scratchOffView];
    
    UILayoutGuide *g = [self.view safeAreaLayoutGuide];
    [NSLayoutConstraint activateConstraints:@[
        
        // constrain background image view to background image size
        [bkgImageView.widthAnchor constraintEqualToConstant:bkgImage.size.width],[bkgImageView.heightAnchor constraintEqualToConstant:bkgImage.size.height],// centered
        [bkgImageView.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],[bkgImageView.centerYAnchor constraintEqualToAnchor:g.centerYAnchor],// constrain giftBox image view to giftBox image size
        [giftImageView.widthAnchor constraintEqualToConstant:giftBoxImage.size.width],[giftImageView.heightAnchor constraintEqualToConstant:giftBoxImage.size.height],// centered horizontally,and a little above vertically
        [giftImageView.centerXAnchor constraintEqualToAnchor:bkgImageView.centerXAnchor],[giftImageView.centerYAnchor constraintEqualToAnchor:bkgImageView.centerYAnchor],// constrain Scratch Off View to giftImageView
        [_scratchOffView.widthAnchor constraintEqualToAnchor:giftImageView.widthAnchor],[_scratchOffView.heightAnchor constraintEqualToAnchor:giftImageView.widthAnchor],[_scratchOffView.centerXAnchor constraintEqualToAnchor:giftImageView.centerXAnchor],[_scratchOffView.centerYAnchor constraintEqualToAnchor:giftImageView.centerYAnchor],]];
    
    // expand the touch bounds of the Scratch Off View by 80-pts
    _scratchOffView.expandedBounds = 80.0;
    
    return;

}

@end

一开始,我们看到:

enter image description here

在圆圈上触摸拖动一点后,我们看到:

enter image description here

如果我们继续拖动触摸,深绿色圆圈最终将完全消失——我们将“刮掉它”。