├── .gitignore ├── .gitmodules ├── README.md ├── cards.js ├── example ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── release │ └── versions ├── example.html ├── example.js ├── example.less ├── packages │ └── paper └── public │ ├── chicago.jpg │ ├── dc.jpg │ ├── houston.jpg │ ├── la.jpg │ ├── nyc.jpg │ ├── philly.jpg │ └── phoenix.jpg ├── package.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_STORE 2 | .idea 3 | .build* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hammer.js"] 2 | path = hammer.js 3 | url = https://github.com/hammerjs/hammer.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Facebook paper interactions rebuilt in javascript. 2 | 3 | This uses [velocity](velocityjs.org) for animations and [hammerjs](https://github.com/hammerjs/hammer.js) for touch events. -------------------------------------------------------------------------------- /cards.js: -------------------------------------------------------------------------------- 1 | var DIRECTION = { 2 | UP: 1, 3 | DOWN: - 1, 4 | STILL: 0 5 | }; 6 | 7 | var Mode = { 8 | NONE: 0, 9 | PANNING: 1, 10 | SCROLLING: 2 11 | }; 12 | 13 | var DECELERATION = .0008, 14 | SPRING = [200, 25], 15 | CIRCULAR_BEZIER = [0.1, 0.57, 0.1, 1], 16 | QUADRATIC_BEZIER = [0.25, 0.46, 0.45, 0.94]; 17 | 18 | var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; 19 | var TEST_ELEMENT = document.createElement('div'); 20 | 21 | var Tools = { 22 | /** 23 | * Map the a value to a result. 24 | * Ex. value = .5, valueRange = [0, 1], resultRange = [0, 100], result = 50 25 | */ 26 | map: function (value, valueRange, resultRange, keepWithinRange, maxDecimalPlaces) { 27 | var valueRangeStart = valueRange[0], valueRangeEnd = valueRange[1], 28 | resultRangeStart = resultRange[0], resultRangeEnd = resultRange[1]; 29 | 30 | var currentProgress = (value - valueRangeStart) / (valueRangeEnd - valueRangeStart); 31 | 32 | var result = currentProgress * (resultRangeEnd - resultRangeStart) + resultRangeStart; 33 | 34 | if (keepWithinRange) { 35 | // if the range is increasing (0 -> 100) 36 | if (resultRangeEnd > resultRangeStart) { 37 | if (result > resultRangeEnd) { 38 | result = resultRangeEnd; 39 | } else if (result < resultRangeStart) { 40 | result = resultRangeStart; 41 | } 42 | } 43 | // if the range is decreasing (100 -> 0) 44 | else { 45 | if (result > resultRangeStart) { 46 | result = resultRangeStart; 47 | } else if (result < resultRangeEnd) { 48 | result = resultRangeEnd; 49 | } 50 | } 51 | } 52 | 53 | if (maxDecimalPlaces) { 54 | var baseTenMultiplier = Math.pow(10, maxDecimalPlaces); 55 | result = Math.floor(result * baseTenMultiplier) / baseTenMultiplier; 56 | } 57 | 58 | return result; 59 | }, 60 | /** 61 | * Get the prefixed property. 62 | */ 63 | prefixed: function (obj, property) { 64 | var prefix, prop; 65 | var camelProp = property[0].toUpperCase() + property.slice(1); 66 | 67 | for (var i = 0, len = VENDOR_PREFIXES.length; i < len; i ++) { 68 | prefix = VENDOR_PREFIXES[i]; 69 | prop = (prefix) ? prefix + camelProp : property; 70 | 71 | if (prop in obj) { 72 | return prop; 73 | } 74 | } 75 | 76 | return undefined; 77 | }, 78 | /** 79 | * Get or set the transform on an element. 80 | */ 81 | transform: function (element, transform) { 82 | if (transform === undefined) return element.style[transformKey]; 83 | 84 | element.style[TRANSFORM_KEY] = transform; 85 | } 86 | }; 87 | 88 | var TRANSFORM_KEY = Tools.prefixed(TEST_ELEMENT.style, 'transform'); 89 | 90 | // Prevent velocity from resetting scale to 1 on the first scale run on android. 91 | // https://github.com/julianshapiro/velocity/blob/master/jquery.velocity.js#L867-L872 92 | // See https://github.com/julianshapiro/velocity/pull/174 93 | $.Velocity.State.isAndroid = false; 94 | 95 | /** 96 | * Allow the user to drag cards up and down. 97 | * Scroll, scale and snap the card into place. 98 | */ 99 | Cards = function (options, callbacks) { 100 | var self = this; 101 | 102 | self.options = _.extend({}, Cards.DEFAULT_OPTIONS, options); 103 | self.callbacks = callbacks || {}; 104 | 105 | self.$scrollview = options.$scrollview; 106 | self.scrollview = self.$scrollview[0]; 107 | self.$cards = options.$cards; 108 | self.cards = self.$cards[0]; 109 | self.cardElements = self.cards.children; 110 | self.parent = self.scrollview.parentElement; 111 | 112 | // Use for manually getting & using velocity animation values. 113 | // XXX manually setup our own spring animation so we do not have the overhead 114 | // of style calculations on the placeholder element. See the spring branch. 115 | self.$animationPlaceholder = $(document.createElement('div')); 116 | 117 | self._panMode = Mode.NONE; 118 | 119 | // Store the last pan event with velocity 120 | // to decide the scale to snap the cards to. 121 | self._lastPanEvent = null; 122 | 123 | // The previous pan event -- used for calculating deltas. 124 | self._previousPan = null; 125 | 126 | self.scale = self.options.initialScale; 127 | 128 | // Value range for scales on the yAxis proportional to 129 | // startY. Set on the first movement along the yAxis. 130 | self._scalePath = null; 131 | 132 | // The target card when moving on the yAxis. 133 | self._targetCardIndex = self.options.initialIndex; 134 | 135 | self._scrollingEnabled = self.options.initialScale !== 1; 136 | 137 | // The card's left translation (like scrollLeft). 138 | self._translateLeft = 0; 139 | 140 | // How far you can "scroll". 141 | self._maxTranslateLeftSmallScale = 0; 142 | 143 | // The translateLeft at small scale before scaling up. 144 | // We track this so we can return to it. 145 | self._smallScaleTranslateLeft = 0; 146 | 147 | // Make the scrollview width large enough to fit the window at min scale. 148 | self._scrollviewWidth = Math.floor(window.innerWidth / self.options.minScale); 149 | self.$scrollview.width(self._scrollviewWidth); 150 | 151 | setTimeout(function () { 152 | self._initialize(callbacks && callbacks.ready); 153 | }, 0); 154 | }; 155 | 156 | Cards.DEFAULT_OPTIONS = { 157 | $scrollview: null, // Required 158 | $cards: null, // Required 159 | 160 | cardMargin: 5, 161 | 162 | marginTop: 0, 163 | 164 | // Snap point 165 | smallScale: .4, 166 | 167 | // Bounds 168 | maxScale: 1.4, 169 | minScale: .3, 170 | 171 | // Start at 172 | initialIndex: 0, 173 | initialScale: .4, 174 | 175 | bounceTime: 600, 176 | 177 | debug: false 178 | }; 179 | 180 | Cards.prototype._initialize = function (callback) { 181 | var self = this; 182 | self.changed(); 183 | self._track(); 184 | callback && callback(self); 185 | }; 186 | 187 | Cards.prototype._track = function () { 188 | var self = this; 189 | 190 | var hammer = self._hammer = new Hammer.Manager(self.scrollview); 191 | 192 | hammer.add(new Hammer.Pan({ 193 | direction: Hammer.DIRECTION_ALL 194 | })); 195 | 196 | hammer.on('pan', self._handlePan.bind(self)); 197 | hammer.on('panend', self._handlePanEnd.bind(self)); 198 | 199 | hammer.add(new Hammer.Tap()); 200 | 201 | hammer.on('tap', self._handleTap.bind(self)); 202 | }; 203 | 204 | var onNextFrame = null, 205 | nextFrameRequested = false; 206 | 207 | /** 208 | * Throttle animation frame requests. 209 | */ 210 | var requestAnimationFrameThrottled = function (func) { 211 | onNextFrame = func; 212 | 213 | // Wait half a second before requesting a new animation frame. 214 | if (nextFrameRequested && new Date() - nextFrameRequested < 500) return; 215 | 216 | nextFrameRequested = new Date(); 217 | requestAnimationFrame(function () { 218 | onNextFrame && onNextFrame(); 219 | onNextFrame = null; 220 | nextFrameRequested = false; 221 | }); 222 | }; 223 | 224 | var calculateTargetCardIndex = function (xPos) { 225 | var self = this; 226 | 227 | var scaledTranslatedX = self._translateLeft + (xPos / self.scale); 228 | var targetCardIndex = Math.floor(scaledTranslatedX / (window.innerWidth + self.options.cardMargin)); 229 | 230 | return targetCardIndex >= 0 && targetCardIndex < self.cardElements.length 231 | ? targetCardIndex 232 | : null; 233 | }; 234 | 235 | /** 236 | * Scroll or scale the cards. 237 | */ 238 | Cards.prototype._handlePan = function (panEvent) { 239 | var self = this; 240 | 241 | // If the touch is not on a card, do nothing. 242 | var targetCardIndex = calculateTargetCardIndex.call(this, panEvent.center.x); 243 | if (targetCardIndex === null) return; 244 | 245 | if (! self._panMode) self._startPan(panEvent, targetCardIndex); 246 | 247 | self.options.debug && console.log((self._panMode === Mode.PANNING ? 'panning' : 'scrolling'), 248 | 'velocityX', panEvent.velocityX, 'velocityY', panEvent.velocityY, 249 | 'deltaX', panEvent.deltaX, 'deltaY', panEvent.deltaY, panEvent); 250 | 251 | // Store the last pan event with velocity. 252 | if (panEvent && (panEvent.velocityX !== 0 || panEvent.velocityX !== 0)) { 253 | self._lastPanEvent = panEvent; 254 | } 255 | 256 | requestAnimationFrameThrottled(function () { 257 | if (! self._panMode) return; 258 | 259 | var deltaX = 0; 260 | 261 | // Scroll the x axis, except at full scale. 262 | if (self.scale !== 1) { 263 | // Calculate the delta from the previous pan event. 264 | deltaX = panEvent.deltaX; 265 | 266 | if (self._previousPan) deltaX -= self._previousPan.deltaX; 267 | 268 | var dxScaled = - deltaX / self.scale; 269 | 270 | // Slow down if scrolling outside the boundaries 271 | var newX = self._translateLeft + dxScaled; 272 | if (self._panMode === Mode.SCROLLING && (newX < 0 || newX > self._maxTranslateLeftSmallScale)) { 273 | newX = self._translateLeft + dxScaled / 3; 274 | } 275 | 276 | self._translateLeft = newX; 277 | } 278 | 279 | // If we are still panning, keep track of the last pan event. 280 | self._previousPan = self._panMode !== Mode.NONE ? panEvent : null; 281 | 282 | var newScale; 283 | if (self._panMode === Mode.PANNING) { 284 | // Figure out the scale based on the scale path and the current y pos. 285 | var yPos = panEvent.center.y - self.options.marginTop; 286 | newScale = Tools.map(yPos, self._scalePath, [self.options.smallScale, 1]); 287 | 288 | // If the new scale is past the min / max bounds do not scale. 289 | if (newScale < self.options.minScale) newScale = self.options.minScale; 290 | if (newScale > self.options.maxScale) newScale = self.options.maxScale; 291 | 292 | // Offset the x position based on the deltaScale 293 | var deltaScale = newScale - self.scale; 294 | var offset = deltaScale * (panEvent.center.x + deltaX) / self.options.smallScale; 295 | offset = (1 - newScale + self.options.smallScale) * (offset / newScale); 296 | 297 | self._translateLeft += offset; 298 | } 299 | 300 | self._transform(newScale); 301 | }); 302 | }; 303 | 304 | /** 305 | * The scale path based on the initial touch point. 306 | */ 307 | var calculateScalePath = function (start) { 308 | var self = this; 309 | 310 | var fullScaleHeight = window.innerHeight - this.options.marginTop; 311 | var smallScaleHeight = fullScaleHeight * self.options.smallScale; 312 | 313 | var cardHeight = fullScaleHeight * self.scale; 314 | var cardTop = fullScaleHeight - cardHeight; 315 | 316 | // Find the y position relative to the current card. 317 | var startY = start.y - self.options.marginTop; 318 | var relativeY = Math.abs((startY - cardTop) / cardHeight); 319 | 320 | // Calculate the y for the card at full scale. 321 | var relativeFullCardY = relativeY * fullScaleHeight; 322 | 323 | // Calculate the y for the card at small scale. 324 | var relativeScaledCardY = relativeY * self.options.smallScale * fullScaleHeight; 325 | 326 | var scaledCardY = fullScaleHeight - (smallScaleHeight - relativeScaledCardY); 327 | 328 | return [scaledCardY, relativeFullCardY]; 329 | }; 330 | 331 | /** 332 | /** 333 | * On the first pan 334 | * - set the mode and target card index 335 | * - stop any in progress animation 336 | * 337 | * If this is a pan (not a scroll) 338 | * - set the scale path 339 | * - trigger a cardPanning event 340 | */ 341 | Cards.prototype._startPan = function (panEvent, targetCardIndex) { 342 | var self = this; 343 | 344 | self._targetCardIndex = targetCardIndex; 345 | self._stopAnimation(); 346 | 347 | self._initialPanEvent = panEvent; 348 | 349 | self.options.debug && console.log('start pan', 'scale', self.scale, 'velocityX', 350 | panEvent.velocityX, 'velocityY', panEvent.velocityY, panEvent); 351 | 352 | // Add .01 tolerance to small scale in case someone 353 | // scrolls right after it is transitioning down. 354 | var smallScale = self.scale < self.options.smallScale + .01; 355 | 356 | if (smallScale && Math.abs(panEvent.deltaX) > Math.abs(panEvent.deltaY)) { 357 | self._panMode = Mode.SCROLLING; 358 | return; 359 | } 360 | 361 | if (smallScale) self._smallScaleTranslateLeft = self._translateLeft; 362 | 363 | self._scalePath = calculateScalePath.call(this, panEvent.center); 364 | self._panMode = Mode.PANNING; 365 | 366 | self.$scrollview.trigger('cardPanning', { 367 | target: self.cardElements[targetCardIndex], 368 | targetIndex: self._targetCardIndex 369 | }); 370 | }; 371 | 372 | /** 373 | * Scale and scroll (translateX) the cards. 374 | */ 375 | Cards.prototype._transform = function (newScale) { 376 | var self = this; 377 | 378 | if (newScale) { 379 | var scale = self.scale = newScale; 380 | 381 | self._updateProgress(); 382 | 383 | // Make the scrollview flush along the x & y axis. 384 | var translateX = - self._scrollviewWidth * (1 - scale) / 2; 385 | translateX = Math.floor(translateX); 386 | 387 | var fullScaleHeight = window.innerHeight - self.options.marginTop; 388 | var translateY = fullScaleHeight * (1 - scale) / 2; 389 | translateY = Math.floor(translateY); 390 | 391 | // cache the transform values on the scrollview element 392 | self.scrollview._transformCache = { 393 | translateX: translateX, 394 | translateY: translateY, 395 | scale: scale 396 | }; 397 | 398 | var scrollviewTransform = 'translate3d(' + translateX + 'px,' + translateY + 'px,0px) scale3d(' + scale + ',' + scale + ',1)'; 399 | Tools.transform(self.scrollview, scrollviewTransform); 400 | } 401 | 402 | if (self._translateLeft) { 403 | self._translateLeft = Math.floor(self._translateLeft); 404 | 405 | var cardsTransform = 'translate3d(' + - self._translateLeft + 'px,0px,0px)'; 406 | Tools.transform(self.cards, cardsTransform); 407 | } 408 | }; 409 | 410 | Cards.prototype._updateProgress = function () { 411 | var self = this; 412 | 413 | var progress = Math.floor(self.scale * 100) / 100; 414 | if (self._progress === progress) return; 415 | 416 | self._progress = progress; 417 | 418 | self.callbacks.progress && self.callbacks.progress(progress); 419 | }; 420 | 421 | Cards.prototype._handlePanEnd = function (event) { 422 | var self = this; 423 | 424 | // If we were not panning ignore the pan end. 425 | if (self._panMode === Mode.NONE) return; 426 | 427 | // Use the last pan event with velocity. 428 | var panEvent = self._lastPanEvent || event; 429 | 430 | self._stopAnimation(); 431 | 432 | if (self._panMode === Mode.PANNING) self._scaleEnded(panEvent); 433 | else self._scrollEnded(panEvent); 434 | 435 | self._lastPanEvent = null; 436 | self._previousPan = null; 437 | 438 | self._panMode = Mode.NONE; 439 | }; 440 | 441 | /** 442 | * Snap to full or small scale. 443 | */ 444 | Cards.prototype._scaleEnded = function (panEvent) { 445 | var self = this; 446 | 447 | self.options.debug && console.log('scale ended at', self.scale, panEvent); 448 | 449 | var yDirection = DIRECTION.STILL; 450 | if (panEvent.velocityY > .1) yDirection = DIRECTION.UP; 451 | else if (panEvent.velocityY < - .1) yDirection = DIRECTION.DOWN; 452 | 453 | var midpoint = 1 - ((1 - self.options.smallScale) / 2); 454 | var absVelocityX = Math.abs(panEvent.velocityX); 455 | 456 | // Snap to full scale and change cards if we are past the midpoint 457 | // and the x velocity is 2x as much as the y velocity. 458 | var panCardsAtFullScale = self.scale > midpoint && absVelocityX > Math.abs(panEvent.velocityY) * 2 && absVelocityX > .5; 459 | 460 | // Snap to full scale if the y direction is up 461 | // or the scale is past the midpoint and the y direction is still. 462 | var snapToFullScale = panCardsAtFullScale || yDirection === DIRECTION.UP || (yDirection === DIRECTION.STILL && self.scale > midpoint); 463 | 464 | // Snap to a target index if we are snapping to full scale. 465 | var targetIndex = snapToFullScale ? self._targetCardIndex : null; 466 | 467 | if (panCardsAtFullScale) { 468 | // Move to the previous or next card. 469 | if (panEvent.velocityX <= - .5) targetIndex --; 470 | else if (panEvent.velocityX >= .5) targetIndex ++; 471 | 472 | // Keep the target index within the bounds. 473 | targetIndex = targetIndex >= 0 && targetIndex < self.cardElements.length ? targetIndex : self._targetCardIndex; 474 | } 475 | 476 | self.snap(snapToFullScale ? 1 : self.options.smallScale, targetIndex); 477 | }; 478 | 479 | var calculateDuration = function (current, destination, time) { 480 | var distance = Math.abs(current - destination); 481 | var speed = distance / time; 482 | return distance / speed; 483 | }; 484 | 485 | /** 486 | * Scroll based on the ending velocity. 487 | */ 488 | Cards.prototype._scrollEnded = function (panEvent) { 489 | var self = this; 490 | 491 | self.options.debug && console.log('scroll ended'); 492 | 493 | var time = panEvent.timeStamp - self._initialPanEvent.timeStamp; 494 | 495 | var momentum = (panEvent.velocityX * panEvent.velocityX) / (2 * DECELERATION) * (panEvent.deltaX > 0 ? - 1 : 1); 496 | var momentumDestination = self._translateLeft + momentum; 497 | 498 | // change easing function when scroller goes out of the boundaries 499 | var easing = QUADRATIC_BEZIER; 500 | 501 | if (momentumDestination > self._maxTranslateLeftSmallScale) { 502 | momentumDestination = self._maxTranslateLeftSmallScale + ( window.innerWidth / 2.5 * ( panEvent.velocityX / 8 ) ); 503 | } else if (momentumDestination < 0) { 504 | momentumDestination = window.innerWidth / 2.5 * ( panEvent.velocityX / 8 ); 505 | } else { 506 | easing = CIRCULAR_BEZIER; 507 | } 508 | 509 | var momentumDuration = calculateDuration(self._translateLeft, momentumDestination, time); 510 | 511 | var momentumAnimation = self._animateTransform({ 512 | duration: momentumDuration, 513 | translateLeft: [momentumDestination, easing, self._translateLeft] 514 | }); 515 | 516 | // If we are outside of the boundaries, bounce back in 517 | var destinationInBoundaries = null; 518 | if (momentumDestination > self._maxTranslateLeftSmallScale) destinationInBoundaries = self._maxTranslateLeftSmallScale; 519 | else if (momentumDestination < 0) destinationInBoundaries = 0; 520 | 521 | if (destinationInBoundaries !== null) { 522 | self._animateTransform({ 523 | duration: self.options.bounceTime - momentumDuration, 524 | translateLeft: [destinationInBoundaries, CIRCULAR_BEZIER, momentumDestination] 525 | }, null, momentumAnimation); 526 | } 527 | }; 528 | 529 | Cards.prototype._handleTap = function (tapEvent) { 530 | var self = this; 531 | 532 | var targetCardIndex = calculateTargetCardIndex.call(self, tapEvent.center.x); 533 | if (targetCardIndex === null) return; 534 | 535 | // Snap the card up 536 | var snapUp = self.scale < 1; 537 | if (snapUp) { 538 | self.snap(1, targetCardIndex); 539 | } 540 | 541 | // Trigger an event on the card element 542 | $(self.cardElements[targetCardIndex]).trigger('cardTap', { index: targetCardIndex, snapUp: snapUp }); 543 | }; 544 | 545 | /** 546 | * Snap the scrollview to a scale. 547 | * @param snapToScale The scale to snap to. 548 | * @param [targetIndex] The target index to center (only when snapping to scale 1). 549 | */ 550 | Cards.prototype.snap = function (snapToScale, targetIndex) { 551 | var self = this, 552 | startScale = self.scale; 553 | 554 | self.options.debug && console.log('snap to', snapToScale, 'to card', targetIndex); 555 | 556 | self._scrollingEnabled = snapToScale !== 1; 557 | 558 | if (typeof targetIndex === 'number') { 559 | self._targetCardIndex = targetIndex; 560 | } 561 | 562 | var animateOpts = { 563 | scale: [snapToScale, SPRING, startScale] 564 | }; 565 | 566 | var translateLeft = self._translateLeft; 567 | if (snapToScale === 1) { 568 | translateLeft = (window.innerWidth + self.options.cardMargin) * targetIndex; 569 | } else { 570 | if (Math.abs((startScale - snapToScale) / (1 - self.options.smallScale)) > .5) { 571 | translateLeft = self._smallScaleTranslateLeft; 572 | } 573 | 574 | // Keep translateLeft within bounds 575 | translateLeft = translateLeft > 0 ? translateLeft : 0; 576 | translateLeft = translateLeft < self._maxTranslateLeftSmallScale ? translateLeft : self._maxTranslateLeftSmallScale; 577 | } 578 | 579 | animateOpts.translateLeft = [translateLeft, SPRING, self._translateLeft]; 580 | 581 | self._animateTransform(animateOpts, function () { 582 | if (snapToScale === 1) { 583 | self.$scrollview.trigger('cardExpanded', { 584 | target: self.cardElements[targetIndex], 585 | targetIndex: targetIndex 586 | }); 587 | } 588 | }); 589 | }; 590 | 591 | Cards.prototype._animateTransform = function (animationOpts, complete, chain) { 592 | var self = this; 593 | 594 | if (animationOpts.translateLeft !== undefined) { 595 | animationOpts.translateX = animationOpts.translateLeft; 596 | delete animationOpts.translateLeft; 597 | } 598 | 599 | chain = chain || self.$animationPlaceholder; 600 | 601 | // Use the animation placeholder element with velocity to get the animation values. 602 | return chain.velocity(animationOpts, { 603 | progress: function (elements) { 604 | var style = elements[0].style, 605 | elementTransform = style.transform || style.webkitTransform; 606 | 607 | var newScale; 608 | 609 | if (animationOpts.scale) { 610 | // Extract scale delta from the transform string. 611 | newScale = elementTransform.substring(elementTransform.indexOf('scale(') + 6); 612 | newScale = + newScale.substring(0, newScale.indexOf(')')); 613 | } 614 | 615 | if (animationOpts.translateX) { 616 | var translateStr = elementTransform.substring(elementTransform.indexOf('translateX') + 11); 617 | translateStr = + translateStr.substring(0, translateStr.indexOf('px)')); 618 | 619 | self._translateLeft = translateStr; 620 | } 621 | 622 | self._transform(newScale); 623 | }, 624 | complete: complete 625 | }); 626 | }; 627 | 628 | Cards.prototype._stopAnimation = function () { 629 | this.$animationPlaceholder.velocity('stop'); 630 | }; 631 | 632 | /** 633 | * Call this whenever the # of cards changed so we can update the width. 634 | * Buffer the cards width so the last card cannot scroll past the edge at small scale. 635 | * We care about small scale, because is the only scale the user can natively scroll. 636 | * Above small scale we "scroll" with translateX. 637 | */ 638 | Cards.prototype.changed = function () { 639 | var self = this; 640 | 641 | var cardWidth = window.innerWidth + self.options.cardMargin; 642 | 643 | self.cardElements = self.cards.children; 644 | 645 | if (self.cardElements.length < 3) { 646 | self._maxTranslateLeftSmallScale = 0; 647 | } else { 648 | var thirdCard = cardWidth / 2 + (self.options.cardMargin * 2); 649 | self._maxTranslateLeftSmallScale = thirdCard + (self.cardElements.length - 3) * cardWidth; 650 | } 651 | 652 | var cardsWidth = cardWidth * self.cardElements.length 653 | /* remove the margin from the last card */ 654 | - self.options.cardMargin; 655 | 656 | // The buffer is the extra space on the scrollview. 657 | // It has extra space because it's width is set to fit at minScale. 658 | var bufferWidth = self._scrollviewWidth 659 | /* the width the scrollview should be at small scale */ 660 | - (window.innerWidth / self.options.smallScale); 661 | 662 | // By adding the buffer width we prevent the user from scrolling past the edge. 663 | self.$cards.width(Math.floor(cardsWidth + bufferWidth)); 664 | 665 | // Set the scale and position. 666 | self._translateLeft = cardWidth * self._targetCardIndex; 667 | self._transform(self.scale); 668 | }; 669 | 670 | Cards.prototype.destroy = function () { 671 | var self = this; 672 | self._stopAnimation(); 673 | 674 | // Wait until after the element is removed to prevent a style recalculation. 675 | Deps.afterFlush(function () { 676 | self._hammer.destroy(); 677 | }); 678 | }; -------------------------------------------------------------------------------- /example/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | -------------------------------------------------------------------------------- /example/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /example/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1unonjw1yupbst1ghpwsj 8 | -------------------------------------------------------------------------------- /example/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | autopublish 7 | insecure 8 | jonperl:paper 9 | meteor-platform 10 | less -------------------------------------------------------------------------------- /example/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@0.9.3.1 2 | -------------------------------------------------------------------------------- /example/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.2 2 | autopublish@1.0.0 3 | autoupdate@1.1.1 4 | base64@1.0.0 5 | binary-heap@1.0.0 6 | blaze-tools@1.0.0 7 | blaze@2.0.1 8 | boilerplate-generator@1.0.0 9 | callback-hook@1.0.0 10 | check@1.0.1 11 | ctl-helper@1.0.3 12 | ctl@1.0.1 13 | ddp@1.0.9 14 | deps@1.0.4 15 | ejson@1.0.3 16 | fastclick@1.0.0 17 | follower-livedata@1.0.1 18 | geojson-utils@1.0.0 19 | html-tools@1.0.1 20 | htmljs@1.0.1 21 | http@1.0.6 22 | id-map@1.0.0 23 | insecure@1.0.0 24 | jonperl:paper@0.0.1 25 | jquery@1.0.0 26 | json@1.0.0 27 | less@1.0.9 28 | livedata@1.0.10 29 | logging@1.0.3 30 | meteor-platform@1.1.1 31 | meteor@1.1.1 32 | minifiers@1.1.0 33 | minimongo@1.0.3 34 | mobile-status-bar@1.0.0 35 | mongo@1.0.6 36 | observe-sequence@1.0.2 37 | ordered-dict@1.0.0 38 | random@1.0.0 39 | reactive-dict@1.0.3 40 | reactive-var@1.0.2 41 | reload@1.1.0 42 | retry@1.0.0 43 | routepolicy@1.0.1 44 | session@1.0.2 45 | spacebars-compiler@1.0.2 46 | spacebars@1.0.2 47 | stevezhu:velocity.js@0.1.0 48 | templating@1.0.7 49 | tracker@1.0.2 50 | ui@1.0.3 51 | underscore@1.0.0 52 | url@1.0.0 53 | webapp-hashing@1.0.0 54 | webapp@1.1.2 55 | -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 |
2 |