Merged release-presentation into master

This commit is contained in:
vinivendra 2016-02-29 17:31:05 -05:00
commit feaaefcb71
29 changed files with 768 additions and 210 deletions

7
.gitignore vendored
View File

@ -1,3 +1,8 @@
.DS*
PixelImage.xcodeproj/xcuserdata/*
PixelImage.xcodeproj/project.xcworkspace/xcuserdata/*
PixelImage.xcodeproj/project.xcworkspace/xcuserdata/*
PixelImage.xcworkspace/xcuserdata/*
Pods/*

14
.tailor.yml Normal file
View File

@ -0,0 +1,14 @@
# Tailor will check for violations to all rules except for the following ones
except:
- constant-k-prefix
- function-whitespace
- leading-whitespace
# include:
# - Source # Inspect all Swift files under "Source/"
exclude:
- Pods # Ignore Swift files under "Pods/"
- PixelImage/SwiftSugar/SwiftSequences

BIN
Examples/Animation.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View File

@ -13,6 +13,7 @@
2426868F1C63B72A00D62456 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2426868E1C63B72A00D62456 /* Assets.xcassets */; };
242686921C63B72A00D62456 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 242686901C63B72A00D62456 /* LaunchScreen.storyboard */; };
2461651B1C6DA4F400A913C8 /* Interpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2461651A1C6DA4F400A913C8 /* Interpolation.swift */; };
246B0F831C70635D00A71D05 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246B0F821C70635D00A71D05 /* UIColor.swift */; };
249CF84C1C6D113C008B0C78 /* Matrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249CF84B1C6D113C008B0C78 /* Matrix.swift */; };
24E1E7D71C67C8B200ECF1C4 /* UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */; };
24E1E7E21C67D37F00ECF1C4 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E1E7E11C67D37F00ECF1C4 /* Metal.framework */; };
@ -21,6 +22,8 @@
24E1E7F01C67D74300ECF1C4 /* Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EE1C67D74300ECF1C4 /* Metal.swift */; };
24E1E7F11C67D74300ECF1C4 /* shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 24E1E7EF1C67D74300ECF1C4 /* shaders.metal */; };
24ED21CD1C6C2397009C40FC /* MetaballDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */; };
24FC38341C7F931E0085C04A /* Metaball.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FC38331C7F931E0085C04A /* Metaball.swift */; };
24FC38361C7F9ABE0085C04A /* Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FC38351C7F9ABE0085C04A /* Dispatch.swift */; };
24FFA86B1C6BA038005ACD60 /* ApproximationOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */; };
24FFA86C1C6BA038005ACD60 /* Collections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84D1C6BA038005ACD60 /* Collections.swift */; };
24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */; };
@ -49,6 +52,7 @@
24FFA8841C6BA038005ACD60 /* TakeDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA8681C6BA038005ACD60 /* TakeDrop.swift */; };
24FFA8851C6BA038005ACD60 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA8691C6BA038005ACD60 /* Zip.swift */; };
24FFA8861C6BA038005ACD60 /* TestCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FFA86A1C6BA038005ACD60 /* TestCommons.swift */; };
2BF7957B6518CA68241BB2CE /* Pods_PixelImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C55F9558B3B267413C3A07A6 /* Pods_PixelImage.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -60,6 +64,7 @@
242686911C63B72A00D62456 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
242686931C63B72A00D62456 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2461651A1C6DA4F400A913C8 /* Interpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Interpolation.swift; path = PixelImage/Interpolation.swift; sourceTree = SOURCE_ROOT; };
246B0F821C70635D00A71D05 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = "<group>"; };
249CF84B1C6D113C008B0C78 /* Matrix.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matrix.swift; sourceTree = "<group>"; };
24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKit.swift; sourceTree = "<group>"; };
24E1E7E11C67D37F00ECF1C4 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
@ -68,6 +73,8 @@
24E1E7EE1C67D74300ECF1C4 /* Metal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metal.swift; sourceTree = "<group>"; };
24E1E7EF1C67D74300ECF1C4 /* shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = shaders.metal; sourceTree = "<group>"; };
24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaballDataSource.swift; sourceTree = "<group>"; };
24FC38331C7F931E0085C04A /* Metaball.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metaball.swift; sourceTree = "<group>"; };
24FC38351C7F9ABE0085C04A /* Dispatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatch.swift; sourceTree = "<group>"; };
24FFA84C1C6BA038005ACD60 /* ApproximationOperator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApproximationOperator.swift; sourceTree = "<group>"; };
24FFA84D1C6BA038005ACD60 /* Collections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collections.swift; sourceTree = "<group>"; };
24FFA84E1C6BA038005ACD60 /* ComparatorChains.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComparatorChains.swift; sourceTree = "<group>"; };
@ -96,6 +103,9 @@
24FFA8681C6BA038005ACD60 /* TakeDrop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TakeDrop.swift; sourceTree = "<group>"; };
24FFA8691C6BA038005ACD60 /* Zip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; };
24FFA86A1C6BA038005ACD60 /* TestCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCommons.swift; sourceTree = "<group>"; };
B1A755EFC03DA2A172C2B82F /* Pods-PixelImage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PixelImage.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PixelImage/Pods-PixelImage.debug.xcconfig"; sourceTree = "<group>"; };
C419D37C90A6BE8B5139806A /* Pods-PixelImage.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PixelImage.release.xcconfig"; path = "Pods/Target Support Files/Pods-PixelImage/Pods-PixelImage.release.xcconfig"; sourceTree = "<group>"; };
C55F9558B3B267413C3A07A6 /* Pods_PixelImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PixelImage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -105,6 +115,7 @@
files = (
24E1E7E41C67D39300ECF1C4 /* MetalKit.framework in Frameworks */,
24E1E7E21C67D37F00ECF1C4 /* Metal.framework in Frameworks */,
2BF7957B6518CA68241BB2CE /* Pods_PixelImage.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -118,6 +129,8 @@
24E1E7E11C67D37F00ECF1C4 /* Metal.framework */,
242686861C63B72A00D62456 /* PixelImage */,
242686851C63B72A00D62456 /* Products */,
F1933B48ED44BA9FCD7A4A1F /* Pods */,
588576572D7A188031904848 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -134,12 +147,15 @@
children = (
24FFA84B1C6BA038005ACD60 /* SwiftSugar */,
242686871C63B72A00D62456 /* AppDelegate.swift */,
242686891C63B72A00D62456 /* ViewController.swift */,
24FC38351C7F9ABE0085C04A /* Dispatch.swift */,
24E1E7D61C67C8B200ECF1C4 /* UIKit.swift */,
24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */,
24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */,
246B0F821C70635D00A71D05 /* UIColor.swift */,
24FC38331C7F931E0085C04A /* Metaball.swift */,
24E1E7EE1C67D74300ECF1C4 /* Metal.swift */,
249CF84B1C6D113C008B0C78 /* Matrix.swift */,
242686891C63B72A00D62456 /* ViewController.swift */,
24ED21CC1C6C2397009C40FC /* MetaballDataSource.swift */,
24E1E7EA1C67D70300ECF1C4 /* MetalMetaballRenderer.swift */,
24E1E7EF1C67D74300ECF1C4 /* shaders.metal */,
2426868B1C63B72A00D62456 /* Main.storyboard */,
2426868E1C63B72A00D62456 /* Assets.xcassets */,
@ -209,6 +225,23 @@
path = SwiftSequences;
sourceTree = "<group>";
};
588576572D7A188031904848 /* Frameworks */ = {
isa = PBXGroup;
children = (
C55F9558B3B267413C3A07A6 /* Pods_PixelImage.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
F1933B48ED44BA9FCD7A4A1F /* Pods */ = {
isa = PBXGroup;
children = (
B1A755EFC03DA2A172C2B82F /* Pods-PixelImage.debug.xcconfig */,
C419D37C90A6BE8B5139806A /* Pods-PixelImage.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -216,9 +249,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 242686961C63B72A00D62456 /* Build configuration list for PBXNativeTarget "PixelImage" */;
buildPhases = (
EF55C7246917A184756D93D8 /* Check Pods Manifest.lock */,
242686801C63B72A00D62456 /* Sources */,
242686811C63B72A00D62456 /* Frameworks */,
242686821C63B72A00D62456 /* Resources */,
C7A2DF67265A9D17DEFBD193 /* Embed Pods Frameworks */,
FADC01F75BFAC01F0260D1C9 /* Copy Pods Resources */,
DD0396CE594E3EAC8D18CC6C /* Tailor */,
);
buildRules = (
);
@ -276,6 +313,68 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
C7A2DF67265A9D17DEFBD193 /* Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PixelImage/Pods-PixelImage-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
DD0396CE594E3EAC8D18CC6C /* Tailor */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = Tailor;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if hash tailor 2>/dev/null; then\n tailor --except=function-whitespace,leading-whitespace,constant-k-prefix --max-severity error\nelse\n echo \"warning: Please install Tailor from https://tailor.sh\"\nfi";
};
EF55C7246917A184756D93D8 /* Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
FADC01F75BFAC01F0260D1C9 /* Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PixelImage/Pods-PixelImage-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
242686801C63B72A00D62456 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@ -290,6 +389,7 @@
24FFA8771C6BA038005ACD60 /* ChunkWindowSplit.swift in Sources */,
24FFA86F1C6BA038005ACD60 /* NSNumber.swift in Sources */,
2461651B1C6DA4F400A913C8 /* Interpolation.swift in Sources */,
24FC38341C7F931E0085C04A /* Metaball.swift in Sources */,
24FFA8861C6BA038005ACD60 /* TestCommons.swift in Sources */,
24FFA8741C6BA038005ACD60 /* SignOperator.swift in Sources */,
24FFA8821C6BA038005ACD60 /* ScanReduce.swift in Sources */,
@ -297,6 +397,7 @@
24FFA8831C6BA038005ACD60 /* Slicing.swift in Sources */,
24FFA8721C6BA038005ACD60 /* Sequence.swift in Sources */,
24FFA87F1C6BA038005ACD60 /* Iterate.swift in Sources */,
24FC38361C7F9ABE0085C04A /* Dispatch.swift in Sources */,
24FFA8761C6BA038005ACD60 /* Categorise.swift in Sources */,
24FFA86D1C6BA038005ACD60 /* ComparatorChains.swift in Sources */,
24FFA8711C6BA038005ACD60 /* PowerOperator.swift in Sources */,
@ -308,6 +409,7 @@
24FFA86E1C6BA038005ACD60 /* Dictionary.swift in Sources */,
24FFA87D1C6BA038005ACD60 /* Hopping.swift in Sources */,
24E1E7EB1C67D70300ECF1C4 /* MetalMetaballRenderer.swift in Sources */,
246B0F831C70635D00A71D05 /* UIColor.swift in Sources */,
24FFA87A1C6BA038005ACD60 /* Enumerate.swift in Sources */,
242686881C63B72A00D62456 /* AppDelegate.swift in Sources */,
24E1E7F01C67D74300ECF1C4 /* Metal.swift in Sources */,
@ -428,6 +530,7 @@
};
242686971C63B72A00D62456 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B1A755EFC03DA2A172C2B82F /* Pods-PixelImage.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = PixelImage/Info.plist;
@ -439,6 +542,7 @@
};
242686981C63B72A00D62456 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C419D37C90A6BE8B5139806A /* Pods-PixelImage.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = PixelImage/Info.plist;

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:PixelImage.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
type = "0"
version = "2.0">
</Bucket>

View File

@ -13,7 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
@ -40,7 +39,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

View File

@ -0,0 +1,6 @@
import Dispatch
func delay(duration: Float = 1, block: () -> ()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(duration * Float(NSEC_PER_SEC))), dispatch_get_main_queue(), block)
}

View File

@ -1,4 +1,6 @@
import Darwin
func interpolateCubeEaseIn<T: Numeric>(linear: T) -> T {
return linear ^ 3
}
@ -15,3 +17,60 @@ func interpolateSmooth<T: Numeric>(linear: T) -> T {
return ((linear) * (linear) * (linear) * ((linear) * ((linear) * 6 - 15) + 10))
}
func createStringKeyframes(bounces: Int = 2, elasticity: Double = 1.1, bounceSpeed: Double) -> [(time: Double, position: Double)] {
// Setup keyframes
var keyFrames: [(time: Double, position: Double)] = [(1, 1)]
let overreach = elasticity - 1
var currentBounceTime: Double = overreach / (bounceSpeed ^ Double(bounces))
var bounceDirection = Double((bounces % 2) * 2 - 1)
var bounceReach = overreach / (2 ^ Double(bounces - 1))
var currentBouncePosition: Double = 1.0 + bounceDirection * bounceReach
print(currentBounceTime)
print(bounceDirection)
print(bounceReach)
print(currentBouncePosition)
keyFrames.append((1.0 - currentBounceTime, currentBouncePosition))
for i in (1..<bounces).reverse() {
currentBounceTime += overreach / (bounceSpeed ^ Double(i + 1))
currentBounceTime += overreach / (bounceSpeed ^ Double(i))
bounceDirection = Double((i % 2) * 2 - 1)
bounceReach = overreach / (2 ^ Double(i - 1))
currentBouncePosition = 1.0 + bounceDirection * bounceReach
keyFrames.append((1.0 - currentBounceTime, currentBouncePosition))
print(currentBounceTime)
print(bounceDirection)
print(bounceReach)
print(currentBouncePosition)
}
keyFrames.append((0, 0))
var reversed = [(time: Double, position: Double)]()
for keyFrame in keyFrames.reverse() {
reversed.append(keyFrame)
}
return reversed
}
func interpolateSpring<T: Numeric>(linear: T, bounces: Int = 2, elasticity: Double = 1.1, bounceSpeed: Double = 1.0) -> T {
let keyFrames: [(time: Double, position: Double)]
keyFrames = createStringKeyframes(bounces, elasticity: elasticity, bounceSpeed: bounceSpeed)
let x = linear.toDouble
let startFrame = keyFrames.filter { $0.time < x }.last ?? (0, 0)
let endFrame = keyFrames.filter { $0.time > x }.first ?? (1, 1)
let adjustedX = (x - startFrame.time) / (endFrame.time - startFrame.time)
let interpolatedX = interpolateSmooth(adjustedX)
let position = startFrame.position + interpolatedX * (endFrame.position - startFrame.position)
return T.fromDouble(position)
}

View File

@ -1,4 +1,3 @@
import UIKit
class Graph<T> {
@ -27,6 +26,7 @@ class Graph<T> {
adjacencyMatrix.reset(i, j)
adjacencyMatrix.reset(j, i)
}
}
struct Matrix: CustomStringConvertible {
@ -70,23 +70,3 @@ struct Matrix: CustomStringConvertible {
}
}
}
class EdgeAnimationParameters {
let startDate: NSDate
let duration: Float
let fadeIn: Bool
let i: Int
let j: Int
func unpack() -> (startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
return (startDate, duration, fadeIn, i, j)
}
init(startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
self.startDate = startDate
self.duration = duration
self.fadeIn = fadeIn
self.i = i
self.j = j
}
}

60
PixelImage/Metaball.swift Normal file
View File

@ -0,0 +1,60 @@
import UIKit
class Metaball: UIView {
var color: UIColor
init(position: CGPoint, color: UIColor = UIColor(randomFlatColorOfShadeStyle: .Light)) {
self.color = color
super.init(frame: CGRect.zero)
size = CGSize(50)
middle = position
}
required init?(coder aDecoder: NSCoder) {
self.color = UIColor(randomFlatColorOfShadeStyle: .Light)
super.init(coder: aDecoder)
}
}
class EdgeAnimationParameters {
let startDate: NSDate
let duration: Float
let fadeIn: Bool
let i: Int
let j: Int
func unpack() -> (startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
return (startDate, duration, fadeIn, i, j)
}
init(startDate: NSDate, duration: Float, fadeIn: Bool, i: Int, j: Int) {
self.startDate = startDate
self.duration = duration
self.fadeIn = fadeIn
self.i = i
self.j = j
}
}
class VertexAnimationParameters {
let startDate: NSDate
let duration: Float
let origin: CGPoint
let destination: CGPoint
let metaball: Metaball
func unpack() -> (startDate: NSDate, duration: Float, origin: CGPoint, destination: CGPoint, metaball: Metaball) {
return (startDate, duration, origin, destination, metaball)
}
init(startDate: NSDate, duration: Float, origin: CGPoint, destination: CGPoint, metaball: Metaball) {
self.startDate = startDate
self.duration = duration
self.origin = origin
self.destination = destination
self.metaball = metaball
}
}

View File

@ -1,4 +1,3 @@
import Metal
protocol MetaballDataSource {
@ -11,4 +10,5 @@ extension MetaballDataSource {
return metaballGraph.vertices
}
}
}
}

View File

@ -40,4 +40,4 @@ class MTLComputeContext {
deinit {
free(imageBuffer)
}
}
}

View File

@ -1,7 +1,8 @@
import Metal
import UIKit
import ChameleonFramework
class MetalMetaballRenderer {
@objc class MetalMetaballRenderer: NSObject {
typealias TargetView = UIImageView
@ -38,8 +39,7 @@ class MetalMetaballRenderer {
let width = Int(frame.width)
let height = Int(frame.height)
let textureDescriptor =
MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
let texture1 = context.device.newTextureWithDescriptor(textureDescriptor)
activeComputeContext = MTLComputeContext(size: targetView.size, texture: texture1)
@ -47,14 +47,15 @@ class MetalMetaballRenderer {
idleComputeContext = MTLComputeContext(size: targetView.size, texture: texture2)
}
func updateTargetView() {
internal func updateTargetView() {
let timeout = dispatch_time(DISPATCH_TIME_NOW, 1000000000)
dispatch_semaphore_wait(semaphore, timeout)
state = .Ending
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.rawValue), 0)) { () -> Void in
let userInteractiveQueue = dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.rawValue), 0)
dispatch_async(userInteractiveQueue) { () -> Void in
let computeContext = self.activeComputeContext
swap(&self.activeComputeContext, &self.idleComputeContext)
@ -73,14 +74,14 @@ class MetalMetaballRenderer {
dispatch_semaphore_signal(self.semaphore)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
// Send image to view
self.targetView.image = uiimage
})
}
}
}
func renderToContext(computeContext: MTLComputeContext) {
internal func renderToContext(computeContext: MTLComputeContext) {
let width = Int(targetView.width)
let height = Int(targetView.height)
@ -93,8 +94,7 @@ class MetalMetaballRenderer {
// Configure info for computing
let threadGroupCounts = MTLSizeMake(8, 8, 1)
let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
height / threadGroupCounts.height, 1)
let threadGroups = MTLSizeMake(width / threadGroupCounts.width, height / threadGroupCounts.height, 1)
// Send commands to metal and render
let commandBuffer = context.commandQueue.commandBuffer()
@ -112,18 +112,16 @@ class MetalMetaballRenderer {
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
} catch _ {
print("Error!")
assertionFailure()
}
}
func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
internal func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
let texture = computeContext.texture
// Get image info
let imageSize = CGSizeMake(CGFloat(texture.width),
CGFloat(texture.height))
let imageSize = CGSizeMake(CGFloat(texture.width), CGFloat(texture.height))
let width = Int(imageSize.width)
let height = Int(imageSize.height)
let imageByteCount = width * height * 4
@ -136,60 +134,68 @@ class MetalMetaballRenderer {
}
// Transfer texture info into image buffer
texture.getBytes(computeContext.imageBuffer, bytesPerRow: bytesPerRow,
fromRegion: region, mipmapLevel: 0)
texture.getBytes(computeContext.imageBuffer, bytesPerRow: bytesPerRow, fromRegion: region, mipmapLevel: 0)
// Get info for UIImage
let provider = CGDataProviderCreateWithData(nil, computeContext.imageBuffer,
imageByteCount, nil)
let provider = CGDataProviderCreateWithData(nil, computeContext.imageBuffer, imageByteCount, nil)
let bitsPerComponent = 8
let bitsperPixel = 32
let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue:
CGImageAlphaInfo.PremultipliedLast.rawValue |
CGBitmapInfo.ByteOrder32Big.rawValue)
let bitmapInfoRaw = CGImageAlphaInfo.PremultipliedLast.rawValue | CGBitmapInfo.ByteOrder32Big.rawValue
let bitmapInfo = CGBitmapInfo(rawValue: bitmapInfoRaw)
let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
// Create UIImage from image buffer
let imageRef = CGImageCreate(width, height,
bitsPerComponent, bitsperPixel, bytesPerRow,
colorSpaceRef, bitmapInfo, provider,
nil, false, renderingIntent)
let image = UIImage(CGImage: imageRef!, scale: 0.0,
orientation: UIImageOrientation.Up)
let imageRef = CGImageCreate(width, height, bitsPerComponent, bitsperPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, nil, false, renderingIntent)
let image = UIImage(CGImage: imageRef!, scale: 0.0, orientation: .Up)
return image
}
func metaballEdgesBuffer() -> MTLBuffer {
internal func metaballEdgesBuffer() -> MTLBuffer {
let floats = dataSource.metaballGraph.adjacencyMatrix.buffer
let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
let bufferLength = floats.count * sizeof(Float)
let device = context.device
let buffer = device.newBufferWithBytes(floats, length: bufferLength, options: .StorageModeShared)
return buffer
}
func metaballBuffer() -> MTLBuffer {
internal func metaballBuffer() -> MTLBuffer {
// Create Float array for buffer
// Exclude metaballs far from the view's bounds
let border: CGFloat = 100
let metaballs = self.dataSource.metaballs
var floats: [Float] = metaballs.reduce([Float(0)]) { (var array, metaball) -> [Float] in
var floats = [Float](count: 5, repeatedValue: 0)
floats = metaballs.reduce(floats) {
(var array, metaball) -> [Float] in
let x = CGFloat(metaball.midX)
let y = CGFloat(metaball.midY)
if -border < x targetView.width + border &&
-border < y targetView.height + border {
array.append(Float(x))
array.append(Float(y))
let color = metaball.color
let components = color.components
array.append(Float(components.red))
array.append(Float(components.green))
array.append(Float(components.blue))
}
return array
}
// Add array size to start of array so metal knows how far to go
floats[0] = Float(floats.count) / 2
floats[0] = Float(floats.count) / 5
// Create buffer
let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
let bufferLength = floats.count * sizeof(Float)
let device = context.device
let buffer = device.newBufferWithBytes(floats, length: bufferLength, options: .StorageModeShared)
return buffer
}
}

View File

@ -1,7 +1,7 @@
extension Dictionary {
subscript(keys: [Key]) -> [Value] {
get {
return keys.unwrappedMap({ self[$0] })
return keys.unwrappedMap { self[$0] }
}
set (values) {
for (key, value) in zip(keys, values) {

View File

@ -2,10 +2,13 @@ import Foundation
extension NSNumber: DoubleValuable {
var toDouble: Double {
get { return self.doubleValue }
get {
return self.doubleValue
}
}
static func fromDouble(double: Double) -> Self {
return self.init(double: double)
}
}

View File

@ -20,43 +20,54 @@ protocol Numeric: DoubleValuable, Incrementable, Comparable,
postfix func ++ (inout _: Self) -> Self
}
protocol FastNumeric: Numeric { }
protocol FastNumeric: Numeric {}
extension Double: FastNumeric {
var toDouble: Double {
get { return self }
get {
return self
}
}
static func fromDouble(double: Double) -> Double {
return double
}
}
extension Float: FastNumeric {
var toDouble: Double {
get { return Double(self) }
get {
return Double(self)
}
}
static func fromDouble(double: Double) -> Float {
return Float(double)
}
}
extension Int: FastNumeric {
var toDouble: Double {
get { return Double(self) }
get {
return Double(self)
}
}
static func fromDouble(double: Double) -> Int {
return Int(double)
}
}
//
extension Int {
func times(@noescape block: () throws -> ()) rethrows {
for _ in 1...self {
try block()
}
}
}

View File

@ -2,7 +2,7 @@ import Darwin
infix operator ^ { associativity right precedence 131 }
func ^<T: DoubleValuable>(left: T, right: T) -> T {
func ^ <T: DoubleValuable>(left: T, right: T) -> T {
return T.fromDouble(pow(left.toDouble, right.toDouble))
}

View File

@ -7,62 +7,66 @@ protocol MemoizedSequence: LazySequenceType {
memoizedFunction: (IndexType -> ReturnType))
}
struct HashMemoizedSequence<IndexType: Hashable, ReturnType>:
MemoizedSequence {
struct HashMemoizedSequence <IndexType: Hashable, ReturnType>:
MemoizedSequence {
let memoizedFunction: (IndexType -> ReturnType)
let increment: ((inout IndexType) -> Void)
let firstIndex: IndexType
let memoizedFunction: (IndexType -> ReturnType)
let increment: ((inout IndexType) -> Void)
let firstIndex: IndexType
var index: IndexType
var index: IndexType
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
memoizedFunction: (IndexType -> ReturnType)) {
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
memoizedFunction: (IndexType -> ReturnType)) {
self.firstIndex = first
self.firstIndex = first
self.index = first
self.increment = incrementer
self.memoizedFunction = memoizedFunction
}
self.index = first
self.increment = incrementer
self.memoizedFunction = memoizedFunction
}
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
closure: ((IndexType -> ReturnType, IndexType) -> ReturnType)) {
let memoizedFunction = memoize(closure)
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
closure: ((IndexType -> ReturnType, IndexType) -> ReturnType)) {
let memoizedFunction = memoize(closure)
self.init(first: first, incrementer: incrementer,
memoizedFunction: memoizedFunction)
}
self.init(first: first, incrementer: incrementer,
memoizedFunction: memoizedFunction)
}
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
function: (IndexType -> ReturnType)) {
init(first: IndexType,
incrementer: ((inout IndexType) -> Void),
function: (IndexType -> ReturnType)) {
let memoizedFunction = memoize(function)
let memoizedFunction = memoize(function)
self.init(first: first, incrementer: incrementer,
memoizedFunction: memoizedFunction)
}
self.init(first: first, incrementer: incrementer,
memoizedFunction: memoizedFunction)
}
}
extension HashMemoizedSequence: GeneratorType {
mutating func next() -> ReturnType? {
let result = memoizedFunction(index)
increment(&index)
return result
}
}
extension HashMemoizedSequence: LazySequenceType {
func generate() -> HashMemoizedSequence {
return self
}
}
extension HashMemoizedSequence where ReturnType: Comparable {
@warn_unused_result
func contains(element: ReturnType) -> Bool {
var index = firstIndex
@ -79,6 +83,7 @@ extension HashMemoizedSequence where ReturnType: Comparable {
increment(&index)
}
}
}
extension HashMemoizedSequence where IndexType: Incrementable {
@ -104,29 +109,29 @@ extension HashMemoizedSequence where IndexType: Incrementable {
}
extension HashMemoizedSequence where IndexType: Incrementable,
IndexType: IntegerLiteralConvertible {
IndexType: IntegerLiteralConvertible {
init(_ closure: ((IndexType -> ReturnType, IndexType) -> ReturnType)) {
self.firstIndex = 1
self.index = 1
self.increment = { (inout index: IndexType) in
index++
}
self.memoizedFunction = memoize(closure)
self.firstIndex = 1
self.index = 1
self.increment = { (inout index: IndexType) in
index++
}
self.memoizedFunction = memoize(closure)
}
init(_ function: (IndexType -> ReturnType)) {
self.firstIndex = 1
self.index = 1
self.increment = { (inout index: IndexType) in
index++
}
self.memoizedFunction = memoize(function)
self.firstIndex = 1
self.index = 1
self.increment = { (inout index: IndexType) in
index++
}
self.memoizedFunction = memoize(function)
}
}
func memoize <H: Hashable, R> (closure: ((H -> R, H) -> R) ) -> (H -> R) {
func memoize<H: Hashable, R>(closure: ((H -> R, H) -> R)) -> (H -> R) {
var table = [H : R]()
var table = [H: R]()
var auxiliaryFunction: (H -> R)!
auxiliaryFunction = { index in
@ -143,9 +148,9 @@ func memoize <H: Hashable, R> (closure: ((H -> R, H) -> R) ) -> (H -> R) {
return auxiliaryFunction
}
func memoize <H: Hashable, R> (function: (H -> R) ) -> (H -> R) {
func memoize<H: Hashable, R>(function: (H -> R)) -> (H -> R) {
var table = [H : R]()
var table = [H: R]()
let result: (H -> R) = { index in
if let result = table[index] {

View File

@ -1,13 +1,12 @@
prefix operator ± { }
prefix func ± <T: Numeric> (number: T) -> T {
prefix func ± <T: Numeric>(number: T) -> T {
if number > 0 {
return 1
} else if number == 0 {
return 0
} else {
return -1
}
if number > 0 {
return 1
} else if number == 0 {
return 0
} else {
return -1
}
}

View File

@ -1,9 +1,12 @@
extension String: DoubleValuable {
var toDouble: Double {
get { return Double(self) ?? Double(0) }
get {
return Double(self) ?? Double(0)
}
}
static func fromDouble(double: Double) -> String {
return "\(double)"
}
}

View File

@ -8,7 +8,7 @@
import Darwin
class Random <T: Numeric> {
class Random<T: Numeric> {
let max: Double = Double(INT32_MAX)
let exponent: Double = 7
@ -92,4 +92,5 @@ class Random <T: Numeric> {
let result = Double((2 * rawRandom) - 1)
return T.fromDouble(result)
}
}

15
PixelImage/UIColor.swift Normal file
View File

@ -0,0 +1,15 @@
import UIKit
extension UIColor {
var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
}
extension UIColor {
convenience init(red255 red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
self.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha / 255)
}
}

View File

@ -1,3 +1,5 @@
// TODO: Unite UIView and CALayer extensions under a common protocol
import UIKit
func - (left: CGPoint, right: CGPoint) -> CGVector {
@ -31,6 +33,7 @@ extension CGVector {
dx = dx / norm
dy = dy / norm
}
}
extension CGFloat: DoubleValuable {
@ -42,19 +45,28 @@ extension CGFloat: DoubleValuable {
static func fromDouble(double: Double) -> CGFloat {
return CGFloat(double)
}
}
extension CGFloat: Numeric {}
extension CGRect {
var x: CGFloat {
get { return origin.x }
set { origin.x = newValue }
get {
return origin.x
}
set {
origin.x = newValue
}
}
var y: CGFloat {
get { return origin.y }
set { origin.y = newValue }
get {
return origin.y
}
set {
origin.y = newValue
}
}
}
@ -66,41 +78,142 @@ extension CGSize {
extension UIView {
var origin: CGPoint {
get { return frame.origin }
set { frame.origin = newValue }
get {
return frame.origin
}
set {
frame.origin = newValue
}
}
var size: CGSize {
get { return frame.size }
set { frame.size = newValue }
get {
return frame.size
}
set {
frame.size = newValue
}
}
var width: CGFloat {
get { return frame.width }
set { frame.size = CGSize(width: newValue, height: height) }
get {
return frame.width
}
set {
frame.size = CGSize(width: newValue, height: height)
}
}
var height: CGFloat {
get { return frame.height }
set { frame.size = CGSize(width: width, height: newValue) }
get {
return frame.height
}
set {
frame.size = CGSize(width: width, height: newValue)
}
}
var middle: CGPoint {
get {
return CGPoint(x: origin.x + width/2, y: origin.y + height/2)
return CGPoint(x: origin.x + width / 2, y: origin.y + height / 2)
}
set {
origin = CGPoint(x: newValue.x - width/2, y: newValue.y - height/2)
origin = CGPoint(x: newValue.x - width / 2, y: newValue.y - height / 2)
}
}
var midX: CGFloat {
get { return middle.x }
set { middle = CGPoint(x: newValue, y: midY) }
get {
return middle.x
}
set {
middle = CGPoint(x: newValue, y: midY)
}
}
var midY: CGFloat {
get { return middle.y }
set { middle = CGPoint(x: midX, y: newValue) }
get {
return middle.y
}
set {
middle = CGPoint(x: midX, y: newValue)
}
}
}
extension CALayer {
var origin: CGPoint {
get {
return frame.origin
}
set {
frame.origin = newValue
}
}
var size: CGSize {
get {
return frame.size
}
set {
frame.size = newValue
}
}
var width: CGFloat {
get {
return frame.width
}
set {
frame.size = CGSize(width: newValue, height: height)
}
}
var height: CGFloat {
get {
return frame.height
}
set {
frame.size = CGSize(width: width, height: newValue)
}
}
var middle: CGPoint {
get {
return CGPoint(x: origin.x + width / 2, y: origin.y + height / 2)
}
set {
origin = CGPoint(x: newValue.x - width / 2, y: newValue.y - height / 2)
}
}
var midX: CGFloat {
get {
return middle.x
}
set {
middle = CGPoint(x: newValue, y: midY)
}
}
var midY: CGFloat {
get {
return middle.y
}
set {
middle = CGPoint(x: midX, y: newValue)
}
}
}
let screenRect = UIScreen.mainScreen().bounds
let screenWidth = screenRect.size.width
let screenHeight = screenRect.size.height
func * <T: Numeric>(vector: CGVector, scalar: T) -> CGVector {
return CGVector(dx: vector.dx * CGFloat(scalar.toDouble), dy: vector.dy * CGFloat(scalar.toDouble))
}
func + (point: CGPoint, vector: CGVector) -> CGPoint {
return CGPoint(x: point.x + vector.dx, y: point.y + vector.dy)
}

View File

@ -1,4 +1,3 @@
import UIKit
class ViewController: UIViewController, MetaballDataSource {
@ -19,33 +18,85 @@ class ViewController: UIViewController, MetaballDataSource {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.whiteColor()
let recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
view.addGestureRecognizer(recognizer)
let metaballs = [CGPoint(x: 70, y: 70), CGPoint(x: 270, y: 70), CGPoint(x: 270, y: 470), CGPoint(x: 70, y: 470)].map {
Metaball(position: $0)
var positions = [CGPoint]()
positions.append(CGPoint(x: Double(screenWidth) / 2, y: Double(screenHeight) / 2))
for i in 1...5 {
let sine = sin(Double(i) * 2 * M_PI / 5)
let cosine = cos(Double(i) * 2 * M_PI / 5)
let radius: Double = 150
let x = Double(screenWidth) / 2 + sine * radius
let y = Double(screenHeight) / 2 + cosine * radius
positions.append(CGPoint(x: x, y: y))
}
metaballGraph = Graph(vertices: metaballs)
metaballGraph.addEdge(0, 1)
metaballGraph.addEdge(0, 3)
metaballGraph.addEdge(1, 2)
metaballGraph.addEdge(2, 3)
let border = 20
let metaballViewFrame = CGRect(x: border/2, y: border/2, width: width, height: height)
let colors =
[UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0),
UIColor(red255: 250, green: 235, blue: 50, alpha: 1.0),
UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0),
UIColor(red255: 90, green: 170, blue: 170, alpha: 1.0),
UIColor(red255: 90, green: 170, blue: 170, alpha: 1.0),
UIColor(red255: 110, green: 220, blue: 220, alpha: 1.0)]
let metaballs = zip(positions, colors).map { Metaball(position: $0.0, color: $0.1) }
metaballGraph = Graph(vertices: metaballs)
let metaballViewFrame = screenRect
renderer = MetalMetaballRenderer(dataSource: self, frame: metaballViewFrame)
metaballView = renderer.targetView
let bigView = UIView(frame: CGRect(x: 10, y: 70, width: width + border, height: height + border))
bigView.backgroundColor = UIColor.redColor()
bigView.addSubview(metaballView)
view.addSubview(bigView)
view.addSubview(metaballView)
for metaball in metaballs {
metaballView.addSubview(metaball)
}
addEdge(0, 2)
delay(1.0) {
self.addEdge(0, 1)
self.addEdge(0, 2)
self.addEdge(0, 3)
self.addEdge(0, 4)
self.addEdge(0, 5)
}
delay(2.3) {
self.removeEdge(0, 1)
self.removeEdge(0, 2)
self.removeEdge(0, 3)
self.removeEdge(0, 4)
self.removeEdge(0, 5)
}
delay(4) {
for i in 1...5 {
UIView.animateWithDuration(1.0, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
let sine = sin(Double(i) * 2 * M_PI / 5)
let cosine = cos(Double(i) * 2 * M_PI / 5)
let radius: Double = 85
let x = Double(screenWidth) / 2 + sine * radius
let y = Double(screenHeight) / 2 + cosine * radius
let metaball = metaballs[i]
self.animateMetaball(metaball, toPoint: CGPoint(x: x, y: y))
}, completion: nil)
}
}
delay(6) { () -> () in
for i in 1...5 {
UIView.animateWithDuration(1.0, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
let sine = sin(Double(i) * 2 * M_PI / 5)
let cosine = cos(Double(i) * 2 * M_PI / 5)
let radius: Double = 150
let x = Double(screenWidth) / 2 + sine * radius
let y = Double(screenHeight) / 2 + cosine * radius
let metaball = metaballs[i]
self.animateMetaball(metaball, toPoint: CGPoint(x: x, y: y))
}, completion: nil)
}
}
renderer.state = .Running
}
@ -58,13 +109,41 @@ class ViewController: UIViewController, MetaballDataSource {
animateEdge(i, j, fadeIn: false)
}
func animateMetaball(metaball: Metaball, toPoint destination: CGPoint) {
let parameters = VertexAnimationParameters(startDate: NSDate(), duration: 1.0, origin: metaball.center, destination: destination, metaball: metaball)
NSTimer.scheduledTimerWithTimeInterval(1.0 / 60.0, target: self, selector: "animateMetaballWithTimer:", userInfo: parameters, repeats: true)
}
func animateMetaball(withTimer timer: NSTimer) {
guard let userInfo = timer.userInfo as? VertexAnimationParameters else { preconditionFailure() }
let (animationStart, duration, origin, destination, metaball) = (userInfo).unpack()
let now = NSDate()
var timeElapsed = Float(now.timeIntervalSinceDate(animationStart))
if timeElapsed > duration {
timer.invalidate()
timeElapsed = duration
}
let linearValue = timeElapsed / duration
let interpolatedValue = interpolateSmooth(linearValue)
print(interpolatedValue)
let position = origin + ((destination - origin) * interpolatedValue)
metaball.center = position
renderer.state = .Running
}
func animateEdge(i: Int, _ j: Int, fadeIn: Bool) {
let parameters = EdgeAnimationParameters(startDate: NSDate(), duration: edgeAnimationDuration, fadeIn: fadeIn, i: i, j: j)
NSTimer.scheduledTimerWithTimeInterval(1.0/60.0, target: self, selector: "animateEdgeWithTimer:", userInfo: parameters, repeats: true)
NSTimer.scheduledTimerWithTimeInterval(1.0 / 60.0, target: self, selector: "animateEdgeWithTimer:", userInfo: parameters, repeats: true)
}
func animateEdge(withTimer timer: NSTimer) {
let (animationStart, duration, fadeIn, i, j) = (timer.userInfo as! EdgeAnimationParameters).unpack()
guard let userInfo = timer.userInfo as? EdgeAnimationParameters else { preconditionFailure() }
let (animationStart, duration, fadeIn, i, j) = (userInfo).unpack()
let now = NSDate()
var timeElapsed = Float(now.timeIntervalSinceDate(animationStart))
@ -88,9 +167,9 @@ class ViewController: UIViewController, MetaballDataSource {
if selectedMetaball == nil {
for metaball in metaballs where
abs(metaball.midX - location.x) < 50 && abs(metaball.midY - location.y) < 50 {
selectedMetaball = metaball
break
abs(metaball.midX - location.x) < 50 && abs(metaball.midY - location.y) < 50 {
selectedMetaball = metaball
break
}
}
@ -104,19 +183,5 @@ class ViewController: UIViewController, MetaballDataSource {
selectedMetaball = nil
}
}
}
class Metaball: UIView {
init(position: CGPoint) {
super.init(frame: CGRect.zero)
size = CGSize(50)
middle = position
backgroundColor = UIColor(red: 0.1306, green: 0.7522, blue: 0.0307, alpha: 0.3)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

View File

@ -3,54 +3,117 @@
using namespace metal;
struct metaballData {
float x;
float y;
float r;
float g;
float b;
};
kernel void
drawMetaballs(texture2d<float, access::write> outTexture[[texture(0)]],
constant float *edgesBuffer[[buffer(1)]],
constant float *metaballBuffer[[buffer(0)]],
constant metaballData *metaballBuffer[[buffer(0)]],
uint2 gid[[thread_position_in_grid]]) {
uint numberOfMetaballs = metaballBuffer[0];
uint metaballArraySize = numberOfMetaballs * 2 + 1;
char numberOfMetaballs = metaballBuffer[0].x - 1;
float sum = 0;
float3 colorSum = float3(0, 0, 0);
float colorAccumulation = 0;
float3 colorSumLink = float3(0, 0, 0);
uint i, j, x, y;
char x, y;
for (i = 1, x = 0; i < metaballArraySize; i += 2, x += 1) {
for (j = i + 2, y = x + 1; j < metaballArraySize; j += 2, y += 1) {
float2 metaball1 = float2(metaballBuffer[i], metaballBuffer[i + 1]);
float metaballDistances[10];
float2 metaballDirections[10];
float3 metaballColors[10];
float2 metaball2 = float2(metaballBuffer[j], metaballBuffer[j + 1]);
for (x = 1; x <= numberOfMetaballs; x += 1) {
metaballData metaball = metaballBuffer[x];
float2 metaballPosition = float2(metaball.x, metaball.y);
float2 vector = float2(metaballPosition.x - gid.x, metaballPosition.y - gid.y);
float dotProduct = dot(vector, vector);
float squaredDistance = dotProduct > 0 ? dotProduct : 1;
float realDistance = sqrt(squaredDistance);
metaballDistances[x - 1] = squaredDistance;
metaballDirections[x - 1] = vector / realDistance;
metaballColors[x - 1] = float3(metaball.r, metaball.g, metaball.b);
}
float2 metaball1Vector
= float2(metaball1.x - gid.x, metaball1.y - gid.y);
float2 metaball2Vector
= float2(metaball2.x - gid.x, metaball2.y - gid.y);
float bendClose = 0.0;
float bendCloseCount = 0.0;
float ball = 0.0;
float value1 = 2048 / (dot(metaball1Vector, metaball1Vector) + 1);
float value2 = 2048 / (dot(metaball2Vector, metaball2Vector) + 1);
for (x = 0; x < numberOfMetaballs; x += 1) {
float distance1 = metaballDistances[x];
float value1 = 1500 / distance1;
float2 direction1 = metaballDirections[x];
float colorContribution = 1 / distance1;
colorAccumulation += colorContribution;
colorSum += metaballColors[x] * colorContribution;
for (y = x + 1; y < numberOfMetaballs; y += 1) {
float distance2 = metaballDistances[y];
float value2 = 1500 / distance2;
float2 direction2 = metaballDirections[y];
float v = value1 + value2;
float weightedValue = 0.5 * v;
char edgeIndex = y * numberOfMetaballs + x;
float edgeWeight = edgesBuffer[edgeIndex];
float2 direction1 = normalize(metaball1Vector);
float2 direction2 = normalize(metaball2Vector);
float cosine = dot(direction1, direction2);
float link = pow(((1 - cosine) * 0.5), 100);
float weightedLink = 0.6 * link * edgeWeight;
float result = weightedValue + weightedLink;
sum += mix(0.0, 1.0, step(0.5, weightedValue + weightedLink));
ball += step(0.4, weightedValue);
float metaballValue = step(0.5, weightedValue + weightedLink);
if (metaballValue > 0.0) {
colorSumLink = float3(0.0, 0.0, 0.0);
colorSumLink += metaballColors[x] / distance1;
colorSumLink += metaballColors[y] / distance2;
colorSumLink /= (1 / distance1) + (1 / distance2);
}
if (result > 0.4) {
bendClose += result;
bendCloseCount += 1.0;
}
sum += metaballValue;
}
}
float result = mix(0.0, 1.0, step(0.4, sum));
// bendClose = bendClose / bendCloseCount;
outTexture.write(float4(result, result / 2, 0, 1), gid);
if (bendClose != 0.0) {
bendClose = (bendClose - 0.4) * (1 / (0.6 - 0.4));
} else {
bendClose = 0.0;
}
float result = step(0.4, sum);
result = clamp(result, 0.0, 1.0);
colorSum = colorSum / colorAccumulation;
if (result > 0.0 && ball < 1.0) {
colorSum = colorSumLink;
}
colorSum *= result;
if (colorSum.r == 0 && colorSum.g == 0 && colorSum.b == 0) {
colorSum = float3(1, 1, 1);
}
outTexture.write(float4(colorSum.bgr, 1), gid);
}

6
Podfile Normal file
View File

@ -0,0 +1,6 @@
platform :ios, '9.2'
use_frameworks!
target 'PixelImage' do
pod 'ChameleonFramework/Swift'
end

12
Podfile.lock Normal file
View File

@ -0,0 +1,12 @@
PODS:
- ChameleonFramework/Default (2.1.0)
- ChameleonFramework/Swift (2.1.0):
- ChameleonFramework/Default
DEPENDENCIES:
- ChameleonFramework/Swift
SPEC CHECKSUMS:
ChameleonFramework: d21a3cc247abfe5e37609a283a8238b03575cf64
COCOAPODS: 0.39.0

View File

@ -1,2 +1,19 @@
# metal-metaballs
A 'playground' project for rendering metaball-like graphics in metal.
![Example Animation](https://raw.githubusercontent.com/vinivendra/metal-metaballs/master/Examples/Animation.gif)
# Metal Metaballs
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.
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.
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:
- [**Metaball**](https://en.wikipedia.org/wiki/Metaballs) math, in an adapted version, to render the graphics.
- [**Metal**](https://developer.apple.com/metal/) compute shaders to draw the metaballs dynamically.
- [**Grand Central Dispatch**](https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/) for doing CPU and GPU calculations simultaneously.
- [**Double buffering**](https://en.wikipedia.org/wiki/Multiple_buffering), to speed up rendering times.
- Different [**interpolations**](http://flexmonkey.blogspot.ca/2016/01/playing-with-interpolation-functions-in.html) for animations.
## Installation
Just download the app and run it on your metal-enabled device of choice :)