├── LICENSE ├── README.md ├── img ├── action-button.jpg ├── disabled-button.jpg ├── dismiss-button.jpg ├── image-button.jpg ├── modal-sheet.gif ├── notification.gif ├── pagination.gif ├── separator.jpg └── status-bar.jpg └── watchkit.coffee /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Adria Jimenez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WatchKit-Framer 2 | Apple Watch Kit for FramerJS to help you easily create Apple Watch interfaces in Framer. 3 | 4 | ## Add it in your Framer Studio project 5 | 6 | - Download the project from github. 7 | - Copy `watchkit.coffee` into `modules/` folder. 8 | - Import it in Framer Studio by writing: `WatchKit = require "watchkit"`. 9 | 10 | **Note:** Components are prepared to be used with 42mm Apple Watch. 11 | **Note:** You need the San Francisco font [provided by Apple](https://developer.apple.com/watchkit/). 12 | 13 | ## Components 14 | 15 | ### Status Bar 16 | 17 | Creates an Apple Watch status bar. 18 | 19 | ![Apple Watch Status Bar](img/status-bar.jpg) 20 | 21 | ```coffeescript 22 | statusBar = new WatchKit.StatusBar 23 | title: "Some title" 24 | back: true # Display or not the back button 25 | time: true # Display or not the time 26 | ``` 27 | 28 | ### Buttons 29 | 30 | Buttons should have the Y position where you want to place them in the screen. 31 | Buttons perform an animation when the user clicks them. 32 | 33 | ##### Button for actions 34 | 35 | ![Apple Watch Action Button](img/action-button.jpg) 36 | 37 | ```coffeescript 38 | actionButton = new WatchKit.ActionButton "Button title", y: 50 39 | ``` 40 | 41 | ##### Button for dismiss 42 | 43 | ![Apple Watch Dismiss Button](img/dismiss-button.jpg) 44 | 45 | ```coffeescript 46 | dismissButton = new WatchKit.DismissButton y: 140 47 | ``` 48 | 49 | ##### Button with image 50 | 51 | Adds an image on the left side of the button. 52 | 53 | ![Apple Watch Image Button](img/image-button.jpg) 54 | 55 | ```coffeescript 56 | imageButton = new WatchKit.ActionButton image: "images/some-icon.jpg", y: 140 57 | ``` 58 | 59 | ##### Disable a button 60 | 61 | Buttons can be disabled using the `disabled: true`. 62 | 63 | ![Apple Watch Disabled Button](img/disabled-button.jpg) 64 | 65 | ### Separator 66 | 67 | Creates a default Apple separator. Specify the background color for a different color than the default one. 68 | 69 | ![Apple Watch Separator](img/separator.jpg) 70 | 71 | ```coffeescript 72 | separator = new WatchKit.Separator y: 100, backgroundColor: "blue" 73 | ``` 74 | 75 | ### Notification 76 | 77 | Creates a configurable notification. 78 | 79 | ![Apple Watch Notification](img/notification.gif) 80 | 81 | ```coffeescript 82 | notification = new WatchKit.Notification 83 | title: "Alert title" 84 | appName: "App title" 85 | image: "image/some-image.jpg" # Set your app image 86 | contentBodyLayer: someLayerToUseAsBody 87 | 88 | notification.show() # Show the notification 89 | ``` 90 | 91 | Additionally you can configure `appNameColor`, `contentBodyBackgroundColor` and `contentTitleBackgroundColor`. 92 | 93 | ##### Using text as body 94 | 95 | `WatchKit.Notification.contentBodyFont` is exposed to be able to use the default font for the body content. 96 | 97 | ```coffeescript 98 | contentBodyLayer = new Layer 99 | html: "Lorem ipsum dolor sit amet." 100 | style: _.extend {}, WatchKit.Notification.contentBodyFont, 101 | padding: "0 14px" # Set some padding for better display 102 | backgroundColor: "transparent" 103 | 104 | notification = new WatchKit.Notification 105 | title: "Alert title" 106 | appName: "App title" 107 | contentBodyLayer: contentBodyLayer 108 | 109 | notification.show() 110 | ``` 111 | 112 | ##### Adding action buttons 113 | 114 | By default there is only a dismiss button. You can add more buttons by using `addActionButton` or `addActionButtons` methods. 115 | 116 | ```coffeescript 117 | actionButton1 = new WatchKit.ActionButton "Action 1" 118 | actionButton2 = new WatchKit.ActionButton "Action 2" 119 | 120 | notification = new WatchKit.Notification 121 | title: "Alert title" 122 | appName: "App title" 123 | 124 | notification.addActionButtons actionButton1, actionButton2 125 | 126 | notification.show() 127 | ``` 128 | 129 | ### Pagination 130 | 131 | Creates a framer page component with the apple watch pagination dots. 132 | Pagination contains a set of pages that can be swiped. 133 | 134 | ![Apple Watch Pagination](img/pagination.gif) 135 | 136 | ```coffeescript 137 | pagination = new WatchKit.Pagination 138 | showPagination: true # Display or not the pagination dots 139 | ``` 140 | 141 | #### Page 142 | 143 | The basic page for the pagination element. 144 | 145 | ```coffeescript 146 | page = new WatchKit.Page 147 | ``` 148 | 149 | Layers can be added inside a page by using the `addLayer` method. 150 | 151 | ```coffeescript 152 | page = new WatchKit.Page 153 | layer = new Layer 154 | 155 | page.addLayer layer 156 | ``` 157 | 158 | In order to add pages to the pagination object, use the `addPage` or `addPages` method. 159 | 160 | ```coffeescript 161 | page1 = new WatchKit.Page image: "images/some_image.jpg" 162 | page2 = new WatchKit.Page 163 | someLayer = new Layer 164 | 165 | page2.addLayer someLayer 166 | 167 | pagination.addPages page1, page2 168 | ``` 169 | 170 | ### Modal Sheet 171 | 172 | Creates a modal sheet exactly with the same behaviour as the one in the watch. 173 | Use `present()` and `dismiss()` to show or hide the modal. 174 | 175 | ![Apple Watch Modal Sheet](img/modal-sheet.gif) 176 | 177 | ```coffeescript 178 | modalSheet = new WatchKit.ModalSheet 179 | dismissTitle: "Dismiss" # Pass a dismiss title to enable a dismiss button 180 | someLayer = new Layer 181 | 182 | modalSheet.addLayer someLayer 183 | 184 | modalSheet.present() 185 | ``` 186 | 187 | ## Other 188 | 189 | ### Click animation curve 190 | 191 | Use this curve for click animations 192 | 193 | ```coffeescript 194 | someLayer.animate 195 | curve: WatchKit.clickAnimationCurve 196 | ``` 197 | -------------------------------------------------------------------------------- /img/action-button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/action-button.jpg -------------------------------------------------------------------------------- /img/disabled-button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/disabled-button.jpg -------------------------------------------------------------------------------- /img/dismiss-button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/dismiss-button.jpg -------------------------------------------------------------------------------- /img/image-button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/image-button.jpg -------------------------------------------------------------------------------- /img/modal-sheet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/modal-sheet.gif -------------------------------------------------------------------------------- /img/notification.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/notification.gif -------------------------------------------------------------------------------- /img/pagination.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/pagination.gif -------------------------------------------------------------------------------- /img/separator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/separator.jpg -------------------------------------------------------------------------------- /img/status-bar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajimix/WatchKit-Framer/04b03f1d51f1acd5c85e646b61a26f09b1d09103/img/status-bar.jpg -------------------------------------------------------------------------------- /watchkit.coffee: -------------------------------------------------------------------------------- 1 | exports.clickAnimationCurve = "spring(500,30,0)" 2 | statusBarVisible = false 3 | statusBarHeight = 40 4 | 5 | class exports.StatusBar extends Layer 6 | constructor: (options = {}) -> 7 | backArrowLayerWidth = 9 8 | timeLayerWidth = 80 9 | defaultFont = 10 | fontFamily: "SanFranciscoText-Regular" 11 | fontSize: "32px" 12 | color: "#9BA0AA" 13 | lineHeight: "38px" 14 | 15 | options.height ?= statusBarHeight 16 | options.width ?= Screen.width 17 | options.backgroundColor ?= "transparent" 18 | 19 | super options 20 | 21 | statusBarVisible = true 22 | 23 | titleLayer = new Layer 24 | html: if options.title? then options.title else "" 25 | width: Screen.width - 26 | (if options.time? then timeLayerWidth else 0) - 27 | (if options.back? then backArrowLayerWidth else 0) - 5 28 | height: statusBarHeight 29 | superLayer: @ 30 | 31 | titleLayer.style = _.extend {}, defaultFont, 32 | backgroundColor: "transparent" 33 | color: "#FF9501" 34 | 35 | if options.back 36 | backArrowLayer = new Layer 37 | x:0, y:13, width:backArrowLayerWidth, height:17, image:"images/backArrow.png", superLayer: @ 38 | titleLayer.x = 15 39 | 40 | if options.time 41 | timeWidth = 80 42 | timeLayer = new Layer x: Screen.width - timeWidth, html: "10:09", width: timeWidth, height: statusBarHeight, superLayer: @ 43 | timeLayer.style = _.extend {}, defaultFont, 44 | backgroundColor: "transparent" 45 | color: "#9BA0AA" 46 | 47 | class exports.Button extends Layer 48 | constructor: (title, options = {}) -> 49 | defaultFont = 50 | fontFamily: "SanFranciscoText-Regular" 51 | fontSize: "30px" 52 | color: "#FFFFFF" 53 | lineHeight: "35px" 54 | 55 | options.width ?= Screen.width 56 | options.height ?= 75 57 | options.backgroundColor ?= "rgba(255, 255, 255, 0.2)" 58 | options.borderRadius ?= 10 59 | options.html = title 60 | 61 | if options.disabled 62 | options.backgroundColor = "rgba(255, 255, 255, .1)" 63 | 64 | super options 65 | 66 | @style = _.extend {}, defaultFont, 67 | textAlign: "center" 68 | paddingTop: "20px" 69 | 70 | if !options.disabled 71 | @on Events.TouchStart, -> 72 | @animate 73 | properties: scale: .98, opacity: .5 74 | curve: exports.clickAnimationCurve 75 | @on Events.TouchEnd, -> 76 | @animate 77 | properties: scale: 1, opacity: 1 78 | curve: exports.clickAnimationCurve 79 | 80 | class exports.ActionButton extends exports.Button 81 | constructor: (title, options = {}) -> 82 | options.backgroundColor ?= "rgba(255, 255, 255, 0.14)" 83 | if options.image? 84 | iconImage = options.image 85 | options.image = null 86 | 87 | super title, options 88 | 89 | if options.image? 90 | imageLayer = new Layer image: iconImage, width: 50, height: 50, superLayer: @, y: -45, x: 15 91 | 92 | class exports.DismissButton extends exports.Button 93 | constructor: (options = {}) -> 94 | title = if options.title? then options.title else "Dismiss" 95 | 96 | super title, options 97 | 98 | class exports.Pagination extends PageComponent 99 | constructor: (options = {}) -> 100 | options.width ?= Screen.width 101 | options.height ?= Screen.height 102 | options.scrollVertical ?= false 103 | options.backgroundColor ?= "transparent" 104 | 105 | super options 106 | 107 | @numberOfPages = 0 108 | @paginationVisible = false 109 | @currentPageIndex = 0 110 | @showPagination = if options.showPagination? then options.showPagination else true 111 | 112 | @.on "change:currentPage", -> 113 | @pageChanged() 114 | 115 | addPage: (page) -> 116 | @numberOfPages++ 117 | @updatePageCounter() 118 | super page 119 | 120 | addPages: (pages...) -> 121 | _.each pages, (page) => 122 | @addPage page 123 | 124 | pageChanged: -> 125 | @currentPageIndex = @horizontalPageIndex(@currentPage) 126 | @updatePageCounter() 127 | 128 | updatePageCounter: -> 129 | @paginationVisible = @showPagination && @numberOfPages > 1 130 | if @paginationLayer? 131 | @paginationLayer.destroy() 132 | @paginationLayer = null 133 | 134 | return if !@paginationVisible 135 | 136 | @paginationLayer = new Layer 137 | width: Screen.width, height: 6, y: Screen.height - 10, backgroundColor: "transparent" 138 | 139 | ballWidth = 6 140 | ballPadding = 5 141 | balls = [] 142 | totalWidth = (@numberOfPages * ballWidth) + (@numberOfPages * ballPadding) 143 | # Create the balls 144 | for i in [0..@numberOfPages - 1] 145 | ball = new Layer 146 | backgroundColor: "rgba(255, 255, 255, 0.35)", width: ballWidth, height: ballWidth, borderRadius: ballWidth, superLayer: @paginationLayer 147 | ball.x = ((ballWidth + ballPadding) * i) 148 | balls.push ball 149 | 150 | @paginationLayer.width = totalWidth - ballPadding 151 | @paginationLayer.centerX() 152 | balls[@currentPageIndex].backgroundColor = "white" 153 | # Make the content shorter just once to accomodate the pagination. 154 | if @height == Screen.height 155 | @height = Screen.height - 16 156 | 157 | class exports.Page extends Layer 158 | constructor: (options = {}) -> 159 | options.width ?= Screen.width 160 | options.height ?= if statusBarVisible then Screen.height - statusBarHeight else Screen.height 161 | options.backgroundColor ?= "transparent" 162 | 163 | super options 164 | 165 | if statusBarVisible 166 | @style.marginTop = "#{statusBarHeight}px" 167 | 168 | addLayer: (layer) -> 169 | layer.superLayer = @ 170 | 171 | class exports.ModalSheet extends Layer 172 | constructor: (options = {}) -> 173 | options.width ?= Screen.width 174 | options.height ?= Screen.height 175 | options.y ?= Screen.height 176 | options.backgroundColor ?= "black" 177 | 178 | super options 179 | 180 | @animationCurve = "spring(300,30,0)" 181 | 182 | if options.dismissTitle 183 | dismissLayer = new Layer 184 | width: Screen.width 185 | height: 40 186 | backgroundColor: "transparent" 187 | html: options.dismissTitle 188 | superLayer: @ 189 | dismissLayer.style = 190 | fontFamily: "SanFranciscoText-Regular" 191 | fontSize: "32px" 192 | color: "#FFFFFF" 193 | lineHeight: "38px" 194 | dismissLayer.on Events.TouchStart, -> 195 | dismissLayer.animate 196 | properties: opacity: .5, scale: .95 197 | curve: exports.clickAnimationCurve 198 | dismissLayer.on Events.TouchEnd, -> 199 | dismissLayer.animate 200 | properties: opacity: 1, scale: 1 201 | curve: exports.clickAnimationCurve 202 | dismissLayer.on Events.Click, => 203 | @dismiss() 204 | 205 | addLayer: (layer) -> 206 | layer.superLayer = @ 207 | 208 | present: -> 209 | @bringToFront() 210 | @animate 211 | properties: y: 0 212 | curve: @animationCurve 213 | 214 | dismiss: -> 215 | @animate 216 | properties: y: Screen.height 217 | curve: @animationCurve 218 | 219 | class exports.Separator extends Layer 220 | constructor: (options = {}) -> 221 | options.height = 4 222 | options.width ?= Screen.width 223 | options.borderRadius = 4 224 | options.backgroundColor ?= "white" 225 | 226 | super options 227 | 228 | @centerX() 229 | 230 | class exports.Notification extends Layer 231 | @contentBodyFont: 232 | fontFamily: "SanFranciscoText-Regular" 233 | fontSize: "30px" 234 | color: "#FFF" 235 | lineHeight: "35px" 236 | 237 | constructor: (options = {}) -> 238 | @launchAnimationCurve = "spring(120,18,0)" 239 | @easeOutAnimationCurve = "spring(320,26,0)" 240 | defaultFont = 241 | fontFamily: "SanFranciscoText-Regular" 242 | color: "#FFF" 243 | 244 | @backgroundFadeLayer = new Layer width: Screen.width, height: Screen.height, backgroundColor: "black", opacity: 0 245 | 246 | iconImage = options.image 247 | options.image = null 248 | options.width = Screen.width 249 | options.height = Screen.height 250 | options.backgroundColor = "transparent" 251 | options.y = Screen.height 252 | 253 | super options 254 | 255 | @iconLayer = new Layer y: Screen.height + 40, width: 196, height: 196, image: iconImage, borderRadius: 98 256 | @iconLayer.backgroundColor = if iconImage? then "transparent" else "#FF2968" 257 | @iconLayer.centerX() 258 | 259 | if options.title? 260 | firstTitleLayer = new Layer 261 | y: 250 262 | width: Screen.width 263 | height: 50 264 | html: options.title 265 | superLayer: @ 266 | style: _.extend {}, defaultFont, 267 | textAlign: "center" 268 | backgroundColor: "transparent" 269 | fontSize: "38px" 270 | lineHeight: "45px" 271 | firstTitleLayer.centerX() 272 | 273 | if options.appName? 274 | firstAppName = new Layer 275 | y: 310 276 | width: Screen.width 277 | height: 50 278 | html: options.appName 279 | superLayer: @ 280 | style: _.extend {}, defaultFont, 281 | textAlign: "center" 282 | backgroundColor: "transparent" 283 | fontSize: "28px" 284 | color: if options.appNameColor? then options.appNameColor else "#FF2968" 285 | letterSpacing: "0.21px" 286 | lineHeight: "34px" 287 | textTransform: "uppercase" 288 | firstAppName.centerX() 289 | 290 | @notificationContentLayer = new ScrollComponent width: Screen.width, height: Screen.height, backgroundColor: "transparent", scrollHorizontal: false, mouseWheelEnabled: true, y: Screen.height 291 | 292 | notificationContentBodyLayer = new Layer 293 | y: 36 294 | borderRadius: "10px" 295 | width: Screen.width 296 | height: if options.contentBodyLayer? then 110 + options.contentBodyLayer.height else 140 297 | backgroundColor: if options.contentBodyBackgroundColor? then options.contentBodyBackgroundColor else "rgba(255, 255, 255, 0.14)" 298 | superLayer: @notificationContentLayer.content 299 | 300 | if options.title? 301 | notificationContentBodyTitleLayer = new Layer 302 | y: 73 303 | x: 14 304 | html: options.title 305 | width: notificationContentBodyLayer.width - 28 306 | height: 38 307 | superLayer: notificationContentBodyLayer 308 | style: _.extend {}, defaultFont, 309 | fontFamily: "SanFranciscoText-Semibold" 310 | fontSize: "30px" 311 | lineHeight: "36px" 312 | backgroundColor: "transparent" 313 | 314 | if options.contentBodyLayer? 315 | options.contentBodyLayer.y = if options.title? then 110 else 73 316 | options.contentBodyLayer.width = notificationContentBodyLayer.width 317 | options.contentBodyLayer.superLayer = notificationContentBodyLayer 318 | 319 | notificationContentTitleLayer = new Layer 320 | height: 54 321 | superLayer: notificationContentBodyLayer 322 | backgroundColor: if options.contentTitleBackgroundColor? then options.contentTitleBackgroundColor else "rgba(255, 255, 255, 0.1)" 323 | width: Screen.width 324 | borderRadius: "10px 10px 0 0" 325 | html: options.appName 326 | style: _.extend {}, defaultFont, 327 | textAlign: "right" 328 | fontSize: "24px" 329 | letterSpacing: "0.6px" 330 | lineHeight: "29px" 331 | textTransform: "uppercase" 332 | padding: "12px 18px 0" 333 | 334 | @lastActionButtonY = notificationContentBodyLayer.height + notificationContentBodyLayer.y 335 | @notificationContentLayer.updateContent() 336 | 337 | show: -> 338 | # Add the dismiss on show so we don't have to care about positioning on the last position 339 | @addActionButton new exports.DismissButton 340 | @iconLayer.bringToFront() 341 | @backgroundFadeLayer.animate 342 | properties: opacity: .8 343 | curve: @launchAnimationCurve 344 | @animate 345 | properties: y: 0 346 | curve: @launchAnimationCurve 347 | @iconLayer.animate 348 | properties: y: 40 349 | curve: @launchAnimationCurve 350 | 351 | Utils.delay 1, => 352 | iconLayerAnimation = @iconLayer.animate 353 | properties: 354 | width: 90 355 | height: 90 356 | x: 15 357 | y: 5 358 | curve: @easeOutAnimationCurve 359 | @backgroundFadeLayer.animate 360 | properties: opacity: 0 361 | curve: @easeOutAnimationCurve 362 | @animate 363 | properties: opacity: 0 364 | curve: @easeOutAnimationCurve 365 | @notificationContentLayer.animate 366 | properties: y: 0 367 | curve: @easeOutAnimationCurve 368 | iconLayerAnimation.on Events.AnimationEnd, => 369 | @iconLayer.superLayer = @notificationContentLayer.content 370 | 371 | addActionButton: (actionButton) -> 372 | buttonPaddingTop = 8 373 | actionButton.superLayer = @notificationContentLayer.content 374 | actionButton.y = @lastActionButtonY + buttonPaddingTop 375 | @lastActionButtonY += actionButton.height + buttonPaddingTop 376 | @notificationContentLayer.updateContent() 377 | 378 | addActionButtons: (actionButtons...) -> 379 | for actionButton in actionButtons 380 | @addActionButton actionButton 381 | --------------------------------------------------------------------------------