酷炫粒子效果合成图片

参考代码 酷炫粒子效果合成图片 code4app

为了实现项目中的一些特效,最近就扒了一些代码,无意中发现了这个特效,感觉效果很酷,本来感觉实现起来应该是非常有难度的,但是看了源码过后,发现实现的方式十分的简单。主要的步骤就是:先把图片分解为像素点,然后再利用 CADisplayLink 与屏幕同步刷新动画。源代码是OC 的,而我需要使用 Swfit 版的,所以就将整个代码又用 Swift3.0 写了一遍。由于 Swift 的一些原因,中途也遇见到了一些问题,所以就在此再记录一遍,温故而知新吗!

这是最终的效果图,粒子的发射速度,时间,起点等都是可以控制的。

问题

遇到的一个问题就是,需要如何解析 UnsafeMutableRawPointer 的数据,解决的方法如下

1
2
3
4
5
6
let rawData: UnsafeMutableRawPointer = calloc(imageH*imageW*bytesPerPixel, MemoryLayout.size(ofValue: CChar()))
.
.
.
let bufferData = UnsafeRawBufferPointer(start: rawData, count: imageH*imageW*bytesPerPixel)

通过 UnsafeRawBufferPointer 来将数据解析出来,count 后是数据所占的字节大小

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//
// BZEmitterLayer.swift
// BZEmitter
//
// Copyright © 2017 SSBun. All rights reserved.
//

import Foundation
import QuartzCore
import UIKit

struct BZParticle {
var color: UIColor
var point: CGPoint
var customColor: UIColor? {
set {
if let value = newValue {
color = value
}
}
get {
return color
}
}
var randomPointRange: CGFloat? {
set {
let value = newValue ?? 0
if value != 0 {
point.x = point.x - value + CGFloat(arc4random_uniform(UInt32(value) * 2))
point.y = point.y - value + CGFloat(arc4random_uniform(UInt32(value) * 2))
}
}
get {
return 0
}
}
let delayTime: UInt32 = arc4random_uniform(30)
let delayDuration: UInt32 = arc4random_uniform(10)
}


protocol BZEmitterLayerDelegate {
func emitterLayerEndAnimation()
}

class BZEmitterLayer: CALayer {

public var beginPoint: CGPoint = .zero// 粒子发射起点
public var ignoredBlack: Bool = false// 忽略黑色粒子
public var ignoredWhite: Bool = false// 忽略白色粒子
public var customColor: UIColor? // 覆盖原粒子颜色,最后会是一个图片的纯色图片
public var randomPointRange: CGFloat = 0// 不能等于0

public var maxParticleCount: UInt32 = 0 // 每行最大的粒子数量
public var image: UIImage? { // 待渲染的图片
didSet {
if let image = image {
particleArray = self.getRGBAs(from: image)
}
}
}
public var emitterDelegate: BZEmitterLayerDelegate?

private var animationTime: CGFloat = 0
private var animationDuration: CGFloat = 2
private var displayLink:CADisplayLink?
private var particleArray:[BZParticle] = []


override init() {
super.init()
self.masksToBounds = false
displayLink = CADisplayLink(target: self, selector: #selector(BZEmitterLayer.emitterAnimation))
displayLink?.add(to: .current, forMode: .commonModes)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func emitterAnimation() {
self.setNeedsDisplay()
animationTime += 0.6
}

override func draw(in ctx: CGContext) {
var count = 0
for particle in particleArray {
if CGFloat(particle.delayTime) > animationTime {
continue
}
var curTime = animationTime - CGFloat(particle.delayTime)
if curTime > animationDuration + CGFloat(particle.delayDuration) {
curTime = animationDuration + CGFloat(particle.delayDuration)
count += 1
}
let curX = self.easeInOutQuad(curTime, beginPoint.x, particle.point.x + self.bounds.size.width/2 - CGFloat(image!.cgImage!.width/2), animationDuration + CGFloat(particle.delayDuration))
let curY = self.easeInOutQuad(curTime, beginPoint.y, particle.point.y + self.bounds.size.height/2 - CGFloat(image!.cgImage!.height/2), animationDuration + CGFloat(particle.delayDuration))

ctx.addRect(CGRect(x:curX, y:curY, width:1, height:1))
let components = particle.color.cgColor.components!
ctx.setFillColor(red: components[0], green: components[1], blue: components[2], alpha: components[3])
ctx.fillPath()
}
if (count == particleArray.count) {
self.reset()
self.emitterDelegate?.emitterLayerEndAnimation()
}
}

func easeInOutQuad(_ time: CGFloat, _ begin: CGFloat, _ end: CGFloat, _ duration: CGFloat) -> CGFloat {
let coverDistance = end - begin
var newTime = time / (duration/2)
if newTime < 1 {
return coverDistance/2.0 * pow(newTime, 2) + begin
}
newTime -= 1
return -coverDistance/2.0 * (newTime * (newTime - 2) - 1) + begin
}


func getRGBAs(from image: UIImage) -> [BZParticle] {
let imageRef = image.cgImage!
let imageW = imageRef.width
let imageH = imageRef.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bytesPerPixel = 4 // 一个像素4个字节
let bytesPerRow = bytesPerPixel * imageW
let rawData: UnsafeMutableRawPointer = calloc(imageH*imageW*bytesPerPixel, MemoryLayout.size(ofValue: CChar()))
let bitsPerComponent = 8
let context = CGContext(data: rawData, width: imageW, height: imageH, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageByteOrderInfo.order32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue)
context?.draw(imageRef, in: CGRect(x: 0, y: 0, width: imageW, height: imageH))


let addY = maxParticleCount == 0 ? 1 : imageH / Int(maxParticleCount)
let addX = maxParticleCount == 0 ? 1 : imageW / Int(maxParticleCount)
var result = [BZParticle]()
let bufferData = UnsafeRawBufferPointer(start: rawData, count: imageH*imageW*bytesPerPixel)

for y in stride(from: 0, to: imageH, by: addY) {
for x in stride(from: 0, to: imageW, by: addX) {
let byteIndex = bytesPerRow*y + bytesPerPixel*x
let red = CGFloat(bufferData[byteIndex]) / 255.0
let green = CGFloat(bufferData[byteIndex + 1]) / 255.0
let blue = CGFloat(bufferData[byteIndex + 2]) / 255.0
let alpha = CGFloat(bufferData[byteIndex + 3]) / 255.0

if alpha == 0 || (ignoredWhite && (red+green+blue == 3)) || (ignoredBlack && (red+green+blue == 0)) {
continue
}
let color = UIColor(red: red, green: green, blue: blue, alpha: alpha)
let point = CGPoint(x: x, y: y)
var particle = BZParticle(color: color, point: point)
if let custom = customColor {
particle.customColor = custom
}
if randomPointRange > 0 {
particle.randomPointRange = randomPointRange
}
result.append(particle)
}
}
free(rawData)
return result
}

func pause() {
displayLink?.isPaused = true
}

func resume() {
displayLink?.isPaused = false
}

func reset() {
displayLink?.invalidate()
displayLink = nil
animationTime = 0
}

func restart() {
self.reset()
displayLink = CADisplayLink(target: self, selector: #selector(BZEmitterLayer.emitterAnimation))
displayLink?.add(to: .current, forMode: .commonModes)
}
}

Demo 地址