├── README.md ├── Squash.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── jkraut.xcuserdatad │ └── xcschemes │ ├── Squash.xcscheme │ └── xcschememanagement.plist └── Squash ├── SMAppDelegate.h ├── SMAppDelegate.m ├── SMSquashLayer.h ├── SMSquashLayer.m ├── SMSquashView.h ├── SMSquashView.m ├── SMViewController.h ├── SMViewController.m ├── SMViewController.xib ├── Squash-Info.plist ├── Squash-Prefix.pch ├── en.lproj └── InfoPlist.strings ├── logo_periwinkle.png └── main.m /README.md: -------------------------------------------------------------------------------- 1 | # Squash 2 | 3 | Automatic squash/stretch animated subclasses of UIView and CALayer. 4 | 5 | SMSquashView and its backing class, SMSquashLayer, squash and stretch themselves automatically in response to motion. They will distort themselves longer in the direction of motion, and shorter in the perpendicular directions. The distortion is proportional to the velocity the object, and scaled such that the volume remains constant. In short: it does exactly what you’d want it to do, with no configuration required. 6 | 7 | ## How It Works 8 | 9 | The math is pretty simple, if you like transformation matrices. Every time the layer’s position changes, we note the time and position delta from last time. Using that information we can calculate the velocity for the current frame of animation. We align a transformation matrix to the direction of travel using the same idea as gluLookAt, then scale according to the magnitude of the velocity. 10 | 11 | ## How to Use It 12 | 13 | Seriously, just drop it in. It supports all the customization you could want: 14 | * `squashFactor` represents the speed above which an object must move to be distorted 15 | * `maxStretch` puts a ceiling on the amount of stretching an object can do 16 | * `minSquash` puts a floor on it 17 | * `smoothMotion` will run a moving average filter to smooth out motion jaggies 18 | 19 | …but these are all set to very reasonable values. You may not need to change them. 20 | 21 | ## What's It Look Like 22 | 23 | ![Squash Demo GIF](http://blog.spacemanlabs.com/wp-content/uploads/2013/07/squash.gif) 24 | 25 | ## More Info 26 | 27 | The original blog post for this project can be found [here](http://blog.spacemanlabs.com/?p=684). 28 | 29 | 30 | LICENSE 31 | ------- 32 | 33 | Copyright (C) 2013 by Spaceman Labs 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in 43 | all copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 51 | THE SOFTWARE. 52 | -------------------------------------------------------------------------------- /Squash.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6B5808591780C72000891C12 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B5808581780C72000891C12 /* Foundation.framework */; }; 11 | 6B58085B1780C72000891C12 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B58085A1780C72000891C12 /* CoreGraphics.framework */; }; 12 | 6B58085D1780C72000891C12 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B58085C1780C72000891C12 /* UIKit.framework */; }; 13 | 6B5808631780C72000891C12 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6B5808611780C72000891C12 /* InfoPlist.strings */; }; 14 | 6B5808651780C72000891C12 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B5808641780C72000891C12 /* main.m */; }; 15 | 6B5808691780C72000891C12 /* SMAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B5808681780C72000891C12 /* SMAppDelegate.m */; }; 16 | 6B58086F1780C72000891C12 /* SMViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B58086E1780C72000891C12 /* SMViewController.m */; }; 17 | 6B5808791780C72000891C12 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B5808581780C72000891C12 /* Foundation.framework */; }; 18 | 6B58087A1780C72000891C12 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B58085C1780C72000891C12 /* UIKit.framework */; }; 19 | 6B5808821780C72000891C12 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6B5808801780C72000891C12 /* InfoPlist.strings */; }; 20 | 6B5808841780C72000891C12 /* SquashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B5808831780C72000891C12 /* SquashTests.m */; }; 21 | 6B58088F1780C74A00891C12 /* SMSquashLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B58088E1780C74A00891C12 /* SMSquashLayer.m */; }; 22 | 6B5808921780C75A00891C12 /* SMSquashView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B5808911780C75A00891C12 /* SMSquashView.m */; }; 23 | 6B5808941780D2EF00891C12 /* logo_periwinkle.png in Resources */ = {isa = PBXBuildFile; fileRef = 6B5808931780D2EF00891C12 /* logo_periwinkle.png */; }; 24 | 6BEE8F29178286CB0095ACB9 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BEE8F28178286CB0095ACB9 /* QuartzCore.framework */; }; 25 | 6BEE8F2B1782873E0095ACB9 /* SMViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6BEE8F2A1782873E0095ACB9 /* SMViewController.xib */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 6B58087B1780C72000891C12 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 6B58084D1780C72000891C12 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 6B5808541780C72000891C12; 34 | remoteInfo = Squash; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 6B5808551780C72000891C12 /* Squash.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Squash.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 6B5808581780C72000891C12 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 41 | 6B58085A1780C72000891C12 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 42 | 6B58085C1780C72000891C12 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 43 | 6B5808601780C72000891C12 /* Squash-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Squash-Info.plist"; sourceTree = ""; }; 44 | 6B5808621780C72000891C12 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 45 | 6B5808641780C72000891C12 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 46 | 6B5808661780C72000891C12 /* Squash-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Squash-Prefix.pch"; sourceTree = ""; }; 47 | 6B5808671780C72000891C12 /* SMAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SMAppDelegate.h; sourceTree = ""; }; 48 | 6B5808681780C72000891C12 /* SMAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SMAppDelegate.m; sourceTree = ""; }; 49 | 6B58086D1780C72000891C12 /* SMViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SMViewController.h; sourceTree = ""; }; 50 | 6B58086E1780C72000891C12 /* SMViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SMViewController.m; sourceTree = ""; }; 51 | 6B5808761780C72000891C12 /* SquashTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SquashTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 6B58087F1780C72000891C12 /* SquashTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SquashTests-Info.plist"; sourceTree = ""; }; 53 | 6B5808811780C72000891C12 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 54 | 6B5808831780C72000891C12 /* SquashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SquashTests.m; sourceTree = ""; }; 55 | 6B58088D1780C74A00891C12 /* SMSquashLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SMSquashLayer.h; sourceTree = ""; }; 56 | 6B58088E1780C74A00891C12 /* SMSquashLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SMSquashLayer.m; sourceTree = ""; }; 57 | 6B5808901780C75A00891C12 /* SMSquashView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SMSquashView.h; sourceTree = ""; }; 58 | 6B5808911780C75A00891C12 /* SMSquashView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SMSquashView.m; sourceTree = ""; }; 59 | 6B5808931780D2EF00891C12 /* logo_periwinkle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo_periwinkle.png; sourceTree = ""; }; 60 | 6BEE8F28178286CB0095ACB9 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 61 | 6BEE8F2A1782873E0095ACB9 /* SMViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SMViewController.xib; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 6B5808521780C72000891C12 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | 6BEE8F29178286CB0095ACB9 /* QuartzCore.framework in Frameworks */, 70 | 6B58085B1780C72000891C12 /* CoreGraphics.framework in Frameworks */, 71 | 6B58085D1780C72000891C12 /* UIKit.framework in Frameworks */, 72 | 6B5808591780C72000891C12 /* Foundation.framework in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | 6B5808731780C72000891C12 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | 6B58087A1780C72000891C12 /* UIKit.framework in Frameworks */, 81 | 6B5808791780C72000891C12 /* Foundation.framework in Frameworks */, 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | 6B58084C1780C72000891C12 = { 89 | isa = PBXGroup; 90 | children = ( 91 | 6B58085E1780C72000891C12 /* Squash */, 92 | 6B58087D1780C72000891C12 /* SquashTests */, 93 | 6B5808571780C72000891C12 /* Frameworks */, 94 | 6B5808561780C72000891C12 /* Products */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | 6B5808561780C72000891C12 /* Products */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 6B5808551780C72000891C12 /* Squash.app */, 102 | 6B5808761780C72000891C12 /* SquashTests.xctest */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 6B5808571780C72000891C12 /* Frameworks */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 6BEE8F28178286CB0095ACB9 /* QuartzCore.framework */, 111 | 6B5808581780C72000891C12 /* Foundation.framework */, 112 | 6B58085A1780C72000891C12 /* CoreGraphics.framework */, 113 | 6B58085C1780C72000891C12 /* UIKit.framework */, 114 | ); 115 | name = Frameworks; 116 | sourceTree = ""; 117 | }; 118 | 6B58085E1780C72000891C12 /* Squash */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 6B5808671780C72000891C12 /* SMAppDelegate.h */, 122 | 6B5808681780C72000891C12 /* SMAppDelegate.m */, 123 | 6B58086D1780C72000891C12 /* SMViewController.h */, 124 | 6B58086E1780C72000891C12 /* SMViewController.m */, 125 | 6BEE8F2A1782873E0095ACB9 /* SMViewController.xib */, 126 | 6B58088D1780C74A00891C12 /* SMSquashLayer.h */, 127 | 6B58088E1780C74A00891C12 /* SMSquashLayer.m */, 128 | 6B5808901780C75A00891C12 /* SMSquashView.h */, 129 | 6B5808911780C75A00891C12 /* SMSquashView.m */, 130 | 6B58085F1780C72000891C12 /* Supporting Files */, 131 | ); 132 | path = Squash; 133 | sourceTree = ""; 134 | }; 135 | 6B58085F1780C72000891C12 /* Supporting Files */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 6B5808931780D2EF00891C12 /* logo_periwinkle.png */, 139 | 6B5808601780C72000891C12 /* Squash-Info.plist */, 140 | 6B5808611780C72000891C12 /* InfoPlist.strings */, 141 | 6B5808641780C72000891C12 /* main.m */, 142 | 6B5808661780C72000891C12 /* Squash-Prefix.pch */, 143 | ); 144 | name = "Supporting Files"; 145 | sourceTree = ""; 146 | }; 147 | 6B58087D1780C72000891C12 /* SquashTests */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 6B5808831780C72000891C12 /* SquashTests.m */, 151 | 6B58087E1780C72000891C12 /* Supporting Files */, 152 | ); 153 | path = SquashTests; 154 | sourceTree = ""; 155 | }; 156 | 6B58087E1780C72000891C12 /* Supporting Files */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 6B58087F1780C72000891C12 /* SquashTests-Info.plist */, 160 | 6B5808801780C72000891C12 /* InfoPlist.strings */, 161 | ); 162 | name = "Supporting Files"; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXGroup section */ 166 | 167 | /* Begin PBXNativeTarget section */ 168 | 6B5808541780C72000891C12 /* Squash */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = 6B5808871780C72000891C12 /* Build configuration list for PBXNativeTarget "Squash" */; 171 | buildPhases = ( 172 | 6B5808511780C72000891C12 /* Sources */, 173 | 6B5808521780C72000891C12 /* Frameworks */, 174 | 6B5808531780C72000891C12 /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = Squash; 181 | productName = Squash; 182 | productReference = 6B5808551780C72000891C12 /* Squash.app */; 183 | productType = "com.apple.product-type.application"; 184 | }; 185 | 6B5808751780C72000891C12 /* SquashTests */ = { 186 | isa = PBXNativeTarget; 187 | buildConfigurationList = 6B58088A1780C72000891C12 /* Build configuration list for PBXNativeTarget "SquashTests" */; 188 | buildPhases = ( 189 | 6B5808721780C72000891C12 /* Sources */, 190 | 6B5808731780C72000891C12 /* Frameworks */, 191 | 6B5808741780C72000891C12 /* Resources */, 192 | ); 193 | buildRules = ( 194 | ); 195 | dependencies = ( 196 | 6B58087C1780C72000891C12 /* PBXTargetDependency */, 197 | ); 198 | name = SquashTests; 199 | productName = SquashTests; 200 | productReference = 6B5808761780C72000891C12 /* SquashTests.xctest */; 201 | productType = "com.apple.product-type.bundle.unit-test"; 202 | }; 203 | /* End PBXNativeTarget section */ 204 | 205 | /* Begin PBXProject section */ 206 | 6B58084D1780C72000891C12 /* Project object */ = { 207 | isa = PBXProject; 208 | attributes = { 209 | CLASSPREFIX = SM; 210 | LastUpgradeCheck = 0500; 211 | ORGANIZATIONNAME = "Joel Kraut"; 212 | TargetAttributes = { 213 | 6B5808751780C72000891C12 = { 214 | TestTargetID = 6B5808541780C72000891C12; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = 6B5808501780C72000891C12 /* Build configuration list for PBXProject "Squash" */; 219 | compatibilityVersion = "Xcode 3.2"; 220 | developmentRegion = English; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = 6B58084C1780C72000891C12; 227 | productRefGroup = 6B5808561780C72000891C12 /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | 6B5808541780C72000891C12 /* Squash */, 232 | 6B5808751780C72000891C12 /* SquashTests */, 233 | ); 234 | }; 235 | /* End PBXProject section */ 236 | 237 | /* Begin PBXResourcesBuildPhase section */ 238 | 6B5808531780C72000891C12 /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 6B5808941780D2EF00891C12 /* logo_periwinkle.png in Resources */, 243 | 6B5808631780C72000891C12 /* InfoPlist.strings in Resources */, 244 | 6BEE8F2B1782873E0095ACB9 /* SMViewController.xib in Resources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | 6B5808741780C72000891C12 /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 6B5808821780C72000891C12 /* InfoPlist.strings in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 6B5808511780C72000891C12 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 6B58088F1780C74A00891C12 /* SMSquashLayer.m in Sources */, 264 | 6B5808691780C72000891C12 /* SMAppDelegate.m in Sources */, 265 | 6B58086F1780C72000891C12 /* SMViewController.m in Sources */, 266 | 6B5808651780C72000891C12 /* main.m in Sources */, 267 | 6B5808921780C75A00891C12 /* SMSquashView.m in Sources */, 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | 6B5808721780C72000891C12 /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 6B5808841780C72000891C12 /* SquashTests.m in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | /* End PBXSourcesBuildPhase section */ 280 | 281 | /* Begin PBXTargetDependency section */ 282 | 6B58087C1780C72000891C12 /* PBXTargetDependency */ = { 283 | isa = PBXTargetDependency; 284 | target = 6B5808541780C72000891C12 /* Squash */; 285 | targetProxy = 6B58087B1780C72000891C12 /* PBXContainerItemProxy */; 286 | }; 287 | /* End PBXTargetDependency section */ 288 | 289 | /* Begin PBXVariantGroup section */ 290 | 6B5808611780C72000891C12 /* InfoPlist.strings */ = { 291 | isa = PBXVariantGroup; 292 | children = ( 293 | 6B5808621780C72000891C12 /* en */, 294 | ); 295 | name = InfoPlist.strings; 296 | sourceTree = ""; 297 | }; 298 | 6B5808801780C72000891C12 /* InfoPlist.strings */ = { 299 | isa = PBXVariantGroup; 300 | children = ( 301 | 6B5808811780C72000891C12 /* en */, 302 | ); 303 | name = InfoPlist.strings; 304 | sourceTree = ""; 305 | }; 306 | /* End PBXVariantGroup section */ 307 | 308 | /* Begin XCBuildConfiguration section */ 309 | 6B5808851780C72000891C12 /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_CONSTANT_CONVERSION = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INT_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 326 | COPY_PHASE_STRIP = NO; 327 | GCC_C_LANGUAGE_STANDARD = gnu99; 328 | GCC_DYNAMIC_NO_PIC = NO; 329 | GCC_OPTIMIZATION_LEVEL = 0; 330 | GCC_PREPROCESSOR_DEFINITIONS = ( 331 | "DEBUG=1", 332 | "$(inherited)", 333 | ); 334 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 336 | GCC_WARN_UNDECLARED_SELECTOR = YES; 337 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 338 | GCC_WARN_UNUSED_FUNCTION = YES; 339 | GCC_WARN_UNUSED_VARIABLE = YES; 340 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 341 | ONLY_ACTIVE_ARCH = YES; 342 | SDKROOT = iphoneos; 343 | TARGETED_DEVICE_FAMILY = 2; 344 | }; 345 | name = Debug; 346 | }; 347 | 6B5808861780C72000891C12 /* Release */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ALWAYS_SEARCH_USER_PATHS = NO; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 352 | CLANG_CXX_LIBRARY = "libc++"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_WARN_BOOL_CONVERSION = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 364 | COPY_PHASE_STRIP = YES; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | GCC_C_LANGUAGE_STANDARD = gnu99; 367 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 368 | GCC_WARN_UNDECLARED_SELECTOR = YES; 369 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 370 | GCC_WARN_UNUSED_FUNCTION = YES; 371 | GCC_WARN_UNUSED_VARIABLE = YES; 372 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 373 | SDKROOT = iphoneos; 374 | TARGETED_DEVICE_FAMILY = 2; 375 | VALIDATE_PRODUCT = YES; 376 | }; 377 | name = Release; 378 | }; 379 | 6B5808881780C72000891C12 /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 384 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 385 | GCC_PREFIX_HEADER = "Squash/Squash-Prefix.pch"; 386 | INFOPLIST_FILE = "Squash/Squash-Info.plist"; 387 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | WRAPPER_EXTENSION = app; 390 | }; 391 | name = Debug; 392 | }; 393 | 6B5808891780C72000891C12 /* Release */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 398 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 399 | GCC_PREFIX_HEADER = "Squash/Squash-Prefix.pch"; 400 | INFOPLIST_FILE = "Squash/Squash-Info.plist"; 401 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 402 | PRODUCT_NAME = "$(TARGET_NAME)"; 403 | WRAPPER_EXTENSION = app; 404 | }; 405 | name = Release; 406 | }; 407 | 6B58088B1780C72000891C12 /* Debug */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Squash.app/Squash"; 411 | FRAMEWORK_SEARCH_PATHS = ( 412 | "$(SDKROOT)/Developer/Library/Frameworks", 413 | "$(inherited)", 414 | "$(SYSTEM_APPS_DIR)/Xcode5-DP2.app/Contents/Developer/Library/Frameworks", 415 | ); 416 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 417 | GCC_PREFIX_HEADER = "Squash/Squash-Prefix.pch"; 418 | GCC_PREPROCESSOR_DEFINITIONS = ( 419 | "DEBUG=1", 420 | "$(inherited)", 421 | ); 422 | INFOPLIST_FILE = "SquashTests/SquashTests-Info.plist"; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | TEST_HOST = "$(BUNDLE_LOADER)"; 425 | WRAPPER_EXTENSION = xctest; 426 | }; 427 | name = Debug; 428 | }; 429 | 6B58088C1780C72000891C12 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Squash.app/Squash"; 433 | FRAMEWORK_SEARCH_PATHS = ( 434 | "$(SDKROOT)/Developer/Library/Frameworks", 435 | "$(inherited)", 436 | "$(SYSTEM_APPS_DIR)/Xcode5-DP2.app/Contents/Developer/Library/Frameworks", 437 | ); 438 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 439 | GCC_PREFIX_HEADER = "Squash/Squash-Prefix.pch"; 440 | INFOPLIST_FILE = "SquashTests/SquashTests-Info.plist"; 441 | PRODUCT_NAME = "$(TARGET_NAME)"; 442 | TEST_HOST = "$(BUNDLE_LOADER)"; 443 | WRAPPER_EXTENSION = xctest; 444 | }; 445 | name = Release; 446 | }; 447 | /* End XCBuildConfiguration section */ 448 | 449 | /* Begin XCConfigurationList section */ 450 | 6B5808501780C72000891C12 /* Build configuration list for PBXProject "Squash" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | 6B5808851780C72000891C12 /* Debug */, 454 | 6B5808861780C72000891C12 /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Release; 458 | }; 459 | 6B5808871780C72000891C12 /* Build configuration list for PBXNativeTarget "Squash" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | 6B5808881780C72000891C12 /* Debug */, 463 | 6B5808891780C72000891C12 /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 6B58088A1780C72000891C12 /* Build configuration list for PBXNativeTarget "SquashTests" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 6B58088B1780C72000891C12 /* Debug */, 472 | 6B58088C1780C72000891C12 /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | /* End XCConfigurationList section */ 478 | }; 479 | rootObject = 6B58084D1780C72000891C12 /* Project object */; 480 | } 481 | -------------------------------------------------------------------------------- /Squash.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Squash.xcodeproj/xcuserdata/jkraut.xcuserdatad/xcschemes/Squash.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Squash.xcodeproj/xcuserdata/jkraut.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Squash.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 6B5808541780C72000891C12 16 | 17 | primary 18 | 19 | 20 | 6B5808751780C72000891C12 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Squash/SMAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SMAppDelegate.h 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SMAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Squash/SMAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SMAppDelegate.m 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import "SMAppDelegate.h" 10 | #import "SMViewController.h" 11 | 12 | @implementation SMAppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | // Override point for customization after application launch. 17 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 18 | // Override point for customization after application launch. 19 | self.window.rootViewController = [[SMViewController alloc] initWithNibName:nil bundle:nil]; 20 | [self.window makeKeyAndVisible]; 21 | return YES; 22 | } 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application 25 | { 26 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 28 | } 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application 31 | { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application 37 | { 38 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 39 | } 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application 42 | { 43 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 44 | } 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application 47 | { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Squash/SMSquashLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // SMSquashLayer.h 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern NSString * const kSquashActionKey; 12 | 13 | @interface SMSquashLayer : CALayer 14 | 15 | // The minimum velocity (in points per second) above which squashing and stretching will occur. 16 | // 17 | // Default value is 3000, which works well on an iPad screen with the default .25s animation duration. 18 | @property (nonatomic, assign) float squashFactor; 19 | 20 | // The upper and lower bounds for distortion. The layer will never scale more than maxStretch in the 21 | // direction of travel, and will never scale less than minSquash in the perpendicular directions. 22 | // 23 | // maxStretch defaults to 1.5; minSquash defaults to 0.5. 24 | @property (nonatomic, assign) float maxStretch; 25 | @property (nonatomic, assign) float minSquash; 26 | 27 | // Smooths input motion with a moving average. May be useful when moving under UIDynamicWhatevers. 28 | // 29 | // Defaults to NO; 30 | @property (nonatomic, assign) BOOL smoothMotion; 31 | @end 32 | -------------------------------------------------------------------------------- /Squash/SMSquashLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // SMSquashLayer.m 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import "SMSquashLayer.h" 10 | 11 | NSString * const kSquashActionKey = @"_animator"; 12 | 13 | @interface SMSquashLayer() { 14 | // we need to track a lot of stuff from the previous frame to calculate deltas 15 | CFTimeInterval lastFrameTime; 16 | CFTimeInterval lastTimeDelta; 17 | CGPoint lastPosition; 18 | float lastZPosition; 19 | CATransform3D lastTransform; 20 | 21 | // for the running average when we're attempting to smooth our input 22 | float average[3]; 23 | } 24 | 25 | // the custom animator we'll use to get per-frame updates to our position 26 | @property (nonatomic, assign) float _animator; 27 | 28 | // all the work will happen here 29 | - (void)respondTo3dPosition:(float)x :(float)y :(float)z; 30 | 31 | @end 32 | 33 | @implementation SMSquashLayer 34 | 35 | @dynamic _animator; 36 | 37 | #pragma mark - Life cycle 38 | 39 | - (id)init 40 | { 41 | if ((self = [super init])) 42 | { 43 | // set reasonable defaults 44 | self.squashFactor = 3000.f; 45 | self.maxStretch = 1.5f; 46 | self.minSquash = .5f; 47 | lastPosition = (CGPoint){NAN, NAN}; 48 | lastZPosition = NAN; 49 | lastTransform = CATransform3DIdentity; 50 | self.smoothMotion = NO; 51 | } 52 | return self; 53 | } 54 | 55 | #pragma mark - Animation triggers 56 | 57 | - (void)setPosition:(CGPoint)position 58 | { 59 | [super setPosition:position]; 60 | // if this is our first appearance or we don't have a size, save this as our "old" state 61 | if (isnan(lastPosition.x) || CGSizeEqualToSize(self.frame.size, CGSizeZero)) 62 | { 63 | lastPosition = position; 64 | lastZPosition = self.zPosition; 65 | } 66 | // otherwise we're probably animating, so kick the custom animator 67 | else 68 | { 69 | lastFrameTime = CACurrentMediaTime(); 70 | self._animator = self._animator ? 0.f : 1.f; 71 | } 72 | } 73 | 74 | - (void)setZPosition:(CGFloat)zPosition 75 | { 76 | [super setZPosition:zPosition]; 77 | if (isnan(lastPosition.x)) 78 | { 79 | lastPosition = self.position; 80 | lastZPosition = zPosition; 81 | } 82 | else 83 | { 84 | self._animator = self._animator ? 0.f : 1.f; 85 | } 86 | } 87 | 88 | #pragma mark - Internal logic 89 | 90 | - (void)animationEnded 91 | { 92 | lastPosition = self.position; 93 | lastZPosition = self.zPosition; 94 | average[0] = 0; 95 | average[1] = 0; 96 | average[2] = 0; 97 | } 98 | 99 | - (void)display 100 | { 101 | [CATransaction begin]; 102 | [CATransaction setDisableActions:YES]; 103 | // use the presentation layer's position, which has been correctly interpolated, to calculate our squash 104 | CALayer *presentationLayer = (CALayer*)self.presentationLayer; 105 | [self respondTo3dPosition:presentationLayer.position.x :presentationLayer.position.y :presentationLayer.zPosition]; 106 | [CATransaction commit]; 107 | 108 | // set up a timer to clear out all our state when the animation ends 109 | [SMSquashLayer cancelPreviousPerformRequestsWithTarget:self selector:@selector(animationEnded) object:nil]; 110 | [self performSelector:@selector(animationEnded) withObject:nil afterDelay:5./60.]; 111 | } 112 | 113 | static inline void cross(float *b, float *c, float *result) 114 | { 115 | result[0] = b[1] * c[2] - b[2] * c[1]; 116 | result[1] = b[2] * c[0] - b[0] * c[2]; 117 | result[2] = b[0] * c[1] - b[1] * c[0]; 118 | } 119 | 120 | static inline void normalize(float *vec) 121 | { 122 | float magnitude = sqrtf(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]); 123 | vec[0] /= magnitude; 124 | vec[1] /= magnitude; 125 | vec[2] /= magnitude; 126 | } 127 | 128 | - (void)respondTo3dPosition:(float)x :(float)y :(float)z 129 | { 130 | CFTimeInterval now = CACurrentMediaTime(); 131 | 132 | // our movement since the last frame 133 | float delta[3] = {x - lastPosition.x, y - lastPosition.y, z - lastZPosition}; 134 | 135 | // if we're smoothing input, put that delta into the running average 136 | if (self.smoothMotion) 137 | { 138 | float averageFactor = .9f; 139 | average[0] = average[0] * averageFactor + delta[0] * (1.f - averageFactor); 140 | average[1] = average[1] * averageFactor + delta[1] * (1.f - averageFactor); 141 | average[2] = average[2] * averageFactor + delta[2] * (1.f - averageFactor); 142 | 143 | delta[0] = average[0]; 144 | delta[1] = average[1]; 145 | delta[2] = average[2]; 146 | } 147 | 148 | // if we haven't actually moved, we have nothing to do here 149 | if (delta[0] == 0 && delta[1] == 0 && delta[2] == 0) 150 | return; 151 | 152 | // use the time the last frame took to get us to the current position 153 | CFTimeInterval timeDelta = lastTimeDelta; 154 | 155 | // the current time can be somewhat variable, so let's snap to the nearest frame 156 | timeDelta *= 60.; 157 | timeDelta = round(timeDelta); 158 | timeDelta /= 60.; 159 | 160 | // if this is our first frame, let's take a guess as to the time step 161 | if (timeDelta >= now) 162 | timeDelta = 1./60.; 163 | if (timeDelta == 0) 164 | timeDelta = 1./60.; 165 | 166 | float velocity[3] = {delta[0] / timeDelta, delta[1] / timeDelta, delta[2] / timeDelta}; 167 | 168 | float magnitude = sqrtf(velocity[0] * velocity[0] + velocity[1] * velocity[1] + velocity[2] * velocity[2]); 169 | 170 | // create a "look at" matrix to orient us along our direction of travel (code modified from gluLookAt) 171 | normalize(velocity); 172 | 173 | CATransform3D squash = CATransform3DIdentity; 174 | 175 | float up[3] = {0, 0, -1}; 176 | float side[3]; 177 | cross(velocity, up, side); 178 | normalize(side); 179 | cross(side, velocity, up); 180 | 181 | squash.m11 = side[0]; 182 | squash.m21 = side[1]; 183 | squash.m31 = side[2]; 184 | squash.m41 = 0; 185 | 186 | squash.m12 = up[0]; 187 | squash.m22 = up[1]; 188 | squash.m32 = up[2]; 189 | squash.m42 = 0; 190 | 191 | squash.m13 = -velocity[0]; 192 | squash.m23 = -velocity[1]; 193 | squash.m33 = -velocity[2]; 194 | squash.m43 = 0; 195 | 196 | // apply a scale in the direction of travel and the directions perpendicular to travel 197 | CATransform3D squashInverse = CATransform3DInvert(squash); 198 | 199 | float scale[3] = {MAX(self.minSquash, MIN(1.f, self.squashFactor / magnitude)), 200 | MAX(self.minSquash, MIN(1.f, self.squashFactor / magnitude)), 201 | MIN(self.maxStretch, MAX(1.f, magnitude / self.squashFactor))}; 202 | 203 | CATransform3D scaleTransform = CATransform3DMakeScale(scale[0], scale[1], scale[2]); 204 | squash = CATransform3DConcat(squash, scaleTransform); 205 | squash = CATransform3DConcat(squash, squashInverse); 206 | 207 | // apply the squash transform 208 | self.transform = CATransform3DConcat(self.transform, CATransform3DInvert(lastTransform)); 209 | self.transform = CATransform3DConcat(self.transform, squash); 210 | 211 | // set new state for next frame 212 | lastPosition = (CGPoint){x, y}; 213 | lastZPosition = z; 214 | lastTimeDelta = now - lastFrameTime; 215 | lastFrameTime = now; 216 | lastTransform = squash; 217 | } 218 | 219 | #pragma mark - Animation Methods 220 | 221 | // tell Core Animation that we need display called every frame while our custom animator is animating 222 | + (BOOL)needsDisplayForKey:(NSString *)key 223 | { 224 | if ([key isEqualToString:kSquashActionKey]) 225 | return YES; 226 | return [super needsDisplayForKey:key]; 227 | } 228 | 229 | // set up a basic animation for our custom animator 230 | + (id)defaultActionForKey:(NSString *)key 231 | { 232 | if ([key isEqualToString:kSquashActionKey]) 233 | { 234 | CABasicAnimation *animation = [CABasicAnimation animation]; 235 | return animation; 236 | } 237 | return [super defaultActionForKey:key]; 238 | } 239 | 240 | @end 241 | -------------------------------------------------------------------------------- /Squash/SMSquashView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SMSquashView.h 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SMSquashView : UIView 12 | 13 | // The minimum velocity (in points per second) above which squashing and stretching will occur. 14 | // 15 | // Default value is 3000, which works well on an iPad screen with the default .25s animation duration. 16 | @property (nonatomic, assign) float squashFactor; 17 | 18 | // The upper and lower bounds for distortion. The layer will never scale more than maxStretch in the 19 | // direction of travel, and will never scale less than minSquash in the perpendicular directions. 20 | // 21 | // maxStretch defaults to 2.0; minSquash defaults to 0.5. 22 | @property (nonatomic, assign) float maxStretch; 23 | @property (nonatomic, assign) float minSquash; 24 | 25 | // Smooths input motion with a moving average. May be useful when moving under UIDynamicWhatevers. 26 | // 27 | // Defaults to NO; 28 | @property (nonatomic, assign) BOOL smoothMotion; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Squash/SMSquashView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SMSquashView.m 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import "SMSquashView.h" 10 | #import "SMSquashLayer.h" 11 | 12 | @interface SMSquashView () { 13 | CFTimeInterval positionDuration; 14 | BOOL positionAnimates; 15 | } 16 | @end 17 | 18 | @implementation SMSquashView 19 | 20 | @dynamic squashFactor, maxStretch, minSquash, smoothMotion; 21 | 22 | + (Class)layerClass 23 | { 24 | return [SMSquashLayer class]; 25 | } 26 | 27 | // by using our layer's delegate methods, we can get and set all the information we need 28 | - (id < CAAction >)actionForLayer:(CALayer *)layer forKey:(NSString *)key 29 | { 30 | id parent = [super actionForLayer:layer forKey:key]; 31 | 32 | // If the position is changing and it's animated, that means it was changed inside an animation block. 33 | // We can take the duration of that animation and apply it to our custom animator's action. 34 | if ([key isEqualToString:@"position"]) 35 | { 36 | positionAnimates = YES; 37 | if ([(NSObject*)parent conformsToProtocol:@protocol(CAMediaTiming)]) 38 | positionDuration = ((id)parent).duration; 39 | else 40 | positionAnimates = NO; 41 | return parent; 42 | } 43 | // Here we apply the position animation's duration to the squash action, which we get from our layer. 44 | else if ([key isEqualToString:kSquashActionKey]) 45 | { 46 | id mine = [[self.layer class] defaultActionForKey:key]; 47 | if ([(NSObject*)mine conformsToProtocol:@protocol(CAMediaTiming)] && positionDuration) 48 | ((id)mine).duration = positionDuration; 49 | positionDuration = 0; 50 | if (positionAnimates) 51 | return mine; 52 | return parent; 53 | } 54 | return parent; 55 | } 56 | 57 | // pass through layer properties 58 | 59 | - (float)squashFactor 60 | { 61 | return ((SMSquashLayer*)self.layer).squashFactor; 62 | } 63 | 64 | - (float)maxStretch 65 | { 66 | return ((SMSquashLayer*)self.layer).maxStretch; 67 | } 68 | 69 | - (float)minSquash 70 | { 71 | return ((SMSquashLayer*)self.layer).minSquash; 72 | } 73 | 74 | - (BOOL)smoothMotion 75 | { 76 | return ((SMSquashLayer*)self.layer).smoothMotion; 77 | } 78 | 79 | - (void)setSquashFactor:(float)squashFactor 80 | { 81 | ((SMSquashLayer*)self.layer).squashFactor = squashFactor; 82 | } 83 | 84 | - (void)setMaxStretch:(float)maxStretch 85 | { 86 | ((SMSquashLayer*)self.layer).maxStretch = maxStretch; 87 | } 88 | 89 | - (void)setMinSquash:(float)minSquash 90 | { 91 | ((SMSquashLayer*)self.layer).minSquash = minSquash; 92 | } 93 | 94 | - (void)setSmoothMotion:(BOOL)smoothMotion 95 | { 96 | ((SMSquashLayer*)self.layer).smoothMotion = smoothMotion; 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /Squash/SMViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SMViewController.h 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SMViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Squash/SMViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SMViewController.m 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2012 Spaceman Labs. All rights reserved. 7 | // 8 | 9 | #import "SMViewController.h" 10 | #import "SMSquashView.h" 11 | #import "SMSquashLayer.h" 12 | 13 | // if USE_CALAYER is defined, an SMSquashLayer will be used, rather than 14 | // an SMSquashView. Because dynamics and spring-style animations both 15 | // require UIViews, interaction will be the same as if no defines were 16 | // enabled, but with a layer rather than a view. 17 | //#define USE_CALAYER 18 | 19 | @interface SMViewController () 20 | { 21 | SMSquashLayer *layer; 22 | SMSquashView *view; 23 | } 24 | @end 25 | 26 | @implementation SMViewController 27 | 28 | - (void)viewDidLoad 29 | { 30 | [super viewDidLoad]; 31 | // Do any additional setup after loading the view, typically from a nib. 32 | } 33 | 34 | - (void)viewDidUnload 35 | { 36 | [super viewDidUnload]; 37 | // Release any retained subviews of the main view. 38 | } 39 | 40 | - (void)viewWillAppear:(BOOL)animated 41 | { 42 | [super viewWillAppear:animated]; 43 | CATransform3D perspective = CATransform3DIdentity; 44 | perspective.m34 = -1.f/1000.f; 45 | self.view.layer.sublayerTransform = perspective; 46 | self.view.backgroundColor = [UIColor grayColor]; 47 | 48 | CGSize viewSize = self.view.bounds.size; 49 | 50 | UIImage *image = [UIImage imageNamed:@"logo_periwinkle"]; 51 | 52 | #ifdef USE_CALAYER 53 | layer = [[SMSquashLayer alloc] init]; 54 | layer.frame = CGRectMake(viewSize.width/2 - 150, viewSize.height/2 - 150, 300, 300); 55 | [self.view.layer addSublayer:layer]; 56 | layer.contents = (id)image.CGImage; 57 | #else 58 | view = [[SMSquashView alloc] init]; 59 | view.frame = CGRectMake(viewSize.width/2 - 150, viewSize.height/2 - 150, 300, 300); 60 | [self.view addSubview:view]; 61 | view.layer.contents = (id)image.CGImage; 62 | #endif 63 | 64 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; 65 | [self.view addGestureRecognizer:pan]; 66 | 67 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)]; 68 | [self.view addGestureRecognizer:tap]; 69 | } 70 | 71 | - (void)pan:(UIPanGestureRecognizer*)pan 72 | { 73 | CGPoint location = [pan locationInView:self.view]; 74 | layer.position = location; 75 | view.center = location; 76 | } 77 | 78 | - (void)tap:(UITapGestureRecognizer*)tap 79 | { 80 | CGPoint location = [tap locationInView:self.view]; 81 | #ifndef USE_CALAYER 82 | [UIView animateWithDuration:1./3. animations:^{ 83 | view.center = location; 84 | }]; 85 | #else 86 | [CATransaction begin]; 87 | [CATransaction setAnimationDuration:1./3.]; 88 | layer.position = location; 89 | [CATransaction commit]; 90 | #endif 91 | } 92 | 93 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 94 | { 95 | return YES; 96 | } 97 | 98 | @end 99 | -------------------------------------------------------------------------------- /Squash/SMViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12E55 6 | 3084 7 | 1187.39 8 | 626.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBProxyObject 15 | IBUIView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBIPadFramework 28 | 29 | 30 | IBFirstResponder 31 | IBIPadFramework 32 | 33 | 34 | 35 | 274 36 | {{0, 20}, {768, 1004}} 37 | 38 | 39 | 40 | 41 | 3 42 | MQA 43 | 44 | 2 45 | 46 | 47 | 48 | 2 49 | 50 | IBIPadFramework 51 | 52 | 53 | 54 | 55 | 56 | 57 | view 58 | 59 | 60 | 61 | 3 62 | 63 | 64 | 65 | 66 | 67 | 0 68 | 69 | 70 | 71 | 72 | 73 | 1 74 | 75 | 76 | 77 | 78 | -1 79 | 80 | 81 | File's Owner 82 | 83 | 84 | -2 85 | 86 | 87 | 88 | 89 | 90 | 91 | SMViewController 92 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 93 | UIResponder 94 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 95 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 96 | 97 | 98 | 99 | 100 | 101 | 3 102 | 103 | 104 | 105 | 106 | SMViewController 107 | UIViewController 108 | 109 | IBProjectSource 110 | ./Classes/SMViewController.h 111 | 112 | 113 | 114 | 115 | 0 116 | IBIPadFramework 117 | YES 118 | 3 119 | YES 120 | 2083 121 | 122 | 123 | -------------------------------------------------------------------------------- /Squash/Squash-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | Spaceman-Labs.${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~ipad 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationPortraitUpsideDown 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Squash/Squash-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /Squash/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Squash/logo_periwinkle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spaceman-Labs/Squash/f6f1412d0813ff19ab8d96871a74a1fe9fee54b3/Squash/logo_periwinkle.png -------------------------------------------------------------------------------- /Squash/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Squash 4 | // 5 | // Created by Joel Kraut on 6/30/13. 6 | // Copyright (c) 2013 Joel Kraut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "SMAppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([SMAppDelegate class])); 17 | } 18 | } 19 | --------------------------------------------------------------------------------