git.iko.soy/metal-metaballs
on master help sign in
Merged feature-async-rendering into dev
commit7288e923f66e0984cea79148e2e27d074ebc1634 authorvinivendra <vinivendra@gmail.com> dateWed 10 Feb 2016, 21:41 (11 years ago) parente27c2dc83725bc
7 files changed+141−216
PixelImage.xcodeproj/project.pbxproj+4−8 PixelImage/ImageMetaballRenderer.swift−115 PixelImage/MetaballDataSource.swift+6 PixelImage/MetaballRenderer.swift−17 PixelImage/Metal.swift+30 PixelImage/MetalMetaballRenderer.swift+84−54 PixelImage/ViewController.swift+17−22
PixelImage.xcodeproj/project.pbxproj+4−8
@@ -13,13 +13,12 @@
1313 2426868F1C63B72A00D62456 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2426868E1C63B72A00D62456 /* Assets.xcassets */; };
1414 242686921C63B72A00D62456 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 242686901C63B72A00D62456 /* LaunchScreen.storyboard */; };
1515 24E1E7D71C67C8B200ECF1C4 /* UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */; };
16− 24E1E7D91C67C9C900ECF1C4 /* MetaballRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7D81C67C9C900ECF1C4 /* MetaballRenderer.swift */; };
17− 24E1E7DB1C67CA3600ECF1C4 /* ImageMetaballRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7DA1C67CA3600ECF1C4 /* ImageMetaballRenderer.swift */; };
1816 24E1E7E21C67D37F00ECF1C4 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E1E7E11C67D37F00ECF1C4 /* Metal.framework */; };
1917 24E1E7E41C67D39300ECF1C4 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E1E7E31C67D39300ECF1C4 /* MetalKit.framework */; };
2018 24E1E7EB1C67D70300ECF1C4 /* MetalMetaballRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */; };
2119 24E1E7F01C67D74300ECF1C4 /* Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */; };
2220 24E1E7F11C67D74300ECF1C4 /* shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */; };
21+ 24ED21CD1C6C2397009C40FC /* MetaballDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */; };
2322 24FFA86B1C6BA038005ACD60 /* ApproximationOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */; };
2423 24FFA86C1C6BA038005ACD60 /* Collections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84D1C6BA038005ACD60 /* Collections.swift */; };
2524 24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */; };
@@ -59,13 +58,12 @@
5958 242686911C63B72A00D62456 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
6059 242686931C63B72A00D62456 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6160 24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKit.swift; sourceTree = "<group>"; };
62− 24E1E7D81C67C9C900ECF1C4 /* MetaballRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaballRenderer.swift; sourceTree = "<group>"; };
63− 24E1E7DA1C67CA3600ECF1C4 /* ImageMetaballRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageMetaballRenderer.swift; sourceTree = "<group>"; };
6461 24E1E7E11C67D37F00ECF1C4 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
6562 24E1E7E31C67D39300ECF1C4 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
6663 24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetalMetaballRenderer.swift; sourceTree = "<group>"; };
6764 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metal.swift; sourceTree = "<group>"; };
6865 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = shaders.metal; sourceTree = "<group>"; };
66+ 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaballDataSource.swift; sourceTree = "<group>"; };
6967 24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApproximationOperator.swift; sourceTree = "<group>"; };
7068 24FFA84D1C6BA038005ACD60 /* Collections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collections.swift; sourceTree = "<group>"; };
7169 24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComparatorChains.swift; sourceTree = "<group>"; };
@@ -134,8 +132,7 @@
134132 242686871C63B72A00D62456 /* AppDelegate.swift */,
135133 242686891C63B72A00D62456 /* ViewController.swift */,
136134 24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */,
137− 24E1E7D81C67C9C900ECF1C4 /* MetaballRenderer.swift */,
138− 24E1E7DA1C67CA3600ECF1C4 /* ImageMetaballRenderer.swift */,
135+ 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */,
139136 24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */,
140137 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */,
141138 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */,
@@ -289,13 +286,13 @@
289286 24FFA8861C6BA038005ACD60 /* TestCommons.swift in Sources */,
290287 24FFA8741C6BA038005ACD60 /* SignOperator.swift in Sources */,
291288 24FFA8821C6BA038005ACD60 /* ScanReduce.swift in Sources */,
289+ 24ED21CD1C6C2397009C40FC /* MetaballDataSource.swift in Sources */,
292290 24FFA8831C6BA038005ACD60 /* Slicing.swift in Sources */,
293291 24FFA8721C6BA038005ACD60 /* Sequence.swift in Sources */,
294292 24FFA87F1C6BA038005ACD60 /* Iterate.swift in Sources */,
295293 24FFA8761C6BA038005ACD60 /* Categorise.swift in Sources */,
296294 24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */,
297295 24FFA8711C6BA038005ACD60 /* PowerOperator.swift in Sources */,
298− 24E1E7D91C67C9C900ECF1C4 /* MetaballRenderer.swift in Sources */,
299296 24E1E7D71C67C8B200ECF1C4 /* UIKit.swift in Sources */,
300297 24FFA8851C6BA038005ACD60 /* Zip.swift in Sources */,
301298 24FFA8791C6BA038005ACD60 /* Cycle.swift in Sources */,
@@ -304,7 +301,6 @@
304301 24FFA87D1C6BA038005ACD60 /* Hopping.swift in Sources */,
305302 24E1E7EB1C67D70300ECF1C4 /* MetalMetaballRenderer.swift in Sources */,
306303 24FFA87A1C6BA038005ACD60 /* Enumerate.swift in Sources */,
307− 24E1E7DB1C67CA3600ECF1C4 /* ImageMetaballRenderer.swift in Sources */,
308304 242686881C63B72A00D62456 /* AppDelegate.swift in Sources */,
309305 24E1E7F01C67D74300ECF1C4 /* Metal.swift in Sources */,
310306 24FFA87C1C6BA038005ACD60 /* FlatMap.swift in Sources */,
PixelImage/ImageMetaballRenderer.swiftdeleted−115
@@ -1,115 +0,0 @@
1−
2−import UIKit
3−
4−class ImageMetaballRenderer: MetaballRenderer {
5−
6− typealias TargetView = UIImageView
7−
8− var supportsDynamicRendering: Bool { get {
9− return false
10− }
11− }
12−
13− let targetView = TargetView()
14−
15− let dataSource: MetaballDataSource
16−
17− required init(dataSource: MetaballDataSource) {
18− self.dataSource = dataSource
19− }
20−
21− func updateTargetView() {
22− let width = Int(targetView.width)
23− let height = Int(targetView.height)
24− let metaballs = dataSource.metaballs
25−
26− let bytesPerPixel = 4;
27− let bytesPerRow = bytesPerPixel * width;
28− let bitsPerComponent = 8;
29−
30− var matrix = PixelMatrix(width: width, height: height)
31− for i in 0..<width {
32− for j in 0..<height {
33− let pixelPosition = CGPoint(x: i, y: j)
34−
35− var sum: CGFloat = 0
36−
37− for combo in metaballs.combos() {
38− let metaball1 = combo[0].middle
39− let metaball2 = combo[1].middle
40−
41− let metaball1Vector = metaball1 - pixelPosition
42− let metaball2Vector = metaball2 - pixelPosition
43−
44− let direction1 = metaball1Vector.normalized
45− let direction2 = metaball2Vector.normalized
46−
47− let cos = direction1 * direction2
48−
49− let value1 = 255000 / (metaball1Vector * metaball1Vector + 1)
50− let value2 = 255000 / (metaball2Vector * metaball2Vector + 1)
51−
52− let v = gradient(value1 + value2)
53− let link = ((1 - cos) * 0.5) ^ 50
54−
55− let weightedLink = 128 * CGFloat(link)
56− let weightedValue = 0.6 * CGFloat(v)
57−
58− sum += weightedValue + weightedLink
59− }
60−
61− let result = threshold(sum)
62−
63− matrix.set(x: i, y: j, r:0, g:result/2, b:result)
64− }
65− }
66−
67− let buffer: UnsafeMutablePointer<Void> = UnsafeMutablePointer(matrix.pixels)
68−
69− let colorSpace = CGColorSpaceCreateDeviceRGB()
70− let context = CGBitmapContextCreate(buffer, width, height, bitsPerComponent, bytesPerRow, colorSpace, CGImageAlphaInfo.PremultipliedLast.rawValue | CGBitmapInfo.ByteOrder32Big.rawValue);
71−
72− let cgImage = CGBitmapContextCreateImage(context)
73− let image = UIImage(CGImage: cgImage!)
74− targetView.image = image
75− }
76−}
77−
78−struct PixelMatrix {
79− var pixels: [UInt8]
80−
81− let width: Int
82− let height: Int
83−
84− init(width: Int, height: Int) {
85− self.width = width
86− self.height = height
87− pixels = [UInt8](count: (4 * width * height), repeatedValue: 255)
88− }
89−
90− mutating func set(x x: Int, y: Int, r: UInt8, g: UInt8, b: UInt8) {
91− let index = 4 * (y * width + x)
92− pixels[index ] = r
93− pixels[index + 1] = g
94− pixels[index + 2] = b
95− }
96−
97− mutating func set(x x: Int, y: Int, white: UInt8) {
98− let index = 4 * (y * width + x)
99− pixels[index ] = white
100− pixels[index + 1] = white
101− pixels[index + 2] = white
102− }
103−}
104−
105−func threshold(value: CGFloat) -> UInt8 {
106− return value < 128 ? 0 : 255
107−}
108−
109−func gradient(value: CGFloat) -> UInt8 {
110− return UInt8(min(max(value, 0), CGFloat(255)))
111−}
112−
113−func thresholdf(value value: CGFloat, cap: CGFloat = 0.5) -> CGFloat {
114− return value < cap ? 0 : 1.0
115−}
PixelImage/MetaballDataSource.swiftnew file+6
@@ -0,0 +1,6 @@
1+
2+import Metal
3+
4+protocol MetaballDataSource {
5+ var metaballs: [Metaball] { get }
6+}
PixelImage/MetaballRenderer.swiftdeleted−17
@@ -1,17 +0,0 @@
1−
2−import UIKit
3−
4−protocol MetaballDataSource {
5− var metaballs: [Metaball] { get }
6−}
7−
8−protocol MetaballRenderer {
9− typealias TargetView: UIView
10−
11− var targetView: TargetView { get }
12− var supportsDynamicRendering: Bool { get }
13−
14− init(dataSource: MetaballDataSource)
15−
16− func updateTargetView()
17−}
PixelImage/Metal.swift+30
@@ -1,4 +1,5 @@
11import Metal
2+import UIKit
23
34class MTLContext {
45 let device: MTLDevice
@@ -11,3 +12,32 @@ class MTLContext {
1112 self.commandQueue = device.newCommandQueue()
1213 }
1314}
15+
16+class MTLComputeContext {
17+ var texture: MTLTexture
18+ var imageBuffer: UnsafeMutablePointer<Void>
19+
20+ init(size: CGSize, texture: MTLTexture) {
21+ let width = Int(size.width)
22+ let height = Int(size.height)
23+ let imageByteCount = width * height * 4
24+ imageBuffer = malloc(imageByteCount)
25+
26+ self.texture = texture
27+ }
28+
29+ func updateToSize(size: CGSize, texture: MTLTexture) {
30+ free(imageBuffer)
31+
32+ let width = Int(size.width)
33+ let height = Int(size.height)
34+ let imageByteCount = width * height * 4
35+ imageBuffer = malloc(imageByteCount)
36+
37+ self.texture = texture
38+ }
39+
40+ deinit {
41+ free(imageBuffer)
42+ }
43+}
PixelImage/MetalMetaballRenderer.swift+84−54
@@ -1,7 +1,3 @@
1−// TODO: Refactor update into 2 calls: metalRendering() and textureToImage()
2−// TODO: Use async dispatch to free up main queue
3−// TODO: Use two textures/images to alternate the 2 calls (like OpenGL double buffers)
4−
51// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
62
73// TODO: Interpolate metaball colors
@@ -11,45 +7,92 @@
117import Metal
128import UIKit
139
14−class MetalMetaballRenderer: MetaballRenderer {
10+var i = 0
1511
16− typealias TargetView = UIImageView
12+class MetalMetaballRenderer {
1713
18− var supportsDynamicRendering: Bool { get {
19− return true
20− }
21− }
14+ typealias TargetView = UIImageView
2215
23− let targetView = TargetView()
24− var previousFrame = CGRect.zero
16+ let targetView: TargetView
2517
2618 let context = MTLContext()
27− var renderingTexture: MTLTexture!
28− var imageBuffer: UnsafeMutablePointer<Void>!
19+ var activeComputeContext: MTLComputeContext
20+ var idleComputeContext: MTLComputeContext
2921
3022 let dataSource: MetaballDataSource
3123
32− required init(dataSource: MetaballDataSource) {
24+ let semaphore = dispatch_semaphore_create(2)
25+
26+ enum RendererState {
27+ case Idle
28+ case Running
29+ case Ending
30+ }
31+
32+ var state: RendererState = .Idle {
33+ willSet {
34+ if state == .Idle {
35+ if newValue == .Running {
36+ updateTargetView()
37+ }
38+ }
39+ }
40+ }
41+
42+ required init(dataSource: MetaballDataSource, frame: CGRect) {
3343 self.dataSource = dataSource
44+
45+ targetView = TargetView(frame: frame)
46+
47+ let width = Int(frame.width)
48+ let height = Int(frame.height)
49+ let textureDescriptor =
50+ MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
51+
52+ let texture1 = context.device.newTextureWithDescriptor(textureDescriptor)
53+ activeComputeContext = MTLComputeContext(size: targetView.size, texture: texture1)
54+ let texture2 = context.device.newTextureWithDescriptor(textureDescriptor)
55+ idleComputeContext = MTLComputeContext(size: targetView.size, texture: texture2)
3456 }
3557
3658 func updateTargetView() {
37− let metaballs = dataSource.metaballs
3859
39− if previousFrame != targetView.frame {
40− previousFrame = targetView.frame
41− updateFrame()
42− }
60+ let timeout = dispatch_time(DISPATCH_TIME_NOW, 1000000000)
61+ dispatch_semaphore_wait(semaphore, timeout)
4362
44− let width = Int(targetView.width)
45− let height = Int(targetView.height)
63+ print("Update \(i++)!")
64+
65+ state = .Ending
4666
47− if renderingTexture == nil {
48− let textureDescriptor =
49− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
67+ dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.rawValue), 0)) { () -> Void in
5068
51− renderingTexture = context.device.newTextureWithDescriptor(textureDescriptor)
69+ let computeContext = self.activeComputeContext
70+ swap(&self.activeComputeContext, &self.idleComputeContext)
71+
72+ // Render graphics to metal texture
73+ self.renderToContext(computeContext)
74+
75+ if self.state != .Running {
76+ self.state = .Idle
77+ } else {
78+ self.updateTargetView()
79+ }
80+
81+ // Transform metal texture into image
82+ let uiimage = self.createImageFromContext(computeContext)
83+
84+ dispatch_semaphore_signal(self.semaphore)
85+
86+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
87+ // Send image to view
88+ self.targetView.image = uiimage
89+ })
5290 }
91+ }
92+
93+ func renderToContext(computeContext: MTLComputeContext) {
94+ let width = Int(targetView.width)
95+ let height = Int(targetView.height)
5396
5497 let kernelFunction: MTLFunction?
5598 let pipeline: MTLComputePipelineState
@@ -63,15 +106,13 @@ class MetalMetaballRenderer: MetaballRenderer {
63106 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
64107 height / threadGroupCounts.height, 1)
65108
66− // Create up-to-date metaball buffer
67− let metaballInfoBuffer = metaballBuffer(metaballs)
68−
69109 // Send commands to metal and render
70110 let commandBuffer = context.commandQueue.commandBuffer()
71111
72112 let commandEncoder = commandBuffer.computeCommandEncoder()
73113 commandEncoder.setComputePipelineState(pipeline)
74− commandEncoder.setTexture(renderingTexture, atIndex: 0)
114+ commandEncoder.setTexture(computeContext.texture, atIndex: 0)
115+ let metaballInfoBuffer = metaballBuffer()
75116 commandEncoder.setBuffer(metaballInfoBuffer, offset: 0, atIndex: 0)
76117 commandEncoder.dispatchThreadgroups(threadGroups,
77118 threadsPerThreadgroup: threadGroupCounts)
@@ -83,12 +124,11 @@ class MetalMetaballRenderer: MetaballRenderer {
83124 } catch _ {
84125 print("Error!")
85126 }
86−
87− // Transform new metal texture into image
88− targetView.image = image(texture: renderingTexture)
89127 }
90128
91− func image(texture texture: MTLTexture) -> UIImage {
129+ func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
130+ let texture = computeContext.texture
131+
92132 // Get image info
93133 let imageSize = CGSizeMake(CGFloat(texture.width),
94134 CGFloat(texture.height))
@@ -99,16 +139,16 @@ class MetalMetaballRenderer: MetaballRenderer {
99139 let region = MTLRegionMake2D(0, 0, width, height)
100140
101141 // Allocate the buffer if needed
102− if imageBuffer == nil {
103− imageBuffer = malloc(imageByteCount)
142+ if computeContext.imageBuffer == nil {
143+ computeContext.imageBuffer = malloc(imageByteCount)
104144 }
105145
106146 // Transfer texture info into image buffer
107− texture.getBytes(imageBuffer, bytesPerRow: bytesPerRow,
147+ texture.getBytes(computeContext.imageBuffer, bytesPerRow: bytesPerRow,
108148 fromRegion: region, mipmapLevel: 0)
109149
110150 // Get info for UIImage
111− let provider = CGDataProviderCreateWithData(nil, imageBuffer,
151+ let provider = CGDataProviderCreateWithData(nil, computeContext.imageBuffer,
112152 imageByteCount, nil)
113153 let bitsPerComponent = 8
114154 let bitsperPixel = 32
@@ -129,17 +169,18 @@ class MetalMetaballRenderer: MetaballRenderer {
129169 return image
130170 }
131171
132− func metaballBuffer(metaballs: [Metaball]) -> MTLBuffer {
172+ func metaballBuffer() -> MTLBuffer {
133173 // Create Float array for buffer
134174 // Exclude metaballs far from the view's bounds
135175 let border: CGFloat = 100
176+ let metaballs = self.dataSource.metaballs
136177 var floats: [Float] = metaballs.reduce([Float(0)]) { (var array, metaball) -> [Float] in
137178 let x = CGFloat(metaball.midX)
138179 let y = CGFloat(metaball.midY)
139180 if -border < x ≤ targetView.width + border &&
140− -border < y ≤ targetView.height + border {
141− array.append(Float(x))
142− array.append(Float(y))
181+ -border < y ≤ targetView.height + border {
182+ array.append(Float(x))
183+ array.append(Float(y))
143184 }
144185 return array
145186 }
@@ -148,18 +189,7 @@ class MetalMetaballRenderer: MetaballRenderer {
148189
149190 // Create buffer
150191 let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
151−
192+
152193 return buffer
153194 }
154−
155− func updateFrame() {
156− if imageBuffer != nil {
157− free(imageBuffer)
158− }
159− imageBuffer = nil
160− }
161−
162− deinit {
163− free(imageBuffer)
164− }
165195}
PixelImage/ViewController.swift+17−22
@@ -3,11 +3,8 @@ import UIKit
33
44class ViewController: UIViewController, MetaballDataSource {
55
6− typealias Renderer = MetalMetaballRenderer // ImageMetaballRenderer // MetalMetaballRenderer
7− typealias TargetView = Renderer.TargetView
8−
9− var metaballView: TargetView!
10− var renderer: Renderer!
6+ var metaballView: UIImageView!
7+ var renderer: MetalMetaballRenderer!
118
129 let width = 350
1310 let height = 600
@@ -17,52 +14,50 @@ class ViewController: UIViewController, MetaballDataSource {
1714 }
1815 var previousLocation: CGPoint!
1916
17+ var selectedMetaball: Metaball?
18+
2019 override func viewDidLoad() {
2120 super.viewDidLoad()
2221
23− renderer = Renderer(dataSource: self)
24−
2522 let recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
2623 view.addGestureRecognizer(recognizer)
2724
28−
2925 let border = 20
3026 let metaballViewFrame = CGRect(x: border/2, y: border/2, width: width, height: height)
27+ renderer = MetalMetaballRenderer(dataSource: self, frame: metaballViewFrame)
3128 metaballView = renderer.targetView
32− metaballView.frame = metaballViewFrame
3329
3430 let bigView = UIView(frame: CGRect(x: 10, y: 70, width: width + border, height: height + border))
3531 bigView.backgroundColor = UIColor.redColor()
3632 bigView.addSubview(metaballView)
3733 view.addSubview(bigView)
3834
39−
4035 for metaball in metaballs {
4136 metaballView.addSubview(metaball)
4237 }
4338
44−
45− renderer.updateTargetView()
39+ renderer.state = .Running
4640 }
4741
4842 func handlePan(recognizer: UIPanGestureRecognizer) {
4943 let location = recognizer.locationInView(metaballView)
5044
51− var selectedMetaball: UIView?
52−
53− for metaball in metaballs where
54− abs(metaball.midX - location.x) < 50 && abs(metaball.midY - location.y) < 50 {
55− selectedMetaball = metaball
56− break
45+ if selectedMetaball == nil {
46+ for metaball in metaballs where
47+ abs(metaball.midX - location.x) < 50 && abs(metaball.midY - location.y) < 50 {
48+ selectedMetaball = metaball
49+ break
50+ }
5751 }
5852
53+ guard selectedMetaball != nil else { return }
54+
5955 selectedMetaball?.middle = location
6056
61− if recognizer.state == .Changed && renderer.supportsDynamicRendering {
62− renderer.updateTargetView()
63− }
57+ renderer.state = .Running
58+
6459 if recognizer.state == .Ended {
65− renderer.updateTargetView()
60+ selectedMetaball = nil
6661 }
6762 }
6863}