├── .gitignore ├── LICENSE.md ├── README.md ├── chief.js ├── example ├── chief │ ├── button.js │ └── index.js └── f1 │ ├── index.js │ ├── parsers.js │ ├── states.js │ └── transitions.js ├── images ├── chief-preview.gif └── f1-preview.gif ├── index.js ├── lib ├── parsers │ └── getParser.js ├── states │ └── parseStates.js ├── targets │ └── parseTargets.js └── transitions │ ├── createTransitions.js │ ├── defaultTransition.js │ └── parseTransitions.js ├── package.json └── test ├── advancedAnimation ├── expectedStates.js ├── expectedUpdates.js └── index.js ├── callbackAndUpdate.js ├── chief ├── getUI.js └── index.js ├── defineConstrutor.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Mikko Haapoja 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # f1 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | F1 is a stateful ui library. F1 is the "core" for modules such as: 6 | - [`f1-dom`](https://www.npmjs.com/f1-dom) 7 | - [`react-f1`](https://www.npmjs.com/react-f1) 8 | 9 | [![Chief Example Preview](images/chief-preview.gif)](https://github.com/Jam3/f1/tree/master/example/chief) 10 | 11 | ## Table Of Contents 12 | 13 | - [Two parts of `f1`](#two-parts-of-f1) 14 | + [`f1`](#f1-1) 15 | + [`chief`](#chief) 16 | - [Examples](#example) 17 | + [`f1`](#example-f1) 18 | + [`chief`](#example-chief) 19 | - [API Documentation](#api-documentation) 20 | + [`var ui = f1()`](#var-ui--requiref1opts) 21 | * [`ui.targets(targets)`](#uitargetstargets) 22 | * [`ui.states(states)`](#uistatesstates) 23 | * [`ui.transitions(transitions)`](#uitransitionstransitions) 24 | * [`ui.parsers(parserDefinition)`](#uiparsersparserdefinition) 25 | * [`ui.init(initState)`](#uiinitinitstate) 26 | * [`ui.go(state, [cb])`](#uigostate-cb-1) 27 | * [`ui.update()`](#uiupdate) 28 | + [`var page = chief()`](#var-page--requiref1chiefopts) 29 | * [`page.targets(targets)`](#pagetargetstargets) 30 | * [`page.states(states)`](#pagestatesstates) 31 | * [`page.transitions(transitions)`](#pagetransitionstransitions) 32 | * [`page.init(initState)`](#pageinitinitstate) 33 | * [`page.go(state, [cb])`](#pagegostate-cb) 34 | 35 | 36 | ## Two parts of `f1` 37 | 38 | There are two parts to this module `f1` and `chief`. 39 | 40 | #### `f1` 41 | 42 | Designed to build complete UI animations. 43 | 44 | First define states/the look of the piece of ui. 45 | 46 | After designing the look you define transitions/animations for the piece of ui. 47 | 48 | Since `f1` is designed to target many platforms you will also need to define parers which transfers the designed look to the target being animated. 49 | 50 | #### `chief` 51 | 52 | Designed to control `f1` ui instances. So let's assume you have a "page" of a site you can have `chief` control ui pieces's states by calling their `go` functions. 53 | 54 | 55 | ## Usage 56 | 57 | [![NPM](https://nodei.co/npm/f1.png)](https://www.npmjs.com/package/f1) 58 | 59 | ### Example 60 | 61 | There are two small examples for `f1` and `Chief`. They exist in the `example/`folder in the `f1` repo. You can run the examples locally via npm scripts. 62 | 63 | Each example is commented to explain as much as possible and can be easily modified and played around with when running via npm scripts mentioned below. 64 | 65 | ### Example `f1` 66 | [![F1 Example Preview](images/f1-preview.gif)](https://github.com/Jam3/f1/tree/master/example/f1) 67 | 68 | ```bash 69 | $ npm run example-f1 70 | ``` 71 | 72 | [Source can be viewed here](https://github.com/Jam3/f1/tree/master/example/f1) 73 | 74 | ### Example `chief` 75 | [![Chief Example Preview](images/chief-preview.gif)](https://github.com/Jam3/f1/tree/master/example/chief) 76 | 77 | ```bash 78 | $ npm run example-chief 79 | ``` 80 | 81 | [Source can be viewed here](https://github.com/Jam3/f1/tree/master/example/f1) 82 | 83 | ## API Documentation 84 | 85 | ### `var ui = require('f1')([opts])` 86 | 87 | To construct an `f1` instance you can pass in an optional settings object. The following are properties you can pass in settings: 88 | ```javascript 89 | { 90 | // an object which contains all elements/items that you will be animating 91 | // eg. bgElement could be a
if working with the DOM 92 | targets: { 93 | bg: bgElement 94 | }, 95 | 96 | // all states for the ui 97 | // states contain ui piece which "hook" up to targets 98 | // eg out.bg defines what targets.bg will look like in the out state 99 | states: { 100 | out: { 101 | bg: { alpha: 0 } 102 | }, 103 | 104 | idle: { 105 | bg: { alpha: 1 } 106 | } 107 | }, 108 | 109 | // an array which defines the transitions for the ui 110 | transitions: [ 111 | // this ui can go from out to idle and idle to out 112 | { from: 'out', to: 'idle', bi: true} 113 | ], 114 | 115 | // An Object contains init and update functions. These will be used 116 | // to initialize your ui elements and apply state to targets during update. 117 | // Parsers are defined to make `f1` work on any platform eg. React, DOM, 118 | // Canvas, SVG, Three.js, etc. 119 | // 120 | // Above in states we define alpha this could be passed 121 | // to the DOM's style.opacity for instance 122 | parsers: { 123 | init: [ initPosition ], 124 | update: [ applyPosition ] 125 | }, 126 | 127 | // callback called whenever f1 reaches a state 128 | onState: listenerState, 129 | 130 | // callback called whenever f1 is updating 131 | onUpdate: listenerUpdater 132 | } 133 | ``` 134 | 135 | ### ui.targets(targets) 136 | 137 | define which items are going to be animated. Pass in an object 138 | which will look something like this: 139 | ```javascript 140 | var ui = require('f1')(); 141 | 142 | ui.targets( { 143 | itemToAnimate1: find('#itemToAnimate1'), 144 | itemToAnimate2: find('#itemToAnimate2') 145 | }); 146 | ``` 147 | The `Object` being passed in should have variable names which will 148 | associate to data which will be defined when setting up states in the 149 | `ui.states` method. In this case `itemToAnimate1` and `itemToAnimate2` should be also defined in `ui.states`. 150 | 151 | The values of the targets object can be for instance DOM elements if working with the DOM or Three.js `THREE.Mesh` if working with Three.js. 152 | 153 | 154 | 155 | 156 | ### `ui.states(states)` 157 | 158 | defines the states which this `f1` instance will use. 159 | 160 | States are defined as objects. It could look something like this: 161 | ```javascript 162 | var ui = require('f1')(); 163 | 164 | ui.states( { 165 | 166 | out: { 167 | itemToAnimate1: { 168 | variableToAnimate: 0 169 | }, 170 | 171 | itemToAnimate2: { 172 | variableToAnimate: 0 173 | } 174 | }, 175 | 176 | idle: { 177 | itemToAnimate1: { 178 | variableToAnimate: 1 179 | }, 180 | 181 | itemToAnimate2: { 182 | variableToAnimate: 2 183 | } 184 | } 185 | }); 186 | ``` 187 | Above two states would be created: `out` and `idle`. Both would animate two 188 | objects: `itemToAnimate1` and `itemToAnimate2`. And in both of those objects 189 | the property `variableToAnimate` is defined. So if we were to transition from 190 | `out` to `idle` in `itemToAnimate1` `variableToAnimate` would transition from 191 | 0 to 1 and in `itemToAnimate2` from 0 to 2. 192 | 193 | States can also be defined by passing in objects for instance the above could 194 | be changed to look like this: 195 | ```javascript 196 | var ui = require('f1')(); 197 | 198 | ui.states( { 199 | out: function(stateName) { 200 | console.log(stateName); // "out" 201 | 202 | return { 203 | itemToAnimate1: { 204 | variableToAnimate: 0 205 | }, 206 | 207 | itemToAnimate2: { 208 | variableToAnimate: 0 209 | } 210 | }; 211 | }, 212 | 213 | idle: function(stateName) { 214 | console.log(stateName); // "idle" 215 | 216 | return { 217 | itemToAnimate1: { 218 | variableToAnimate: 1 219 | }, 220 | 221 | itemToAnimate2: { 222 | variableToAnimate: 2 223 | } 224 | }; 225 | } 226 | }); 227 | ``` 228 | The above can be handy when there are many items which states must be defined for instance a menu with many buttons. 229 | 230 | 231 | 232 | ### `ui.transitions(transitions)` 233 | 234 | defines how this `f1` instance can animate between states. 235 | 236 | For instance if we had two states out and idle you could define your transitions like this: 237 | 238 | ```javascript 239 | var ui = require('f1')(); 240 | 241 | ui.transitions( [ 242 | { from: 'idle', to: 'rollOver', animation: { duration: 0.25 } }, 243 | { from: 'rollOver', to: 'idle', animation: { duration: 0.1 } } 244 | ]); 245 | ``` 246 | 247 | Note that transitions are not bi-directional. If you'd like to create a bi-directional transition use `bi: true`: 248 | ```javascript 249 | var ui = require('f1')(); 250 | 251 | ui.transitions( [ 252 | { from: 'idle', to: 'rollOver', bi: true, animation: { duration: 0.25 } } 253 | ]); 254 | ``` 255 | 256 | If you simply just defined `from` and `to` and omitted the `animation` Object a default animation would be applied between states. This default transition will have a duration of 0.5 seconds and use a Linear ease. 257 | 258 | If you want to modify the animation duration and ease you can define your transitions like this: 259 | 260 | ```javascript 261 | var eases = require('eases'); 262 | var ui = require('f1')(); 263 | 264 | ui.transitions( [ 265 | { 266 | from: 'idle', 267 | to: 'rollOver', 268 | animation: { 269 | duration: 0.25, 270 | ease: eases.quadOut 271 | } 272 | }, 273 | { 274 | from: 'rollOver', 275 | to: 'idle', 276 | animation: { 277 | duration: 0.1 278 | ease: eases.expoOut 279 | } 280 | } 281 | ]); 282 | ``` 283 | 284 | Defining your transitions using the above syntax will cause all properties to animate using the duration and ease defined. 285 | 286 | Ease functions should take a t or time value between 0-1 and return a modified time value between 0-1. Typically you might use the [`eases`](https://www.npmjs.com/eases) module. 287 | 288 | You can also animate ui properties individually: 289 | ```javascript 290 | var eases = require('eases'); 291 | var ui = require('f1')(); 292 | 293 | ui.transitions( [ 294 | { 295 | from: 'out', 296 | to: 'idle', 297 | animation: { 298 | duration: 1, ease: eases.expoOut, 299 | 300 | position: { duration: 0.5, delay: 0.5, ease: eases.quadOut }, 301 | alpha: { duration: 0.5 } 302 | } 303 | }, 304 | { 305 | from: 'idle', 306 | to: 'out', 307 | animation: { duration: 0.5, ease: eases.expoIn } 308 | } 309 | ]); 310 | ``` 311 | 312 | In the above example every property besides `position` and `alpha` will have a duration of one second using `eases.quadOut` ease. `position` will have a duration of 0.5 seconds and will be delayed 0.5 seconds and will use the `eases.quadOut` easing function. `alpha` will simply have a duration of 0.5 seconds. 313 | 314 | For advanced transitions you can pass in a function instead like so: 315 | ```javascript 316 | 317 | ui.transitions( [ 318 | { 319 | from: 'out', 320 | to: 'idle', 321 | animation: { 322 | duration: 1, ease: eases.expoOut, 323 | alpha: function(time, start, end) { 324 | return (end - start) * time + start; 325 | } 326 | } 327 | }, 328 | { 329 | from: 'idle', 330 | to: 'out', 331 | animation: { duration: 0.5, ease: eases.expoIn } 332 | } 333 | ]); 334 | ``` 335 | 336 | The animation is the same as in the previous example however `alpha` will be calculated using a custom transition function. 337 | 338 | 339 | 340 | 341 | ### `ui.parsers(parserDefinition)` 342 | 343 | `f1` can target many different platforms. How it does this is by using parsers which can target different platforms. Parsers apply calculated state objects to targets. 344 | 345 | If working with the DOM for instance your state could define values which will be applied by the parser to the DOM elements style object. (see the `f1` example) 346 | 347 | When calling parsers pass in an Object that can contain variables `init`, and `update`. Both should contain Array's of functions which will be used to either init or update ui. 348 | 349 | `init` functions will receive: states definition, targets definition, and transitions definition. `init` will only be called once when the `f1` instance will inited. 350 | 351 | Example: 352 | ```javascript 353 | function initPosition(states, targets, transitions) { 354 | // usesPosition would check if the position property is used 355 | // by states if so initialize targets to be able to do something 356 | // with position 357 | if(usesPosition(states)) { 358 | // do whatever is needed to targets 359 | // if the position property is used 360 | } 361 | } 362 | ``` 363 | 364 | `update` functions will receive: target and state. Where target could be for instance a dom element and state is the currently calculated state. 365 | 366 | Example: 367 | ```javascript 368 | function updatePosition(target, state) { 369 | target.style.left = state.position[ 0 ] + 'px'; 370 | target.style.top = state.position[ 1 ] + 'px'; 371 | } 372 | ``` 373 | 374 | It should be noted that parsers can be called multiple times with different definitions and `init` and `update` functions will be merged. 375 | 376 | 377 | 378 | 379 | ### `ui.init(initState)` 380 | 381 | Initializes `f1`. `init` will throw errors if required parameters such as 382 | states, transitions, targets, and parsers are missing. The `initState` for the `f1` instance should be passed in as string. 383 | 384 | 385 | 386 | 387 | ### `ui.go(state, [cb])` 388 | 389 | Will tell `f1` to animate to another state. Calling `go` will cause `f1` to calculate a path defined through transitions to the state which was passed to it. 390 | 391 | An optional callback can be passed which is called once `f1` has reached that state. 392 | 393 | 394 | 395 | 396 | ### `ui.go(state, [cb])` 397 | 398 | Will tell `f1` to immediately jump to another state. If an animation is in progress that animation is stopped immediately. 399 | 400 | 401 | 402 | 403 | ### `ui.update()` 404 | 405 | Will force `f1` to update. This is useful if updating a state values dynamically by mouse movement or using some other method. 406 | 407 | Call `ui.update` to ensure the state gets applied through `parsers` to `targets`. 408 | 409 | 410 | 411 | ### `var page = require('f1/chief')([opts])` 412 | 413 | `chief` is designed to control `f1` instances so it's ideal for creating "pages" or ui components which merge many other ui components. 414 | 415 | `require('f1/chief')` is a function that you can optionally pass options/settings to. It should be noted that all options have an associated function. You can pass in the following: 416 | 417 | ```javascript 418 | { 419 | // ui and chief instances this chief instance will control 420 | // it should be noted that one chief instance can 421 | // control another chief instance 422 | targets: { 423 | ui1: f1Instance, 424 | ui2: f1Instance, 425 | ui3: chiefInstance, 426 | }, 427 | 428 | // define the states in which all above ui instances per state 429 | states: { 430 | out: { 431 | ui1: 'out', 432 | ui2: 'out', 433 | ui3: 'out' 434 | }, 435 | 436 | idle: { 437 | ui1: 'idle', 438 | ui2: 'idle', 439 | ui3: 'idle' 440 | } 441 | }, 442 | 443 | // defines transitions between chief's states 444 | // it should be noted you can apply delay's when defining 445 | // animations. 446 | transitions: [ 447 | { 448 | from: 'out', to: 'idle', bi: true, animation: { 449 | ui3: { delay: 0.5 } 450 | } 451 | } 452 | ] 453 | } 454 | ``` 455 | 456 | 457 | 458 | 459 | ### `page.targets(targets)` 460 | 461 | `targets` is an Object that will define what ui/`f1` instances `chief` will control. It should be noted that chiefs can control other chiefs. 462 | 463 | The `targets` arguments might look like this: 464 | ```javascript 465 | targets: { 466 | ui1: f1Instance, 467 | ui2: f1Instance, 468 | ui3: chiefInstance, 469 | } 470 | ``` 471 | 472 | ### `page.states(states)` 473 | 474 | `states` is an Object that will define what state the associated targets should be in as the `chief` instance changes states. 475 | 476 | An example `states` arguments: 477 | ```javascript 478 | states: { 479 | out: { 480 | ui1: 'out', 481 | ui2: 'out', 482 | ui3: 'out' 483 | }, 484 | 485 | idle: { 486 | ui1: 'idle', 487 | ui2: 'idle', 488 | ui3: 'idle' 489 | } 490 | } 491 | ``` 492 | 493 | 494 | 495 | ### `page.transitions(transitions)` 496 | 497 | `transitions` is an Array that will define how chief will be abe to traverse/navigate through states. 498 | 499 | Example `transitions` argument: 500 | ``` 501 | [ 502 | { 503 | from: 'out', to: 'idle', bi: true, animation: { 504 | ui3: { delay: 0.5 } 505 | } 506 | } 507 | ] 508 | ``` 509 | 510 | 511 | 512 | ### `page.init(initState)` 513 | 514 | `init` will simply initialize chief to be in a state. 515 | 516 | 517 | 518 | ### `page.go(state, [cb])` 519 | 520 | `go` will tell chief to animate to a state. 521 | 522 | 523 | ## License 524 | 525 | MIT, see [LICENSE.md](http://github.com/mikkoh/f1/blob/master/LICENSE.md) for details. 526 | -------------------------------------------------------------------------------- /chief.js: -------------------------------------------------------------------------------- 1 | var kimi = require('kimi'); 2 | var noOp = require('no-op'); 3 | var parseStates = require('./lib/states/parseStates'); 4 | var parseTransitions = require('./lib/transitions/parseTransitions'); 5 | var extend = require('deep-extend'); 6 | 7 | module.exports = function(options) { 8 | 9 | var opts = options || {}; 10 | var driver = kimi({ 11 | manualStep: opts.autoUpdate === undefined ? false : !opts.autoUpdate, 12 | onUpdate: onUpdate.bind(undefined, onTargetInState) 13 | }); 14 | var onInState = noOp; 15 | var stateChief; 16 | var currentTargetState; 17 | var targetsInState; 18 | 19 | var chief = { 20 | targets: function(targets) { 21 | opts.targets = targets; 22 | }, 23 | 24 | states: function(states) { 25 | opts.states = states; 26 | }, 27 | 28 | transitions: function(transitions) { 29 | opts.transitions = transitions; 30 | }, 31 | 32 | init: function(state) { 33 | var transitions; 34 | 35 | if(opts.targets === undefined) { 36 | throw new Error('You must pass in targets to chief'); 37 | } 38 | 39 | if(opts.states === undefined) { 40 | throw new Error('You must pass in states to chief'); 41 | } 42 | 43 | if(opts.transitions === undefined) { 44 | throw new Error('You must pass in transitions to chief'); 45 | } 46 | 47 | // for chief we want to make the default duration to be 0 48 | transitions = opts.transitions.map(function(transition) { 49 | transition = extend( 50 | {}, 51 | transition 52 | ); 53 | 54 | transition.animation = transition.animation || {}; 55 | transition.animation.duration = transition.animation.duration === undefined ? 0 : transition.animation.duration; 56 | 57 | return transition; 58 | }); 59 | 60 | parseStates(driver, opts.states); 61 | parseTransitions(driver, opts.states, transitions); 62 | 63 | stateChief = state; 64 | currentTargetState = {}; 65 | 66 | driver.init(state); 67 | 68 | return this; 69 | }, 70 | 71 | destroy: function() { 72 | driver.destroy(); 73 | }, 74 | 75 | go: function(state, onComplete) { 76 | stateChief = state; 77 | currentTargetState = {}; 78 | 79 | targetsInState = Object.keys(opts.states[ state ]) 80 | .reduce(function(targetsInState, key) { 81 | targetsInState[ key ] = false; 82 | 83 | return targetsInState; 84 | }, {}); 85 | 86 | onInState = onComplete || noOp; 87 | 88 | driver.go(state); 89 | 90 | return this; 91 | }, 92 | 93 | step: function(deltaTime) { 94 | 95 | driver.step(deltaTime); 96 | 97 | return this; 98 | } 99 | }; 100 | 101 | return chief; 102 | 103 | function onUpdate(onTargetInState, state, stateName, time) { 104 | for(var target in state) { 105 | var ui = opts.targets[ target ]; 106 | var toState = state[ target ]; 107 | 108 | if(ui) { 109 | if(!ui.isInitialized) { 110 | currentTargetState[ target ] = toState; 111 | ui.init(toState); 112 | } else if(currentTargetState[ target ] !== toState) { 113 | currentTargetState[ target ] = toState; 114 | ui.go(toState, onTargetInState.bind(undefined, target, toState)); 115 | } 116 | } 117 | } 118 | 119 | if(opts.onUpdate) { 120 | opts.onUpdate(state, stateName); 121 | } 122 | } 123 | 124 | function onTargetInState(target, targetState) { 125 | // set the current target as being in the state it should be in 126 | targetsInState[ target ] = opts.states[ stateChief ][ target ] === targetState; 127 | 128 | // now check if all states are in the state they should be in 129 | var allInState = Object.keys(targetsInState) 130 | .reduce(function(allInState, key) { 131 | return allInState && targetsInState[ key ]; 132 | }, true); 133 | 134 | if(allInState) { 135 | onInState(opts.states[ stateChief ], stateChief); 136 | } 137 | } 138 | }; -------------------------------------------------------------------------------- /example/chief/button.js: -------------------------------------------------------------------------------- 1 | var f1 = require('../..'); 2 | 3 | module.exports = function(opts) { 4 | 5 | opts = opts || {}; 6 | opts.width = opts.width || 200; 7 | opts.height = opts.height || 40; 8 | opts.color1 = opts.color1 || '#00CAFE'; 9 | opts.color2 = opts.color2 || '#CAFE00'; 10 | opts.text = opts.text || "I'm a button"; 11 | opts.zIndex = opts.zIndex || 0; 12 | 13 | var elButton = document.createElement('div'); 14 | var elColor1 = document.createElement('div'); 15 | var elColor2 = document.createElement('div'); 16 | 17 | elButton.style.zIndex = opts.zIndex; 18 | 19 | elColor1.style.boxSizing = 20 | elColor2.style.boxSizing = 'border-box'; 21 | 22 | elColor1.style.fontFamily = 23 | elColor1.style.fontFamily = 'Georgia, serif'; 24 | 25 | elColor1.innerText = elColor2.innerText = opts.text; 26 | 27 | elButton.style.position = 'relative'; 28 | elColor1.style.position = elColor2.style.position = 'absolute'; 29 | 30 | elButton.style.width = 31 | elColor1.style.width = 32 | elColor2.style.width = opts.width + 'px'; 33 | 34 | elButton.style.height = 35 | elColor1.style.height = 36 | elColor2.style.height = opts.height + 'px'; 37 | 38 | elColor1.style.backgroundColor = opts.color1; 39 | elColor2.style.backgroundColor = opts.color2; 40 | elColor1.style.transformOrigin = '0% 0%'; 41 | elColor2.style.transformOrigin = '0% 0%'; 42 | 43 | elColor1.style.paddingLeft = 44 | elColor2.style.paddingLeft = 45 | elColor1.style.paddingTop = 46 | elColor2.style.paddingTop = '12px'; 47 | 48 | elButton.appendChild(elColor1); 49 | elButton.appendChild(elColor2); 50 | 51 | var ui = f1({ 52 | targets: { 53 | color1: elColor1, 54 | color2: elColor2 55 | }, 56 | 57 | states: { 58 | out: { 59 | color1: { 60 | rotate: 0, 61 | alpha: 0, 62 | brightness: 1 63 | }, 64 | 65 | color2: { 66 | rotate: -90, 67 | alpha: 0, 68 | brightness: 0.8 69 | } 70 | }, 71 | 72 | idle: { 73 | color1: { 74 | rotate: 0, 75 | alpha: 1, 76 | brightness: 1 77 | }, 78 | 79 | color2: { 80 | rotate: -90, 81 | alpha: 1, 82 | brightness: 0.8 83 | } 84 | }, 85 | 86 | over: { 87 | color1: { 88 | rotate: 90, 89 | alpha: 1, 90 | brightness: 0.4 91 | }, 92 | 93 | color2: { 94 | rotate: 0, 95 | alpha: 1, 96 | brightness: 1 97 | } 98 | }, 99 | 100 | selected: { 101 | color1: { 102 | rotate: -20, 103 | alpha: 1, 104 | brightness: 0.9 105 | }, 106 | 107 | color2: { 108 | rotate: -90, 109 | alpha: 1, 110 | brightness: 0.8 111 | } 112 | } 113 | }, 114 | 115 | transitions: [ 116 | { from: 'out', to: 'idle', bi: true }, 117 | { from: 'idle', to: 'over', bi: true }, 118 | { from: 'over', to: 'selected', bi: true } 119 | ], 120 | 121 | parsers: { 122 | init: [], 123 | update: [ 124 | function(target, state) { 125 | target.style.opacity = state.alpha; 126 | target.style.transform = 'perspective(600px) rotateY(' + state.rotate + 'deg)'; 127 | target.style.filter = 'brightness(' + state.brightness + ')'; 128 | target.style.webkitFilter = 'brightness(' + state.brightness + ')'; 129 | } 130 | ] 131 | } 132 | }); 133 | 134 | return { 135 | el: elButton, 136 | ui: ui 137 | }; 138 | }; -------------------------------------------------------------------------------- /example/chief/index.js: -------------------------------------------------------------------------------- 1 | var chief = require('../../chief'); 2 | var button = require('./button'); 3 | 4 | // we'll use this variable to keep track of the button we've selected previously 5 | var selectedButton = null; 6 | 7 | // the button function returns an object which will have two variables 8 | // ui - an f1 instance 9 | // el - a dom element we should append to the dom 10 | var button1 = button({ text: 'Toronto', zIndex: 4 }); 11 | var button2 = button({ text: 'Helsinki', zIndex: 3 }); 12 | var button3 = button({ text: 'Vancouver', zIndex: 2 }); 13 | var button4 = button({ text: 'Seinäjoki', zIndex: 1 }); 14 | 15 | // the following events will add mouse events to all buttons 16 | addMouseEvents(button1, 1); 17 | addMouseEvents(button2, 2); 18 | addMouseEvents(button3, 3); 19 | addMouseEvents(button4, 4); 20 | 21 | // add the elements to the body 22 | document.body.appendChild(button1.el); 23 | document.body.appendChild(button2.el); 24 | document.body.appendChild(button3.el); 25 | document.body.appendChild(button4.el); 26 | 27 | // this will create our chief instance 28 | var menu = chief({ 29 | // here we define what f1 instances 30 | // this chief instance will target 31 | targets: { 32 | button1: button1.ui, 33 | button2: button2.ui, 34 | button3: button3.ui, 35 | button4: button4.ui 36 | }, 37 | 38 | // here we'll define the states of the chief instance 39 | states: { 40 | 41 | // an out state 42 | // each value willd define what state 43 | // each ui instance should be in when we're in the 44 | // out state eg button1 should be in "out" when this 45 | // chief is in it's "out" state 46 | out: { 47 | button1: 'out', 48 | button2: 'out', 49 | button3: 'out', 50 | button4: 'out' 51 | }, 52 | 53 | // an idle state 54 | idle: { 55 | button1: 'idle', 56 | button2: 'idle', 57 | button3: 'idle', 58 | button4: 'idle' 59 | }, 60 | 61 | // a state when button1 is selected 62 | selected1: { 63 | button1: 'selected', 64 | button2: 'idle', 65 | button3: 'idle', 66 | button4: 'idle' 67 | }, 68 | 69 | // a state when button2 is selected 70 | selected2: { 71 | button1: 'idle', 72 | button2: 'selected', 73 | button3: 'idle', 74 | button4: 'idle' 75 | }, 76 | 77 | // a state when button3 is selected 78 | selected3: { 79 | button1: 'idle', 80 | button2: 'idle', 81 | button3: 'selected', 82 | button4: 'idle' 83 | }, 84 | 85 | // a state when button4 is selected 86 | selected4: { 87 | button1: 'idle', 88 | button2: 'idle', 89 | button3: 'idle', 90 | button4: 'selected' 91 | } 92 | }, 93 | 94 | // this will define the transitions of this chief instance 95 | // you cannot use durations or eases 96 | transitions: [ 97 | // for out to idle we'll want to stagger animate in 98 | // the buttons 99 | { 100 | from: 'out', to: 'idle', bi: true, animation: { 101 | button2: { delay: 0.3 }, 102 | button3: { delay: 0.6 }, 103 | button4: { delay: 0.9 } 104 | } 105 | }, 106 | // to go from the selected states we'll want to go 107 | // back to the idle state and then go to the selected 108 | // state 109 | { from: 'idle', to: 'selected1', bi: true }, 110 | { from: 'idle', to: 'selected2', bi: true }, 111 | { from: 'idle', to: 'selected3', bi: true }, 112 | { from: 'idle', to: 'selected4', bi: true } 113 | ] 114 | }); 115 | 116 | // initialize the chief instance 117 | menu.init('out'); 118 | 119 | // animate the chief instance to the idle state 120 | menu.go('idle', function() { 121 | console.log('is in idle'); 122 | }); 123 | 124 | function addMouseEvents(button, buttonNum) { 125 | 126 | // when we mouse over and out we'll animate individual 127 | // buttons if they are not selected 128 | button.el.addEventListener('mouseover', function() { 129 | if(selectedButton !== buttonNum) { 130 | button.ui.go('over'); 131 | } 132 | }); 133 | 134 | button.el.addEventListener('mouseout', function() { 135 | if(selectedButton !== buttonNum) { 136 | button.ui.go('idle'); 137 | } 138 | }); 139 | 140 | // when a button is selected we want to tell the menu 141 | // to animate to the proper selected state 142 | button.el.addEventListener('click', function() { 143 | selectedButton = buttonNum; 144 | page.go('selected' + buttonNum, function() { 145 | console.log('is in', 'selected' + buttonNum); 146 | }); 147 | }); 148 | } -------------------------------------------------------------------------------- /example/f1/index.js: -------------------------------------------------------------------------------- 1 | var f1 = require('./../..'); 2 | 3 | // lets make a div we'll animate 4 | // really this could be whatever an object 5 | // whatever you want 6 | var itemToAnimate = document.body.appendChild( 7 | document.createElement('div') 8 | ); 9 | 10 | // then we'll create the ui instance which will animate 11 | // the div we crated 12 | var ui = f1({ 13 | // this will define what the targets are that will be animating 14 | // it will be associated in states 15 | targets: { 16 | item: itemToAnimate 17 | }, 18 | // states defines what the div should look like in each state 19 | states: require('./states'), 20 | transitions: require('./transitions'), 21 | parsers: require('./parsers') 22 | }); 23 | 24 | // this will initialize the ui instance in a state 25 | // it will cause the parsers update functions to be run for the first time 26 | ui.init('out'); 27 | 28 | // this will tell the ui to animate to the idle state 29 | ui.go('idle'); 30 | 31 | // on mouse over we'll want to animate to the over state 32 | itemToAnimate.addEventListener('mouseover', function() { 33 | ui.go('over'); 34 | }); 35 | 36 | // on mouse out we'll want to animate back to the idle state 37 | itemToAnimate.addEventListener('mouseout', function() { 38 | ui.go('idle'); 39 | }); 40 | 41 | // on mouse click we'll want to animate out the button 42 | itemToAnimate.addEventListener('click', function() { 43 | ui.go('out'); 44 | }); -------------------------------------------------------------------------------- /example/f1/parsers.js: -------------------------------------------------------------------------------- 1 | // parsers is an object which has two arrays init and update 2 | // the main purpose of parsers is have the ability to apply 3 | // animated properties to targets 4 | module.exports = { 5 | 6 | // init is an array of functions that will be run once the 7 | // f1 instance initializes 8 | // typically you'd drop code in here that is required to initialize 9 | // the targets to animate. 10 | // 11 | // In this case we'll just give the item we want to animate a width, height 12 | // and add a background color so it can be seen 13 | init: [ 14 | function(states, targets, transitions) { 15 | 16 | for(var i in targets) { 17 | var itemToAnimate = targets[ i ]; 18 | 19 | itemToAnimate.style.position = 'absolute'; 20 | itemToAnimate.style.backgroundColor = '#CAFE00'; 21 | itemToAnimate.style.width = itemToAnimate.style.height = '100px'; 22 | } 23 | } 24 | ], 25 | 26 | // update is an array of functions that will be run each time 27 | // the ui should be animated. It accepts the target and currently 28 | // calculated state. 29 | // 30 | // In our case we will parse out the x, y, alpha properties and apply to 31 | // css style left, top, and opacity 32 | update: [ 33 | function(target, state) { 34 | target.style.left = state.x + 'px'; 35 | target.style.top = state.y + 'px'; 36 | target.style.opacity = state.alpha; 37 | 38 | target.innerText = state.text; 39 | } 40 | ] 41 | }; -------------------------------------------------------------------------------- /example/f1/states.js: -------------------------------------------------------------------------------- 1 | // states is an object which defines what targets will look like in each 2 | // state of the ui 3 | module.exports = { 4 | // out is a state name 5 | // later we can say ui.go('out') to 6 | // animate to the out state 7 | out: { 8 | // the following will define what the item 9 | // should look like in the out state 10 | item: { 11 | // x, y, and alpha are obviously not valid style properties 12 | // we define in parsers how this will be handled 13 | x: 200, 14 | y: -100, 15 | alpha: 0, 16 | text: '' 17 | } 18 | }, 19 | 20 | preIdle: { 21 | item: { 22 | x: 200, 23 | y: 100, 24 | alpha: 1, 25 | text: '' 26 | } 27 | }, 28 | 29 | // idle is a state name 30 | idle: { 31 | // the following will define what the item 32 | // should look like in the idle state 33 | item: { 34 | x: 100, 35 | y: 100, 36 | alpha: 1, 37 | text: 'ROLL ME' 38 | } 39 | }, 40 | 41 | // over is a state name 42 | over: { 43 | // the following will define what the item 44 | // should look like in the over state 45 | item: { 46 | x: 100, 47 | y: 105, 48 | alpha: 0.5, 49 | text: 'PRESS ME' 50 | } 51 | } 52 | }; -------------------------------------------------------------------------------- /example/f1/transitions.js: -------------------------------------------------------------------------------- 1 | // transitions is an array which defines how the the ui component 2 | // should animate through states 3 | module.exports = [ 4 | // this defines that we can animate from the out state to the idle state 5 | // and from the idle state to the out state because we say it's a "bi-directional" 6 | // transition by saying `bi: true` 7 | // 8 | // You generally would not have bi: true and would be required to define something like 9 | // { from: 'out', to: 'idle' }, 10 | // { from: 'idle', to: 'out' } 11 | { from: 'out', to: 'preIdle', bi: true }, 12 | 13 | { from: 'preIdle', to: 'idle', bi: true }, 14 | 15 | // this defines how we can animate to the over state from idle and back again 16 | // for this specific example we're also defining the duration of the animation 17 | { from: 'idle', to: 'over', bi: true, animation: { 18 | duration: 0.25 19 | } 20 | } 21 | ]; -------------------------------------------------------------------------------- /images/chief-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/f1/31bf0f9f4491f08d8b453aff744ba22ceab94607/images/chief-preview.gif -------------------------------------------------------------------------------- /images/f1-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/f1/31bf0f9f4491f08d8b453aff744ba22ceab94607/images/f1-preview.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var kimi = require('kimi'); 2 | var getTween = require('tween-function'); 3 | var noop = require('no-op'); 4 | var extend = require('deep-extend'); 5 | var Emitter = require('events').EventEmitter; 6 | 7 | var getParser = require('./lib/parsers/getParser'); 8 | var parseStates = require('./lib/states/parseStates'); 9 | var parseTransitions = require('./lib/transitions/parseTransitions'); 10 | var parseTargets = require('./lib/targets/parseTargets'); 11 | 12 | var numInstances = 0; 13 | 14 | module.exports = f1; 15 | 16 | /** 17 | * To construct a new `f1` instance you can do it in two ways. 18 | * 19 | * ```javascript 20 | * ui = f1([ settigns ]); 21 | * ``` 22 | * or 23 | * ```javascript 24 | * ui = new f1([ settings ]); 25 | * ``` 26 | * 27 | * To construct an `f1` instance you can pass in an optional settings object. The following are properties you can pass in settings: 28 | * ```javascript 29 | * { 30 | * onState: listenerState, // this callback will be called whenever f1 reaches a state 31 | * onUpdate: listenerUpdater, // this callback will be called whenever f1 is updating 32 | * 33 | * // you can pass a name for the ui. This is useful when you're using an external tool or want 34 | * // to differentiate between f1 instances 35 | * name: 'someNameForTheUI', 36 | * 37 | * // this is an object which contains all elements/items that you will be animating 38 | * targets: { 39 | * bg: bgElement 40 | * }, 41 | * 42 | * // all states for the ui 43 | * // states are the top level object and anything after that are the properties 44 | * // for that state 45 | * states: { 46 | * out: { 47 | * 48 | * bg: { alpha: 0 } 49 | * }, 50 | * 51 | * idle: { 52 | * 53 | * bg: { alpha: 1 } 54 | * } 55 | * }, 56 | * 57 | * // an array which defines the transitions for the ui 58 | * transitions: [ 59 | * 'out', 'idle', // this ui can go from out to idle 60 | * 'idle', 'out' // and idle to out 61 | * ], 62 | * 63 | * // an Object contains init and update functions. These will be used 64 | * // to initialize your ui elements and apply state to targets during update 65 | * parsers: { 66 | * init: [ initPosition ], 67 | * update: [ applyPosition ] 68 | * } 69 | * } 70 | * ``` 71 | * 72 | * @param {Object} [settings] An optional settings Object described above 73 | * @chainable 74 | */ 75 | function f1(settings) { 76 | 77 | if(!(this instanceof f1)) { 78 | 79 | return new f1(settings); 80 | } 81 | 82 | settings = settings || {}; 83 | 84 | var emitter = this; 85 | var onUpdate = settings.onUpdate || noop; 86 | var onState = settings.onState || noop; 87 | 88 | // this is used to generate a "name" for an f1 instance if one isn't given 89 | numInstances++; 90 | 91 | this.onState = function() { 92 | emitter.emit.apply(emitter, getEventArgs('state', arguments)); 93 | 94 | if(onState) { 95 | onState.apply(undefined, arguments); 96 | } 97 | }; 98 | 99 | this.onUpdate = function() { 100 | emitter.emit.apply(emitter, getEventArgs('update', arguments)); 101 | 102 | if(onUpdate) { 103 | onUpdate.apply(undefined, arguments); 104 | } 105 | }; 106 | 107 | this.name = settings.name || 'ui_' + numInstances; 108 | this.isInitialized = false; 109 | this.data = null; // current animation data 110 | this.defTargets = null; 111 | this.defStates = null; 112 | this.defTransitions = null; 113 | this.parser = null; 114 | 115 | if(settings.transitions) { 116 | this.transitions(settings.transitions); 117 | } 118 | 119 | if(settings.states) { 120 | this.states(settings.states); 121 | } 122 | 123 | if(settings.targets) { 124 | this.targets(settings.targets); 125 | } 126 | 127 | if(settings.parsers) { 128 | this.parsers(settings.parsers); 129 | } 130 | 131 | // kimi is the man who does all the work under the hood 132 | this.driver = kimi( { 133 | 134 | manualStep: settings.autoUpdate === undefined ? false : !settings.autoUpdate, 135 | onState: _onState.bind(this), 136 | onUpdate: _onUpdate.bind(this) 137 | }); 138 | } 139 | 140 | f1.prototype = extend(Emitter.prototype, { 141 | 142 | /** 143 | * define which items are going to be animated. Pass in an object 144 | * which will look something like this: 145 | * ```javascript 146 | * var ui = require('f1')(); 147 | * 148 | * ui.targets( { 149 | * 150 | * itemToAnimate1: find('#itemToAnimate1'), 151 | * itemToAnimate2: find('#itemToAnimate2') 152 | * }); 153 | * ``` 154 | * The `Object` being passed in should have variable names which will 155 | * associate to data which will be defined when setting up states in the 156 | * `f1.states` method. The value which you pass these can be anything. 157 | * 158 | * In this case `itemToAnimate1` and `itemToAnimate2` will be a HTML Elements. 159 | * 160 | * @param {Object} targets An Object which will define which items will be animated 161 | * @chainable 162 | */ 163 | targets: function(targets) { 164 | 165 | this.defTargets = targets; 166 | this.parsedTargets = parseTargets(targets); 167 | 168 | return this; 169 | }, 170 | 171 | /** 172 | * defines the states which this `f1` instance will use. 173 | * 174 | * States are defined as objects. It could look something like this: 175 | * ```javascript 176 | * var ui = require('f1')(); 177 | * 178 | * ui.states( { 179 | * 180 | * out: { 181 | * itemToAnimate1: { 182 | * variableToAnimate: 0 183 | * }, 184 | * 185 | * itemToAnimate2: { 186 | * variableToAnimate: 0 187 | * } 188 | * }, 189 | * 190 | * idle: { 191 | * itemToAnimate1: { 192 | * variableToAnimate: 1 193 | * }, 194 | * 195 | * itemToAnimate2: { 196 | * variableToAnimate: 2 197 | * } 198 | * } 199 | * }); 200 | * ``` 201 | * Above two states would be created: `out` and `idle`. Both would animate two 202 | * objects: `itemToAnimate1` and `itemToAnimate2`. And in both of those objects 203 | * the property `variableToAnimate` is defined. So if we were to transition from 204 | * `out` to `idle` in `itemToAnimate1` `variableToAnimate` would transition from 205 | * 0 to 1 and in `itemToAnimate2` from 0 to 2. 206 | * 207 | * States can also be defined by passing in objects for instance the above could 208 | * be changed to look like this: 209 | * ```javascript 210 | * var ui = require('f1')(); 211 | * 212 | * ui.states( { 213 | * 214 | * out: function(stateName) { 215 | * 216 | * console.log(stateName); // "out" 217 | * 218 | * return { 219 | * itemToAnimate1: { 220 | * variableToAnimate: 0 221 | * }, 222 | * 223 | * itemToAnimate2: { 224 | * variableToAnimate: 0 225 | * } 226 | * }; 227 | * }, 228 | * 229 | * idle: function(stateName) { 230 | * 231 | * console.log(stateName); // "idle" 232 | * 233 | * return { 234 | * itemToAnimate1: { 235 | * variableToAnimate: 1 236 | * }, 237 | * 238 | * itemToAnimate2: { 239 | * variableToAnimate: 2 240 | * } 241 | * }; 242 | * } 243 | * }); 244 | * ``` 245 | * The above can be handy when there are many items which states must be defined for 246 | * instance a menu with many buttons. 247 | * 248 | * @param {Object} states defines all of the states for an `f1` instance 249 | * @chainable 250 | */ 251 | states: function(states) { 252 | 253 | this.defStates = states; 254 | 255 | return this; 256 | }, 257 | 258 | /** 259 | * defines how this `f1` instance can move between states. 260 | * 261 | * For instance if we had two states out and idle you could define your transitions 262 | * like this: 263 | * 264 | * ```javascript 265 | * var ui = require('f1')(); 266 | * 267 | * ui.transitions( [ 268 | * 'out', 'idle', // defines that you can go from the out state to the idle state 269 | * 'idle', 'out' // defines that you can go from the idle state to the out state 270 | * ]); 271 | * ``` 272 | * 273 | * Note that transitions are not bi-directional. 274 | * 275 | * If you simply just defined state names a default animation would be applied between 276 | * states. This default transition will have a duration of 0.5 seconds and use no ease. 277 | * 278 | * If you want to modify the animation duration and ease you can define your transitions 279 | * like this: 280 | * 281 | * ```javascript 282 | * var eases = require('eases'); 283 | * var ui = require('f1')(); 284 | * 285 | * ui.transitions( [ 286 | * 'out', 'idle', { duration: 1, ease: eases.expoOut }, 287 | * 'idle', 'out', { duration: 0.5, ease: eases.expoIn } 288 | * ]); 289 | * ``` 290 | * 291 | * Defining your transitions using the above syntax will cause all properties to animate 292 | * using the duration and ease defined. 293 | * 294 | * Ease functions should take a time property between 0-1 and return a modified value between 295 | * 0-1. 296 | * 297 | * You can also animate properties individually. Here passing a delay maybe sometimes 298 | * userful: 299 | * 300 | * ```javascript 301 | * var eases = require('eases'); 302 | * var ui = require('f1')(); 303 | * 304 | * ui.transitions( [ 305 | * 'out', 'idle', { 306 | * duration: 1, ease: eases.expoOut, 307 | * 308 | * position: { duration: 0.5, delay: 0.5, ease: eases.quadOut }, 309 | * alpha: { duration: 0.5 } 310 | * }, 311 | * 'idle', 'out', { duration: 0.5, ease: eases.expoIn } 312 | * ]); 313 | * ``` 314 | * 315 | * In that example every property besides `position` and `alpha` will have a duration of one second 316 | * using the `eases.quadOut` ease equation. `position` will have a duration of 0.5 seconds and will 317 | * be delayed 0.5 seconds and will use the `eases.quadOut` easing function. `alpha` will simply have 318 | * a duration of 0.5 seconds. 319 | * 320 | * For advanced transitions you can pass in a function instead like so: 321 | * ```javascript 322 | * 323 | * ui.transitions( [ 324 | * 'out', 'idle', { 325 | * duration: 1, ease: eases.expoOut, 326 | * 327 | * position: { duration: 0.5, delay: 0.5, ease: eases.quadOut }, 328 | * 329 | * alpha: function(time, start, end) { 330 | * 331 | * return (end - start) * time + start; 332 | * } 333 | * }, 334 | * 'idle', 'out', { duration: 0.5, ease: eases.expoIn } 335 | * ]); 336 | * ``` 337 | * 338 | * There the animation is the same as in the previous example however `alpha` will be calculated using 339 | * a custom transition function. 340 | * 341 | * @param {Array} transitions An array which descriptes transitions 342 | * @chainable 343 | */ 344 | transitions: function(transitions) { 345 | 346 | this.defTransitions = Array.isArray(transitions) ? transitions : Array.prototype.slice.apply(arguments); 347 | 348 | return this; 349 | }, 350 | 351 | /** 352 | * `f1` can target many different platforms. How it does this is by using parsers which 353 | * can target different platforms. Parsers apply calculated state objects to targets. 354 | * 355 | * If working with the dom for instance your state could define values which will be applied 356 | * by the parser to the dom elements style object. 357 | * 358 | * When calling parsers pass in an Object that can contain variables init, and update. Both should contain 359 | * an Array's of functions which will be used to either init or update ui. 360 | * 361 | * init's functions will receive: states definition, targets definition, and transitions definition. 362 | * update functions will receive: target and state. Where target could be for instance a dom element and 363 | * state is the currently calculated state. 364 | * 365 | * @param {Object} parsersDefinitions an Object which may define arrays of init and update functions 366 | * @chainable 367 | */ 368 | parsers: function(parsersDefinitions) { 369 | 370 | // check that the parsersDefinitions is an object 371 | if(typeof parsersDefinitions !== 'object' || Array.isArray(parsersDefinitions)) { 372 | throw new Error('parsers should be an Object that contains arrays of functions under init and update'); 373 | } 374 | 375 | this.parser = this.parser || getParser(); 376 | 377 | this.parser.add(parsersDefinitions); 378 | 379 | return this; 380 | }, 381 | 382 | /** 383 | * Initializes `f1`. `init` will throw errors if required parameters such as 384 | * states and transitions are missing. The initial state for the `f1` instance 385 | * should be passed in. 386 | * 387 | * @param {String} Initial state for the `f1` instance 388 | * @chainable 389 | */ 390 | init: function(initState) { 391 | 392 | if(!this.isInitialized) { 393 | this.isInitialized = true; 394 | 395 | var driver = this.driver; 396 | 397 | if(!this.defStates) { 398 | 399 | throw new Error('You must define states before attempting to call init'); 400 | } else if(!this.defTransitions) { 401 | 402 | throw new Error('You must define transitions before attempting to call init'); 403 | } else if(!this.parser) { 404 | 405 | throw new Error('You must define parsers before attempting to call init'); 406 | } else if(!this.defTargets) { 407 | 408 | throw new Error('You must define targets before attempting to call init'); 409 | } else { 410 | 411 | parseStates(driver, this.defStates); 412 | parseTransitions(driver, this.defStates, this.defTransitions); 413 | 414 | this.parser.init(this.defStates, this.defTargets, this.defTransitions); 415 | 416 | driver.init(initState); 417 | } 418 | 419 | if(global.__f1__) { 420 | global.__f1__.init(this); 421 | } 422 | } 423 | 424 | return this; 425 | }, 426 | 427 | /** 428 | * Destroys an `f1` instances. This should be called when you don't need the f1 instance anymore. 429 | */ 430 | destroy: function() { 431 | 432 | if(global.__f1__) { 433 | global.__f1__.destroy(this); 434 | } 435 | 436 | this.driver.destroy(); 437 | }, 438 | 439 | /** 440 | * Will tell `f1` to go to animate to another state. Calling `go` will cause `f1` to calculate a path defined 441 | * through transitions to the state which was passed to it. 442 | * 443 | * @param {String} state The new state you'd like to go to 444 | * @param {Function} [cb] An optional callback which will be called once f1 reaches the state 445 | * @chainable 446 | */ 447 | go: function(state, cb) { 448 | 449 | this.driver.go(state, cb); 450 | 451 | return this; 452 | }, 453 | 454 | /** 455 | * Will tell `f1` to go to immediately jump to another state without animating. If an animation is currently 456 | * happening that animation is stopped and the jump to state will happen immediately. 457 | * 458 | * @param {String} state The new state you'd like to go to 459 | * @chainable 460 | */ 461 | set: function(state) { 462 | 463 | this.driver.set(state); 464 | 465 | return this; 466 | }, 467 | 468 | /** 469 | * This method can be used to manually update f1 by certain deltaTime. deltaTime should be in milliseconds. 470 | * In order to use this function you must pass in autoUpdate: false otherwise a raf loop will be run after 471 | * go. 472 | * 473 | * @param {Number} deltaTime How much time has passed since the last render in milliseconds 474 | * @chainable 475 | */ 476 | step: function(deltaTime) { 477 | this.driver.step(deltaTime); 478 | 479 | return this; 480 | }, 481 | 482 | /** 483 | * Will force `f1` to update. This is useful if you're updating a states values lets say by mouse movement. 484 | * You'd call `f1.update` to ensure the state gets applied. 485 | * 486 | * @chainable 487 | */ 488 | update: function() { 489 | 490 | _onUpdate.call(this, this.data, this.state, this.time, this.duration); 491 | 492 | return this; 493 | }, 494 | 495 | /** 496 | * An advanced method where you can apply the current state f1 497 | * has calculated to any object. 498 | * 499 | * Basically allows you to have one f1 object control multiple objects 500 | * or manually apply animations to objects. 501 | * 502 | * @param {String} pathToTarget A path in the current state to the object you'd like to apply. The path should 503 | * be defined using dot notation. So if your state had an object named `thing` and it 504 | * contained another object you'd like to apply called `data`. Your `pathToTarget` 505 | * would be `'thing.data'` 506 | * @param {Object} target The object you'd like to apply the currently calculated state to. For instance target 507 | * could be an html element. 508 | * @param {Object} [parserDefinition] An optional Object which defines init and update functions for a parser. 509 | */ 510 | apply: function(pathToTarget, target, parserDefinition) { 511 | 512 | var data = this.data; 513 | var parser = this.parser; 514 | var animationData; 515 | 516 | // if parse functions were passed in then create a new parser 517 | if(parserDefinition) { 518 | 519 | parser = new getParser(parserDefinition); 520 | } 521 | 522 | // if we have a parser then apply the parsers (parsers set css etc) 523 | if(parser) { 524 | 525 | if(typeof pathToTarget === 'string') { 526 | 527 | pathToTarget = pathToTarget.split('.'); 528 | } 529 | 530 | animationData = data[ pathToTarget[ 0 ] ]; 531 | 532 | for(var i = 1, len = pathToTarget.length; i < len; i++) { 533 | 534 | animationData = animationData[ pathToTarget[ i ] ]; 535 | } 536 | 537 | parser.update(target, animationData); 538 | } 539 | } 540 | }); 541 | 542 | function getEventArgs(name, args) { 543 | 544 | args = Array.prototype.slice.apply(args); 545 | 546 | args.unshift(name); 547 | 548 | return args; 549 | } 550 | 551 | function _onUpdate(data, state, time, duration) { 552 | 553 | var pathToTarget; 554 | var target; 555 | 556 | if(data !== undefined && state !== undefined && time !== undefined) { 557 | 558 | this.data = data; 559 | this.state = state; 560 | this.time = time; 561 | this.duration = duration; 562 | 563 | if(this.parsedTargets) { 564 | 565 | for(var i = 0, len = this.parsedTargets.length; i < len; i += 2) { 566 | 567 | pathToTarget = this.parsedTargets[ i ]; 568 | target = this.parsedTargets[ i + 1 ]; 569 | 570 | this.apply(pathToTarget, target); 571 | } 572 | } 573 | 574 | // this is kind nasty because _onUpdate is called manually on update manual calls 575 | // in this case we should emit an event duration could be undefined in that case 576 | if(duration !== undefined) { 577 | this.onUpdate(data, state, time, duration); 578 | } 579 | } 580 | } 581 | 582 | function _onState(data, state) { 583 | 584 | this.data = data; 585 | this.state = state; 586 | this.time = 0; 587 | this.duration = undefined; 588 | 589 | this.onState(data, state); 590 | } -------------------------------------------------------------------------------- /lib/parsers/getParser.js: -------------------------------------------------------------------------------- 1 | module.exports = function getParser(parserDefinition) { 2 | 3 | var initMethods = []; 4 | var parserMethods = []; 5 | 6 | var parser = { 7 | 8 | add: function(parserDefinition) { 9 | if(parserDefinition.init) { 10 | parserDefinition.init.forEach(function(initFunc) { 11 | parser.addInit(initFunc); 12 | }); 13 | } 14 | 15 | if(parserDefinition.update) { 16 | parserDefinition.update.forEach(function(parserFunc) { 17 | parser.addUpdate(parserFunc); 18 | }); 19 | } 20 | }, 21 | 22 | addInit: function(init) { 23 | initMethods.push(init); 24 | }, 25 | 26 | addUpdate: function(parser) { 27 | 28 | parserMethods.push(parser); 29 | }, 30 | 31 | /** 32 | * This will be called when the `f1` instance is initialized. 33 | * 34 | * @param {Object} states states the `f1` instance currently is using 35 | * @param {Object} targets targets the `f1` instance currently is using 36 | * @param {Array} transitions transitions the `f1` instance currently is using 37 | */ 38 | init: function(states, targets, transitions) { 39 | 40 | initMethods.forEach( function(method) { 41 | 42 | method(states, targets, transitions); 43 | }); 44 | }, 45 | 46 | /** 47 | * This will be called when `f1` has calculated state updates. 48 | * 49 | * @param {Object} item This will be an item defined in targets 50 | * @param {Object} calculatedState current calculated state from f1 51 | */ 52 | update: function(item, calculatedState) { 53 | 54 | parserMethods.forEach( function(method) { 55 | 56 | method(item, calculatedState); 57 | }); 58 | } 59 | }; 60 | 61 | // if a parserDefinition was passed then we want to add all to this parser 62 | if(parserDefinition) { 63 | parser.add(parserDefinition); 64 | } 65 | 66 | return parser; 67 | }; -------------------------------------------------------------------------------- /lib/states/parseStates.js: -------------------------------------------------------------------------------- 1 | module.exports = function(driver, states) { 2 | 3 | var state, stateName; 4 | 5 | for(var stateName in states) { 6 | 7 | state = states[ stateName ]; 8 | 9 | // if the state is defined as a function 10 | // call the function and expect it to return a 11 | // state Object back 12 | if(typeof state == 'function') { 13 | 14 | state = states[ stateName ] = state(stateName); 15 | } 16 | 17 | driver.state(stateName, state); 18 | } 19 | }; -------------------------------------------------------------------------------- /lib/targets/parseTargets.js: -------------------------------------------------------------------------------- 1 | module.exports = function(animatables) { 2 | 3 | var returnValue = []; 4 | 5 | // if it's an array we expect the data to be in correct format eg 6 | // [ 7 | // 'outer.inner.item', DOMElement 8 | // ] 9 | if(Array.isArray( animatables )) { 10 | 11 | for(var i = 0, len = animatables.length; i < len; i += 2) { 12 | 13 | returnValue.push(animatables[ i ].split( '.' )); 14 | returnValue.push(animatables[ i + 1 ]); 15 | } 16 | // if it's not an array but an Object instead we will iterate 17 | // over each item and split the key by . 18 | } else { 19 | 20 | for(var i in animatables) { 21 | 22 | returnValue.push(i.split( '.' ), animatables[ i ]); 23 | } 24 | } 25 | 26 | return returnValue; 27 | }; -------------------------------------------------------------------------------- /lib/transitions/createTransitions.js: -------------------------------------------------------------------------------- 1 | var tweenFunction = require('tween-function'); 2 | var defaultTransition = require('./defaultTransition'); 3 | 4 | module.exports = function createTransitions(animation, stateFrom, stateTo) { 5 | 6 | var paths = []; 7 | var pathAnimations; 8 | 9 | // paths will contain an array of arrays which will be paths to all properties to evaluate 10 | // we will pass both stateFrom and stateTo so that we can evaluate that both states contain 11 | // the prop to animate 12 | getPathsToProperties(stateFrom, stateTo, paths); 13 | 14 | // build animation definitions to each property using the path created above 15 | pathAnimations = paths.map(getAnimationDefinitions.bind(undefined, animation)); 16 | 17 | return buildTransitionDefinition(paths, pathAnimations, stateFrom); 18 | }; 19 | 20 | function thisOrThat(value1, value2) { 21 | return value1 !== undefined ? value1 : value2; 22 | } 23 | 24 | function getPathsToProperties(stateFrom, stateTo, paths, keys) { 25 | 26 | var newKeys; 27 | 28 | keys = keys || []; 29 | 30 | for(var i in stateFrom) { 31 | 32 | // both states have this property 33 | if(stateTo[ i ] !== undefined) { 34 | 35 | newKeys = keys.slice(); 36 | newKeys.push(i); 37 | 38 | if(typeof stateFrom[ i ] === 'object') { 39 | 40 | getPathsToProperties(stateFrom[ i ], stateTo[ i ], paths, newKeys); 41 | } else { 42 | 43 | paths.push(newKeys); 44 | } 45 | } 46 | } 47 | } 48 | 49 | function getAnimationDefinitions(animation, path) { 50 | 51 | var animationDefinition = animation; 52 | var reducedDefinition = path.reduce( function(curDef, pathPart) { 53 | 54 | animationDefinition = animationDefinition[ pathPart ] || {}; 55 | 56 | return { 57 | duration: thisOrThat(animationDefinition.duration, curDef.duration), 58 | delay: thisOrThat(animationDefinition.delay, curDef.delay), 59 | ease: thisOrThat(animationDefinition.ease, curDef.ease) 60 | }; 61 | }, { duration: animation.duration, delay: animation.delay, ease: animation.ease }); 62 | 63 | // apply the defaults if no durations and delays and eases have been created 64 | reducedDefinition.duration = thisOrThat(reducedDefinition.duration, defaultTransition.duration); 65 | reducedDefinition.delay = thisOrThat(reducedDefinition.delay, defaultTransition.delay); 66 | reducedDefinition.ease = thisOrThat(reducedDefinition.ease, defaultTransition.ease); 67 | 68 | return reducedDefinition; 69 | } 70 | 71 | function buildTransitionDefinition(paths, pathAnimations, state) { 72 | var transitions = {}; 73 | var overallDuration = pathAnimations.reduce( function(longestDuration, animationDef) { 74 | var curDuration = animationDef.duration + animationDef.delay; 75 | 76 | return curDuration > longestDuration ? curDuration : longestDuration; 77 | }, 0); 78 | var transitionsDef; 79 | var stateDef; 80 | var aniDef; 81 | 82 | // now create the transitions object 83 | paths.forEach( function(path, i) { 84 | var duration; 85 | var delay; 86 | var ease; 87 | 88 | transitionsDef = transitions; 89 | stateDef = state; 90 | aniDef = pathAnimations[ i ]; 91 | 92 | duration = aniDef.duration; 93 | delay = aniDef.delay; 94 | ease = aniDef.ease; 95 | 96 | path.forEach( function(pathPart, j) { 97 | if(j === path.length - 1) { 98 | 99 | // Special consideration if animating percents 100 | if (typeof stateDef[ pathPart ] === 'string' && stateDef[ pathPart ].charAt(stateDef[ pathPart ].length-1)==='%') { 101 | transitionsDef[ pathPart ] = percentTweenFunction( { 102 | duration: duration / overallDuration, 103 | delay: delay / overallDuration, 104 | ease: ease, 105 | cap: true 106 | }); 107 | 108 | // if the value is a number the transition will be built from tweenFunction 109 | } else if(typeof stateDef[ pathPart ] === 'number') { 110 | 111 | transitionsDef[ pathPart ] = tweenFunction( { 112 | duration: duration / overallDuration, 113 | delay: delay / overallDuration, 114 | ease: ease, 115 | cap: true 116 | }); 117 | // if the value is a String we'll need to use this transition function 118 | } else if(typeof stateDef[ pathPart ] === 'string' || typeof stateDef[ pathPart ] === 'boolean') { 119 | 120 | transitionsDef[ pathPart ] = function(time, start, end) { 121 | 122 | if(time * overallDuration < duration + delay) { 123 | 124 | return start; 125 | } else { 126 | 127 | return end; 128 | } 129 | }; 130 | } 131 | } else { 132 | if(transitionsDef[ pathPart ] === undefined) { 133 | transitionsDef[ pathPart ] = {}; 134 | } 135 | 136 | stateDef = stateDef[ pathPart ]; 137 | transitionsDef = transitionsDef[ pathPart ]; 138 | } 139 | }); 140 | }); 141 | 142 | return { 143 | duration: overallDuration, 144 | transitions: transitions 145 | }; 146 | } 147 | 148 | function percentTweenFunction(obj) { 149 | var tween = tweenFunction(obj); 150 | return function(time,start,end) { 151 | return tween(time,Number(start.slice(0,-1)),Number(end.slice(0,-1)))+'%'; 152 | } 153 | }; -------------------------------------------------------------------------------- /lib/transitions/defaultTransition.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | duration: 0.5, 3 | delay: 0 4 | }; -------------------------------------------------------------------------------- /lib/transitions/parseTransitions.js: -------------------------------------------------------------------------------- 1 | var getInterpolation = require('interpolation-builder'); 2 | var createTransitions = require('./createTransitions'); 3 | 4 | module.exports = function(driver, states, transitions) { 5 | 6 | // go through each transition and setup kimi with a function that works 7 | // with values between 0 and 1 8 | transitions.forEach( function(transition, i) { 9 | 10 | var from = transition.from || throwError('from', i, transition); 11 | var to = transition.to || throwError('to', i, transition); 12 | var animation = transition.animation; 13 | var duration; 14 | var animationDefinition; 15 | 16 | // if animation is an object then it's a Tween like definition 17 | // otherwise we'll assume that animation is a function and we can simply 18 | // pass that to the driver 19 | if(typeof animation == 'object' || animation === undefined) { 20 | animation = animation || {}; 21 | 22 | animationDefinition = createTransitions(animation, states[ from ], states[ to ]); 23 | 24 | // this 25 | animation = getInterpolation( 26 | animationDefinition.transitions 27 | ); 28 | 29 | duration = animationDefinition.duration; 30 | } else { 31 | 32 | duration = animation.duration; 33 | } 34 | 35 | // animation will either be a function passed in or generated from an object definition 36 | driver.fromTo(from, to, duration, animation); 37 | 38 | // handle adding bi directional transitions 39 | if(transition.bi) { 40 | driver.fromTo(to, from, duration, animation); 41 | } 42 | }); 43 | }; 44 | 45 | function throwError(type, idx, transition) { 46 | 47 | throw new Error( 48 | 'For the transition at ' + idx + ':\n' + 49 | JSON.stringify(transition, undefined, ' ') + '\n' + 50 | 'you did not define ' + type + '\n\n' 51 | ); 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f1", 3 | "version": "8.0.0", 4 | "description": "A stateful ui library", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Mikko Haapoja", 9 | "email": "me@mikkoh.com", 10 | "url": "https://github.com/jam3" 11 | }, 12 | "dependencies": { 13 | "deep-extend": "^0.4.0", 14 | "interpolation-builder": "^2.1.0", 15 | "kimi": "^6.0.0", 16 | "no-op": "^1.0.3", 17 | "tween-function": "^2.0.0" 18 | }, 19 | "devDependencies": { 20 | "budo": "^8.0.4", 21 | "eases": "^1.0.8", 22 | "right-now": "^1.0.0", 23 | "tape": "^3.5.0", 24 | "test-fuzzy-array": "^1.0.1" 25 | }, 26 | "scripts": { 27 | "test": "node test/", 28 | "testBrowser": "budo test/ --live --open", 29 | "example-f1": "budo example/f1/ --live --open", 30 | "example-chief": "budo example/chief/ --live --open", 31 | "f1-example": "npm run example-f1", 32 | "chief-example": "npm run example-chief" 33 | }, 34 | "keywords": [ 35 | "state,stateful,ui,ease,tween" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git://github.com/jam3/f1.git" 40 | }, 41 | "homepage": "https://github.com/jam3/f1", 42 | "bugs": { 43 | "url": "https://github.com/jam3/f1/issues" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/advancedAnimation/expectedStates.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | [ 3 | { 4 | "item": { 5 | "delayedValue": 0, 6 | "easedValue": 0, 7 | "durationValue": 0 8 | } 9 | }, 10 | "out" 11 | ], 12 | [ 13 | { 14 | "item": { 15 | "delayedValue": 100, 16 | "easedValue": 100, 17 | "durationValue": 100 18 | } 19 | }, 20 | "idle" 21 | ] 22 | ]; -------------------------------------------------------------------------------- /test/advancedAnimation/expectedUpdates.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "delayedValue":[0,0,0,0,0,0,0,0,0,0,0,0,0,2.0000000000000044,6.000000000000005,10.000000000000005,14.000000000000007,18,22,26.00000000000001,30.000000000000004,34.000000000000014,38.00000000000001,42,46.00000000000001,50,54.000000000000014,58.00000000000001,62.000000000000014,66,70.00000000000001,74.00000000000001,78,82,85.99999999999999,90.00000000000001,94,98, 100], 3 | "easedValue":[0,24.2141716744801,42.56508225014826,56.472471835193794,67.01230223067765,75,81.05354291862002,85.64127056253706,89.11811795879845,91.7530755576694,93.75,95.263385729655,96.41031764063428,97.27952948969961,97.93826888941736,98.4375,98.81584643241375,99.10257941015857,99.3198823724249,99.48456722235434,99.609375,99.70396160810344,99.77564485253964,99.82997059310622,99.87114180558858,100,100,100,100,100,100,100,100,100,100,100,100,100,100], 4 | "durationValue":[0,8.000000000000002,16.000000000000004,24.000000000000004,32.00000000000001,40,48.00000000000001,56.00000000000001,64.00000000000001,72,80,88,96.00000000000001,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100] 5 | }; -------------------------------------------------------------------------------- /test/advancedAnimation/index.js: -------------------------------------------------------------------------------- 1 | var f1 = require('./../..'); 2 | var eases = require('eases'); 3 | var getFuzzyTest = require('test-fuzzy-array'); 4 | 5 | var EXPECTED_STATES = require('./expectedStates'); 6 | var EXPECTED_UPDATES = require('./expectedUpdates'); 7 | 8 | module.exports = function(t) { 9 | 10 | var cbStates = []; 11 | var cbUpdates = { 12 | delayedValue: [], 13 | easedValue: [], 14 | durationValue: [] 15 | }; 16 | 17 | var item = {}; 18 | var ui = f1({ 19 | autoUpdate: false, 20 | onState: callBackState, 21 | onUpdate: callBackUpdate, 22 | states: { 23 | out: { 24 | item: { 25 | delayedValue: 0, 26 | easedValue: 0, 27 | durationValue: 0 28 | } 29 | }, 30 | 31 | idle: { 32 | item: { 33 | delayedValue: 100, 34 | easedValue: 100, 35 | durationValue: 100 36 | } 37 | } 38 | }, 39 | transitions: [ 40 | { from: 'out', to: 'idle', animation: { 41 | duration: 1, 42 | 43 | item: { 44 | 45 | delayedValue: { 46 | delay: 0.5 47 | }, 48 | 49 | easedValue: { 50 | ease: eases.expoOut 51 | }, 52 | 53 | durationValue: { 54 | duration: 0.5 55 | } 56 | } 57 | } 58 | } 59 | ], 60 | targets: { 61 | item: item 62 | }, 63 | parsers: { 64 | update: [ 65 | function(target, state) { 66 | for(var i in state) { 67 | target[ i ] = state[ i ]; 68 | } 69 | } 70 | ] 71 | } 72 | }); 73 | 74 | ui.init('out'); 75 | ui.go('idle'); 76 | 77 | for(var i = 0; i < 100000; i++) { 78 | ui.step(40); 79 | } 80 | 81 | getFuzzyTest(t, 0.01)(cbUpdates.delayedValue, EXPECTED_UPDATES.delayedValue, 'delayedValue were correctish'); 82 | getFuzzyTest(t, 0.01)(cbUpdates.easedValue, EXPECTED_UPDATES.easedValue, 'easedValue were correctish'); 83 | getFuzzyTest(t, 0.01)(cbUpdates.durationValue, EXPECTED_UPDATES.durationValue, 'durationValue were correctish'); 84 | t.end(); 85 | 86 | function callBackState() { 87 | cbStates.push(Array.prototype.slice.call(arguments)); 88 | } 89 | 90 | function callBackUpdate(value) { 91 | 92 | cbUpdates.delayedValue.push(item.delayedValue); 93 | cbUpdates.durationValue.push(item.durationValue); 94 | cbUpdates.easedValue.push(item.easedValue); 95 | } 96 | }; -------------------------------------------------------------------------------- /test/callbackAndUpdate.js: -------------------------------------------------------------------------------- 1 | var f1 = require('./..'); 2 | 3 | var EXPECTED_GO_CALLBACK = [ { item: { value: 100 } }, 'idle' ]; 4 | 5 | var EXPECTED_STATES = [ 6 | [ 7 | { 8 | "item": { 9 | "value": 0 10 | } 11 | }, 12 | "out" 13 | ], 14 | [ 15 | { 16 | "item": { 17 | "value": 100 18 | } 19 | }, 20 | "idle" 21 | ] 22 | ]; 23 | 24 | 25 | var EXPECTED_UPDATES = [ 26 | [ 27 | { 28 | "item": { 29 | "value": 0 30 | } 31 | }, 32 | "out", 33 | 0, 34 | 0 35 | ], 36 | [ 37 | { 38 | "item": { 39 | "value": 30 40 | } 41 | }, 42 | "out", 43 | 0.3, 44 | 1 45 | ], 46 | [ 47 | { 48 | "item": { 49 | "value": 60 50 | } 51 | }, 52 | "out", 53 | 0.6, 54 | 1 55 | ], 56 | [ 57 | { 58 | "item": { 59 | "value": 90 60 | } 61 | }, 62 | "out", 63 | 0.9, 64 | 1 65 | ], 66 | [ 67 | { 68 | "item": { 69 | "value": 100 70 | } 71 | }, 72 | "idle", 73 | 0, 74 | 1 75 | ] 76 | ]; 77 | 78 | 79 | 80 | 81 | 82 | 83 | module.exports = function(t) { 84 | 85 | var eventStates = []; 86 | var eventUpdates = []; 87 | var cbStates = []; 88 | var cbUpdates = []; 89 | var goCallback = null; 90 | 91 | var item = {}; 92 | var ui = f1({ 93 | autoUpdate: false, 94 | onState: callBackState, 95 | onUpdate: callBackUpdate 96 | }); 97 | 98 | ui.states({ 99 | out: { 100 | item: { 101 | value: 0 102 | } 103 | }, 104 | 105 | idle: { 106 | item: { 107 | value: 100 108 | } 109 | } 110 | }); 111 | 112 | ui.transitions([ 113 | { from: 'out', to: 'idle', animation: { 114 | duration: 1 115 | } 116 | } 117 | ]); 118 | 119 | ui.targets({ 120 | item: item 121 | }); 122 | 123 | ui.parsers({ 124 | update: [ 125 | function(target, state) { 126 | for(var i in state) { 127 | target[ i ] = state[ i ]; 128 | } 129 | } 130 | ] 131 | }); 132 | 133 | ui.on('state', eventState); 134 | ui.on('update', eventUpdate); 135 | 136 | ui.init('out'); 137 | ui.go('idle', function() { 138 | goCallback = Array.prototype.slice.call(arguments); 139 | }); 140 | 141 | ui.step(300); 142 | ui.step(300); 143 | ui.step(300); 144 | ui.step(300); 145 | ui.step(300); 146 | ui.step(300); 147 | ui.step(300); 148 | ui.step(300); 149 | 150 | t.deepEqual(goCallback, EXPECTED_GO_CALLBACK, 'go callback was correct'); 151 | t.deepEqual(eventStates, EXPECTED_STATES, 'event states matched expected'); 152 | t.deepEqual(eventUpdates, EXPECTED_UPDATES, 'event updates matched expected'); 153 | t.deepEqual(cbStates, EXPECTED_STATES, 'callback states matched expected'); 154 | t.deepEqual(cbUpdates, EXPECTED_UPDATES, 'callback updates matched expected'); 155 | t.end(); 156 | 157 | function eventState() { 158 | eventStates.push(Array.prototype.slice.call(arguments)); 159 | } 160 | 161 | function eventUpdate() { 162 | eventUpdates.push(Array.prototype.slice.call(arguments)); 163 | } 164 | 165 | function callBackState() { 166 | cbStates.push(Array.prototype.slice.call(arguments)); 167 | } 168 | 169 | function callBackUpdate() { 170 | cbUpdates.push(Array.prototype.slice.call(arguments)); 171 | } 172 | }; -------------------------------------------------------------------------------- /test/chief/getUI.js: -------------------------------------------------------------------------------- 1 | var f1 = require('../..'); 2 | 3 | module.exports = function(target, values, onUpdate) { 4 | 5 | target = target || {}; 6 | values = values || { 7 | out: 0, 8 | idle: 100, 9 | rolled: 200 10 | }; 11 | 12 | var states = { 13 | out: { 14 | item: { 15 | value: values.out 16 | } 17 | }, 18 | 19 | idle: { 20 | item: { 21 | value: values.idle 22 | } 23 | }, 24 | 25 | rolled: { 26 | item: { 27 | value: values.rolled 28 | } 29 | } 30 | }; 31 | 32 | var transitions = [ 33 | { from: 'out', to: 'idle' }, 34 | { from: 'idle', to: 'rolled', bi: true } 35 | ]; 36 | 37 | var parsers = { 38 | update: [ 39 | function(target, state) { 40 | for(var i in state) { 41 | target[ i ] = state[ i ]; 42 | } 43 | } 44 | ] 45 | }; 46 | 47 | return f1({ 48 | states: states, 49 | 50 | transitions: transitions, 51 | 52 | targets: { 53 | item: target 54 | }, 55 | 56 | parsers: parsers, 57 | 58 | onUpdate: onUpdate 59 | }); 60 | }; -------------------------------------------------------------------------------- /test/chief/index.js: -------------------------------------------------------------------------------- 1 | var chief = require('../../chief'); 2 | var getUI = require('./getUI'); 3 | 4 | module.exports = function(t) { 5 | 6 | var hasGoneIntoRolled = false; 7 | var hasGoneIntoRolled2 = false; 8 | var hasGoneIntoIdle = false; 9 | 10 | var values = { 11 | out: 0, 12 | idle: 100, 13 | rolled: 200 14 | }; 15 | var target1 = {}; 16 | var target2 = {}; 17 | 18 | var ui1 = getUI(target1, values); 19 | var ui2 = getUI(target2, values); 20 | 21 | var controller = chief({ 22 | // autoUpdate: false, 23 | 24 | states: { 25 | out: { 26 | ui1: 'out', 27 | ui2: 'out' 28 | }, 29 | 30 | idle: { 31 | ui1: 'idle', 32 | ui2: 'idle' 33 | }, 34 | 35 | rolled: { 36 | ui1: 'rolled', 37 | ui2: 'rolled' 38 | }, 39 | 40 | rolled2: { 41 | ui1: 'idle', 42 | ui2: 'rolled' 43 | } 44 | }, 45 | 46 | targets: { 47 | ui1: ui1, 48 | ui2: ui2 49 | }, 50 | 51 | transitions: [ 52 | { from: 'out', to: 'idle', animation: { 53 | 54 | ui2: { 55 | delay: 0.5 56 | } 57 | } 58 | }, 59 | 60 | { from: 'idle', to: 'rolled', bi: true }, 61 | { from: 'rolled', to: 'rolled2', bi: true } 62 | ] 63 | }); 64 | 65 | controller.init('out'); 66 | t.equal(target1.value, values.out, 'target1 init to out'); 67 | t.equal(target2.value, values.out, 'target2 init to out'); 68 | 69 | // just a regular animation 70 | controller.go('rolled', function() { 71 | hasGoneIntoRolled = true; 72 | t.equal(target1.value, values.rolled, 'target1 went to rolled'); 73 | t.equal(target2.value, values.rolled, 'target2 went to rolled'); 74 | 75 | 76 | // this is to test attempting to control ui that are going to the same states 77 | controller.go('rolled2', function() { 78 | hasGoneIntoRolled2 = true; 79 | t.equal(target1.value, values.idle, 'target1 went to idle'); 80 | t.equal(target2.value, values.rolled, 'target2 went to rolled'); 81 | 82 | // test going back to idle 83 | controller.go('idle', function() { 84 | hasGoneIntoIdle = true; 85 | t.equal(target1.value, values.idle, 'target1 went to idle'); 86 | t.equal(target2.value, values.idle, 'target2 went to idle'); 87 | }); 88 | }); 89 | }); 90 | 91 | setTimeout(function() { 92 | t.ok(hasGoneIntoRolled, 'went from from out to rolled state'); 93 | t.ok(hasGoneIntoRolled2, 'went from rolled to rolled2 state'); 94 | t.ok(hasGoneIntoIdle, 'went from rolled2 to idle state'); 95 | t.end(); 96 | }, 3000); 97 | }; -------------------------------------------------------------------------------- /test/defineConstrutor.js: -------------------------------------------------------------------------------- 1 | var f1 = require('./..'); 2 | 3 | 4 | var EXPECTED_STATES = [ 5 | [ 6 | { 7 | "item": { 8 | "value": 0 9 | } 10 | }, 11 | "out" 12 | ], 13 | [ 14 | { 15 | "item": { 16 | "value": 100 17 | } 18 | }, 19 | "idle" 20 | ] 21 | ]; 22 | 23 | 24 | var EXPECTED_UPDATES = [ 25 | [ 26 | { 27 | "item": { 28 | "value": 0 29 | } 30 | }, 31 | "out", 32 | 0, 33 | 0 34 | ], 35 | [ 36 | { 37 | "item": { 38 | "value": 30 39 | } 40 | }, 41 | "out", 42 | 0.3, 43 | 1 44 | ], 45 | [ 46 | { 47 | "item": { 48 | "value": 60 49 | } 50 | }, 51 | "out", 52 | 0.6, 53 | 1 54 | ], 55 | [ 56 | { 57 | "item": { 58 | "value": 90 59 | } 60 | }, 61 | "out", 62 | 0.9, 63 | 1 64 | ], 65 | [ 66 | { 67 | "item": { 68 | "value": 100 69 | } 70 | }, 71 | "idle", 72 | 0, 73 | 1 74 | ] 75 | ]; 76 | 77 | 78 | 79 | 80 | 81 | 82 | module.exports = function(t) { 83 | 84 | var cbStates = []; 85 | var cbUpdates = []; 86 | 87 | var item = {}; 88 | var ui = f1({ 89 | autoUpdate: false, 90 | onState: callBackState, 91 | onUpdate: callBackUpdate, 92 | states: { 93 | out: { 94 | item: { 95 | value: 0 96 | } 97 | }, 98 | 99 | idle: { 100 | item: { 101 | value: 100 102 | } 103 | } 104 | }, 105 | transitions: [ 106 | { from: 'out', to: 'idle', animation: { 107 | duration: 1 108 | } 109 | } 110 | ], 111 | targets: { 112 | item: item 113 | }, 114 | parsers: { 115 | update: [ 116 | function(target, state) { 117 | for(var i in state) { 118 | target[ i ] = state[ i ]; 119 | } 120 | } 121 | ] 122 | } 123 | }); 124 | 125 | ui.init('out'); 126 | ui.go('idle'); 127 | 128 | ui.step(300); 129 | ui.step(300); 130 | ui.step(300); 131 | ui.step(300); 132 | ui.step(300); 133 | ui.step(300); 134 | ui.step(300); 135 | ui.step(300); 136 | 137 | t.deepEqual(cbStates, EXPECTED_STATES, 'callback states matched expected'); 138 | t.deepEqual(cbUpdates, EXPECTED_UPDATES, 'callback updates matched expected'); 139 | t.end(); 140 | 141 | function callBackState() { 142 | cbStates.push(Array.prototype.slice.call(arguments)); 143 | } 144 | 145 | function callBackUpdate() { 146 | cbUpdates.push(Array.prototype.slice.call(arguments)); 147 | } 148 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | test('callback and events return same', require('./callbackAndUpdate')); 4 | test('passing states, targets, transitions, parsers through constructor', require('./defineConstrutor')); 5 | test('advanced animation', require('./advancedAnimation')); 6 | 7 | // theres some issues with kimi step 8 | test('chief', require('./chief')); --------------------------------------------------------------------------------