├── Demo ├── VGDemoViewController.h ├── VGAppDelegate.h ├── VGAppDelegate.m └── VGDemoViewController.m ├── Resources ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Icon@2x.png │ │ ├── Icon-ipad.png │ │ ├── Icon-ipad@2x.png │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ ├── Default@2x.png │ │ ├── Default-568h@2x.png │ │ ├── Default-Landscape.png │ │ ├── Default-Portrait.png │ │ ├── Default-Landscape@2x.png │ │ ├── Default-Portrait@2x.png │ │ └── Contents.json └── Info.plist ├── .gitignore ├── LICENSE ├── Sources ├── Vangogh.h └── Vangogh.m ├── README.md └── Vangogh.xcodeproj └── project.pbxproj /Demo/VGDemoViewController.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | 5 | @interface VGDemoViewController : UIViewController 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/Icon-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/AppIcon.appiconset/Icon-ipad.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/Icon-ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/AppIcon.appiconset/Icon-ipad@2x.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default@2x.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default-Landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default-Landscape.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default-Portrait.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shilgapira/Vangogh/HEAD/Resources/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png -------------------------------------------------------------------------------- /Demo/VGAppDelegate.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | 5 | @interface VGAppDelegate : UIResponder 6 | 7 | @property (nonatomic,strong) UIWindow *window; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | project.xcworkspace 14 | *.xccheckout 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /Demo/VGAppDelegate.m: -------------------------------------------------------------------------------- 1 | 2 | #import "VGAppDelegate.h" 3 | #import "Vangogh.h" 4 | #import "VGDemoViewController.h" 5 | 6 | 7 | @implementation VGAppDelegate 8 | 9 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 10 | VGDemoViewController *demo = [VGDemoViewController new]; 11 | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:demo]; 12 | 13 | self.window = [[VGWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; 14 | self.window.rootViewController = nav; 15 | [self.window makeKeyAndVisible]; 16 | 17 | return YES; 18 | } 19 | 20 | @end 21 | 22 | 23 | int main(int argc, char *argv[]) { 24 | @autoreleasepool { 25 | return UIApplicationMain(argc, argv, nil, NSStringFromClass(VGAppDelegate.class)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Gil Shapira (http://gil.sh/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "size" : "60x60", 15 | "idiom" : "iphone", 16 | "filename" : "Icon@2x.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "idiom" : "ipad", 21 | "size" : "29x29", 22 | "scale" : "1x" 23 | }, 24 | { 25 | "idiom" : "ipad", 26 | "size" : "29x29", 27 | "scale" : "2x" 28 | }, 29 | { 30 | "idiom" : "ipad", 31 | "size" : "40x40", 32 | "scale" : "1x" 33 | }, 34 | { 35 | "idiom" : "ipad", 36 | "size" : "40x40", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "76x76", 41 | "idiom" : "ipad", 42 | "filename" : "Icon-ipad.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "76x76", 47 | "idiom" : "ipad", 48 | "filename" : "Icon-ipad@2x.png", 49 | "scale" : "2x" 50 | } 51 | ], 52 | "info" : { 53 | "version" : 1, 54 | "author" : "xcode" 55 | } 56 | } -------------------------------------------------------------------------------- /Sources/Vangogh.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | 5 | /** 6 | * Integrate Vangogh into your app by creating an instance of @c VGWindow as your application's 7 | * key window (instead of UIWindow). This is usually done in the application delegate or main 8 | * storyboard. 9 | * 10 | * Once the application is running, shake the device (or press Cmd-Ctrl-Z on the Simulator) to 11 | * activate filtering. 12 | * 13 | * A details view displays the current type of color blindness and how common it is for males 14 | * and females. You can switch filters by tapping the arrow buttons and pause the current filter 15 | * by tapping between them. You can also limit filtering to one side of the screen by panning 16 | * from the middle of the view to the left or right. 17 | * 18 | * You can dismiss the details view by swiping it downwards. When you're done, shake the device 19 | * again to stop filtering completely. 20 | */ 21 | @interface VGWindow : UIWindow 22 | 23 | @end 24 | 25 | 26 | /** 27 | * If you don't want to use shake gestures you can create a @c VGFilterWindow and control 28 | * filtering manually by toggling its @c hidden property. Be sure to pass an appropriate 29 | * frame to the @c initWithFrame: initializer. 30 | */ 31 | @interface VGFilterWindow : UIWindow 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /Demo/VGDemoViewController.m: -------------------------------------------------------------------------------- 1 | 2 | #import "VGDemoViewController.h" 3 | 4 | 5 | static NSString * const kTestURL = @"http://flickr.com/explore"; 6 | 7 | 8 | @interface VGDemoViewController () 9 | 10 | @property (nonatomic,strong) UIWebView *webView; 11 | 12 | @end 13 | 14 | 15 | @implementation VGDemoViewController 16 | 17 | - (id)init { 18 | if (self = [super init]) { 19 | self.navigationItem.title = @"Loading..."; 20 | } 21 | return self; 22 | } 23 | 24 | - (void)viewDidLoad { 25 | [super viewDidLoad]; 26 | 27 | self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; 28 | self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 29 | self.webView.delegate = self; 30 | [self.view addSubview:self.webView]; 31 | 32 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:kTestURL]]; 33 | [self.webView loadRequest:request]; 34 | } 35 | 36 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 37 | #if TARGET_IPHONE_SIMULATOR 38 | self.navigationItem.title = @"Press Cmd-Ctrl-Z to Toggle"; 39 | #else 40 | self.navigationItem.title = @"Shake to Toggle"; 41 | #endif 42 | } 43 | 44 | - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { 45 | self.navigationItem.title = error.localizedDescription; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.gilshapira.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "filename" : "Default@2x.png", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "extent" : "full-screen", 13 | "idiom" : "iphone", 14 | "subtype" : "retina4", 15 | "filename" : "Default-568h@2x.png", 16 | "minimum-system-version" : "7.0", 17 | "orientation" : "portrait", 18 | "scale" : "2x" 19 | }, 20 | { 21 | "orientation" : "portrait", 22 | "idiom" : "ipad", 23 | "extent" : "full-screen", 24 | "minimum-system-version" : "7.0", 25 | "filename" : "Default-Portrait.png", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "orientation" : "landscape", 30 | "idiom" : "ipad", 31 | "extent" : "full-screen", 32 | "minimum-system-version" : "7.0", 33 | "filename" : "Default-Landscape.png", 34 | "scale" : "1x" 35 | }, 36 | { 37 | "orientation" : "portrait", 38 | "idiom" : "ipad", 39 | "extent" : "full-screen", 40 | "minimum-system-version" : "7.0", 41 | "filename" : "Default-Portrait@2x.png", 42 | "scale" : "2x" 43 | }, 44 | { 45 | "orientation" : "landscape", 46 | "idiom" : "ipad", 47 | "extent" : "full-screen", 48 | "minimum-system-version" : "7.0", 49 | "filename" : "Default-Landscape@2x.png", 50 | "scale" : "2x" 51 | } 52 | ], 53 | "info" : { 54 | "version" : 1, 55 | "author" : "xcode" 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vangogh 2 | 3 | Vangogh is an iOS library for testing how well an application works for people with 4 | various kinds of color vision deficiencies. 5 | 6 | It lets you interact with your app as you normally would while applying one of several color 7 | transformations to the user interface in realtime. 8 | 9 | Tested on Xcode 5 and iOS 7 10 | 11 | 12 | ## Example 13 | 14 | The repo comes with a simple demo that opens Flickr in a webview: 15 | 16 | https://user-images.githubusercontent.com/694968/225000143-83931ebd-b712-4644-a819-39d3fb13cf3d.mp4 17 | 18 | 19 | ## Usage 20 | 21 | 1. Clone the repo with `git clone https://github.com/shilgapira/Vangogh.git` 22 | 2. Copy and add `Vangogh.h` and `Vangogh.m` to your project 23 | 3. Replace your app's `UIWindow`-based key window with an instance of `VGWindow` 24 | 4. Add an import for the `Accelerate.framework` if needed 25 | 5. Run your app and shake the device (`Cmd-Ctrl-Z` on the Simulator) to activate filtering 26 | 27 | 28 | ## Details 29 | 30 | Vangogh uses a `CADisplayLink` to periodically take a snapshot of the running application 31 | and then uses the `Accelerate.framework` to multiply the image with a filter matrix. The 32 | resulting image is displayed in a separate window that passes through any touch events. The 33 | framerate is capped to 30 FPS by default. 34 | 35 | While a filter is active a details view displays the current type of color blindness and how 36 | common it is for males and females. You can switch filters by tapping the arrow buttons and 37 | pause the current filter by tapping between them. You can also limit filtering to one side of 38 | the screen by panning from the middle of the view to the left or right. 39 | 40 | You can dismiss the details view by swiping it downwards. Shake the device again to stop filtering completely. 41 | 42 | The filters are based on values from this [archived colorjack.com page](http://web.archive.org/web/20080422231727/http://www.colorjack.com/labs/colormatrix/). 43 | 44 | 45 | ## Limitations 46 | 47 | - The details view is always shown in portrait orientation 48 | - There might be issues when running on iOS 8 beta devices 49 | 50 | 51 | ## License 52 | 53 | All source code is licensed under the MIT License. See the LICENSE file for more info. 54 | -------------------------------------------------------------------------------- /Vangogh.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2B3E67B71965CC4C002ADB3C /* Vangogh.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B3E67B61965CC4C002ADB3C /* Vangogh.m */; }; 11 | 2B8E84971965C66A0082E388 /* VGAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B8E84961965C66A0082E388 /* VGAppDelegate.m */; }; 12 | 2B8E84991965C66A0082E388 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B8E84981965C66A0082E388 /* Images.xcassets */; }; 13 | 2B8E84B81965C7EC0082E388 /* VGDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B8E84B71965C7EC0082E388 /* VGDemoViewController.m */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 2B3E67B51965CC4C002ADB3C /* Vangogh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Vangogh.h; sourceTree = ""; }; 18 | 2B3E67B61965CC4C002ADB3C /* Vangogh.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Vangogh.m; sourceTree = ""; }; 19 | 2B8E84831965C66A0082E388 /* Vangogh.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vangogh.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 2B8E848E1965C66A0082E388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 21 | 2B8E84951965C66A0082E388 /* VGAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VGAppDelegate.h; sourceTree = ""; }; 22 | 2B8E84961965C66A0082E388 /* VGAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VGAppDelegate.m; sourceTree = ""; }; 23 | 2B8E84981965C66A0082E388 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 24 | 2B8E84B61965C7EC0082E388 /* VGDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VGDemoViewController.h; sourceTree = ""; }; 25 | 2B8E84B71965C7EC0082E388 /* VGDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VGDemoViewController.m; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 2B8E84801965C66A0082E388 /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 2B3E67B41965CC4C002ADB3C /* Sources */ = { 40 | isa = PBXGroup; 41 | children = ( 42 | 2B3E67B51965CC4C002ADB3C /* Vangogh.h */, 43 | 2B3E67B61965CC4C002ADB3C /* Vangogh.m */, 44 | ); 45 | path = Sources; 46 | sourceTree = ""; 47 | }; 48 | 2B8E847A1965C66A0082E388 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 2B3E67B41965CC4C002ADB3C /* Sources */, 52 | 2B8E848C1965C66A0082E388 /* Demo */, 53 | 2B8E84B51965C71E0082E388 /* Resources */, 54 | 2B8E84841965C66A0082E388 /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | 2B8E84841965C66A0082E388 /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 2B8E84831965C66A0082E388 /* Vangogh.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | 2B8E848C1965C66A0082E388 /* Demo */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 2B8E84951965C66A0082E388 /* VGAppDelegate.h */, 70 | 2B8E84961965C66A0082E388 /* VGAppDelegate.m */, 71 | 2B8E84B61965C7EC0082E388 /* VGDemoViewController.h */, 72 | 2B8E84B71965C7EC0082E388 /* VGDemoViewController.m */, 73 | ); 74 | path = Demo; 75 | sourceTree = ""; 76 | }; 77 | 2B8E84B51965C71E0082E388 /* Resources */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 2B8E84981965C66A0082E388 /* Images.xcassets */, 81 | 2B8E848E1965C66A0082E388 /* Info.plist */, 82 | ); 83 | path = Resources; 84 | sourceTree = ""; 85 | }; 86 | /* End PBXGroup section */ 87 | 88 | /* Begin PBXNativeTarget section */ 89 | 2B8E84821965C66A0082E388 /* Vangogh */ = { 90 | isa = PBXNativeTarget; 91 | buildConfigurationList = 2B8E84AF1965C66A0082E388 /* Build configuration list for PBXNativeTarget "Vangogh" */; 92 | buildPhases = ( 93 | 2B8E847F1965C66A0082E388 /* Sources */, 94 | 2B8E84801965C66A0082E388 /* Frameworks */, 95 | 2B8E84811965C66A0082E388 /* Resources */, 96 | ); 97 | buildRules = ( 98 | ); 99 | dependencies = ( 100 | ); 101 | name = Vangogh; 102 | productName = Vangogh; 103 | productReference = 2B8E84831965C66A0082E388 /* Vangogh.app */; 104 | productType = "com.apple.product-type.application"; 105 | }; 106 | /* End PBXNativeTarget section */ 107 | 108 | /* Begin PBXProject section */ 109 | 2B8E847B1965C66A0082E388 /* Project object */ = { 110 | isa = PBXProject; 111 | attributes = { 112 | CLASSPREFIX = VG; 113 | LastUpgradeCheck = 0510; 114 | ORGANIZATIONNAME = "Gil Shapira"; 115 | }; 116 | buildConfigurationList = 2B8E847E1965C66A0082E388 /* Build configuration list for PBXProject "Vangogh" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = English; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | ); 123 | mainGroup = 2B8E847A1965C66A0082E388; 124 | productRefGroup = 2B8E84841965C66A0082E388 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 2B8E84821965C66A0082E388 /* Vangogh */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 2B8E84811965C66A0082E388 /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 2B8E84991965C66A0082E388 /* Images.xcassets in Resources */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXResourcesBuildPhase section */ 143 | 144 | /* Begin PBXSourcesBuildPhase section */ 145 | 2B8E847F1965C66A0082E388 /* Sources */ = { 146 | isa = PBXSourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 2B3E67B71965CC4C002ADB3C /* Vangogh.m in Sources */, 150 | 2B8E84B81965C7EC0082E388 /* VGDemoViewController.m in Sources */, 151 | 2B8E84971965C66A0082E388 /* VGAppDelegate.m in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin XCBuildConfiguration section */ 158 | 2B8E84AD1965C66A0082E388 /* Debug */ = { 159 | isa = XCBuildConfiguration; 160 | buildSettings = { 161 | ALWAYS_SEARCH_USER_PATHS = NO; 162 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 163 | CLANG_CXX_LIBRARY = "libc++"; 164 | CLANG_ENABLE_MODULES = YES; 165 | CLANG_ENABLE_OBJC_ARC = YES; 166 | CLANG_WARN_BOOL_CONVERSION = YES; 167 | CLANG_WARN_CONSTANT_CONVERSION = YES; 168 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 169 | CLANG_WARN_EMPTY_BODY = YES; 170 | CLANG_WARN_ENUM_CONVERSION = YES; 171 | CLANG_WARN_INT_CONVERSION = YES; 172 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 173 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 174 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 175 | COPY_PHASE_STRIP = NO; 176 | GCC_C_LANGUAGE_STANDARD = gnu99; 177 | GCC_DYNAMIC_NO_PIC = NO; 178 | GCC_OPTIMIZATION_LEVEL = 0; 179 | GCC_PREPROCESSOR_DEFINITIONS = ( 180 | "DEBUG=1", 181 | "$(inherited)", 182 | ); 183 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 184 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 185 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 186 | GCC_WARN_UNDECLARED_SELECTOR = YES; 187 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 188 | GCC_WARN_UNUSED_FUNCTION = YES; 189 | GCC_WARN_UNUSED_VARIABLE = YES; 190 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 191 | ONLY_ACTIVE_ARCH = YES; 192 | SDKROOT = iphoneos; 193 | TARGETED_DEVICE_FAMILY = "1,2"; 194 | }; 195 | name = Debug; 196 | }; 197 | 2B8E84AE1965C66A0082E388 /* Release */ = { 198 | isa = XCBuildConfiguration; 199 | buildSettings = { 200 | ALWAYS_SEARCH_USER_PATHS = NO; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_EMPTY_BODY = YES; 209 | CLANG_WARN_ENUM_CONVERSION = YES; 210 | CLANG_WARN_INT_CONVERSION = YES; 211 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 214 | COPY_PHASE_STRIP = YES; 215 | ENABLE_NS_ASSERTIONS = NO; 216 | GCC_C_LANGUAGE_STANDARD = gnu99; 217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 219 | GCC_WARN_UNDECLARED_SELECTOR = YES; 220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 221 | GCC_WARN_UNUSED_FUNCTION = YES; 222 | GCC_WARN_UNUSED_VARIABLE = YES; 223 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 224 | SDKROOT = iphoneos; 225 | TARGETED_DEVICE_FAMILY = "1,2"; 226 | VALIDATE_PRODUCT = YES; 227 | }; 228 | name = Release; 229 | }; 230 | 2B8E84B01965C66A0082E388 /* Debug */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 234 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 235 | INFOPLIST_FILE = Resources/Info.plist; 236 | PRODUCT_NAME = "$(TARGET_NAME)"; 237 | WRAPPER_EXTENSION = app; 238 | }; 239 | name = Debug; 240 | }; 241 | 2B8E84B11965C66A0082E388 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 245 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 246 | INFOPLIST_FILE = Resources/Info.plist; 247 | PRODUCT_NAME = "$(TARGET_NAME)"; 248 | WRAPPER_EXTENSION = app; 249 | }; 250 | name = Release; 251 | }; 252 | /* End XCBuildConfiguration section */ 253 | 254 | /* Begin XCConfigurationList section */ 255 | 2B8E847E1965C66A0082E388 /* Build configuration list for PBXProject "Vangogh" */ = { 256 | isa = XCConfigurationList; 257 | buildConfigurations = ( 258 | 2B8E84AD1965C66A0082E388 /* Debug */, 259 | 2B8E84AE1965C66A0082E388 /* Release */, 260 | ); 261 | defaultConfigurationIsVisible = 0; 262 | defaultConfigurationName = Release; 263 | }; 264 | 2B8E84AF1965C66A0082E388 /* Build configuration list for PBXNativeTarget "Vangogh" */ = { 265 | isa = XCConfigurationList; 266 | buildConfigurations = ( 267 | 2B8E84B01965C66A0082E388 /* Debug */, 268 | 2B8E84B11965C66A0082E388 /* Release */, 269 | ); 270 | defaultConfigurationIsVisible = 0; 271 | defaultConfigurationName = Release; 272 | }; 273 | /* End XCConfigurationList section */ 274 | }; 275 | rootObject = 2B8E847B1965C66A0082E388 /* Project object */; 276 | } 277 | -------------------------------------------------------------------------------- /Sources/Vangogh.m: -------------------------------------------------------------------------------- 1 | 2 | #import "Vangogh.h" 3 | #import 4 | 5 | 6 | 7 | /*******************************************************************************/ 8 | #pragma mark - Filters 9 | /*******************************************************************************/ 10 | 11 | typedef struct { 12 | __unsafe_unretained NSString *title; 13 | __unsafe_unretained NSString *subtitle; 14 | double values[9]; 15 | } VGFilter; 16 | 17 | 18 | static const VGFilter kPauseFilter = 19 | { @"Paused", @"---", { 1.000, 0.000, 0.000, 0.000, 1.000, 0.000, 0.000, 0.000, 1.000 } }; 20 | 21 | 22 | static const VGFilter kFilters[] = { 23 | { @"Deuteranomaly", @"5% M | 0.35% F", { 0.800, 0.200, 0.000, 0.258, 0.741, 0.000, 0.000, 0.141, 0.858 } }, 24 | { @"Deuteranopia", @"1.2% M | 0.01% F", { 0.625, 0.375, 0.000, 0.700, 0.300, 0.000, 0.000, 0.300, 0.700 } }, 25 | { @"Protanomaly", @"1.3% M | 0.02% F", { 0.817, 0.183, 0.000, 0.333, 0.667, 0.000, 0.000, 0.123, 0.875 } }, 26 | { @"Protanopia", @"1.3% M | 0.02% F", { 0.567, 0.433, 0.000, 0.558, 0.442, 0.000, 0.000, 0.242, 0.758 } }, 27 | { @"Tritanomaly", @"0.0001% MF", { 0.967, 0.033, 0.000, 0.000, 0.733, 0.267, 0.000, 0.183, 0.817 } }, 28 | { @"Tritanopia", @"0.001% M | 0.03% F", { 0.950, 0.050, 0.000, 0.000, 0.433, 0.567, 0.000, 0.475, 0.525 } }, 29 | }; 30 | 31 | static const NSUInteger kNumFilters = sizeof(kFilters) / sizeof(kFilters[0]); 32 | 33 | 34 | static UIImage *VGFilterApply(VGFilter filter, UIImage *image) { 35 | CGImageRef srcImage = image.CGImage; 36 | CFDataRef srcDataRef = CGDataProviderCopyData(CGImageGetDataProvider(srcImage)); 37 | 38 | void *srcData = (void *) CFDataGetBytePtr(srcDataRef); 39 | vImage_Buffer src = { 40 | .width = CGImageGetWidth(srcImage), 41 | .height = CGImageGetHeight(srcImage), 42 | .rowBytes = CGImageGetBytesPerRow(srcImage), 43 | .data = srcData, 44 | }; 45 | 46 | void *destData = malloc(src.height * src.rowBytes); 47 | vImage_Buffer dest = { 48 | .width = src.width, 49 | .height = src.height, 50 | .rowBytes = src.rowBytes, 51 | .data = destData, 52 | }; 53 | 54 | double *rgb = filter.values; 55 | double bgra[16] = { 56 | rgb[8], rgb[5], rgb[2], 0, 57 | rgb[7], rgb[4], rgb[1], 0, 58 | rgb[6], rgb[3], rgb[0], 0, 59 | 0, 0, 0, 1, 60 | }; 61 | 62 | int32_t divisor = 16384; 63 | int16_t matrix[16]; 64 | for (NSUInteger i = 0; i < 16; i++) { 65 | matrix[i] = (int16_t) round(bgra[i] * divisor); 66 | } 67 | 68 | vImage_Error error = vImageMatrixMultiply_ARGB8888(&src, &dest, matrix, divisor, NULL, NULL, kvImageNoFlags); 69 | if (error) { 70 | NSLog(@"vImageMatrixMultiply failed: %@", @(error)); 71 | return nil; 72 | } 73 | 74 | CGContextRef ctx = CGBitmapContextCreate(dest.data, dest.width, dest.height, 8, dest.rowBytes, CGImageGetColorSpace(srcImage), CGImageGetBitmapInfo(srcImage)); 75 | 76 | CGImageRef destImage = CGBitmapContextCreateImage(ctx); 77 | UIImage *result = [UIImage imageWithCGImage:destImage]; 78 | CGImageRelease(destImage); 79 | 80 | CGContextRelease(ctx); 81 | 82 | free(destData); 83 | CFRelease(srcDataRef); 84 | 85 | return result; 86 | } 87 | 88 | 89 | 90 | /*******************************************************************************/ 91 | #pragma mark - Details View 92 | /*******************************************************************************/ 93 | 94 | @class VGDetailsView; 95 | 96 | 97 | @protocol VGDetailsViewDelegate 98 | 99 | - (void)detailsViewDidPressPrevious:(VGDetailsView *)detailsView; 100 | 101 | - (void)detailsViewDidPressNext:(VGDetailsView *)detailsView; 102 | 103 | - (void)detailsView:(VGDetailsView *)detailsView didChangePeek:(CGFloat)peek; 104 | 105 | - (void)detailsViewDidTogglePause:(VGDetailsView *)detailsView; 106 | 107 | - (void)detailsViewDidDismiss:(VGDetailsView *)detailsView; 108 | 109 | @end 110 | 111 | 112 | @interface VGDetailsView : UIView 113 | 114 | @property (nonatomic,weak) id delegate; 115 | 116 | @property (nonatomic,copy) NSString *title; 117 | 118 | @property (nonatomic,copy) NSString *subtitle; 119 | 120 | @end 121 | 122 | 123 | @interface VGDetailsView () 124 | 125 | @property (nonatomic,strong) UILabel *titleLabel; 126 | 127 | @property (nonatomic,strong) UILabel *subtitleLabel; 128 | 129 | @property (nonatomic,strong) UIButton *prevButton; 130 | 131 | @property (nonatomic,strong) UIButton *nextButton; 132 | 133 | @end 134 | 135 | 136 | @implementation VGDetailsView 137 | 138 | - (id)initWithFrame:(CGRect)frame { 139 | if (self = [super initWithFrame:frame]) { 140 | self.opaque = NO; 141 | self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.75]; 142 | 143 | UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizeSwipe:)]; 144 | swipe.direction = UISwipeGestureRecognizerDirectionDown; 145 | [self addGestureRecognizer:swipe]; 146 | 147 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizePan:)]; 148 | [pan requireGestureRecognizerToFail:swipe]; 149 | [self addGestureRecognizer:pan]; 150 | 151 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizeTap:)]; 152 | [self addGestureRecognizer:tap]; 153 | 154 | _titleLabel = [UILabel new]; 155 | _titleLabel.font = [UIFont boldSystemFontOfSize:14]; 156 | _titleLabel.textColor = UIColor.whiteColor; 157 | _titleLabel.textAlignment = NSTextAlignmentCenter; 158 | [self addSubview:_titleLabel]; 159 | 160 | _subtitleLabel = [UILabel new]; 161 | _subtitleLabel.font = [UIFont systemFontOfSize:12]; 162 | _subtitleLabel.textColor = [UIColor colorWithWhite:0.75 alpha:1]; 163 | _subtitleLabel.textAlignment = NSTextAlignmentCenter; 164 | [self addSubview:_subtitleLabel]; 165 | 166 | _prevButton = [UIButton buttonWithType:UIButtonTypeCustom]; 167 | _prevButton.showsTouchWhenHighlighted = YES; 168 | [_prevButton setTitle:@"◀︎" forState:UIControlStateNormal]; 169 | [_prevButton addTarget:self action:@selector(didPressPrevious) forControlEvents:UIControlEventTouchUpInside]; 170 | [self addSubview:_prevButton]; 171 | 172 | _nextButton = [UIButton buttonWithType:UIButtonTypeCustom]; 173 | _nextButton.showsTouchWhenHighlighted = YES; 174 | [_nextButton setTitle:@"▶︎" forState:UIControlStateNormal]; 175 | [_nextButton addTarget:self action:@selector(didPressNext) forControlEvents:UIControlEventTouchUpInside]; 176 | [self addSubview:_nextButton]; 177 | } 178 | return self; 179 | } 180 | 181 | - (void)setTitle:(NSString *)title { 182 | _title = [title copy]; 183 | self.titleLabel.text = _title; 184 | } 185 | 186 | - (void)setSubtitle:(NSString *)subtitle { 187 | _subtitle = [subtitle copy]; 188 | self.subtitleLabel.text = _subtitle; 189 | } 190 | 191 | - (void)didRecognizeSwipe:(UIGestureRecognizer *)gestureRecognizer { 192 | [self.delegate detailsViewDidDismiss:self]; 193 | } 194 | 195 | - (void)didRecognizePan:(UIPanGestureRecognizer *)gestureRecognizer { 196 | if (gestureRecognizer.state != UIGestureRecognizerStateChanged) { 197 | [self.delegate detailsView:self didChangePeek:0]; 198 | } else { 199 | CGPoint translation = [gestureRecognizer translationInView:self]; 200 | CGFloat factor = (2 * translation.x) / self.bounds.size.width; 201 | factor = MIN(MAX(factor, -1), 1); 202 | [self.delegate detailsView:self didChangePeek:factor]; 203 | } 204 | } 205 | 206 | - (void)didRecognizeTap:(UIGestureRecognizer *)gestureRecognizer { 207 | [self.delegate detailsViewDidTogglePause:self]; 208 | } 209 | 210 | - (void)didPressPrevious { 211 | [self.delegate detailsViewDidPressPrevious:self]; 212 | } 213 | 214 | - (void)didPressNext { 215 | [self.delegate detailsViewDidPressNext:self]; 216 | } 217 | 218 | - (void)layoutSubviews { 219 | [super layoutSubviews]; 220 | 221 | CGRect b = self.bounds; 222 | _titleLabel.frame = CGRectMake(0, b.size.height / 2 - 18, b.size.width, 18); 223 | _subtitleLabel.frame = CGRectMake(0, b.size.height / 2, b.size.width, 18); 224 | _prevButton.frame = CGRectMake(0, 0, 44, b.size.height); 225 | _nextButton.frame = CGRectMake(b.size.width - 44, 0, 44, b.size.height); 226 | } 227 | 228 | @end 229 | 230 | 231 | 232 | /*******************************************************************************/ 233 | #pragma mark - Filter Window 234 | /*******************************************************************************/ 235 | 236 | @interface VGFilterWindow () 237 | 238 | @property (nonatomic,strong) UIView *outputView; 239 | 240 | @property (nonatomic,strong) VGDetailsView *detailsView; 241 | 242 | @property (nonatomic,assign) VGFilter filter; 243 | 244 | @property (nonatomic,assign) NSUInteger index; 245 | 246 | @property (nonatomic,assign) BOOL paused; 247 | 248 | @property (nonatomic,assign) CGFloat peek; 249 | 250 | @property (nonatomic,strong) CADisplayLink *displayLink; 251 | 252 | @property (nonatomic,assign) CFTimeInterval lastTimestamp; 253 | 254 | @end 255 | 256 | 257 | @implementation VGFilterWindow 258 | 259 | - (id)initWithFrame:(CGRect)frame { 260 | if (self = [super initWithFrame:frame]) { 261 | self.windowLevel = UIWindowLevelStatusBar - 1; 262 | self.clipsToBounds = YES; 263 | self.opaque = NO; 264 | self.backgroundColor = UIColor.clearColor; 265 | 266 | _outputView = [UIView new]; 267 | _outputView.userInteractionEnabled = NO; 268 | [self addSubview:_outputView]; 269 | 270 | _detailsView = [VGDetailsView new]; 271 | _detailsView.delegate = self; 272 | [self addSubview:_detailsView]; 273 | 274 | self.filter = kFilters[self.index]; 275 | } 276 | return self; 277 | } 278 | 279 | #pragma mark Filtering 280 | 281 | - (void)start { 282 | [self updateDetails]; 283 | 284 | [self refreshIfNeeded]; 285 | 286 | if (!self.displayLink) { 287 | self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshIfNeeded)]; 288 | self.displayLink.frameInterval = 2; 289 | [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; 290 | } 291 | } 292 | 293 | - (void)stop { 294 | [self.displayLink invalidate]; 295 | self.displayLink = nil; 296 | self.outputView.layer.contents = nil; 297 | } 298 | 299 | - (void)refreshIfNeeded { 300 | if (!self.hidden) { 301 | [self refresh]; 302 | } 303 | } 304 | 305 | - (void)refresh { 306 | CFTimeInterval timestamp = CACurrentMediaTime(); 307 | 308 | __block UIImage *image = [self snapshotImage]; 309 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ 310 | image = VGFilterApply(self.filter, image); 311 | dispatch_sync(dispatch_get_main_queue(), ^{ 312 | if (image && !self.hidden && timestamp > self.lastTimestamp) { 313 | self.lastTimestamp = timestamp; 314 | self.outputView.layer.contents = (id)image.CGImage; 315 | } 316 | }); 317 | }); 318 | } 319 | 320 | - (void)setFilter:(VGFilter)filter { 321 | _filter = filter; 322 | [self updateDetails]; 323 | } 324 | 325 | - (UIImage *)snapshotImage { 326 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); 327 | 328 | for (UIWindow *window in UIApplication.sharedApplication.windows) { 329 | if (!window.hidden && window != self) { 330 | [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; 331 | } 332 | } 333 | 334 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 335 | 336 | UIGraphicsEndImageContext(); 337 | 338 | return image; 339 | } 340 | 341 | #pragma mark Pause 342 | 343 | - (void)setPaused:(BOOL)paused { 344 | if (_paused != paused) { 345 | _paused = paused; 346 | if (_paused) { 347 | self.filter = kPauseFilter; 348 | } else { 349 | self.filter = kFilters[self.index]; 350 | } 351 | } 352 | } 353 | 354 | #pragma mark Peek 355 | 356 | - (void)setPeek:(CGFloat)peek { 357 | if (_peek != peek) { 358 | _peek = peek; 359 | [self updateMask]; 360 | } 361 | } 362 | 363 | - (void)updateMask { 364 | CAShapeLayer *mask = nil; 365 | 366 | if (fabs(self.peek) > 0.1) { 367 | mask = [CAShapeLayer new]; 368 | 369 | CGRect rect = self.outputView.layer.bounds; 370 | if (self.peek < 0) { 371 | rect.size.width += self.peek * rect.size.width; 372 | } else { 373 | rect.origin.x += self.peek * rect.size.width; 374 | rect.size.width -= self.peek * rect.size.width; 375 | } 376 | 377 | CGPathRef path = CGPathCreateWithRect(rect, NULL); 378 | mask.path = path; 379 | CGPathRelease(path); 380 | } 381 | 382 | self.outputView.layer.mask = mask; 383 | } 384 | 385 | #pragma mark Info 386 | 387 | - (void)updateDetails { 388 | self.detailsView.hidden = NO; 389 | self.detailsView.alpha = 1; 390 | self.detailsView.title = self.filter.title; 391 | self.detailsView.subtitle = self.filter.subtitle; 392 | } 393 | 394 | - (void)detailsViewDidPressPrevious:(VGDetailsView *)detailsView { 395 | self.index = (self.index + kNumFilters - 1) % kNumFilters; 396 | self.filter = kFilters[self.index]; 397 | } 398 | 399 | - (void)detailsViewDidPressNext:(VGDetailsView *)detailsView { 400 | self.index = (self.index + kNumFilters + 1) % kNumFilters; 401 | self.filter = kFilters[self.index]; 402 | } 403 | 404 | - (void)detailsViewDidDismiss:(VGDetailsView *)detailsView { 405 | if (self.paused) { 406 | self.paused = NO; 407 | } else { 408 | CGRect detailsFrame = self.detailsView.frame; 409 | [UIView animateWithDuration:0.3 animations:^{ 410 | self.detailsView.alpha = 0; 411 | self.detailsView.frame = CGRectOffset(detailsFrame, 0, 30); 412 | } completion:^(BOOL finished) { 413 | self.detailsView.hidden = YES; 414 | self.detailsView.frame = detailsFrame; 415 | }]; 416 | } 417 | } 418 | 419 | - (void)detailsView:(VGDetailsView *)detailsView didChangePeek:(CGFloat)peek { 420 | self.peek = peek; 421 | } 422 | 423 | - (void)detailsViewDidTogglePause:(VGDetailsView *)detailsView { 424 | self.paused = !self.paused; 425 | } 426 | 427 | #pragma mark UIView 428 | 429 | - (void)setHidden:(BOOL)hidden { 430 | [super setHidden:hidden]; 431 | 432 | if (hidden) { 433 | [self stop]; 434 | } else { 435 | [self start]; 436 | } 437 | } 438 | 439 | - (void)layoutSubviews { 440 | [super layoutSubviews]; 441 | 442 | CGRect b = self.bounds; 443 | self.outputView.frame = b; 444 | self.detailsView.frame = CGRectMake(b.size.width / 2 - 120, b.size.height - 70, 240, 50); 445 | } 446 | 447 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { 448 | if (self.hidden || !self.userInteractionEnabled) { 449 | return NO; 450 | } 451 | 452 | if (self.clipsToBounds && !CGRectContainsPoint(self.bounds, point)) { 453 | return NO; 454 | } 455 | 456 | for (UIView *view in self.subviews) { 457 | if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) { 458 | return YES; 459 | } 460 | } 461 | 462 | return NO; 463 | } 464 | 465 | @end 466 | 467 | 468 | 469 | /*******************************************************************************/ 470 | #pragma mark - Window 471 | /*******************************************************************************/ 472 | 473 | @interface VGWindow () 474 | 475 | @property (nonatomic,strong) VGFilterWindow *filterWindow; 476 | 477 | @property (nonatomic,assign,getter=isActive) BOOL active; 478 | 479 | @end 480 | 481 | 482 | @implementation VGWindow 483 | 484 | - (id)initWithFrame:(CGRect)frame { 485 | if (self = [super initWithFrame:frame]) { 486 | _filterWindow = [[VGFilterWindow alloc] initWithFrame:frame]; 487 | _filterWindow.hidden = YES; 488 | } 489 | return self; 490 | } 491 | 492 | - (void)setActive:(BOOL)active { 493 | if (_active != active) { 494 | _active = active; 495 | self.filterWindow.hidden = !_active; 496 | } 497 | } 498 | 499 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { 500 | [super motionEnded:motion withEvent:event]; 501 | 502 | if (motion == UIEventSubtypeMotionShake) { 503 | self.active = !self.active; 504 | } 505 | } 506 | 507 | @end 508 | --------------------------------------------------------------------------------