美文网首页iOSIOS的开发技巧iOS CALayer
【iOS效果集】自绘制酷炫粒子效果合成图片

【iOS效果集】自绘制酷炫粒子效果合成图片

作者: 阿曌 | 来源:发表于2017-01-18 15:50 被阅读1407次
    qq.gif qcloud.gif

    想要学习此效果需要掌握的技能有:
    1.了解CALayer及自定义
    2.了解CADisplayLink及其用法
    3.了解CoreGraphics及相关API

    核心思想就是:获取一张图片的每个像素的信息,然后生成相应个数的像素粒子,绘制在自定义的CALayer上,再使用CADisplayLink改变每个粒子的位置,并重新绘制,达到动画的效果。

    一、获取图片元数据

    根据CoreGraphics的相关API能获取到UIImage的元数据(rawData),rawData是个一维数组,里面存着每个像素点的RGBA数据,储存顺序为RGBARGBARGBA...分别是从第一行开始第一个像素的RGBA,第二个元素的RGBA……
    根据CGImageGetWidth()|CGImageGetHeight()能得到以像素为单位的图片宽高,然后就可以用两个for循环得到所有像素点的位置和颜色信息了,然后再用自定义model保存起来。

    - (NSArray*)getRGBAsFromImage:(UIImage*)image {
        //1. get the image into your data buffer.
        CGImageRef imageRef = [image CGImage];
        NSUInteger imageW = CGImageGetWidth(imageRef);
        NSUInteger imageH = CGImageGetHeight(imageRef);
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        NSUInteger bytesPerPixel = 4; //一个像素4字节
        NSUInteger bytesPerRow = bytesPerPixel * imageW;
        unsigned char *rawData = (unsigned char*)calloc(imageH*imageW*bytesPerPixel, sizeof(unsigned char)); //元数据
        NSUInteger bitsPerComponent = 8;
        CGContextRef context = CGBitmapContextCreate(rawData, imageW, imageH, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big);
        CGColorSpaceRelease(colorSpace);
        CGContextDrawImage(context, CGRectMake(0, 0, imageW, imageH), imageRef);
        CGContextRelease(context);
        
        //2. Now your rawData contains the image data in the RGBA8888 pixel format.
        CGFloat addY = (_maxParticleCount == 0) ? 1 : (imageH/_maxParticleCount);
        CGFloat addX = (_maxParticleCount == 0) ? 1 : (imageW/_maxParticleCount);
        NSMutableArray *result = [NSMutableArray new];
        for (int y = 0; y < imageH; y+=addY) {
            for (int x = 0; x < imageW; x+=addX) {
                NSUInteger byteIndex = bytesPerRow*y + bytesPerPixel*x;
                //rawData一维数组存储方式RGBA(第一个像素)RGBA(第二个像素)...
                CGFloat red   = ((CGFloat) rawData[byteIndex]     ) / 255.0f;
                CGFloat green = ((CGFloat) rawData[byteIndex + 1] ) / 255.0f;
                CGFloat blue  = ((CGFloat) rawData[byteIndex + 2] ) / 255.0f;
                CGFloat alpha = ((CGFloat) rawData[byteIndex + 3] ) / 255.0f;
                
                if (alpha == 0 ||
                    (_ignoredWhite && (red+green+blue == 3)) ||
                    (_ignoredBlack && (red+green+blue == 0))) {
                    //要忽略的粒子
                    continue;
                }
                
                AZParticle *particle = [AZParticle new];
                particle.color = [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
                particle.point = CGPointMake(x, y);
                if (_customColor) {
                    particle.customColor = _customColor;
                }
                if (_randomPointRange > 0) {
                    particle.randomPointRange = _randomPointRange;
                }
    
                [result addObject:particle];
            }
        }
        free(rawData);
        return result;
    }
    

    上面有个_maxParticleCount,是为了限制如果图片过大,像素过多,会导致绘制性能差,比如设_maxParticleCount = 150,即每行每列都最多只有150个像素粒子,这样绘制出来的图片效果是这样的:

    对比图.jpg

    另外有个判断条件用来忽略粒子的:

      if (alpha == 0 ||  (_ignoredWhite && (red+green+blue == 3)) ||  (_ignoredBlack && (red+green+blue == 0))) {
         //要忽略的粒子
         continue;
    }
    

    这里是为了过滤透明的粒子,透明就没有必要生成粒子了,有些图片是白色的背景,我们没来得及弄成透明的png格式,所以这里加了个接口_ignoredWhite来忽略白色的背景,同理还有个忽略黑色粒子的。

    _customColor是为了改变粒子为自定义的颜色;_randomPointRange是为了给粒子加偏移量,比如上面qcloud.gif的效果,会发现粒子是不规则的,就是这个参数起了作用:

    - (void)setRandomPointRange:(CGFloat)randomPointRange {
            _randomPointRange = randomPointRange;
            if (_randomPointRange != 0) {
                _point.x = _point.x - _randomPointRange + arc4random_uniform(_randomPointRange*2);
                _point.y = _point.y - _randomPointRange + arc4random_uniform(_randomPointRange*2);
            }
    }
    

    二、绘制到自定义Layer上
    自定义一个CALayer,并重写-(void)drawInContext:(CGContextRef)ctx就可以啦,绘制比较简单

    -(void)drawInContext:(CGContextRef)ctx {
        for (AZParticle *particle in _particleArray) {
            CGContextAddEllipseInRect(ctx, CGRectMake(particle.position.x , particle.position.y , 1, 1));
            const CGFloat* components = CGColorGetComponents(particle.color.CGColor);
            CGContextSetRGBFillColor(ctx, components[0], components[1], components[2], components[3]);
            CGContextFillPath(ctx);
        }
    }
    

    三、用CADisplayLink加上动画
    CADisplayLink 默认每1s刷新60次(屏幕刷新频率),我们可以根据定时刷新这点来做点文章。
    首先理清楚思路:我们要的是粒子移动,那么肯定要有起始点(beginPosition)和终点(endPosition),起始点可以随意给个,终点就是最开始保存的每个粒子的位置。另外我们需要根据时间来调整粒子的位置,那么就要有个当前时间(curTime)和总时间(duration),总时间也是我们随便定个,当前时间就要用到CADisplayLink的刷新规律了,每次刷新时都自增一定时间即可。

    @implementation AZEmitterLayer
    - (instancetype)init {
        self = [super init];
        if (self) {
            _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(emitterAnim:)];
            [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
            _animTime = 0;
            _animDuration = 10;
            _beginPoint = CGPointMake(0, 0);
        }
        return self;
    }
    - (void)emitterAnim:(CADisplayLink*)displayLink {
        [self setNeedsDisplay];
        _animTime += 0.2;
    }
    -(void)drawInContext:(CGContextRef)ctx {
        CGFloat curTime = _animTime;
        for (AZParticle *particle in _particleArray) {
            CGFloat curX = [self easeInOutQuad:curTime begin:_beginPoint.x end:particle.point.x  duration:_animDuration];
            CGFloat curY = [self easeInOutQuad:curTime begin:_beginPoint.y end:particle.point.y  duration:_animDuration];
            CGContextAddEllipseInRect(ctx, CGRectMake(curX , curY , 1, 1));
            const CGFloat* components = CGColorGetComponents(particle.color.CGColor);
            CGContextSetRGBFillColor(ctx, components[0], components[1], components[2], components[3]);
            CGContextFillPath(ctx);
        }
    }
    @end
    

    这里用到个数学函数easeInOutQuad是Tween缓动动画的函数,可以在《利用tween.js算法生成缓动效果》看到具体计算方法,这里传入的参数分别是:当前时间、起始位置、终点位置和总时间。


    可以看到现在的效果太过统一,根本看不出来是粒子效果,反而像是一张图片放大的过程。
    为了解决这个情况,我们需要“分批”显示粒子,即在一条时间队列上,一直有新粒子出来,我们给particle加个delayTime属性,用来延迟自己的显示时间,这个时间用一个随机数来生成:_delayTime = arc4random_uniform(30);
    然后我们再来改drawInContext函数:
    -(void)drawInContext:(CGContextRef)ctx {
        for (AZParticle *particle in _particleArray) {
            if (particle.delayTime > _animTime) {
                continue;
            } 
            CGFloat curTime = _animTime - particle.delayTime;
            //后面都一样
        }
    }
    

    再来看下效果:


    诶,好像效果出来了,但是隐隐觉得哪里不对,换张图看下:

    哇靠——这一坨一坨的到底是个啥?!
    原因出在刚刚我们的“分批”,同一批的粒子的相对位置其实还是一样的(因为他们的当前时间,动画总时间,起始位置都是一样的),所以我们还需要加一个随机变量,使得同一批粒子也要错开时间。
    想了下,当前时间,起始位置和总时间,好像就总时间这里可以稍微变化下。我们给粒子加上个delayDuration属性,也是随机生成:_delayDuration = arc4random_uniform(10);
    然后再来改drawInContext函数:
    -(void)drawInContext:(CGContextRef)ctx {
        int count = 0;
        for (AZParticle *particle in _particleArray) {
            if (particle.delayTime > _animTime) {
                continue;
            }
    
            CGFloat curTime = _animTime - particle.delayTime;
            if (curTime >= _animDuration + particle.delayDuration) { //到达了目的地的粒子原地等待下没到达的粒子
                curTime =  _animDuration + particle.delayDuration;
                count ++;
            }
            
            CGFloat curX = [self easeInOutQuad:curTime begin:_beginPoint.x end:particle.point.x + self.bounds.size.width/2-CGImageGetWidth(_image.CGImage)/2 duration:_animDuration + particle.delayDuration];
            CGFloat curY = [self easeInOutQuad:curTime begin:_beginPoint.y end:particle.point.y + self.bounds.size.height/2 - CGImageGetHeight(_image.CGImage)/2 duration:_animDuration + particle.delayDuration];
            CGContextAddEllipseInRect(ctx, CGRectMake(curX , curY , 1, 1));
            const CGFloat* components = CGColorGetComponents(particle.color.CGColor);
            CGContextSetRGBFillColor(ctx, components[0], components[1], components[2], components[3]);
            CGContextFillPath(ctx);
        }
        if (count == _particleArray.count) {
            [self reset];
            if (_azDelegate && [_azDelegate respondsToSelector:@selector(onAnimEnd)]) {
                [_azDelegate onAnimEnd];
            }
        }
    }
    

    需要注意如果有粒子curTime已经到了总时间duration,就把curTime置为duration并且计数+1,并且这里的总时间duratioin都要改为原来的总时间(_animDuration)加上每个粒子的延迟时间 (delayDuration)。

    因图片太大做了加快处理

    源代码:https://github.com/Xieyupeng520/AZEmitter
    如果你喜欢这个效果,请给我Github上一个Star鼓励一下O(∩_∩)O谢谢!

    相关文章

      网友评论

      • Yuency:楼主你好! 我很喜欢你的这个 QQ 的粒子效果动画, 我用 Swift 把你的重新写了一下, 我想放到我的简书文章里, 我会在代码头部 给出 你的 简书地址 和 GitHub 地址, 可以吗?
        Yuency:@阿曌 这是地址 https://github.com/gityuency/SwiftAnimations, 在我的简书里也有。我觉得拿粒子去拼图片这个思路很6,怎么想到的。
        阿曌:好的,可以哈,你到时候链接也发我一份哈!~
      • 5ec1da87f063:发现粒子合成之后的图片较原图有点失真,会有灰暗的感觉.请问这是何故?
        Yuency:图片颜色变浅是因为图片的像素点和屏幕的像素点没有一一对应的缘故, 所以才会有灰暗的感觉, 这点可以通过调整代码来进行改善.
      • Little_Mango:最近在看《iOS Core Animation Advanced Techniques》,然后想到去年看到类似的粒子图片,突然心血来潮搜索了一下,就看到博主的文章了。:smile:
      • 捞月亮的猴子:楼主好,看了代码和效果,出来的图颜色变浅了,不知道是哪里设置了吗?

      本文标题:【iOS效果集】自绘制酷炫粒子效果合成图片

      本文链接:https://www.haomeiwen.com/subject/tigvbttx.html