├── LICENSE ├── README.md └── Sketch Motion ├── GIFX └── Motion.sketchplugin ├── Contents └── Sketch │ ├── Tween.js │ ├── common.js │ ├── constants.js │ ├── flatten.js │ ├── gif.js │ ├── helpers.js │ ├── manifest.json │ ├── motion.js │ └── timeline.js └── Resources ├── EasingCurves.sketch └── easingCurves.png /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicholas Wallen 4 | 5 | Tween.js Copyright (c) 2010-2012 Tween.js authors. 6 | 7 | Easing equations Copyright (c) 2001 Robert Penner http://robertpenner.com/easing/ 8 | 9 | GIFX and GIF generation code adapted from https://github.com/nathco/Generate-GIF 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![intro](http://nwallen.github.io/Sketch-Motion/static/Sketch-Motion-Intro.gif) 2 | 3 | Plugin to quickly create, preview, and export motion designs ... in [Sketch](http://bohemiancoding.com/sketch/). 4 | 5 | **Warning** 6 | Early proof-of-concept version tested in sketch 3.3.3 -- use at your own risk 7 | 8 | ## What's it for? 9 | UIs need motion. On the Web, on mobile, on anything with a screen: motion illustrates and delights. There are [many](https://facebook.github.io/origami/) [excellent](http://www.pixate.com/) [tools](http://framerjs.com/) for creating interactive, animated prototypes. However, if you're designing in Sketch, motion currently means a context switch to another application, which slows down your ability to iterate. The intent of Sketch Motion is to provide a lightweight, coding-free method to explore motion ideas *in Sketch* with no context switch. It's not going to replace After Effects or Origami, but it will help you try out motion ideas *fast*. Here are some ways you might use Sketch Motion: 10 | 11 | - Explore several UI transitions before jumping into a dedicated prototyping tool 12 | - Create animated GIFs for use in an Invision, Flinto, or Marvel clickable prototype 13 | - Create animated assets like loaders 14 | 15 | ## How it works 16 | Sketch Motion builds on how you might intially spec out an animation in Sketch: creating a series of artboards to document the animation's key moments. To animate, simply point Sketch Motion at these "keyframe" artboards. It will generate a smooth animation, which you can preview in Sketch or export as an animated GIF. 17 | 18 | The plugin automatically provides artboards to adjust the timing and easing of each animation. Animations are modular -- allowing you to duplicate and composite them together. 19 | 20 | **A simple animation** 21 | 22 | ![capture](http://nwallen.github.io/Sketch-Motion/static/quickUsageCapture.gif) 23 | 24 | ## Installation 25 | 1. **[Download a Release](https://github.com/nwallen/Sketch-Motion/releases)** and unzip 26 | 2. Open Sketch and select `Plugins ▸ Reveal Plugins Folder...` from the menu bar 27 | 3. Copy the `Sketch Motion` directory into the plugins folder 28 | 4. Locate the `GIFX` file and double-click (if prompted, give GIFX permission to run) 29 | 30 | ## Usage 31 | 32 | **Define Animation Keyframes** 33 | 34 | ![keyframes](http://nwallen.github.io/Sketch-Motion/static/keyframes.png) 35 | 36 | - Animations are referenced with "tags". A tag is any word wrapped in curly braces e.g. an animation called "example" would use the tag `{example}` 37 | - Tag the names of the artboards to include them as keyframes in an animation: e.g. artboards named `{example} 1`, `{example} 2` would compose an "example" animation. 38 | - Tagged artboards play in alphabetical order based on their names 39 | - You may freely add and remove tagged artboards -- the plugin will adjust 40 | 41 | **Create Transitions** 42 | 43 | ![transition groups](http://nwallen.github.io/Sketch-Motion/static/transitionGroups.png) 44 | 45 | - Animation transitions are automatically created by comparing groups **with the same name** on tagged artboards 46 | - Differences in these properties are animated : `position` `size` `rotation` `opacity` 47 | - The name of the group does not matter -- just that it is the same on all tagged artboards 48 | - Multiple groups with the same name on the same artboard are automatically given a unique name 49 | - The group must exist on the first artboard -- otherwise it will not be included in the animation 50 | 51 | 52 | **Add a Player** 53 | 54 | ![player](http://nwallen.github.io/Sketch-Motion/static/player.png) 55 | 56 | - Animations are played via "players". Create a player by tagging a group with the name of the animation you would like to play: e.g. renaming `myGroup ▸ myGroup {example}` creates a player for the "loader" animation 57 | - To play the animation select the artboard containing the tagged group by clicking its name. Then select `Plugins ▸ Motion ▸ Animate` from the Sketch toolbar or use the keyboard shortcut `control` + `⌘ command` + `a` 58 | - The animation will play. Two additional artboards will be added to your document (the "Timeline" and the "Legend") which you may use to adjust the animation 59 | - You can add multiple players to an artboard -- for the same or different animations 60 | 61 | **Adjust Timing with the Timeline** 62 | 63 | ![timeline](http://nwallen.github.io/Sketch-Motion/static/timeline.png) 64 | 65 | - Each block on the timeline represents a transition between tagged artboards 66 | - Edit the width of a block to change the duration of the transition `1 pixel = 1 millisecond` 67 | - Adding spacing between blocks will add delay between animations `1 pixel = 1 millisecond` of delay 68 | - The sequence of the transitions cannot be changed on the timeline 69 | 70 | **Adjust Curves with the Legend** 71 | 72 | ![legend](http://nwallen.github.io/Sketch-Motion/static/legend.png) 73 | 74 | - The legend lists all detected transitions in a single animation 75 | - To change the easing of a transition, `⌘ command` + click the current easing curve. Drag to the left or right. On previewing the animation, the new easing curve will be applied. 76 | 77 | **Export a GIF** 78 | - To export a GIF of an animation select an artboard containing a player(s) by clicking its name. Then select `Plugins ▸ Motion ▸ Export GIF` from the Sketch toolbar or use the keyboard shortcut `control` + `⌘ command` + `e` 79 | - Follow the prompts 80 | 81 | ## Resources 82 | [See the Gallery](https://github.com/nwallen/Sketch-Motion-Gallery/) for examples. 83 | 84 | ## Feedback 85 | Please add an issue to this repository to report problems or make suggestions for the development of this plugin. You can also find me on Twitter [@nawt](https://twitter.com/nawt) 86 | 87 | ## Props 88 | - GIF Generation - Nathan Rutzky https://github.com/nathco/Generate-GIF 89 | - Tweens - https://github.com/tweenjs/tween.js/ 90 | - The Sketch Team - http://bohemiancoding.com/sketch/ 91 | -------------------------------------------------------------------------------- /Sketch Motion/GIFX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwallen/Sketch-Motion/62642e7e128384e673dd515588a5a7479d0c20dd/Sketch Motion/GIFX -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/Tween.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tween.js - Licensed under the MIT license 3 | * https://github.com/sole/tween.js 4 | * ---------------------------------------------- 5 | * 6 | * See https://github.com/sole/tween.js/graphs/contributors for the full list of contributors. 7 | * Thank you all, you're awesome! 8 | */ 9 | 10 | 11 | // performance.now polyfill 12 | // ( function ( root ) { 13 | 14 | // if ( 'performance' in root === false ) { 15 | // root.performance = {}; 16 | // } 17 | 18 | // // IE 8 19 | // Date.now = ( Date.now || function () { 20 | // return new Date().getTime(); 21 | // } ); 22 | 23 | // if ( 'now' in root.performance === false ) { 24 | // var offset = root.performance.timing && root.performance.timing.navigationStart ? performance.timing.navigationStart 25 | // : Date.now(); 26 | 27 | // root.performance.now = function () { 28 | // return Date.now() - offset; 29 | // }; 30 | // } 31 | 32 | // } )( this ); 33 | 34 | var TWEEN = TWEEN || ( function () { 35 | 36 | var _tweens = []; 37 | 38 | return { 39 | 40 | REVISION: '14', 41 | 42 | getAll: function () { 43 | 44 | return _tweens; 45 | 46 | }, 47 | 48 | removeAll: function () { 49 | 50 | _tweens = []; 51 | 52 | }, 53 | 54 | add: function ( tween ) { 55 | 56 | _tweens.push( tween ); 57 | 58 | }, 59 | 60 | remove: function ( tween ) { 61 | 62 | var i = _tweens.indexOf( tween ); 63 | 64 | if ( i !== -1 ) { 65 | 66 | _tweens.splice( i, 1 ); 67 | 68 | } 69 | 70 | }, 71 | 72 | update: function ( time ) { 73 | 74 | if ( _tweens.length === 0 ) return false; 75 | 76 | var i = 0; 77 | 78 | time = time !== undefined ? time : window.performance.now(); 79 | 80 | while ( i < _tweens.length ) { 81 | 82 | if ( _tweens[ i ].update( time ) ) { 83 | 84 | i++; 85 | 86 | } else { 87 | 88 | _tweens.splice( i, 1 ); 89 | 90 | } 91 | 92 | } 93 | 94 | return true; 95 | 96 | } 97 | }; 98 | 99 | } )(); 100 | 101 | TWEEN.Tween = function ( object ) { 102 | 103 | var _object = object; 104 | var _valuesStart = {}; 105 | var _valuesEnd = {}; 106 | var _valuesStartRepeat = {}; 107 | var _duration = 1000; 108 | var _repeat = 0; 109 | var _yoyo = false; 110 | var _isPlaying = false; 111 | var _reversed = false; 112 | var _delayTime = 0; 113 | var _startTime = null; 114 | var _easingFunction = TWEEN.Easing.Linear.None; 115 | var _interpolationFunction = TWEEN.Interpolation.Linear; 116 | var _chainedTweens = []; 117 | var _onStartCallback = null; 118 | var _onStartCallbackFired = false; 119 | var _onUpdateCallback = null; 120 | var _onCompleteCallback = null; 121 | var _onStopCallback = null; 122 | 123 | // Set all starting values present on the target object 124 | for ( var field in object ) { 125 | 126 | _valuesStart[ field ] = parseFloat(object[field], 10); 127 | 128 | } 129 | 130 | this.to = function ( properties, duration ) { 131 | 132 | if ( duration !== undefined ) { 133 | 134 | _duration = duration; 135 | 136 | } 137 | 138 | _valuesEnd = properties; 139 | 140 | return this; 141 | 142 | }; 143 | 144 | this.start = function ( time ) { 145 | 146 | TWEEN.add( this ); 147 | 148 | _isPlaying = true; 149 | 150 | _onStartCallbackFired = false; 151 | 152 | _startTime = time !== undefined ? time : window.performance.now(); 153 | _startTime += _delayTime; 154 | 155 | for ( var property in _valuesEnd ) { 156 | 157 | // check if an Array was provided as property value 158 | if ( _valuesEnd[ property ] instanceof Array ) { 159 | 160 | if ( _valuesEnd[ property ].length === 0 ) { 161 | 162 | continue; 163 | 164 | } 165 | 166 | // create a local copy of the Array with the start value at the front 167 | _valuesEnd[ property ] = [ _object[ property ] ].concat( _valuesEnd[ property ] ); 168 | 169 | } 170 | 171 | _valuesStart[ property ] = _object[ property ]; 172 | 173 | if( ( _valuesStart[ property ] instanceof Array ) === false ) { 174 | _valuesStart[ property ] *= 1.0; // Ensures we're using numbers, not strings 175 | } 176 | 177 | _valuesStartRepeat[ property ] = _valuesStart[ property ] || 0; 178 | 179 | } 180 | 181 | return this; 182 | 183 | }; 184 | 185 | this.stop = function () { 186 | 187 | if ( !_isPlaying ) { 188 | return this; 189 | } 190 | 191 | TWEEN.remove( this ); 192 | _isPlaying = false; 193 | 194 | if ( _onStopCallback !== null ) { 195 | 196 | _onStopCallback.call( _object ); 197 | 198 | } 199 | 200 | this.stopChainedTweens(); 201 | return this; 202 | 203 | }; 204 | 205 | this.stopChainedTweens = function () { 206 | 207 | for ( var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++ ) { 208 | 209 | _chainedTweens[ i ].stop(); 210 | 211 | } 212 | 213 | }; 214 | 215 | this.delay = function ( amount ) { 216 | 217 | _delayTime = amount; 218 | return this; 219 | 220 | }; 221 | 222 | this.repeat = function ( times ) { 223 | 224 | _repeat = times; 225 | return this; 226 | 227 | }; 228 | 229 | this.yoyo = function( yoyo ) { 230 | 231 | _yoyo = yoyo; 232 | return this; 233 | 234 | }; 235 | 236 | 237 | this.easing = function ( easing ) { 238 | 239 | _easingFunction = easing; 240 | return this; 241 | 242 | }; 243 | 244 | this.interpolation = function ( interpolation ) { 245 | 246 | _interpolationFunction = interpolation; 247 | return this; 248 | 249 | }; 250 | 251 | this.chain = function () { 252 | 253 | _chainedTweens = arguments; 254 | return this; 255 | 256 | }; 257 | 258 | this.onStart = function ( callback ) { 259 | 260 | _onStartCallback = callback; 261 | return this; 262 | 263 | }; 264 | 265 | this.onUpdate = function ( callback ) { 266 | 267 | _onUpdateCallback = callback; 268 | return this; 269 | 270 | }; 271 | 272 | this.onComplete = function ( callback ) { 273 | 274 | _onCompleteCallback = callback; 275 | return this; 276 | 277 | }; 278 | 279 | this.onStop = function ( callback ) { 280 | 281 | _onStopCallback = callback; 282 | return this; 283 | 284 | }; 285 | 286 | this.update = function ( time ) { 287 | 288 | var property; 289 | 290 | if ( time < _startTime ) { 291 | 292 | return true; 293 | 294 | } 295 | 296 | if ( _onStartCallbackFired === false ) { 297 | 298 | if ( _onStartCallback !== null ) { 299 | 300 | _onStartCallback.call( _object ); 301 | 302 | } 303 | 304 | _onStartCallbackFired = true; 305 | 306 | } 307 | 308 | var elapsed = ( time - _startTime ) / _duration; 309 | elapsed = elapsed > 1 ? 1 : elapsed; 310 | 311 | var value = _easingFunction( elapsed ); 312 | 313 | for ( property in _valuesEnd ) { 314 | 315 | var start = _valuesStart[ property ] || 0; 316 | var end = _valuesEnd[ property ]; 317 | 318 | if ( end instanceof Array ) { 319 | 320 | _object[ property ] = _interpolationFunction( end, value ); 321 | 322 | } else { 323 | 324 | // Parses relative end values with start as base (e.g.: +10, -3) 325 | if ( typeof(end) === "string" ) { 326 | end = start + parseFloat(end, 10); 327 | } 328 | 329 | // protect against non numeric properties. 330 | if ( typeof(end) === "number" ) { 331 | _object[ property ] = start + ( end - start ) * value; 332 | } 333 | 334 | } 335 | 336 | } 337 | 338 | if ( _onUpdateCallback !== null ) { 339 | 340 | _onUpdateCallback.call( _object, value ); 341 | 342 | } 343 | 344 | if ( elapsed == 1 ) { 345 | 346 | if ( _repeat > 0 ) { 347 | 348 | if( isFinite( _repeat ) ) { 349 | _repeat--; 350 | } 351 | 352 | // reassign starting values, restart by making startTime = now 353 | for( property in _valuesStartRepeat ) { 354 | 355 | if ( typeof( _valuesEnd[ property ] ) === "string" ) { 356 | _valuesStartRepeat[ property ] = _valuesStartRepeat[ property ] + parseFloat(_valuesEnd[ property ], 10); 357 | } 358 | 359 | if (_yoyo) { 360 | var tmp = _valuesStartRepeat[ property ]; 361 | _valuesStartRepeat[ property ] = _valuesEnd[ property ]; 362 | _valuesEnd[ property ] = tmp; 363 | } 364 | 365 | _valuesStart[ property ] = _valuesStartRepeat[ property ]; 366 | 367 | } 368 | 369 | if (_yoyo) { 370 | _reversed = !_reversed; 371 | } 372 | 373 | _startTime = time + _delayTime; 374 | 375 | return true; 376 | 377 | } else { 378 | 379 | if ( _onCompleteCallback !== null ) { 380 | 381 | _onCompleteCallback.call( _object ); 382 | 383 | } 384 | 385 | for ( var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++ ) { 386 | 387 | _chainedTweens[ i ].start( time ); 388 | 389 | } 390 | 391 | return false; 392 | 393 | } 394 | 395 | } 396 | 397 | return true; 398 | 399 | }; 400 | 401 | }; 402 | 403 | 404 | TWEEN.Easing = { 405 | 406 | Linear: { 407 | 408 | None: function ( k ) { 409 | 410 | return k; 411 | 412 | } 413 | 414 | }, 415 | 416 | Quadratic: { 417 | 418 | In: function ( k ) { 419 | 420 | return k * k; 421 | 422 | }, 423 | 424 | Out: function ( k ) { 425 | 426 | return k * ( 2 - k ); 427 | 428 | }, 429 | 430 | InOut: function ( k ) { 431 | 432 | if ( ( k *= 2 ) < 1 ) return 0.5 * k * k; 433 | return - 0.5 * ( --k * ( k - 2 ) - 1 ); 434 | 435 | } 436 | 437 | }, 438 | 439 | Cubic: { 440 | 441 | In: function ( k ) { 442 | 443 | return k * k * k; 444 | 445 | }, 446 | 447 | Out: function ( k ) { 448 | 449 | return --k * k * k + 1; 450 | 451 | }, 452 | 453 | InOut: function ( k ) { 454 | 455 | if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k; 456 | return 0.5 * ( ( k -= 2 ) * k * k + 2 ); 457 | 458 | } 459 | 460 | }, 461 | 462 | Quartic: { 463 | 464 | In: function ( k ) { 465 | 466 | return k * k * k * k; 467 | 468 | }, 469 | 470 | Out: function ( k ) { 471 | 472 | return 1 - ( --k * k * k * k ); 473 | 474 | }, 475 | 476 | InOut: function ( k ) { 477 | 478 | if ( ( k *= 2 ) < 1) return 0.5 * k * k * k * k; 479 | return - 0.5 * ( ( k -= 2 ) * k * k * k - 2 ); 480 | 481 | } 482 | 483 | }, 484 | 485 | Quintic: { 486 | 487 | In: function ( k ) { 488 | 489 | return k * k * k * k * k; 490 | 491 | }, 492 | 493 | Out: function ( k ) { 494 | 495 | return --k * k * k * k * k + 1; 496 | 497 | }, 498 | 499 | InOut: function ( k ) { 500 | 501 | if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k * k * k; 502 | return 0.5 * ( ( k -= 2 ) * k * k * k * k + 2 ); 503 | 504 | } 505 | 506 | }, 507 | 508 | Sinusoidal: { 509 | 510 | In: function ( k ) { 511 | 512 | return 1 - Math.cos( k * Math.PI / 2 ); 513 | 514 | }, 515 | 516 | Out: function ( k ) { 517 | 518 | return Math.sin( k * Math.PI / 2 ); 519 | 520 | }, 521 | 522 | InOut: function ( k ) { 523 | 524 | return 0.5 * ( 1 - Math.cos( Math.PI * k ) ); 525 | 526 | } 527 | 528 | }, 529 | 530 | Exponential: { 531 | 532 | In: function ( k ) { 533 | 534 | return k === 0 ? 0 : Math.pow( 1024, k - 1 ); 535 | 536 | }, 537 | 538 | Out: function ( k ) { 539 | 540 | return k === 1 ? 1 : 1 - Math.pow( 2, - 10 * k ); 541 | 542 | }, 543 | 544 | InOut: function ( k ) { 545 | 546 | if ( k === 0 ) return 0; 547 | if ( k === 1 ) return 1; 548 | if ( ( k *= 2 ) < 1 ) return 0.5 * Math.pow( 1024, k - 1 ); 549 | return 0.5 * ( - Math.pow( 2, - 10 * ( k - 1 ) ) + 2 ); 550 | 551 | } 552 | 553 | }, 554 | 555 | Circular: { 556 | 557 | In: function ( k ) { 558 | 559 | return 1 - Math.sqrt( 1 - k * k ); 560 | 561 | }, 562 | 563 | Out: function ( k ) { 564 | 565 | return Math.sqrt( 1 - ( --k * k ) ); 566 | 567 | }, 568 | 569 | InOut: function ( k ) { 570 | 571 | if ( ( k *= 2 ) < 1) return - 0.5 * ( Math.sqrt( 1 - k * k) - 1); 572 | return 0.5 * ( Math.sqrt( 1 - ( k -= 2) * k) + 1); 573 | 574 | } 575 | 576 | }, 577 | 578 | Elastic: { 579 | 580 | In: function ( k ) { 581 | 582 | var s, a = 0.1, p = 0.4; 583 | if ( k === 0 ) return 0; 584 | if ( k === 1 ) return 1; 585 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 586 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 587 | return - ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); 588 | 589 | }, 590 | 591 | Out: function ( k ) { 592 | 593 | var s, a = 0.1, p = 0.4; 594 | if ( k === 0 ) return 0; 595 | if ( k === 1 ) return 1; 596 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 597 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 598 | return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 ); 599 | 600 | }, 601 | 602 | InOut: function ( k ) { 603 | 604 | var s, a = 0.1, p = 0.4; 605 | if ( k === 0 ) return 0; 606 | if ( k === 1 ) return 1; 607 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 608 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 609 | if ( ( k *= 2 ) < 1 ) return - 0.5 * ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); 610 | return a * Math.pow( 2, -10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) * 0.5 + 1; 611 | 612 | } 613 | 614 | }, 615 | 616 | Back: { 617 | 618 | In: function ( k ) { 619 | 620 | var s = 1.70158; 621 | return k * k * ( ( s + 1 ) * k - s ); 622 | 623 | }, 624 | 625 | Out: function ( k ) { 626 | 627 | var s = 1.70158; 628 | return --k * k * ( ( s + 1 ) * k + s ) + 1; 629 | 630 | }, 631 | 632 | InOut: function ( k ) { 633 | 634 | var s = 1.70158 * 1.525; 635 | if ( ( k *= 2 ) < 1 ) return 0.5 * ( k * k * ( ( s + 1 ) * k - s ) ); 636 | return 0.5 * ( ( k -= 2 ) * k * ( ( s + 1 ) * k + s ) + 2 ); 637 | 638 | } 639 | 640 | }, 641 | 642 | Bounce: { 643 | 644 | In: function ( k ) { 645 | 646 | return 1 - TWEEN.Easing.Bounce.Out( 1 - k ); 647 | 648 | }, 649 | 650 | Out: function ( k ) { 651 | 652 | if ( k < ( 1 / 2.75 ) ) { 653 | 654 | return 7.5625 * k * k; 655 | 656 | } else if ( k < ( 2 / 2.75 ) ) { 657 | 658 | return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75; 659 | 660 | } else if ( k < ( 2.5 / 2.75 ) ) { 661 | 662 | return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375; 663 | 664 | } else { 665 | 666 | return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375; 667 | 668 | } 669 | 670 | }, 671 | 672 | InOut: function ( k ) { 673 | 674 | if ( k < 0.5 ) return TWEEN.Easing.Bounce.In( k * 2 ) * 0.5; 675 | return TWEEN.Easing.Bounce.Out( k * 2 - 1 ) * 0.5 + 0.5; 676 | 677 | } 678 | 679 | } 680 | 681 | }; 682 | 683 | TWEEN.Interpolation = { 684 | 685 | Linear: function ( v, k ) { 686 | 687 | var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = TWEEN.Interpolation.Utils.Linear; 688 | 689 | if ( k < 0 ) return fn( v[ 0 ], v[ 1 ], f ); 690 | if ( k > 1 ) return fn( v[ m ], v[ m - 1 ], m - f ); 691 | 692 | return fn( v[ i ], v[ i + 1 > m ? m : i + 1 ], f - i ); 693 | 694 | }, 695 | 696 | Bezier: function ( v, k ) { 697 | 698 | var b = 0, n = v.length - 1, pw = Math.pow, bn = TWEEN.Interpolation.Utils.Bernstein, i; 699 | 700 | for ( i = 0; i <= n; i++ ) { 701 | b += pw( 1 - k, n - i ) * pw( k, i ) * v[ i ] * bn( n, i ); 702 | } 703 | 704 | return b; 705 | 706 | }, 707 | 708 | CatmullRom: function ( v, k ) { 709 | 710 | var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = TWEEN.Interpolation.Utils.CatmullRom; 711 | 712 | if ( v[ 0 ] === v[ m ] ) { 713 | 714 | if ( k < 0 ) i = Math.floor( f = m * ( 1 + k ) ); 715 | 716 | return fn( v[ ( i - 1 + m ) % m ], v[ i ], v[ ( i + 1 ) % m ], v[ ( i + 2 ) % m ], f - i ); 717 | 718 | } else { 719 | 720 | if ( k < 0 ) return v[ 0 ] - ( fn( v[ 0 ], v[ 0 ], v[ 1 ], v[ 1 ], -f ) - v[ 0 ] ); 721 | if ( k > 1 ) return v[ m ] - ( fn( v[ m ], v[ m ], v[ m - 1 ], v[ m - 1 ], f - m ) - v[ m ] ); 722 | 723 | return fn( v[ i ? i - 1 : 0 ], v[ i ], v[ m < i + 1 ? m : i + 1 ], v[ m < i + 2 ? m : i + 2 ], f - i ); 724 | 725 | } 726 | 727 | }, 728 | 729 | Utils: { 730 | 731 | Linear: function ( p0, p1, t ) { 732 | 733 | return ( p1 - p0 ) * t + p0; 734 | 735 | }, 736 | 737 | Bernstein: function ( n , i ) { 738 | 739 | var fc = TWEEN.Interpolation.Utils.Factorial; 740 | return fc( n ) / fc( i ) / fc( n - i ); 741 | 742 | }, 743 | 744 | Factorial: ( function () { 745 | 746 | var a = [ 1 ]; 747 | 748 | return function ( n ) { 749 | 750 | var s = 1, i; 751 | if ( a[ n ] ) return a[ n ]; 752 | for ( i = n; i > 1; i-- ) s *= i; 753 | return a[ n ] = s; 754 | 755 | }; 756 | 757 | } )(), 758 | 759 | CatmullRom: function ( p0, p1, p2, p3, t ) { 760 | 761 | var v0 = ( p2 - p0 ) * 0.5, v1 = ( p3 - p1 ) * 0.5, t2 = t * t, t3 = t * t2; 762 | return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( - 3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1; 763 | 764 | } 765 | 766 | } 767 | 768 | }; 769 | 770 | // UMD (Universal Module Definition) 771 | // ( function ( root ) { 772 | 773 | // if ( typeof define === 'function' && define.amd ) { 774 | 775 | // // AMD 776 | // define( [], function () { 777 | // return TWEEN; 778 | // } ); 779 | 780 | // } else if ( typeof exports === 'object' ) { 781 | 782 | // // Node.js 783 | // module.exports = TWEEN; 784 | 785 | // } else { 786 | 787 | // // Global variable 788 | // root.TWEEN = TWEEN; 789 | 790 | // } 791 | 792 | // } )( this ); -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/common.js: -------------------------------------------------------------------------------- 1 | // polyfill common JS browser APIs 2 | 3 | 4 | // timestamps 5 | var window = window || {}; 6 | var Date = Date || {}; 7 | var performance = performance || {}; 8 | 9 | Date.now = function() { 10 | return ([NSDate timeIntervalSinceReferenceDate] * 1000); 11 | } 12 | 13 | performance.now = function() { 14 | return ([NSDate timeIntervalSinceReferenceDate] * 1000); 15 | } 16 | 17 | window.Date = Date; 18 | window.performance = performance; -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/constants.js: -------------------------------------------------------------------------------- 1 | var MSPERPIXEL = 1; // number of ms that each pixel on the timeline represents 2 | var RESOURCESPATH = "Motion.sketchplugin/Resources/"; 3 | var TIMELINELAYOUT = { 4 | height: 125, 5 | margin: 120 6 | } 7 | var LEGENDLAYOUT = { 8 | easeTileHeight : 125, 9 | easeTileWidth : 125, 10 | margin: 60, 11 | rowWidth: 800, 12 | textHeight: 50, 13 | }; 14 | var LEGENDCOLORS = { 15 | background: "#747373", 16 | index: "#FAFAFA", 17 | info: "#FAFAFA", 18 | curve: "#FAFAFA", 19 | highlight: "#76F6B3" 20 | } 21 | var ANIMATIONCURVEFILENAME = "easingCurves.png"; 22 | var ANIMATIONCURVEOPTIONS = [ 23 | {ease: TWEEN.Easing.Linear.None}, 24 | {ease: TWEEN.Easing.Sinusoidal.In}, 25 | {ease: TWEEN.Easing.Sinusoidal.Out}, 26 | {ease: TWEEN.Easing.Sinusoidal.InOut}, 27 | {ease: TWEEN.Easing.Elastic.In}, 28 | {ease: TWEEN.Easing.Elastic.Out}, 29 | {ease: TWEEN.Easing.Elastic.InOut} 30 | ]; 31 | var TIMELINECOLORS = { 32 | background: "#C7C5C5", 33 | block: "#FAFAFA", 34 | blockBorder: "#A6A6A6", 35 | text: "#A6A6A6", 36 | highlight: "#76F6B3" 37 | } -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/flatten.js: -------------------------------------------------------------------------------- 1 | // flattening artwork for faster animation 2 | 3 | var flattenArtwork = function(container){ 4 | var layers = container.layers(); 5 | var layersToFlatten = []; 6 | for(var i=0; i < layers.count(); i++){ 7 | var layer = layers.objectAtIndex(i); 8 | // unregister all symbol instances to avoid replacing original artwork with flattened artwork 9 | if(layer.isSymbol()){ 10 | doc.documentData().layerSymbols().unregisterInstance(layer); 11 | } 12 | if(layer.isMemberOfClass(MSLayerGroup)){ 13 | flattenArtwork(layer); 14 | } 15 | else { 16 | layersToFlatten.push(layer); 17 | } 18 | } 19 | 20 | if(layersToFlatten.length > 0){ 21 | replaceLayersWithImage(layersToFlatten, container); 22 | } 23 | } 24 | 25 | var replaceLayersWithImage = function(layers, container){ 26 | var tempPath = NSTemporaryDirectory(); 27 | var exportDebugPath = pluginPath + '/temp/'; 28 | var string = [[NSProcessInfo processInfo] globallyUniqueString]; 29 | var imageExportPath = [tempPath stringByAppendingPathComponent: string]; 30 | 31 | var fileManger = [NSFileManager defaultManager] 32 | [fileManger createDirectoryAtPath:imageExportPath withIntermediateDirectories:true attributes:nil error:nil] 33 | 34 | // add artboard to export 35 | var tempExportArtboard = [MSArtboardGroup new] 36 | updateFrame({ 37 | x:10000, 38 | y:10000, 39 | width:10000, 40 | height:10000 41 | }, tempExportArtboard) 42 | doc.currentPage().addLayers([tempExportArtboard]); 43 | // add a group to hold artwork 44 | var exportGroup = tempExportArtboard.addLayerOfType('group'); 45 | // set frame to 1x1 so that it snaps to artworks 46 | updateFrame({ 47 | width:1, 48 | height:1 49 | }, exportGroup) 50 | // place duplicate artwork in export group (sizes to match artwork) 51 | for(var l=0; l < layers.length; l++){ 52 | var thisLayer = layers[l]; 53 | var layerCopy = thisLayer.copy(); 54 | exportGroup.addLayers([layerCopy]); 55 | thisLayer.removeFromParent(); 56 | } 57 | // match artboard size to export group 58 | updateFrame({ 59 | width: exportGroup.absoluteDirtyRect().size.width, 60 | height: exportGroup.absoluteDirtyRect().size.height 61 | }, tempExportArtboard) 62 | // export artboard 63 | var filename = imageExportPath + container.name() + '-flattened.png'; 64 | [doc saveArtboardOrSlice:tempExportArtboard toFile:filename]; 65 | tempExportArtboard.removeFromParent(); 66 | // add exported image to original player 67 | var flattenedImage = addImage(filename, container, container.name() + '-flattened'); 68 | updateFrame({ 69 | x:0, 70 | y:0 71 | }, flattenedImage) 72 | // remove temporary files 73 | [fileManger removeItemAtPath:imageExportPath error:nil] 74 | } -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/gif.js: -------------------------------------------------------------------------------- 1 | // GIF Export 2 | // Adapted from github.com/nathco/Generate-GIF 3 | 4 | var gifx, gifPath, tempPath, string, gifsetPath; 5 | var gifSetIndex; 6 | var gifFileManager; 7 | //var exportDebugPath; 8 | 9 | var initGIFexport = function() { 10 | gifx = pluginPath + "GIFX"; 11 | gifPath = savePath(); 12 | tempPath = NSTemporaryDirectory(); 13 | var string = [[NSProcessInfo processInfo] globallyUniqueString]; 14 | gifsetPath = [tempPath stringByAppendingPathComponent: string + @".gifset"]; 15 | 16 | gifFileManager = [NSFileManager defaultManager] 17 | [gifFileManager createDirectoryAtPath:gifsetPath withIntermediateDirectories:true attributes:nil error:nil] 18 | 19 | gifSetIndex = 1; 20 | } 21 | 22 | var exportArtboardToGIFset = function(artboard){ 23 | var artboardName = artboard.name(); 24 | var gifFileComponent = numberPad(gifSetIndex, 8) + ".png"; 25 | var fileName = [gifsetPath stringByAppendingPathComponent: gifFileComponent] 26 | [doc saveArtboardOrSlice:artboard toFile:fileName] 27 | gifSetIndex ++; 28 | } 29 | 30 | var createGIF = function(fps, loops) { 31 | var loop = "-l"; 32 | if(loops > 1 ) loop += (loops - 1) 33 | if(loops == 1 ) loop = "" 34 | var delay = Math.round((1000/fps) / 10) 35 | var convertTask = [[NSTask alloc] init] 36 | var createsTask = [[NSTask alloc] init] 37 | var convertGIF = "find \"" + gifsetPath + "\" -name '*.png' -exec sips -s format gif -o {}.gif {} \\;" 38 | var option = "find \"" + gifsetPath + "\" -name '*.png.gif' -execdir bash -c '\"" + gifx + "\" " + loop + " -d " + delay + " '*.png.gif' -o \"" + gifPath + "\"' \\;" 39 | 40 | [doc showMessage:@"Saving GIF..."] 41 | 42 | [convertTask setLaunchPath:@"/bin/bash"] 43 | [convertTask setArguments:["-c", convertGIF]] 44 | [convertTask launch] 45 | [convertTask waitUntilExit] 46 | [createsTask setLaunchPath:@"/bin/bash"] 47 | [createsTask setArguments:["-c", option]] 48 | [createsTask launch] 49 | [createsTask waitUntilExit] 50 | 51 | if ([createsTask terminationStatus] == 0) { 52 | 53 | [doc showMessage:@"Export Complete..."] 54 | 55 | } else { 56 | 57 | var error = [[NSTask alloc] init] 58 | 59 | [error setLaunchPath:@"/bin/bash"] 60 | [error setArguments:["-c", "afplay /System/Library/Sounds/Basso.aiff"]] 61 | [error launch] 62 | [doc showMessage:@"Export Failed..."] 63 | } 64 | 65 | [gifFileManager removeItemAtPath:gifsetPath error:nil] 66 | } 67 | 68 | var exportOptionsDialog = function(){ 69 | var alert = COSAlertWindow.new(); 70 | alert.setMessageText("GIF export options"); 71 | // FPS 72 | alert.addTextLabelWithValue("Framerate (FPS)"); 73 | alert.addTextFieldWithValue("30"); 74 | alert.addTextLabelWithValue("higher is smoother, but exports a larger file"); 75 | alert.addTextLabelWithValue(""); 76 | // Loops 77 | alert.addTextLabelWithValue("Number of Loops"); 78 | alert.addTextFieldWithValue("0"); 79 | alert.addTextLabelWithValue("enter 0 to loop FOREVER"); 80 | alert.addTextLabelWithValue(""); 81 | 82 | alert.runModal(); 83 | 84 | return { fps: parseInt(alert.viewAtIndex(1).stringValue()), loops: parseInt(alert.viewAtIndex(5).stringValue()) } 85 | } 86 | 87 | var savePath = function() { 88 | var filePath = [doc fileURL] ? [[[doc fileURL] path] stringByDeletingLastPathComponent] : @"~" 89 | var fileName = [[doc displayName] stringByDeletingPathExtension] 90 | var savePanel = [NSSavePanel savePanel] 91 | 92 | [savePanel setTitle:@"Export Animated GIF"] 93 | [savePanel setNameFieldLabel:@"Export To:"] 94 | [savePanel setPrompt:@"Export"] 95 | [savePanel setAllowedFileTypes: [NSArray arrayWithObject:@"gif"]] 96 | [savePanel setAllowsOtherFileTypes:false] 97 | [savePanel setCanCreateDirectories:true] 98 | [savePanel setDirectoryURL:[NSURL fileURLWithPath:filePath]] 99 | [savePanel setNameFieldStringValue:fileName] 100 | 101 | if ([savePanel runModal] != NSOKButton) { 102 | exit 103 | } 104 | 105 | return [[savePanel URL] path] 106 | } -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/helpers.js: -------------------------------------------------------------------------------- 1 | // useful functions 2 | 3 | // finding elements 4 | 5 | var filterLayersByName = function(name, layerSet){ 6 | var layers = []; 7 | for(var l=0; l < [layerSet count]; l++){ 8 | var layer = layerSet.objectAtIndex(l); 9 | var children = layer.children(); 10 | for(var c=0; c < [children count]; c++){ 11 | var child = children[c]; 12 | if(child.name() == name){ 13 | layers.push(layer); 14 | continue; 15 | } 16 | } 17 | } 18 | return layers; 19 | } 20 | 21 | var findLayerGroupsWithName = function(name, container){ 22 | var children = container.children(); 23 | var layers = []; 24 | for(var c=0; c < [children count]; c++){ 25 | var child = children[c]; 26 | if(child.name() == name && child.isMemberOfClass(MSLayerGroup)){ 27 | layers.push(child); 28 | } 29 | } 30 | return layers; 31 | } 32 | 33 | var findTextWithName = function(name, container){ 34 | var children = container.children(); 35 | var layers = []; 36 | for(var c=0; c < [children count]; c++){ 37 | var child = children[c]; 38 | if(child.name() == name && child.isMemberOfClass(MSTextLayer)){ 39 | layers.push(child); 40 | } 41 | } 42 | return layers; 43 | } 44 | 45 | var findImageWithName = function(name, container){ 46 | var children = container.children(); 47 | var layers = []; 48 | for(var c=0; c < [children count]; c++){ 49 | var child = children[c]; 50 | if(child.name() == name && child.isMemberOfClass(MSBitmapLayer)){ 51 | layers.push(child); 52 | } 53 | } 54 | return layers; 55 | } 56 | 57 | var findShapeWithName = function(name, container){ 58 | var children = container.children(); 59 | var layers = []; 60 | for(var c=0; c < [children count]; c++){ 61 | var child = children[c]; 62 | if(child.name() == name && child.isMemberOfClass(MSShapeGroup)){ 63 | layers.push(child); 64 | } 65 | } 66 | return layers; 67 | } 68 | 69 | var findArtboardsWithName = function(name, container, artboardArray){ 70 | var children = container.children(); 71 | var layers = artboardArray || []; 72 | for(var c=0; c < [children count]; c++){ 73 | var child = children[c]; 74 | if(child.name() == name && child.isMemberOfClass(MSArtboardGroup)){ 75 | layers.push(child); 76 | } 77 | } 78 | return layers; 79 | } 80 | 81 | var getArtboardsWithNameInDocument = function(layerName){ 82 | var layers = []; 83 | var pages = doc.pages(); 84 | for(var p=0; p < [pages count]; p++){ 85 | var page = pages.objectAtIndex(p); 86 | layers = findArtboardsWithName(layerName, page, layers); 87 | } 88 | return layers; 89 | } 90 | 91 | var artboardWithNameExistsInDocument = function(layerName){ 92 | var layerExists = null; 93 | var artboards = getArtboardsWithNameInDocument(layerName) 94 | if(artboards.length > 0){ 95 | layerExists = true; 96 | } 97 | return layerExists; 98 | } 99 | 100 | // naming functions 101 | 102 | var deDupeGroupNames = function(container){ 103 | var increments = {}; 104 | var children = container.children(); 105 | for(var i=0; i < children.count(); i++){ 106 | var child = children.objectAtIndex(i); 107 | if(child.isMemberOfClass(MSLayerGroup)){ 108 | groups = findLayerGroupsWithName(child.name(), container); 109 | if(groups.length > 1){ 110 | for(var g=0; g < groups.length; g++){ 111 | var group = groups[g]; 112 | group.setName(group.name() + " copy " + g) 113 | } 114 | } 115 | } 116 | } 117 | return children; 118 | } 119 | 120 | var getLegendName = function(animationName){ 121 | return stripTagSymbols(animationName) + " detected transitions"; 122 | } 123 | 124 | var getTransitionName = function(animationName, startKeyframeIndex, endKeyframeIndex){ 125 | return stripTagSymbols(animations[animationName].keyframes[startKeyframeIndex].layer.name()) + " > " + stripTagSymbols(animations[animationName].keyframes[endKeyframeIndex].layer.name()); 126 | } 127 | 128 | var getCurveSelectorName = function(transitionName){ 129 | return transitionName + " curveSelector"; 130 | } 131 | 132 | var checkForAnimationReference = function(layerName) { 133 | return layerName.match(/\{(\S+)\}/g); 134 | } 135 | 136 | var stripTagSymbols = function(string) { 137 | return string.replace(/{|}/g, ''); 138 | } 139 | 140 | var animationTimelineName = function(animationName) { 141 | return stripTagSymbols(animationName) + " timeline"; 142 | } 143 | 144 | // working with styles 145 | // TODO: Add style get helpers 146 | 147 | var updateLayerProperties = function(properties, layer){ 148 | if(!layer) return 149 | var frame = layer.rect(); 150 | if(properties.x != undefined){ 151 | frame.origin.x = properties.x; 152 | } 153 | if(properties.y != undefined){ 154 | frame.origin.y = properties.y; 155 | } 156 | if(properties.height != undefined){ 157 | frame.size.height = properties.height; 158 | } 159 | if(properties.width != undefined){ 160 | frame.size.width = properties.width; 161 | } 162 | if(properties.rotation != undefined){ 163 | layer.setRotation(properties.rotation); 164 | } 165 | layer.setRect(frame); 166 | var style = layer.style(); 167 | if(properties.opacity != undefined){ 168 | style.contextSettings().opacity = properties.opacity; 169 | } 170 | layer.setStyle(style); 171 | } 172 | 173 | var updateFrame = function(frame, layer) { 174 | if(frame.width != undefined){ 175 | layer.frame().setWidth(frame.width); 176 | } 177 | if(frame.height != undefined){ 178 | layer.frame().setHeight(frame.height); 179 | } 180 | if(frame.x != undefined){ 181 | layer.frame().setX(frame.x); 182 | } 183 | if(frame.y != undefined){ 184 | layer.frame().setY(frame.y); 185 | } 186 | } 187 | 188 | var updateShapeStyle = function(style, shape) { 189 | // fill 190 | if(style.fill != undefined){ 191 | var shapeFills = shape.style().fills().array(); 192 | // shape has a fill 193 | if(shapeFills.count() > 0){ 194 | shapeFills[0].setColor(MSColor.colorWithSVGString(style.fill)); 195 | } 196 | else { 197 | var newFill = shape.style().fills().addNewStylePart(); 198 | newFill.setColor(MSColor.colorWithSVGString(style.fill)); 199 | } 200 | } 201 | // border 202 | if(style.border != undefined){ 203 | var shapeBorders = shape.style().borders().array(); 204 | // shape has a fill 205 | if(shapeBorders.count() > 0){ 206 | if(style.border.color != undefined){ 207 | shapeBorders[0].setColor(MSColor.colorWithSVGString(style.border.color)); 208 | } 209 | if(style.border.thickness != undefined){ 210 | shapeBorders[0].setThickness(parseFloat(style.border.thickness)); 211 | } 212 | } 213 | else { 214 | var newBorder = shape.style().borders().addNewStylePart(); 215 | if(style.border.color != undefined){ 216 | newBorder.setColor(MSColor.colorWithSVGString(style.border.color)); 217 | } 218 | if(style.border.thickness != undefined){ 219 | newBorder.setThickness(parseFloat(style.border.thickness)); 220 | } 221 | } 222 | } 223 | // TODO: Add all other style properities 224 | 225 | } 226 | 227 | var updateTextStyle = function(style, text) { 228 | if(text && text.isMemberOfClass(MSTextLayer)){ 229 | if(style.color != undefined){ 230 | text.textColor = MSColor.colorWithSVGString(style.color); 231 | } 232 | if(style.size != undefined){ 233 | text.fontSize = style.size; 234 | } 235 | if(style.font != undefined){ 236 | text.fontPostscriptName = style.font; 237 | } 238 | } 239 | // TODO: Add all other style properities 240 | } 241 | 242 | var addImage = function(imagePath, container, name) { 243 | var image = [[NSImage alloc] initWithContentsOfFile:imagePath]; 244 | var layerName = name || "image"; 245 | var imageLayer = [MSBitmapLayer new]; 246 | container.addLayers([imageLayer]); 247 | 248 | imageLayer.setConstrainProportions(false); 249 | imageLayer.setRawImage_convertColourspace_collection(image, false, doc.documentData().images()); 250 | imageLayer.setName(name); 251 | imageLayer.frame().setWidth(image.size().width); 252 | imageLayer.frame().setHeight(image.size().height); 253 | imageLayer.setConstrainProportions(true); 254 | 255 | return imageLayer; 256 | } 257 | 258 | // number padding 259 | 260 | var numberPad = function(number, padding) { 261 | var output = number + ''; 262 | while (output.length < padding) { 263 | output = '0' + output; 264 | } 265 | return output; 266 | } 267 | 268 | // mixed name sorting 269 | // http://www.bennadel.com/blog/2495-user-friendly-sort-of-alpha-numeric-data-in-javascript.htm 270 | 271 | var normalizeMixedDataValue = function( value ){ 272 | var padding = "000000000000000"; 273 | value = value.replace( 274 | /(\d+)((\.\d+)+)?/g, 275 | function( $0, integer, decimal, $3 ) { 276 | if ( decimal !== $3 ) { 277 | return( 278 | padding.slice( integer.length ) + 279 | integer + 280 | decimal 281 | ); 282 | } 283 | decimal = ( decimal || ".0" ); 284 | return( 285 | padding.slice( integer.length ) + 286 | integer + 287 | decimal + 288 | padding.slice( decimal.length ) 289 | ); 290 | } 291 | ); 292 | return( value ); 293 | } 294 | 295 | // objects 296 | 297 | var countObjectKeys = function(thisObject){ 298 | var count = 0; 299 | for (k in thisObject) if (thisObject.hasOwnProperty(k)) count++; 300 | return count; 301 | } 302 | 303 | 304 | // Only used for debugging. Very helpful for object introspection 305 | // https://github.com/tylergaw/day-player/blob/master/lib/utils.js 306 | var dump = function(obj) { 307 | log("#####################################################################################") 308 | log("## Dumping object " + obj ) 309 | log("## obj class is: " + [obj className]) 310 | log("#####################################################################################") 311 | log("obj.properties:") 312 | log([obj class].mocha().properties()) 313 | log("obj.propertiesWithAncestors:") 314 | log([obj class].mocha().propertiesWithAncestors()) 315 | log("obj.classMethods:") 316 | log([obj class].mocha().classMethods()) 317 | log("obj.classMethodsWithAncestors:") 318 | log([obj class].mocha().classMethodsWithAncestors()) 319 | log("obj.instanceMethods:") 320 | log([obj class].mocha().instanceMethods()) 321 | log("obj.instanceMethodsWithAncestors:") 322 | log([obj class].mocha().instanceMethodsWithAncestors()) 323 | log("obj.protocols:") 324 | log([obj class].mocha().protocols()) 325 | log("obj.protocolsWithAncestors:") 326 | log([obj class].mocha().protocolsWithAncestors()) 327 | log("obj.treeAsDictionary():") 328 | log(obj.treeAsDictionary()) 329 | } -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier" : "com.nicholaswallen.sketchplugins.motion", 3 | "version" : "0.1.0", 4 | "description" : "A sample plugin which shows and hides a tagged selection of layers.", 5 | "name" : "Motion", 6 | "author" : "Nicholas Wallen", 7 | "commands" : [ 8 | { 9 | "script" : "motion.js", 10 | "handler" : "playAnimations", 11 | "shortcut" : "cmd ctrl a", 12 | "name" : "Animate", 13 | "identifier" : "playAnimations" 14 | }, 15 | { 16 | "script" : "motion.js", 17 | "handler" : "exportGIF", 18 | "shortcut" : "cmd ctrl e", 19 | "name" : "Export GIF", 20 | "identifier" : "exportGIF" 21 | } 22 | ], 23 | "menu": { 24 | "items": [ 25 | "playAnimations", 26 | "exportGIF" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/motion.js: -------------------------------------------------------------------------------- 1 | @import 'common.js' 2 | @import 'Tween.js' 3 | @import 'constants.js' 4 | @import 'helpers.js' 5 | @import 'timeline.js' 6 | @import 'gif.js' 7 | @import 'flatten.js' 8 | 9 | var doc; 10 | var selection; 11 | var selectedArtboard; 12 | var pluginPath; 13 | var pluginStartTime; // used to sync all animations 14 | var animations = {}; 15 | 16 | var onStart = function(context){ 17 | //TODO: resolve bug -- calling script multiple times in quick succession crashes Sketch 18 | [[COScript currentCOScript] setShouldKeepAround:true] 19 | doc = context.document; 20 | selection = context.selection; 21 | pluginStartTime = Date.now() 22 | var scriptPath = context.scriptPath; 23 | var pluginFolder = scriptPath.match(/Plugins\/([\w -])*/)[0] + "/"; 24 | var basePath = scriptPath.split("Plugins")[0]; 25 | pluginPath = basePath + pluginFolder 26 | 27 | // Find animation artboards (keyframes) 28 | initAnimations(); 29 | } 30 | 31 | var compareLayerProperties = function(inFrameLayer, outFrameLayer){ 32 | var states = { 33 | in: {}, 34 | out:{} 35 | }; 36 | // Frame properties 37 | inRect = inFrameLayer.rect(); 38 | outRect = outFrameLayer.rect(); 39 | //x 40 | if(inRect.origin.x != outRect.origin.x){ 41 | states.in.x = parseFloat(inRect.origin.x); 42 | states.out.x = parseFloat(outRect.origin.x); 43 | } 44 | //y 45 | if(inRect.origin.y != outRect.origin.y){ 46 | states.in.y = parseFloat(inRect.origin.y); 47 | states.out.y = parseFloat(outRect.origin.y); 48 | } 49 | //width 50 | if(inRect.size.width != outRect.size.width){ 51 | states.in.width = parseFloat(inRect.size.width); 52 | states.out.width = parseFloat(outRect.size.width); 53 | } 54 | //height 55 | if(inRect.size.height != outRect.size.height){ 56 | states.in.height = parseFloat(inRect.size.height); 57 | states.out.height = parseFloat(outRect.size.height); 58 | } 59 | //rotation 60 | if(inFrameLayer.rotation() != outFrameLayer.rotation()){ 61 | states.in.rotation = parseFloat(inFrameLayer.rotation()); 62 | states.out.rotation = parseFloat(outFrameLayer.rotation()); 63 | } 64 | // Style properties 65 | inStyle = inFrameLayer.style(); 66 | outStyle = outFrameLayer.style(); 67 | // opacity 68 | if(inStyle.contextSettings().opacity() != outStyle.contextSettings().opacity()){ 69 | states.in.opacity = parseFloat(inStyle.contextSettings().opacity()); 70 | states.out.opacity = parseFloat(outStyle.contextSettings().opacity()); 71 | } 72 | 73 | if(countObjectKeys(states.in) == 0){ 74 | return null 75 | } 76 | 77 | return states 78 | } 79 | 80 | var compareKeyframes = function(inFrame, outFrame){ 81 | var transitions = []; 82 | deDupeGroupNames(inFrame); 83 | deDupeGroupNames(outFrame); 84 | var inFrameLayers = inFrame.children(); 85 | var outFrameLayers = outFrame.children(); 86 | for(var l=0; l < [inFrameLayers count]; l++){ 87 | var inFrameLayer = inFrameLayers.objectAtIndex(l); 88 | var inFrameLayerName = inFrameLayer.name(); 89 | //loop through each group in inFrame artboard 90 | if(inFrameLayer.isMemberOfClass(MSLayerGroup)){ 91 | for(var o=0; o < [outFrameLayers count]; o++){ 92 | var outFrameLayer = outFrameLayers.objectAtIndex(o); 93 | var outFrameLayerName = outFrameLayer.name(); 94 | //search for same group in the outFrame artboard 95 | if(inFrameLayerName == outFrameLayerName && outFrameLayer.isMemberOfClass(MSLayerGroup)){ 96 | //if properties are different save states 97 | var states = compareLayerProperties(inFrameLayer, outFrameLayer); 98 | if(states != null){ 99 | var transition = { 100 | target: inFrameLayer, 101 | states: states 102 | } 103 | //return object containing in and out states for each target layer 104 | transitions.push(transition); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | return transitions; 111 | } 112 | 113 | var calculateTransitions = function(){ 114 | for(var animationName in animations){ 115 | if(animations.hasOwnProperty(animationName)){ 116 | var animation = animations[animationName]; 117 | var keyframeCount = animation.keyframes.length; 118 | var transitionCount = keyframeCount - 1; // always one less transition than keyframes 119 | if(transitionCount === 0) continue; 120 | animation.transitions = {}; 121 | // compare keyframes for all transitions 122 | for(var t=0; t < transitionCount; t++){ 123 | var inFrame = animation.keyframes[t].layer; // starting artboard 124 | var outFrame = animation.keyframes[t+1].layer; // ending artboard 125 | var transitions = compareKeyframes(inFrame, outFrame); // compare groups on artboards 126 | for(var i=0; i < transitions.length; i++){ 127 | var transition = transitions[i]; 128 | transition.keyframeIndex = t + 1; // corresponding keyframe is outFrame 129 | animation.transitions[transition.target.name()] = animation.transitions[transition.target.name()] || []; 130 | animation.transitions[transition.target.name()].push(transition); 131 | } 132 | } 133 | } 134 | } 135 | 136 | } 137 | 138 | 139 | var initAnimations = function(){ 140 | // Detect and save all tagged animations in document 141 | // Check artboards on all document pages 142 | var pages = doc.pages(); 143 | for(var p=0; p < [pages count]; p++){ 144 | var page = pages.objectAtIndex(p); 145 | var layers = page.children(); 146 | for(var l=0; l < [layers count]; l++){ 147 | var layer = layers.objectAtIndex(l); 148 | if(layer.isMemberOfClass(MSArtboardGroup)){ 149 | var animationName = checkForAnimationReference(layer.name()) 150 | if(animationName){ 151 | animations[animationName] = animations[animationName] || {}; 152 | animations[animationName].name = animationName[0]; 153 | animations[animationName].keyframes = animations[animationName].keyframes || []; 154 | var keyframe = { 155 | layer: layer, 156 | timing: { 157 | duration: 500, 158 | delay: 0, 159 | easingIndex: 0, 160 | easing: TWEEN.Easing.Linear.None 161 | } 162 | } 163 | animations[animationName].keyframes.push(keyframe); 164 | animations[animationName].keyframes.sort(function(a,b){ 165 | var aMixed = normalizeMixedDataValue( a.layer.name() ); 166 | var bMixed = normalizeMixedDataValue( b.layer.name() ); 167 | return( aMixed < bMixed ? -1 : 1 ); 168 | }) 169 | } 170 | } 171 | } 172 | } 173 | refreshAnimationTimelines(); 174 | calculateTransitions(); 175 | } 176 | 177 | var createTween = function(states, targetLayer, containerLayer, timing, animationName, transitionName) { 178 | var layers = findLayerGroupsWithName(targetLayer.name(), containerLayer); 179 | var layer = layers[0]; 180 | var tween = new TWEEN.Tween(states.in) 181 | .to(states.out, timing.duration ) 182 | .easing( timing.easing ) 183 | .delay( timing.delay ) 184 | .onStart(function(){ 185 | //log('animation start ' + targetLayer.name() + " ---- ") 186 | if(animationName && transitionName){ 187 | highlightTimelineFrame(transitionName, animationName); 188 | highlightLegendName(transitionName, animationName); 189 | } 190 | }) 191 | .onComplete(function(){ 192 | //log('animation stop ' + targetLayer.name()) 193 | if(animationName && transitionName){ 194 | unHighlightTimelineFrame(transitionName, animationName); 195 | unHighlightLegendName(transitionName, animationName); 196 | } 197 | }) 198 | .onUpdate(function(){ 199 | updateLayerProperties(this, layer); 200 | }); 201 | return tween; 202 | } 203 | 204 | var initTweens = function(animation, containerLayer){ 205 | var transitions = animation.transitions; 206 | // iterate keyframe transitions 207 | for(var transition in transitions){ 208 | if(transitions.hasOwnProperty(transition)){ 209 | var layerTransitions = transitions[transition]; 210 | // iterate individual layer transitions 211 | var tweens = []; 212 | for(var t=0; t < layerTransitions.length; t++){ 213 | var layerTransition = layerTransitions[t]; 214 | var index = layerTransition.keyframeIndex; 215 | var timing = animation.keyframes[index].timing; 216 | var animationName = animation.name; 217 | var transitionName = getTransitionName(animationName, index - 1, index); 218 | var tween = createTween(layerTransition.states, layerTransition.target, containerLayer, timing, animationName, transitionName); 219 | tweens[t] = tween; 220 | 221 | var keyframeStartTime = parseInt(timing.startTime); 222 | var keyframeDelay = parseInt(timing.delay); 223 | var startTime = pluginStartTime + (keyframeStartTime - keyframeDelay); 224 | tweens[t].start(startTime); 225 | } 226 | startPlayhead(animation.name); 227 | } 228 | } 229 | } 230 | 231 | var animate = function() { 232 | var fps = 60; 233 | // run animation loop 234 | var animationStartTime; 235 | [coscript scheduleWithRepeatingInterval:(1/fps) jsFunction:function(cinterval){ 236 | if(animationStartTime == undefined){ 237 | animationStartTime = Date.now(); //note time of first animation loop 238 | } 239 | var runTime = Date.now() - animationStartTime; // calculate how long animation has run 240 | TWEEN.update(pluginStartTime + runTime); // move animation by runtime 241 | doc.currentView().refresh(); 242 | // kill loop when tweens are done 243 | if(TWEEN.getAll().length == 0){ 244 | //log("animation took " + runTime + "ms to run" ); 245 | [cinterval cancel] 246 | } 247 | }]; 248 | } 249 | 250 | var animateAndSaveGIF = function() { 251 | var animationTime = pluginStartTime; // time when plugin was launched 252 | var exportOptions = exportOptionsDialog(); 253 | var fps = exportOptions.fps || 30; 254 | var loops = exportOptions.loops; 255 | initGIFexport(); 256 | [coscript scheduleWithRepeatingInterval:(1/fps) jsFunction:function(cinterval){ 257 | TWEEN.update(animationTime); 258 | doc.currentView().refresh(); 259 | exportArtboardToGIFset(selectedArtboard) 260 | animationTime += 1000/fps; // 1000/fps = ms/frame -- manually increment to not drop frames 261 | // kill loop when tweens are done 262 | if(TWEEN.getAll().length == 0){ 263 | [cinterval cancel] 264 | createGIF(fps, loops); 265 | } 266 | }]; 267 | } 268 | 269 | var playAnimation = function(name, containerLayer){ 270 | var targetAnimation = animations[name]; 271 | // Clear target group(s) on selected artboard 272 | var children = [containerLayer children] 273 | for(var c=0; c < [children count]; c++){ 274 | var child = children.objectAtIndex(c); 275 | if(child != containerLayer){ 276 | [child removeFromParent]; 277 | } 278 | } 279 | // resize containerlayer to match first keyframe artboard 280 | var keyframeFrame = targetAnimation.keyframes[0].layer.rect(); 281 | var containerFrame = containerLayer.rect(); 282 | containerFrame.size = keyframeFrame.size; 283 | containerLayer.setRect(containerFrame); 284 | // Copy all top-level layer groups from first keyframe to containerlayer 285 | var layers = targetAnimation.keyframes[0].layer.layers(); 286 | for(var t=0; t < [layers count]; t++){ 287 | var layer = layers.objectAtIndex(t); 288 | var layerCopy = [layer copy]; 289 | containerLayer.addLayers([layerCopy]); 290 | } 291 | // flatten artwork for better performance 292 | flattenArtwork(containerLayer); 293 | // create tweens 294 | initTweens(targetAnimation, containerLayer); 295 | } 296 | 297 | var playAnimations = function(context, playAndExport){ 298 | onStart(context); // init animations and globals 299 | var artboards = []; 300 | for(var s=0; s < [selection count]; s++){ 301 | if(selection[s].isMemberOfClass(MSArtboardGroup)){ 302 | artboards.push(selection[s]) 303 | } 304 | } 305 | // requires user to select a single artboard 306 | if (artboards.length !== 1){ 307 | doc.showMessage("please select a single artboard") 308 | return; 309 | } 310 | else { 311 | doc.showMessage("animating...") 312 | selectedArtboard = artboards[0]; 313 | // Find animation(s) referenced in selected artboard 314 | var artboardChildren = artboards[0].children(); 315 | for(var l=0; l < [artboardChildren count]; l++){ 316 | var child = artboardChildren.objectAtIndex(l); 317 | var childName = child.name(); 318 | if(child.isMemberOfClass(MSLayerGroup)){ 319 | var animationName = checkForAnimationReference(childName); 320 | if(animationName){ 321 | playAnimation(animationName, child); 322 | } 323 | } 324 | } 325 | if(playAndExport){ 326 | animateAndSaveGIF(); 327 | } 328 | else { 329 | animate(); 330 | } 331 | } 332 | } 333 | 334 | var exportGIF = function(context){ 335 | playAnimations(context, true); 336 | } 337 | 338 | -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Contents/Sketch/timeline.js: -------------------------------------------------------------------------------- 1 | // Manage timeline UI elements 2 | var highlightedLegends = {}; 3 | var highlightedSegments = {}; 4 | 5 | var createTimelineSegment = function(x, y, width, height, index, transitionName, timelineArtboard){ 6 | // group 7 | var group = timelineArtboard.addLayerOfType('group'); 8 | group.setName(transitionName); 9 | // size & position 10 | updateFrame({ 11 | x : x, 12 | y : y, 13 | width : width, 14 | height : height 15 | }, group); 16 | // rectangle 17 | var rectangle = group.addLayerOfType('rectangle'); 18 | rectangle.setName('timelineSegment'); 19 | updateShapeStyle({ 20 | fill: TIMELINECOLORS.block, 21 | border: {color: TIMELINECOLORS.blockBorder, thickness:1} 22 | }, rectangle) 23 | updateFrame({ 24 | width: group.rect().size.width, 25 | height: group.rect().size.height 26 | }, rectangle) 27 | // title text 28 | var text = group.addLayerOfType('text'); 29 | text.setName('timelineSegmentTitle'); 30 | text.stringValue = index + ""; 31 | updateTextStyle({ 32 | font: "HelveticaNeue-Bold", 33 | size: 55, 34 | color: TIMELINECOLORS.text 35 | },text) 36 | updateFrame({ 37 | y: TIMELINELAYOUT.height * .2, 38 | width: group.rect().size.width, 39 | height: group.rect().size.height 40 | }, text) 41 | text.textAlignment = 2; // align center 42 | 43 | return group; 44 | } 45 | 46 | var createLegendDetail = function(k, transitionName, timing){ 47 | var detail = [MSLayerGroup new] 48 | updateFrame({ 49 | x : LEGENDLAYOUT.margin, 50 | y : ((k-1) * (LEGENDLAYOUT.easeTileHeight + LEGENDLAYOUT.margin)) + LEGENDLAYOUT.margin, 51 | width : LEGENDLAYOUT.rowWidth, 52 | height : LEGENDLAYOUT.easeTileHeight 53 | }, detail); 54 | detail.setName(transitionName); 55 | 56 | var index = detail.addLayerOfType('text'); 57 | index.setName('animationIndex') 58 | index.stringValue = k + ''; 59 | updateTextStyle({ 60 | font: "HelveticaNeue-Bold", 61 | size: 35, 62 | color: LEGENDCOLORS.index 63 | }, index) 64 | updateFrame({ 65 | x : LEGENDLAYOUT.easeTileWidth + LEGENDLAYOUT.margin, 66 | y : 0, 67 | width : LEGENDLAYOUT.rowWidth - LEGENDLAYOUT.easeTileWidth - (LEGENDLAYOUT.margin * 3) 68 | }, index); 69 | 70 | var info = detail.addLayerOfType('text'); 71 | info.setName('animationInfo'); 72 | info.stringValue = transitionName + "\ndelay " + timing.delay + "ms / duration " + timing.duration + "ms"; 73 | updateTextStyle({ 74 | font: "HelveticaNeue-Thin", 75 | size: 30, 76 | color: LEGENDCOLORS.info 77 | }, info) 78 | updateFrame({ 79 | x : LEGENDLAYOUT.easeTileWidth + LEGENDLAYOUT.margin, 80 | y : LEGENDLAYOUT.easeTileHeight * .40, 81 | width : LEGENDLAYOUT.rowWidth - LEGENDLAYOUT.easeTileWidth - (LEGENDLAYOUT.margin * 3) 82 | }, info); 83 | 84 | var curve = detail.addLayerOfType('group'); 85 | curve.setName('curve'); 86 | 87 | var curveMask = curve.addLayerOfType('rectangle'); 88 | curveMask.setName('curveMask'); 89 | updateFrame({ 90 | width : LEGENDLAYOUT.easeTileWidth, 91 | height : LEGENDLAYOUT.easeTileHeight 92 | }, curveMask); 93 | curveMask.setHasClippingMask(true); 94 | 95 | var imagePath = pluginPath + RESOURCESPATH + ANIMATIONCURVEFILENAME; 96 | var image = addImage(imagePath, curve, 'animationCurve'); 97 | 98 | return detail 99 | } 100 | 101 | var initTimelineArtboard = function(animationName, timelineArtboardName){ 102 | var animationKeyframes = animations[animationName].keyframes; 103 | var firstKeyframe = animationKeyframes[0].layer; 104 | var keyframeFrame = firstKeyframe.rect(); 105 | // setup timeline artboard 106 | var timelineArtboard = [MSArtboardGroup new]; 107 | timelineArtboard.setHasBackgroundColor(true); 108 | timelineArtboard.setBackgroundColor(MSColor.colorWithSVGString(TIMELINECOLORS.background)); 109 | timelineArtboard.setName(timelineArtboardName); 110 | updateFrame({ 111 | x: keyframeFrame.origin.x + LEGENDLAYOUT.rowWidth + (LEGENDLAYOUT.margin * 3), 112 | y: keyframeFrame.origin.y + keyframeFrame.size.height + 120, 113 | height: TIMELINELAYOUT.height 114 | },timelineArtboard) 115 | // don't maintain proportions on resize 116 | timelineArtboard.frame().setConstrainProportions(0); 117 | // save reference to artboards in animation config object 118 | animations[animationName].timelineArtboard = timelineArtboard; 119 | // add artboard to page 120 | doc.currentPage().addLayers([timelineArtboard]); 121 | // add segments and details 122 | updateTimeline(animationName); 123 | } 124 | 125 | var initTimelineLegendArtboard = function(animationName, timelineArtboardName){ 126 | var animationKeyframes = animations[animationName].keyframes; 127 | var firstKeyframe = animationKeyframes[0].layer; 128 | var keyframeFrame = firstKeyframe.rect(); 129 | // setup timeline legend 130 | var timelineLegendArtboard = [MSArtboardGroup new]; 131 | timelineLegendArtboard.setHasBackgroundColor(true); 132 | timelineLegendArtboard.setBackgroundColor(MSColor.colorWithSVGString(LEGENDCOLORS.background)); 133 | var timelineLegendArtboardName = getLegendName(animationName); 134 | timelineLegendArtboard.setName(timelineLegendArtboardName); 135 | updateFrame({ 136 | x: keyframeFrame.origin.x , 137 | y: keyframeFrame.origin.y + keyframeFrame.size.height + 120, 138 | height: LEGENDLAYOUT.margin, 139 | width: LEGENDLAYOUT.rowWidth + (LEGENDLAYOUT.margin * 3) 140 | }, timelineLegendArtboard) 141 | // don't maintain proportions on resize 142 | timelineLegendArtboard.frame().setConstrainProportions(0); 143 | // save reference to artboards in animation config object 144 | animations[animationName].timelineLegendArtboard = timelineLegendArtboard; 145 | // add artboard to page 146 | doc.currentPage().addLayers([timelineLegendArtboard]); 147 | // add segments and details 148 | updateTimelineLegend(animationName); 149 | } 150 | 151 | var matchTimelineHeightToLegendHeight = function(animationName){ 152 | if(animations[animationName].timelineArtboard && animations[animationName].timelineLegendArtboard){ 153 | animations[animationName].timelineArtboard.frame().setConstrainProportions(0); 154 | updateFrame({ 155 | height: animations[animationName].timelineLegendArtboard.rect().size.height 156 | }, animations[animationName].timelineArtboard) 157 | addTimelinePlayhead(animationName); 158 | } 159 | } 160 | 161 | var updateTimelineLegend = function(animationName){ 162 | var animationKeyframes = animations[animationName].keyframes; 163 | var details = animations[animationName].timelineLegendArtboard.layers(); 164 | var prevGroup = null; 165 | // check for extra details 166 | if(details.count() > (animationKeyframes.length-1)){ 167 | // more details than transitions -- delete extra 168 | var delta = details.count() - (animationKeyframes.length-1); 169 | for(var d=0; d < delta; d++){ 170 | var detailToDelete = details.objectAtIndex(details.count() - 1); 171 | [detailToDelete removeFromParent]; 172 | animations[animationName].timelineLegendArtboard.frame().setConstrainProportions(0); 173 | animations[animationName].timelineLegendArtboard.frame().subtractHeight(LEGENDLAYOUT.easeTileHeight + LEGENDLAYOUT.margin); 174 | matchTimelineHeightToLegendHeight(animationName); 175 | } 176 | } 177 | for( var k=1;k < animationKeyframes.length; k++){ 178 | var keyframe = animationKeyframes[k]; 179 | var timing = keyframe.timing; 180 | var transitionName = getTransitionName(animationName, k-1, k); 181 | // update details 182 | if((k-1) > (details.count() - 1)){ 183 | // no details -- add new detail set 184 | var detail = createLegendDetail(k, transitionName, timing); 185 | animations[animationName].timelineLegendArtboard.addLayers([detail]); 186 | animations[animationName].timelineLegendArtboard.frame().setConstrainProportions(0); 187 | animations[animationName].timelineLegendArtboard.frame().addHeight(LEGENDLAYOUT.easeTileHeight + LEGENDLAYOUT.margin) 188 | matchTimelineHeightToLegendHeight(animationName); 189 | } 190 | else { 191 | // details exist - update 192 | var detailToUpdate = details.objectAtIndex(k - 1); 193 | detailToUpdate.setName(transitionName); 194 | var textToUpdate = findTextWithName('animationInfo', detailToUpdate)[0]; 195 | 196 | if(textToUpdate){ 197 | textToUpdate.stringValue = transitionName + "\ndelay " + timing.delay + "ms / duration " + timing.duration + "ms"; 198 | } 199 | 200 | var extractedEasing = extractEasingCurve(transitionName, animationName); 201 | if(extractedEasing){ 202 | timing.easing = extractedEasing.ease; 203 | timing.easingIndex = extractedEasing.easingIndex; 204 | } 205 | 206 | var imageToUpdate = findImageWithName('animationCurve', detailToUpdate)[0]; 207 | if(imageToUpdate){ 208 | updateFrame({ 209 | x: -(LEGENDLAYOUT.easeTileWidth * timing.easingIndex) + 1, 210 | y:1 211 | }, imageToUpdate) 212 | } 213 | 214 | } 215 | } 216 | } 217 | 218 | var addTimelinePlayhead = function(animationName){ 219 | var artboard = animations[animationName].timelineArtboard; 220 | var rectangle = findShapeWithName('playhead', artboard)[0]; 221 | var segments = filterLayersByName('timelineSegment', artboard.layers()); 222 | // add playhead 223 | if(!rectangle){ 224 | rectangle = artboard.addLayerOfType('rectangle'); 225 | rectangle.frame().setWidth(2); 226 | rectangle.frame().setX(0); 227 | rectangle.setName('playhead'); 228 | var rectangleFill = rectangle.style().fills().addNewStylePart(); 229 | rectangleFill.color = MSColor.colorWithSVGString('#000000'); 230 | } 231 | rectangle.frame().setHeight(artboard.frame().height()); 232 | var lastSegment = segments[segments.length-1]; 233 | var timelineEndX = lastSegment.frame().x() + lastSegment.frame().width(); 234 | animations[animationName].timelineTween = new TWEEN.Tween({x:0}) 235 | .to({x:timelineEndX} , (timelineEndX * MSPERPIXEL)) 236 | .onUpdate(function(){ 237 | rectangle.frame().setX(this.x); 238 | }) 239 | .onComplete(function(){ 240 | rectangle.removeFromParent(); 241 | }); 242 | } 243 | 244 | var startPlayhead = function(animationName){ 245 | animations[animationName].timelineTween.start(pluginStartTime); 246 | } 247 | 248 | var updateTimeline = function(animationName) { 249 | var animationKeyframes = animations[animationName].keyframes; 250 | var segments = animations[animationName].timelineArtboard.layers(); 251 | segments = filterLayersByName('timelineSegment', segments); 252 | var prevSegment = null; 253 | // check for extra segments 254 | if(segments.length > (animationKeyframes.length-1)){ 255 | // more segments than transitions -- delete extra 256 | var delta = segments.length - (animationKeyframes.length-1); 257 | for(var d=0; d < delta; d++){ 258 | var segmentToDelete = segments[segments.length - 1]; 259 | [segmentToDelete removeFromParent]; 260 | segments.pop(); 261 | animations[animationName].timelineArtboard.frame().setConstrainProportions(0); 262 | animations[animationName].timelineArtboard.frame().subtractWidth(500); 263 | } 264 | } 265 | for( var k=1;k < animationKeyframes.length; k++){ 266 | var keyframe = animationKeyframes[k]; 267 | var timing = keyframe.timing; 268 | var transitionName = getTransitionName(animationName, k-1, k); 269 | // update segments 270 | if((k-1) > (segments.length - 1)){ 271 | // no segment -- add new segment 272 | var x = timing.delay / MSPERPIXEL; 273 | var y = (k-1) * TIMELINELAYOUT.height + TIMELINELAYOUT.margin ; 274 | var width = timing.duration / MSPERPIXEL; 275 | var height = TIMELINELAYOUT.height; 276 | if(prevSegment){ 277 | var prevSegmentFrame = prevSegment.rect(); 278 | x = prevSegmentFrame.origin.x + prevSegmentFrame.size.width + (timing.delay / 10); 279 | timing.startTime = x; 280 | 281 | } 282 | animations[animationName].timelineArtboard.frame().setConstrainProportions(0); 283 | animations[animationName].timelineArtboard.frame().addWidth(width + 100); 284 | prevSegment = createTimelineSegment(x, y, width, height, k, transitionName, animations[animationName].timelineArtboard); 285 | } 286 | else{ 287 | // segment exists -- update animation config based on segment position 288 | var segment = segments[k-1]; // segment indexes are offset by 1 289 | var newTiming = extractAnimationValues(prevSegment, segment); 290 | timing.delay = newTiming.delay; 291 | timing.duration = newTiming.duration; 292 | timing.startTime = newTiming.startTime; 293 | segment.setName(transitionName); 294 | prevSegment = segment; 295 | 296 | // resizing timeline segments messes up text size 297 | var text = findTextWithName('timelineSegmentTitle', segment); 298 | text[0].stringValue = k + ""; 299 | text[0].setFontSize(55); 300 | 301 | } 302 | } 303 | matchTimelineHeightToLegendHeight(animationName); 304 | addTimelinePlayhead(animationName); 305 | } 306 | 307 | var extractEasingCurve = function(transitionName, animationName){ 308 | var artboard = animations[animationName].timelineLegendArtboard; 309 | var group = findLayerGroupsWithName(transitionName, artboard) 310 | var imageLayers = findImageWithName('animationCurve', group[0]); 311 | if(imageLayers[0]){ 312 | var selectorX = imageLayers[0].frame().x(); 313 | var selectorIndex = Math.abs(Math.round(selectorX/LEGENDLAYOUT.easeTileWidth)); 314 | return { easingIndex: selectorIndex, ease:ANIMATIONCURVEOPTIONS[selectorIndex].ease } 315 | } 316 | } 317 | 318 | var extractAnimationValues = function(prev, current){ 319 | var currentFrame = current.rect(); 320 | var returnObj = {}; 321 | 322 | if(prev){ 323 | var prevFrame = prev.rect(); 324 | returnObj.delay = Math.round((currentFrame.origin.x - (prevFrame.origin.x + prevFrame.size.width)) * MSPERPIXEL); 325 | } 326 | else { 327 | returnObj.delay = Math.round(currentFrame.origin.x * MSPERPIXEL); 328 | } 329 | returnObj.duration = Math.round(currentFrame.size.width * MSPERPIXEL); 330 | returnObj.startTime = Math.round(currentFrame.origin.x * MSPERPIXEL); 331 | return returnObj; 332 | } 333 | 334 | var getAnimationValuesFromTimelineArtboard = function(animationName, timelineArtboardName){ 335 | // find the relevant artboards 336 | var timelineArtboard = getArtboardsWithNameInDocument(timelineArtboardName); 337 | var timelineLegendArtboard = getArtboardsWithNameInDocument(getLegendName(animationName)); 338 | // save in animation config object 339 | animations[animationName].timelineArtboard = timelineArtboard[0]; 340 | animations[animationName].timelineLegendArtboard = timelineLegendArtboard[0]; 341 | updateTimeline(animationName); 342 | updateTimelineLegend(animationName); 343 | } 344 | 345 | var highlightTimelineFrame = function(transitionName, animationName){ 346 | if(!highlightedSegments[transitionName]){ 347 | var artboard = animations[animationName].timelineArtboard; 348 | var layers = findLayerGroupsWithName(transitionName, artboard); 349 | var shapes = findShapeWithName('timelineSegment', layers[0]); 350 | if(shapes[0]){ 351 | updateShapeStyle({fill:TIMELINECOLORS.highlight}, shapes[0]); 352 | highlightedSegments[transitionName] = true; 353 | } 354 | } 355 | } 356 | 357 | var unHighlightTimelineFrame = function(transitionName, animationName){ 358 | if(highlightedSegments[transitionName]){ 359 | var artboard = animations[animationName].timelineArtboard; 360 | var layers = findLayerGroupsWithName(transitionName, artboard); 361 | var shapes = findShapeWithName('timelineSegment', layers[0]); 362 | if(shapes[0]){ 363 | updateShapeStyle({fill:TIMELINECOLORS.block}, shapes[0]); 364 | highlightedSegments[transitionName] = false; 365 | } 366 | } 367 | } 368 | 369 | var highlightLegendName = function(transitionName, animationName){ 370 | if(!highlightedLegends[transitionName]){ 371 | var artboard = animations[animationName].timelineLegendArtboard; 372 | var layers = findLayerGroupsWithName(transitionName, artboard); 373 | var text = findTextWithName('animationInfo', layers[0]); 374 | if(text[0]){ 375 | updateTextStyle({color:LEGENDCOLORS.highlight}, text[0]); 376 | highlightedLegends[transitionName] = true; 377 | } 378 | } 379 | } 380 | 381 | var unHighlightLegendName = function(transitionName, animationName){ 382 | if(highlightedLegends[transitionName]){ 383 | var artboard = animations[animationName].timelineLegendArtboard; 384 | var layers = findLayerGroupsWithName(transitionName, artboard); 385 | var text = findTextWithName('animationInfo', layers[0]); 386 | if(text[0]){ 387 | updateTextStyle({color:LEGENDCOLORS.info}, text[0]); 388 | highlightedLegends[transitionName] = null; 389 | } 390 | } 391 | } 392 | 393 | var refreshAnimationTimeline = function(animationName){ 394 | var timelineArtboardName = animationTimelineName(animationName); 395 | var timelineLegendArtboardName = getLegendName(animationName); 396 | //check for existing timeline artboard 397 | if(!artboardWithNameExistsInDocument(timelineArtboardName)){ 398 | initTimelineArtboard(animationName, timelineArtboardName); 399 | } 400 | if(!artboardWithNameExistsInDocument(timelineLegendArtboardName)){ 401 | initTimelineLegendArtboard(animationName, timelineLegendArtboardName); 402 | } 403 | getAnimationValuesFromTimelineArtboard(animationName, timelineArtboardName); 404 | } 405 | 406 | var refreshAnimationTimelines = function(){ 407 | for(var animationName in animations){ 408 | if(animations.hasOwnProperty(animationName)){ 409 | refreshAnimationTimeline(animationName); 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Resources/EasingCurves.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwallen/Sketch-Motion/62642e7e128384e673dd515588a5a7479d0c20dd/Sketch Motion/Motion.sketchplugin/Resources/EasingCurves.sketch -------------------------------------------------------------------------------- /Sketch Motion/Motion.sketchplugin/Resources/easingCurves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwallen/Sketch-Motion/62642e7e128384e673dd515588a5a7479d0c20dd/Sketch Motion/Motion.sketchplugin/Resources/easingCurves.png --------------------------------------------------------------------------------