git.iko.soy/metal-metaballs
on master help sign in
Merged feature-presentation into dev
commit923b8f4b889e27b1a9423bdd362fa7f69e175f37 authorvinivendra <vinivendra@gmail.com> dateMon 29 Feb 2016, 17:29 (11 years ago) parent120a2c46eba596
12 files changed+353−70
Examples/Animation.gif PixelImage.xcodeproj/project.pbxproj+11−3 PixelImage/Dispatch.swift+6 PixelImage/Interpolation.swift+61 PixelImage/Matrix.swift−20 PixelImage/Metaball.swift+60 PixelImage/MetalMetaballRenderer.swift+8−8 PixelImage/UIColor.swift+6 PixelImage/UIKit.swift+79 PixelImage/ViewController.swift+94−31 PixelImage/shaders.metal+9−6 README.md+19−2
Examples/Animation.gifnew file

Binary file not shown.

PixelImage.xcodeproj/project.pbxproj+11−3
@@ -22,6 +22,8 @@
2222 24E1E7F01C67D74300ECF1C4 /* Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */; };
2323 24E1E7F11C67D74300ECF1C4 /* shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */; };
2424 24ED21CD1C6C2397009C40FC /* MetaballDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */; };
25+ 24FC38341C7F931E0085C04A /* Metaball.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FC38331C7F931E0085C04A /* Metaball.swift */; };
26+ 24FC38361C7F9ABE0085C04A /* Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FC38351C7F9ABE0085C04A /* Dispatch.swift */; };
2527 24FFA86B1C6BA038005ACD60 /* ApproximationOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */; };
2628 24FFA86C1C6BA038005ACD60 /* Collections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84D1C6BA038005ACD60 /* Collections.swift */; };
2729 24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */; };
@@ -71,6 +73,8 @@
7173 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metal.swift; sourceTree = "<group>"; };
7274 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = shaders.metal; sourceTree = "<group>"; };
7375 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaballDataSource.swift; sourceTree = "<group>"; };
76+ 24FC38331C7F931E0085C04A /* Metaball.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metaball.swift; sourceTree = "<group>"; };
77+ 24FC38351C7F9ABE0085C04A /* Dispatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatch.swift; sourceTree = "<group>"; };
7478 24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApproximationOperator.swift; sourceTree = "<group>"; };
7579 24FFA84D1C6BA038005ACD60 /* Collections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collections.swift; sourceTree = "<group>"; };
7680 24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComparatorChains.swift; sourceTree = "<group>"; };
@@ -143,13 +147,15 @@
143147 children = (
144148 24FFA84B1C6BA038005ACD60 /* SwiftSugar */,
145149 242686871C63B72A00D62456 /* AppDelegate.swift */,
146− 242686891C63B72A00D62456 /* ViewController.swift */,
150+ 24FC38351C7F9ABE0085C04A /* Dispatch.swift */,
147151 24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */,
148152 246B0F821C70635D00A71D05 /* UIColor.swift */,
149− 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */,
150− 24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */,
153+ 24FC38331C7F931E0085C04A /* Metaball.swift */,
151154 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */,
152155 249CF84B1C6D113C008B0C78 /* Matrix.swift */,
156+ 242686891C63B72A00D62456 /* ViewController.swift */,
157+ 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */,
158+ 24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */,
153159 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */,
154160 2426868B1C63B72A00D62456 /* Main.storyboard */,
155161 2426868E1C63B72A00D62456 /* Assets.xcassets */,
@@ -383,6 +389,7 @@
383389 24FFA8771C6BA038005ACD60 /* ChunkWindowSplit.swift in Sources */,
384390 24FFA86F1C6BA038005ACD60 /* NSNumber.swift in Sources */,
385391 2461651B1C6DA4F400A913C8 /* Interpolation.swift in Sources */,
392+ 24FC38341C7F931E0085C04A /* Metaball.swift in Sources */,
386393 24FFA8861C6BA038005ACD60 /* TestCommons.swift in Sources */,
387394 24FFA8741C6BA038005ACD60 /* SignOperator.swift in Sources */,
388395 24FFA8821C6BA038005ACD60 /* ScanReduce.swift in Sources */,
@@ -390,6 +397,7 @@
390397 24FFA8831C6BA038005ACD60 /* Slicing.swift in Sources */,
391398 24FFA8721C6BA038005ACD60 /* Sequence.swift in Sources */,
392399 24FFA87F1C6BA038005ACD60 /* Iterate.swift in Sources */,
400+ 24FC38361C7F9ABE0085C04A /* Dispatch.swift in Sources */,
393401 24FFA8761C6BA038005ACD60 /* Categorise.swift in Sources */,
394402 24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */,
395403 24FFA8711C6BA038005ACD60 /* PowerOperator.swift in Sources */,
PixelImage/Dispatch.swiftnew file+6
@@ -0,0 +1,6 @@
1+
2+import Dispatch
3+
4+func delay(duration: Float = 1, block: () -> ()) {
5+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(duration * Float(NSEC_PER_SEC))), dispatch_get_main_queue(), block)
6+}
PixelImage/Interpolation.swift+61
@@ -1,3 +1,6 @@
1+
2+import Darwin
3+
14func interpolateCubeEaseIn<T: Numeric>(linear: T) -> T {
25 return linear ^ 3
36}
@@ -13,3 +16,61 @@ func interpolateSquareEaseOut<T: Numeric>(linear: T) -> T {
1316func interpolateSmooth<T: Numeric>(linear: T) -> T {
1417 return ((linear) * (linear) * (linear) * ((linear) * ((linear) * 6 - 15) + 10))
1518}
19+
20+func createStringKeyframes(bounces: Int = 2, elasticity: Double = 1.1, bounceSpeed: Double) -> [(time: Double, position: Double)] {
21+ // Setup keyframes
22+ var keyFrames: [(time: Double, position: Double)] = [(1, 1)]
23+
24+ let overreach = elasticity - 1
25+
26+ var currentBounceTime: Double = overreach / (bounceSpeed ^ Double(bounces))
27+ var bounceDirection = Double((bounces % 2) * 2 - 1)
28+ var bounceReach = overreach / (2 ^ Double(bounces - 1))
29+ var currentBouncePosition: Double = 1.0 + bounceDirection * bounceReach
30+
31+ print(currentBounceTime)
32+ print(bounceDirection)
33+ print(bounceReach)
34+ print(currentBouncePosition)
35+
36+ keyFrames.append((1.0 - currentBounceTime, currentBouncePosition))
37+
38+ for i in (1..<bounces).reverse() {
39+ currentBounceTime += overreach / (bounceSpeed ^ Double(i + 1))
40+ currentBounceTime += overreach / (bounceSpeed ^ Double(i))
41+ bounceDirection = Double((i % 2) * 2 - 1)
42+ bounceReach = overreach / (2 ^ Double(i - 1))
43+ currentBouncePosition = 1.0 + bounceDirection * bounceReach
44+
45+ keyFrames.append((1.0 - currentBounceTime, currentBouncePosition))
46+
47+ print(currentBounceTime)
48+ print(bounceDirection)
49+ print(bounceReach)
50+ print(currentBouncePosition)
51+ }
52+
53+ keyFrames.append((0, 0))
54+
55+ var reversed = [(time: Double, position: Double)]()
56+ for keyFrame in keyFrames.reverse() {
57+ reversed.append(keyFrame)
58+ }
59+
60+ return reversed
61+}
62+
63+func interpolateSpring<T: Numeric>(linear: T, bounces: Int = 2, elasticity: Double = 1.1, bounceSpeed: Double = 1.0) -> T {
64+ let keyFrames: [(time: Double, position: Double)]
65+ keyFrames = createStringKeyframes(bounces, elasticity: elasticity, bounceSpeed: bounceSpeed)
66+
67+ let x = linear.toDouble
68+
69+ let startFrame = keyFrames.filter { $0.time < x }.last ?? (0, 0)
70+ let endFrame = keyFrames.filter { $0.time > x }.first ?? (1, 1)
71+ let adjustedX = (x - startFrame.time) / (endFrame.time - startFrame.time)
72+ let interpolatedX = interpolateSmooth(adjustedX)
73+ let position = startFrame.position + interpolatedX * (endFrame.position - startFrame.position)
74+
75+ return T.fromDouble(position)
76+}
PixelImage/Matrix.swift−20
@@ -70,23 +70,3 @@ struct Matrix: CustomStringConvertible {
7070 }
7171 }
7272}
73−
74−class EdgeAnimationParameters {
75− let startDate: NSDate
76− let duration: Float
77− let fadeIn: Bool
78− let i: Int
79− let j: Int
80−
81− func unpack() -> (startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
82− return (startDate, duration, fadeIn, i, j)
83− }
84−
85− init(startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
86− self.startDate = startDate
87− self.duration = duration
88− self.fadeIn = fadeIn
89− self.i = i
90− self.j = j
91− }
92−}
PixelImage/Metaball.swiftnew file+60
@@ -0,0 +1,60 @@
1+
2+import UIKit
3+
4+class Metaball: UIView {
5+
6+ var color: UIColor
7+
8+ init(position: CGPoint, color: UIColor = UIColor(randomFlatColorOfShadeStyle: .Light)) {
9+ self.color = color
10+ super.init(frame: CGRect.zero)
11+
12+ size = CGSize(50)
13+ middle = position
14+ }
15+
16+ required init?(coder aDecoder: NSCoder) {
17+ self.color = UIColor(randomFlatColorOfShadeStyle: .Light)
18+ super.init(coder: aDecoder)
19+ }
20+}
21+
22+class EdgeAnimationParameters {
23+ let startDate: NSDate
24+ let duration: Float
25+ let fadeIn: Bool
26+ let i: Int
27+ let j: Int
28+
29+ func unpack() -> (startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
30+ return (startDate, duration, fadeIn, i, j)
31+ }
32+
33+ init(startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
34+ self.startDate = startDate
35+ self.duration = duration
36+ self.fadeIn = fadeIn
37+ self.i = i
38+ self.j = j
39+ }
40+}
41+
42+class VertexAnimationParameters {
43+ let startDate: NSDate
44+ let duration: Float
45+ let origin: CGPoint
46+ let destination: CGPoint
47+ let metaball: Metaball
48+
49+ func unpack() -> (startDate: NSDate, duration: Float, origin: CGPoint, destination: CGPoint, metaball: Metaball) {
50+ return (startDate, duration, origin, destination, metaball)
51+ }
52+
53+ init(startDate: NSDate, duration: Float, origin: CGPoint, destination: CGPoint, metaball: Metaball) {
54+ self.startDate = startDate
55+ self.duration = duration
56+ self.origin = origin
57+ self.destination = destination
58+ self.metaball = metaball
59+ }
60+}
PixelImage/MetalMetaballRenderer.swift+8−8
@@ -2,7 +2,7 @@ import Metal
22import UIKit
33import ChameleonFramework
44
5−class MetalMetaballRenderer {
5+@objc class MetalMetaballRenderer: NSObject {
66
77 typealias TargetView = UIImageView
88
@@ -47,7 +47,7 @@ class MetalMetaballRenderer {
4747 idleComputeContext = MTLComputeContext(size: targetView.size, texture: texture2)
4848 }
4949
50− func updateTargetView() {
50+ internal func updateTargetView() {
5151
5252 let timeout = dispatch_time(DISPATCH_TIME_NOW, 1000000000)
5353 dispatch_semaphore_wait(semaphore, timeout)
@@ -81,7 +81,7 @@ class MetalMetaballRenderer {
8181 }
8282 }
8383
84− func renderToContext(computeContext: MTLComputeContext) {
84+ internal func renderToContext(computeContext: MTLComputeContext) {
8585 let width = Int(targetView.width)
8686 let height = Int(targetView.height)
8787
@@ -113,11 +113,11 @@ class MetalMetaballRenderer {
113113 commandBuffer.commit()
114114 commandBuffer.waitUntilCompleted()
115115 } catch _ {
116− print("Error!")
116+ assertionFailure()
117117 }
118118 }
119119
120− func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
120+ internal func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
121121 let texture = computeContext.texture
122122
123123 // Get image info
@@ -152,7 +152,7 @@ class MetalMetaballRenderer {
152152 return image
153153 }
154154
155− func metaballEdgesBuffer() -> MTLBuffer {
155+ internal func metaballEdgesBuffer() -> MTLBuffer {
156156 let floats = dataSource.metaballGraph.adjacencyMatrix.buffer
157157
158158 let bufferLength = floats.count * sizeof(Float)
@@ -162,7 +162,7 @@ class MetalMetaballRenderer {
162162 return buffer
163163 }
164164
165− func metaballBuffer() -> MTLBuffer {
165+ internal func metaballBuffer() -> MTLBuffer {
166166 // Create Float array for buffer
167167 // Exclude metaballs far from the view's bounds
168168 let border: CGFloat = 100
@@ -179,7 +179,7 @@ class MetalMetaballRenderer {
179179 array.append(Float(x))
180180 array.append(Float(y))
181181
182− let color = metaball.backgroundColor!
182+ let color = metaball.color
183183 let components = color.components
184184 array.append(Float(components.red))
185185 array.append(Float(components.green))
PixelImage/UIColor.swift+6
@@ -7,3 +7,9 @@ extension UIColor {
77 return (r, g, b, a)
88 }
99}
10+
11+extension UIColor {
12+ convenience init(red255 red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
13+ self.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha / 255)
14+ }
15+}
PixelImage/UIKit.swift+79
@@ -1,3 +1,5 @@
1+// TODO: Unite UIView and CALayer extensions under a common protocol
2+
13import UIKit
24
35func - (left: CGPoint, right: CGPoint) -> CGVector {
@@ -138,3 +140,80 @@ extension UIView {
138140 }
139141 }
140142}
143+
144+extension CALayer {
145+ var origin: CGPoint {
146+ get {
147+ return frame.origin
148+ }
149+ set {
150+ frame.origin = newValue
151+ }
152+ }
153+
154+ var size: CGSize {
155+ get {
156+ return frame.size
157+ }
158+ set {
159+ frame.size = newValue
160+ }
161+ }
162+
163+ var width: CGFloat {
164+ get {
165+ return frame.width
166+ }
167+ set {
168+ frame.size = CGSize(width: newValue, height: height)
169+ }
170+ }
171+
172+ var height: CGFloat {
173+ get {
174+ return frame.height
175+ }
176+ set {
177+ frame.size = CGSize(width: width, height: newValue)
178+ }
179+ }
180+
181+ var middle: CGPoint {
182+ get {
183+ return CGPoint(x: origin.x + width / 2, y: origin.y + height / 2)
184+ }
185+ set {
186+ origin = CGPoint(x: newValue.x - width / 2, y: newValue.y - height / 2)
187+ }
188+ }
189+
190+ var midX: CGFloat {
191+ get {
192+ return middle.x
193+ }
194+ set {
195+ middle = CGPoint(x: newValue, y: midY)
196+ }
197+ }
198+
199+ var midY: CGFloat {
200+ get {
201+ return middle.y
202+ }
203+ set {
204+ middle = CGPoint(x: midX, y: newValue)
205+ }
206+ }
207+}
208+
209+let screenRect = UIScreen.mainScreen().bounds
210+let screenWidth = screenRect.size.width
211+let screenHeight = screenRect.size.height
212+
213+func * <T: Numeric>(vector: CGVector, scalar: T) -> CGVector {
214+ return CGVector(dx: vector.dx * CGFloat(scalar.toDouble), dy: vector.dy * CGFloat(scalar.toDouble))
215+}
216+
217+func + (point: CGPoint, vector: CGVector) -> CGPoint {
218+ return CGPoint(x: point.x + vector.dx, y: point.y + vector.dy)
219+}
PixelImage/ViewController.swift+94−31
@@ -18,34 +18,85 @@ class ViewController: UIViewController, MetaballDataSource {
1818 override func viewDidLoad() {
1919 super.viewDidLoad()
2020
21+ view.backgroundColor = UIColor.whiteColor()
22+
2123 let recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
2224 view.addGestureRecognizer(recognizer)
2325
24− let metaballs = [CGPoint(x: 70, y: 70), CGPoint(x: 270, y: 70), CGPoint(x: 270, y: 470), CGPoint(x: 70, y: 470)].map {
25− Metaball(position: $0)
26+ var positions = [CGPoint]()
27+ positions.append(CGPoint(x: Double(screenWidth) / 2, y: Double(screenHeight) / 2))
28+ for i in 1...5 {
29+ let sine = sin(Double(i) * 2 * M_PI / 5)
30+ let cosine = cos(Double(i) * 2 * M_PI / 5)
31+ let radius: Double = 150
32+ let x = Double(screenWidth) / 2 + sine * radius
33+ let y = Double(screenHeight) / 2 + cosine * radius
34+ positions.append(CGPoint(x: x, y: y))
2635 }
36+
37+ let colors =
38+ [UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0),
39+ UIColor(red255: 250, green: 235, blue: 50, alpha: 1.0),
40+ UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0),
41+ UIColor(red255: 90, green: 170, blue: 170, alpha: 1.0),
42+ UIColor(red255: 90, green: 170, blue: 170, alpha: 1.0),
43+ UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0)]
44+ let metaballs = zip(positions, colors).map { Metaball(position: $0.0, color: $0.1) }
45+
2746 metaballGraph = Graph(vertices: metaballs)
28− metaballGraph.addEdge(0, 1)
29− metaballGraph.addEdge(0, 3)
30− metaballGraph.addEdge(1, 2)
31− metaballGraph.addEdge(1, 3)
32− metaballGraph.addEdge(2, 3)
33−
34− let border = 20
35− let metaballViewFrame = CGRect(x: border / 2, y: border / 2, width: width, height: height)
47+
48+ let metaballViewFrame = screenRect
3649 renderer = MetalMetaballRenderer(dataSource: self, frame: metaballViewFrame)
3750 metaballView = renderer.targetView
38−
39− let bigView = UIView(frame: CGRect(x: 10, y: 70, width: width + border, height: height + border))
40− bigView.backgroundColor = UIColor.redColor()
41− bigView.addSubview(metaballView)
42− view.addSubview(bigView)
51+ view.addSubview(metaballView)
4352
4453 for metaball in metaballs {
4554 metaballView.addSubview(metaball)
4655 }
4756
48− addEdge(0, 2)
57+ delay(1.0) {
58+ self.addEdge(0, 1)
59+ self.addEdge(0, 2)
60+ self.addEdge(0, 3)
61+ self.addEdge(0, 4)
62+ self.addEdge(0, 5)
63+ }
64+
65+ delay(2.3) {
66+ self.removeEdge(0, 1)
67+ self.removeEdge(0, 2)
68+ self.removeEdge(0, 3)
69+ self.removeEdge(0, 4)
70+ self.removeEdge(0, 5)
71+ }
72+
73+ delay(4) {
74+ for i in 1...5 {
75+ UIView.animateWithDuration(1.0, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
76+ let sine = sin(Double(i) * 2 * M_PI / 5)
77+ let cosine = cos(Double(i) * 2 * M_PI / 5)
78+ let radius: Double = 85
79+ let x = Double(screenWidth) / 2 + sine * radius
80+ let y = Double(screenHeight) / 2 + cosine * radius
81+ let metaball = metaballs[i]
82+ self.animateMetaball(metaball, toPoint: CGPoint(x: x, y: y))
83+ }, completion: nil)
84+ }
85+ }
86+
87+ delay(6) { () -> () in
88+ for i in 1...5 {
89+ UIView.animateWithDuration(1.0, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
90+ let sine = sin(Double(i) * 2 * M_PI / 5)
91+ let cosine = cos(Double(i) * 2 * M_PI / 5)
92+ let radius: Double = 150
93+ let x = Double(screenWidth) / 2 + sine * radius
94+ let y = Double(screenHeight) / 2 + cosine * radius
95+ let metaball = metaballs[i]
96+ self.animateMetaball(metaball, toPoint: CGPoint(x: x, y: y))
97+ }, completion: nil)
98+ }
99+ }
49100
50101 renderer.state = .Running
51102 }
@@ -58,6 +109,33 @@ class ViewController: UIViewController, MetaballDataSource {
58109 animateEdge(i, j, fadeIn: false)
59110 }
60111
112+ func animateMetaball(metaball: Metaball, toPoint destination: CGPoint) {
113+ let parameters = VertexAnimationParameters(startDate: NSDate(), duration: 1.0, origin: metaball.center, destination: destination, metaball: metaball)
114+ NSTimer.scheduledTimerWithTimeInterval(1.0 / 60.0, target: self, selector: "animateMetaballWithTimer:", userInfo: parameters, repeats: true)
115+ }
116+
117+ func animateMetaball(withTimer timer: NSTimer) {
118+ guard let userInfo = timer.userInfo as? VertexAnimationParameters else { preconditionFailure() }
119+ let (animationStart, duration, origin, destination, metaball) = (userInfo).unpack()
120+
121+ let now = NSDate()
122+ var timeElapsed = Float(now.timeIntervalSinceDate(animationStart))
123+
124+ if timeElapsed > duration {
125+ timer.invalidate()
126+ timeElapsed = duration
127+ }
128+
129+ let linearValue = timeElapsed / duration
130+ let interpolatedValue = interpolateSmooth(linearValue)
131+ print(interpolatedValue)
132+
133+ let position = origin + ((destination - origin) * interpolatedValue)
134+ metaball.center = position
135+
136+ renderer.state = .Running
137+ }
138+
61139 func animateEdge(i: Int, _ j: Int, fadeIn: Bool) {
62140 let parameters = EdgeAnimationParameters(startDate: NSDate(), duration: edgeAnimationDuration, fadeIn: fadeIn, i: i, j: j)
63141 NSTimer.scheduledTimerWithTimeInterval(1.0 / 60.0, target: self, selector: "animateEdgeWithTimer:", userInfo: parameters, repeats: true)
@@ -107,18 +185,3 @@ class ViewController: UIViewController, MetaballDataSource {
107185 }
108186
109187}
110−
111−class Metaball: UIView {
112−
113− init(position: CGPoint) {
114− super.init(frame: CGRect.zero)
115−
116− size = CGSize(50)
117− middle = position
118− backgroundColor = UIColor(randomFlatColorOfShadeStyle: .Light)
119− }
120−
121− required init?(coder aDecoder: NSCoder) {
122− super.init(coder: aDecoder)
123− }
124−}
PixelImage/shaders.metal+9−6
@@ -33,9 +33,9 @@ kernel void
3333 for (x = 1; x <= numberOfMetaballs; x += 1) {
3434 metaballData metaball = metaballBuffer[x];
3535 float2 metaballPosition = float2(metaball.x, metaball.y);
36− float2 vector
37− = float2(metaballPosition.x - gid.x, metaballPosition.y - gid.y);
38− float squaredDistance = dot(vector, vector);
36+ float2 vector = float2(metaballPosition.x - gid.x, metaballPosition.y - gid.y);
37+ float dotProduct = dot(vector, vector);
38+ float squaredDistance = dotProduct > 0 ? dotProduct : 1;
3939 float realDistance = sqrt(squaredDistance);
4040 metaballDistances[x - 1] = squaredDistance;
4141 metaballDirections[x - 1] = vector / realDistance;
@@ -48,7 +48,7 @@ kernel void
4848
4949 for (x = 0; x < numberOfMetaballs; x += 1) {
5050 float distance1 = metaballDistances[x];
51− float value1 = 2048 / (distance1 + 1);
51+ float value1 = 1500 / distance1;
5252 float2 direction1 = metaballDirections[x];
5353
5454 float colorContribution = 1 / distance1;
@@ -57,7 +57,7 @@ kernel void
5757
5858 for (y = x + 1; y < numberOfMetaballs; y += 1) {
5959 float distance2 = metaballDistances[y];
60− float value2 = 2048 / (distance2 + 1);
60+ float value2 = 1500 / distance2;
6161 float2 direction2 = metaballDirections[y];
6262
6363 float v = value1 + value2;
@@ -109,8 +109,11 @@ kernel void
109109 colorSum = colorSumLink;
110110 }
111111
112−
113112 colorSum *= result;
114113
114+ if (colorSum.r == 0 && colorSum.g == 0 && colorSum.b == 0) {
115+ colorSum = float3(1, 1, 1);
116+ }
117+
115118 outTexture.write(float4(colorSum.bgr, 1), gid);
116119}
README.md+19−2
@@ -1,2 +1,19 @@
1−# metal-metaballs
2−A 'playground' project for rendering metaball-like graphics in metal.
1+![Physics simulation](https://raw.githubusercontent.com/vinivendra/metal-metaballs/master/Examples/Animation.gif)
2+
3+# Metal Metaballs
4+
5+Metal Metaballs is a framework for rendering graphs in an aesthetically pleasing way. It uses an adapted version of [metaball](https://en.wikipedia.org/wiki/Metaballs) math to create fluid graphics, allow vertices to merge with each other and allow edges to flow and animate smoothly.
6+
7+Vertices and edges may be added and removed at will and can interact with gestures. Rendering a fullscreen view continuously uses only about 19% of the CPU at 60 fps, though normally the view will only be updated when needed. All rendering is done in a background thread, so the main thread remains almost completely free. These measurements were done using an iPhone 6S Plus (which has more pixels to calculate but more power to do so), so YMMV.
8+
9+This framework is being developed to make me learn several different design patters, APIs and features used in iOS and in graphics applications. So far, it uses:
10+
11+- [**Metaball**](https://en.wikipedia.org/wiki/Metaballs) math, in an adapted version, to render the graphics.
12+- [**Metal**](https://developer.apple.com/metal/) compute shaders to draw the metaballs dynamically.
13+- [**Grand Central Dispatch**](https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/) for doing CPU and GPU calculations simultaneously.
14+- [**Double buffering**](https://en.wikipedia.org/wiki/Multiple_buffering), to speed up rendering times.
15+- Different [**interpolations**](http://flexmonkey.blogspot.ca/2016/01/playing-with-interpolation-functions-in.html) for animations.
16+
17+## Installation
18+
19+Just download the app and run it on your metal-enabled device of choice :)