阅读 1230

Quartz2D + 粒子动画 + CoreAnimation实现简约版嫦娥奔月

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前言

正值中秋来临之际,突然发现掘金小盆友又在偷(da)偷(zhang)摸(qi)摸(gu)地搞活动。那这个热闹我必须要凑一波啊~在此先提前祝各位老铁新年快乐!!!

思路

无意间看到了进军的王小二的作品:嫦娥飞天动画简易版。这不就是我脑海中想象的嫦娥奔月的大致场景吗!perfect~ 既然他用Web端的技术实现,那我用iOS原生也来实现一个。我可真是个小机灵鬼~

夜色背景

嫦娥奔月,顾名思义,有月亮肯定是夜间。所以首先要添加一个静谧祥和的夜色背景。有需要的小伙伴,资源图可从我的项目中获取。

月亮

月亮的绘制
  • 中秋节月亮必须是满月,一个完美的圆形。
  • 添加一个由内而外的颜色渐变,这样更能凸显出月亮的3d效果。
  • 并且在此基础上,可以添加阴影,达到类似月晕的效果。

核心代码:

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    
    // 1.获取CGContextRef
    UIGraphicsBeginImageContext(self.bounds.size);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 2.绘制path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, CGRectGetMidX(rect), CGRectGetMidY(rect), rect.size.width * 0.5, 0, M_PI * 2, false);
    CGPathCloseSubpath(path);
    // 3.绘制渐变
    [self drawRadialGradient:ctx path:path];
    // 4.绘制阴影
    self.layer.shadowColor = RGBA(247, 247, 9, 255).CGColor;
    self.layer.shadowOffset = CGSizeMake(20, 20);
    self.layer.shadowRadius = self.bounds.size.width * 0.5;
    self.layer.shadowOpacity = 0.8;
    self.layer.shadowPath = path;
    // 5.释放path 有create就对应有release
    CGPathRelease(path);
    // 6.从上下文中获取图像,显示在界面上
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [self addSubview:imageView];
}

- (void)drawRadialGradient:(CGContextRef)context path:(CGPathRef)path {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[( __bridge id) RGBA(196, 196, 60, 255).CGColor, ( __bridge id) RGBA(240, 240, 9, 255).CGColor];

    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, ( __bridge CFArrayRef) colors, locations);
    
    CGRect pathRect = CGPathGetBoundingBox(path);
    CGPoint center = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMidY(pathRect));
    CGFloat radius = MAX(pathRect.size.width * 0.5, pathRect.size.height * 0.5) * sqrt(2);

    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextEOClip(context);
    
    CGContextDrawRadialGradient(context, gradient, center, 0, center, radius, 0);
    CGContextRestoreGState(context);

    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
复制代码
给月亮添加动画

因为嫦娥是奔向月亮的,所以我们给月亮添加一个由远及近的缩放动画

CABasicAnimation *moonScaleAnim = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
moonScaleAnim.fromValue = [NSNumber numberWithFloat:0.8];
moonScaleAnim.toValue = [NSNumber numberWithFloat:1.5];
moonScaleAnim.duration = 4;
moonScaleAnim.autoreverses = NO;
moonScaleAnim.removedOnCompletion = NO;
moonScaleAnim.fillMode = kCAFillModeForwards;
moonScaleAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.moonView.layer addAnimation:moonScaleAnim forKey:nil];
复制代码

嫦娥

嫦娥的绘制

嫦娥使用了进军的王小二文章里提供的gif动图。有需要的小伙伴也可以到我的项目中获取。采用SDWebImage三方库进行加载

核心代码:

- (UIImageView *)changeImgView {
    if (_changeImgView == nil) {
        _changeImgView = [[UIImageView alloc] init];
        NSData *changeData = [[NSDataAsset alloc] initWithName:@"change" bundle:[NSBundle mainBundle]].data;
        UIImage *changeImage = [UIImage sd_animatedGIFWithData:changeData];
        _changeImgView.image = changeImage;
        _changeImgView.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height, CHANGE_WIDTH, CHANGE_WIDTH * changeImage.size.width / changeImage.size.height);
    }
    return _changeImgView;

}
复制代码
给嫦娥添加螺旋飞天动画
  • 嫦娥是飞向远处的月亮,所以其必然会有一个位移动画
  • 由近及远,其身影会越来越小。所以还需要一个缩放动画
  • 当小到一定程度,其背影会渐渐模糊。故还添加了一个延迟的透明度渐变

所以嫦娥这里存在3个动画:位移、缩放、透明度。其中缩放位移动画我要的精确度比较高,所以采取了CAKeyframeAnimation帧动画

// 缩放
CAKeyframeAnimation *changeScaleAnim = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
changeScaleAnim.values = @[[NSNumber numberWithFloat:1.2],
                           [NSNumber numberWithFloat:0.8],
                           [NSNumber numberWithFloat:0.5]];
changeScaleAnim.keyTimes = @[[NSNumber numberWithFloat:0.0],
                             [NSNumber numberWithFloat:0.5],
                             [NSNumber numberWithFloat:1.0]];
changeScaleAnim.beginTime = 0;

// 透明度
CABasicAnimation *changeOpacityAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
changeOpacityAnim.fromValue = @(1.0);
changeOpacityAnim.toValue = @(0.0);
// 延迟模糊
changeOpacityAnim.beginTime = 2.5;

// 位移
CAKeyframeAnimation *changePositionAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
// 来个螺旋升天(贝塞尔曲线)
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
[path addQuadCurveToPoint:self.moonView.center controlPoint:CGPointMake(CGRectGetMinX(self.changeImgView.frame) - 100, CGRectGetMinY(**self**.moonView.frame))];
changePositionAnim.path = path.CGPath;
changePositionAnim.beginTime = 0;

// 动画组
CAAnimationGroup *changeAnim = [CAAnimationGroup animation];
changeAnim.duration = 4;
changeAnim.repeatCount = 1;
changeAnim.removedOnCompletion = NO;
changeAnim.fillMode = kCAFillModeForwards;
changeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
changeAnim.animations = @[changeScaleAnim, changePositionAnim, changeOpacityAnim];
[self.changeImgView.layer addAnimation:changeAnim forKey:nil];
复制代码

点点繁星

繁星的绘制

思路:一个个带阴影的小白点,随机分布屏幕之间。并且其大小也进行了随机范围内的设置,让每一个星星个体都互相有差异

核心代码:

for (int i = 0; i < 100; i ++) {
    // 创建小星星
    UIView *starView = [[UIView alloc] init];
    CGFloat starViewX = random() % ((int)[UIScreen mainScreen].bounds.size.width);
    CGFloat starViewY = random() % ((int)([UIScreen mainScreen].bounds.size.height));
    CGFloat starViewWH = random() % 3 + 2;
    starView.frame = CGRectMake(starViewX, starViewY, starViewWH, starViewWH);
    starView.backgroundColor = [UIColor whiteColor];
    starView.layer.shadowColor = [UIColor whiteColor].CGColor;
    starView.layer.shadowOffset = CGSizeMake(0, 0);
    starView.layer.shadowRadius = starViewWH * 0.5;
    starView.layer.shadowOpacity = 0.8;
    starView.layer.cornerRadius = starViewWH * 0.5;
    [self.view insertSubview:starView belowSubview:self.moonView];
    ...
}
复制代码
让繁星闪烁起来

使用CoreAnimation核心动画让其透明度变化起来,并且每个小白点的动画开始时间动画时长透明度变化值都进行随机处理。这样使得繁星不仅在位置尺寸上有静态性差异,并且在效果的动态性上也存在差异,就惟妙惟肖地完成了点点繁星的效果

核心代码:

// 小星星闪烁动画
CGFloat delayTime = (random() % 10) / 10.f + 0.5;
CGFloat duration = (random() % 2) + 1;
CGFloat toValue = (random() % 6) / 10.f;
CABasicAnimation *starOpacityAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
starOpacityAnim.fromValue = @(1.0);
starOpacityAnim.toValue = @(toValue);
starOpacityAnim.duration = duration;
starOpacityAnim.beginTime = delayTime;
starOpacityAnim.autoreverses = YES;
starOpacityAnim.repeatCount = MAXFLOAT;
starOpacityAnim.removedOnCompletion = NO;
starOpacityAnim.fillMode = kCAFillModeForwards;
starOpacityAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[starView.layer addAnimation:starOpacityAnim forKey:**nil**];
复制代码

流星

流星的绘制

思路:流星的绘制思路我是采取线性渐变 + mask遮罩进行实现。将绘制的流星转为UIImage备孕,以便于后面的粒子动画使用

核心代码:

#pragma mark - lazy loading
- (CAShapeLayer *)shaperLayer {
    if (_shaperLayer == nil) {
        _shaperLayer = [CAShapeLayer layer];
        _shaperLayer.lineCap = kCALineCapButt;
        _shaperLayer.lineJoin = kCALineJoinRound;
        _shaperLayer.strokeColor = [UIColor redColor].CGColor;
        _shaperLayer.fillColor = [[UIColor redColor] CGColor];
        _shaperLayer.lineWidth = 3;
        _shaperLayer.backgroundColor = [UIColor clearColor].CGColor;
    }
    return _shaperLayer;

}

- (CAGradientLayer *)gradientLayer {
    if (_gradientLayer == nil) {
        _gradientLayer = [CAGradientLayer layer];
        _gradientLayer.colors = @[
            ( __bridge id)[UIColor whiteColor].CGColor,
            ( __bridge id)[[UIColor whiteColor] colorWithAlphaComponent:0.6].CGColor,
            ( __bridge id)[[UIColor whiteColor] colorWithAlphaComponent:0.3].CGColor,
            ( __bridge id)[[UIColor whiteColor] colorWithAlphaComponent:0].CGColor
        ];
        _gradientLayer.startPoint = CGPointMake(0, 1);
        _gradientLayer.endPoint = CGPointMake(1, 0);
        _gradientLayer.locations = @[@0, @0.3, @0.6, @1];
    }
    return _gradientLayer;
}

#pragma mark - private methods
- (void)makeUI {
    [self.layer addSublayer:self.gradientLayer];
    
    // 设置遮罩路径
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, CGRectGetMinX(self.bounds) + 5, CGRectGetMaxY(self.bounds) - 5);
    CGPathAddLineToPoint(path, NULL, CGRectGetMaxX(self.bounds) - 5, CGRectGetMinY(self.bounds) + 5);
    CGPathCloseSubpath(path);
    self.shaperLayer.path = path;

    CGPathRelease(path);
    self.layer.mask = **self**.shaperLayer;
}

#pragma mark - public methods
// 转为屏幕快照
- (UIImage *)convertViewToImage {
    UIImage *starRainImage = [[UIImage alloc] init];
    // 设置背景透明背景,并且不失真
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);
    [self.layer renderInContext:UIGraphicsGetCurrentContext()];
    starRainImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return starRainImage;
}
复制代码
粒子动画实现流星雨效果

上面也提到了,把绘制的流星转化为UIImage备孕,原因就是流星雨的效果采用了粒子动画进行实现。而粒子动画的contents一般都是CGImage的集合

核心代码:

- (void)startStarRainAnimation {
    CAEmitterLayer *meteor = [CAEmitterLayer layer];
    // 发射位置
    meteor.emitterPosition = CGPointMake(-300, 0);
    // 粒子产生系数,默认为1
    meteor.birthRate = 1;
    // 发射器的尺寸
    meteor.emitterSize = CGSizeMake(4000, 0);
    // 发射的形状
    meteor.emitterShape = kCAEmitterLayerCuboid;
    // 发射的模式
    meteor.emitterMode = kCAEmitterLayerLine;
    // 渲染模式
    meteor.renderMode = kCAEmitterLayerAdditive;
    
    CAEmitterCell *cell = [CAEmitterCell emitterCell];
    // 设置粒子每秒的生成数量
    cell.birthRate = 15;
    // 设置生成时的初始速度的变化范围
    cell.velocity = 500.0f;
    cell.velocityRange = 300.0f;
    // 设置粒子的Y轴加速度
    cell.yAcceleration = 0.0f;
    // 缩放比例
    cell.scale= 0.5f;
    // 缩放速度
    cell.scaleSpeed = 0.1;
    // 粒子存活时长
    cell.lifetime = 2.5f/*1.8f*/;
    // 粒子的初始发射方向
    cell.emissionLongitude = 0.75 * M_PI;
    // 展示流星快照
    cell.contents = ( __bridge id _Nullable)(self.starRainImage.CGImage);
    // 设置粒子颜色
    cell.color = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.6].CGColor;
    
    meteor.emitterDepth = 10.0f;
    meteor.shadowOpacity = 0.0f;
    meteor.shadowRadius = 0.0f;
    meteor.emitterCells = [NSArray arrayWithObject:cell];
    [self.view.layer insertSublayer:meteor below:self.moonView.layer];
}
复制代码

奔月开始

做完上面的一切,点击运行,嫦娥奔月的整体效果就出来了。 嫦娥飞天.gif

项目地址戳这里 >>>

推荐使用iPad Pro真机运行,效果最佳

小结

麻雀虽小,五脏俱全。通过这个小小的实践,里面涉及的知识点还是有一些的

并且发现掘金的活动iOSer好像不怎么活跃,相关活动的iOS文章少之又少。希望本篇文章能抛砖引玉,把更多的iOS大佬们吸引过来,让其他端的小伙伴可以见识到我们移动端的魅力~

最后,再次祝各位老铁中秋愉快,单身的小伙伴们早日收获爱情

多多点赞 + 评论哦~

你懂得.webp

文章分类
iOS
文章标签