├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── scrollability.js └── static ├── examples ├── pages.html ├── s1.jpg ├── s2.jpg ├── s3.jpg ├── s4.jpg ├── s5.jpg └── tableview.html ├── scrollbar-btm.png ├── scrollbar-mid.png ├── scrollbar-top.png ├── scrollbar.css └── scrollbar.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Joe Hewitt 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scrollability 2 | ============= 3 | 4 | Scrollability adds a good imitation of iOS native scrolling to your mobile web apps. 5 | 6 | Scrollability is a single script, it's small, and it has no external dependencies. Drop it into your page, add a few CSS classes to scrollable elements, and scroll away. 7 | 8 | Status 9 | ------ 10 | 11 | This project is a work-in-progress, but I hope to have a stable, documented release in time for iOS 5. Stay tuned! 12 | 13 | Plan 14 | ---- 15 | 16 | As of this writing, Scrollability supports only basic vertical or horizontal scrolling. Future plans include: 17 | 18 | * Snapping to pages 19 | * Sticky table headers 20 | * Photo browser 21 | * More customization of the animation details 22 | 23 | License 24 | ------- 25 | 26 | Copyright 2011 Joe Hewitt 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollability", 3 | "description": "Native-like scrolling for the web", 4 | "version": "0.0.3", 5 | "homepage": "http://github.com/joehewitt/scrollability", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/joehewitt/scrollability.git" 9 | }, 10 | "keywords": [ 11 | "client" 12 | ], 13 | "author": "Joe Hewitt ", 14 | "contributors": [], 15 | "dependencies": {}, 16 | "engines": { 17 | "node": ">=0.4.0" 18 | }, 19 | "main": "./scrollability", 20 | "directories": {}, 21 | "app.js": { 22 | "static": "./static" 23 | }, 24 | "devDependencies": {}, 25 | "optionalDependencies": {} 26 | } 27 | -------------------------------------------------------------------------------- /scrollability.js: -------------------------------------------------------------------------------- 1 | /* See LICENSE for terms of usage */ 2 | 3 | "style scrollability/scrollbar.css" 4 | 5 | // var logs = []; 6 | 7 | // function D() { 8 | // var args = []; args.push.apply(args, arguments); 9 | // console.log(args.join(' ')); 10 | // // logs.push(args.join(' ')); 11 | // } 12 | 13 | // window.showLog = function() { 14 | // document.querySelector('.scrollable').innerHTML = logs.join('
'); 15 | // document.querySelector('.scrollable').style.webkitAnimation = ''; 16 | // document.querySelector('.scrollable').style.webkitTransform = 'translate3d(0,0,0)'; 17 | // } 18 | 19 | // ************************************************************************************************* 20 | 21 | var isWebkit = "webkitTransform" in document.documentElement.style; 22 | var isiOS5 = isWebkit && /OS 5_/.exec(navigator.userAgent); 23 | var isTouch = "ontouchstart" in window; 24 | 25 | // ************************************************************************************************* 26 | 27 | // The friction applied while decelerating 28 | var kFriction = 0.9925; 29 | 30 | // If the velocity is below this threshold when the finger is released, animation will stop 31 | var kStoppedThreshold = 4; 32 | 33 | // Number of pixels finger must move to determine horizontal or vertical motion 34 | var kLockThreshold = 10; 35 | 36 | // Percentage of the page which content can be overscrolled before it must bounce back 37 | var kBounceLimit = 0.75; 38 | 39 | // Rate of deceleration when content has overscrolled and is slowing down before bouncing back 40 | var kBounceDecelRate = 0.01; 41 | 42 | // Duration of animation when bouncing back 43 | var kBounceTime = 240; 44 | var kPageBounceTime = 160; 45 | 46 | // Percentage of viewport which must be scrolled past in order to snap to the next page 47 | var kPageLimit = 0.5; 48 | 49 | // Velocity at which the animation will advance to the next page 50 | var kPageEscapeVelocity = 2; 51 | 52 | // Vertical margin of scrollbar 53 | var kScrollbarMargin = 2; 54 | 55 | // The width or height of the scrollbar along the animated axis 56 | var kScrollbarSize = 7; 57 | 58 | // The number of milliseconds to increment while simulating animation 59 | var kAnimationStep = 4; 60 | 61 | // The number of milliseconds of animation to condense into a keyframe 62 | var kKeyframeIncrement = 24; 63 | 64 | // ************************************************************************************************* 65 | 66 | var startX, startY, touchX, touchY, touchMoved; 67 | var animationInterval = 0; 68 | var touchAnimators = []; 69 | var animationIndex = 0; 70 | var globalStyleSheet; 71 | 72 | var directions = { 73 | 'horizontal': createXDirection, 74 | 'vertical': createYDirection 75 | }; 76 | 77 | exports.directions = directions; 78 | 79 | exports.flashIndicators = function() { 80 | // var scrollables = document.querySelectorAll('.scrollable.vertical'); 81 | // for (var i = 0; i < scrollables.length; ++i) { 82 | // exports.scrollTo(scrollables[i], 0, 0, 20, true); 83 | // } 84 | } 85 | 86 | function onLoad() { 87 | var ss = document.createElement("style"); 88 | document.head.appendChild(ss); 89 | globalStyleSheet = document.styleSheets[document.styleSheets.length-1]; 90 | 91 | // exports.flashIndicators(); 92 | } 93 | 94 | require.ready(function() { 95 | document.addEventListener(isTouch ? 'touchstart' : 'mousedown', onTouchStart, false); 96 | window.addEventListener('load', onLoad, false); 97 | }); 98 | 99 | function onTouchStart(event) { 100 | var touch = isTouch ? event.touches[0] : event; 101 | var touched = null; 102 | 103 | touchX = startX = touch.clientX; 104 | touchY = startY = touch.clientY; 105 | touchMoved = false; 106 | 107 | touchAnimators = getTouchAnimators(event.target, touchX, touchY, event.timeStamp); 108 | if (!touchAnimators.length) { 109 | return true; 110 | } 111 | 112 | var touchCandidate = event.target; 113 | var holdTimeout = setTimeout(function() { 114 | holdTimeout = 0; 115 | touched = setTouched(touchCandidate); 116 | }, 50); 117 | 118 | document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onTouchMove, false); 119 | document.addEventListener(isTouch ? 'touchend' : 'mouseup', onTouchEnd, false); 120 | 121 | // if (D) event.preventDefault(); 122 | 123 | function onTouchMove(event) { 124 | event.preventDefault(); 125 | touchMoved = true; 126 | 127 | if (holdTimeout) { 128 | clearTimeout(holdTimeout); 129 | holdTimeout = 0; 130 | } 131 | if (touched) { 132 | releaseTouched(touched); 133 | touched = null; 134 | } 135 | 136 | var touch = isTouch ? event.touches[0] : event; 137 | touchX = touch.clientX; 138 | touchY = touch.clientY; 139 | 140 | // Reduce the candidates down to the one whose axis follows the finger most closely 141 | if (touchAnimators.length > 1) { 142 | for (var i = 0; i < touchAnimators.length; ++i) { 143 | var animator = touchAnimators[i]; 144 | if (animator.disable && animator.disable(touchX, touchY, startX, startY)) { 145 | animator.terminate(); 146 | touchAnimators.splice(i--, 1); 147 | 148 | if (touchAnimators.length == 1) { 149 | var locked = touchAnimators[0]; 150 | dispatch("scrollability-lock", locked.node, {direction: locked.direction}); 151 | } 152 | } 153 | } 154 | } 155 | 156 | touchAnimators.forEach(function(animator) { 157 | var touch = animator.filter(touchX, touchY); 158 | animator.track(touch, event.timeStamp); 159 | }); 160 | } 161 | 162 | function onTouchEnd(event) { 163 | // Simulate a click event when releasing the finger 164 | if (touched) { 165 | var evt = document.createEvent('MouseEvents'); 166 | evt.initMouseEvent('click', true, true, window, 1); 167 | touched[0].dispatchEvent(evt); 168 | releaseTouched(touched); 169 | } 170 | 171 | document.removeEventListener(isTouch ? 'touchmove' : 'mousemove', onTouchMove, false); 172 | document.removeEventListener(isTouch ? 'touchend' : 'mouseup', onTouchEnd, false); 173 | 174 | touchAnimators.forEach(function(animator) { 175 | animator.takeoff(); 176 | }); 177 | } 178 | } 179 | 180 | function wrapAnimator(animator, startX, startY, startTime) { 181 | var node = animator.node; 182 | var constrained = animator.constrained; 183 | var paginated = animator.paginated; 184 | var viewport = animator.viewport || 0; 185 | var scrollbar = animator.scrollbar; 186 | var position = animator.position; 187 | var min = animator.min; 188 | var max = animator.max; 189 | var absMin = min; 190 | var absMax = Math.round(max/viewport)*viewport; 191 | var pageSpacing = 0; 192 | var velocity = 0; 193 | var bounceTime = paginated ? kPageBounceTime : kBounceTime; 194 | var bounceLimit = animator.bounce; 195 | var pageLimit = viewport * kPageLimit; 196 | var lastTouch = startTouch = animator.filter(startX, startY); 197 | var lastTime = startTime; 198 | var timeStep = 0; 199 | var stopped = 0; 200 | var tracked = []; 201 | var offset = node.scrollableOffset||0; 202 | 203 | if (!animator.mute) { 204 | var event = { 205 | position: position, 206 | min: min, 207 | max: max, 208 | track: addTracker, 209 | setSpacing: setSpacing, 210 | setOffset: setOffset, 211 | setBounds: setBounds 212 | }; 213 | if (!dispatch("scrollability-start", node, event)) { 214 | return null; 215 | } 216 | } 217 | 218 | if (paginated) { 219 | if (pageSpacing === undefined) { 220 | var excess = Math.round(Math.abs(absMin) % viewport); 221 | var pageCount = ((Math.abs(absMin)-excess) / viewport)+1; 222 | pageSpacing = excess / pageCount; 223 | } 224 | 225 | var pageIndex = Math.round(position/(viewport+pageSpacing)); 226 | min = max = pageIndex * (viewport+pageSpacing); 227 | absMin += pageSpacing; 228 | } 229 | 230 | if (scrollbar) { 231 | addTracker(scrollbar, trackScrollbar); 232 | if (!scrollbar.parentNode) { 233 | node.parentNode.appendChild(scrollbar); 234 | } 235 | } 236 | 237 | if (node.earlyEnd) { 238 | play(node); 239 | tracked.forEach(function(item) { 240 | play(item.node); 241 | }); 242 | 243 | node.earlyEnd(); 244 | 245 | update(position); 246 | } 247 | 248 | animator.reposition = update; 249 | animator.track = track; 250 | animator.takeoff = takeoff; 251 | animator.terminate = terminate; 252 | return animator; 253 | 254 | function addTracker(node, callback) { 255 | tracked.push({node: node, callback: callback, keyframes: []}); 256 | } 257 | 258 | function setSpacing(x) { 259 | pageSpacing = x 260 | } 261 | 262 | function setOffset(x) { 263 | offset = x; 264 | 265 | track(lastTouch, lastTime); 266 | } 267 | 268 | function setBounds(newMin, newMax) { 269 | min = newMin; 270 | max = newMax; 271 | } 272 | 273 | function track(touch, time) { 274 | timeStep = time - lastTime; 275 | lastTime = time; 276 | 277 | velocity = touch - lastTouch; 278 | lastTouch = touch; 279 | 280 | if (Math.abs(velocity) >= kStoppedThreshold) { 281 | if (stopped) { 282 | --stopped; 283 | } 284 | stopped = 0; 285 | } else { 286 | ++stopped; 287 | } 288 | 289 | // Apply resistance along the edges 290 | if (constrained) { 291 | if (position > max && absMax == max) { 292 | var excess = position - max; 293 | velocity *= (1.0 - excess / bounceLimit)*kBounceLimit; 294 | } else if (position < min && absMin == min) { 295 | var excess = min - position; 296 | velocity *= (1.0 - excess / bounceLimit)*kBounceLimit; 297 | } 298 | } 299 | 300 | position += velocity; 301 | 302 | update(position); 303 | 304 | node.style.webkitAnimationName = ''; 305 | tracked.forEach(function(item) { 306 | item.node.style.webkitAnimationName = ''; 307 | }); 308 | return true; 309 | } 310 | 311 | function trackScrollbar(position) { 312 | var range = max - min; 313 | if (scrollbar && min < 0) { 314 | var viewable = viewport - kScrollbarMargin*2; 315 | var height = (viewable/(range+viewport)) * viewable; 316 | var scrollPosition; 317 | if (position > max) { 318 | height = Math.max(height - (position-max), kScrollbarSize); 319 | scrollPosition = 0; 320 | } else if (position < min) { 321 | var h = height - (min - position); 322 | height = Math.max(height - (min - position), kScrollbarSize); 323 | scrollPosition = viewable-height; 324 | } else { 325 | scrollPosition = (Math.abs((max-position)) / range) * (viewable-height); 326 | } 327 | scrollPosition += kScrollbarMargin; 328 | 329 | return 'translate3d(0, ' + Math.round(scrollPosition) + 'px, 0) ' 330 | + 'scaleY(' + Math.round(height) + ')'; 331 | } 332 | } 333 | 334 | function takeoff() { 335 | dispatch("scrollability-takeoff", node, { 336 | position: position, 337 | min: min, 338 | max: max, 339 | setBounds: setBounds 340 | }); 341 | 342 | if (stopped) { 343 | velocity = 0; 344 | } 345 | 346 | position += velocity; 347 | update(position); 348 | 349 | velocity = (velocity/timeStep) * kAnimationStep; 350 | 351 | var timeline = createTimeline(); 352 | if (!timeline.time) { 353 | terminate(); 354 | return; 355 | } 356 | 357 | dispatch("scrollability-animate", node, { 358 | direction: animator.direction, 359 | time: timeline.time, 360 | keyframes: timeline.keyframes 361 | }); 362 | 363 | if (node.cleanup) { 364 | node.cleanup(); 365 | } 366 | 367 | globalStyleSheet.insertRule(timeline.css, 0); 368 | 369 | tracked.forEach(function(item, i) { 370 | item.name = 'scrollability-track'+(animationIndex++); 371 | var css = generateCSSKeyframes(animator, item.keyframes, item.name, timeline.time); 372 | globalStyleSheet.insertRule(css, 0); 373 | }); 374 | 375 | node.earlyEnd = function() { 376 | terminex(true); 377 | } 378 | node.normalEnd = function() { 379 | reposition(timeline.keyframes[timeline.keyframes.length-1].position); 380 | terminex(); 381 | } 382 | 383 | node.cleanup = function() { 384 | delete node.cleanup; 385 | globalStyleSheet.deleteRule(0); 386 | tracked.forEach(function(item) { 387 | globalStyleSheet.deleteRule(0); 388 | }); 389 | } 390 | 391 | node.addEventListener("webkitAnimationEnd", node.normalEnd, false); 392 | 393 | play(node, timeline.name, timeline.time); 394 | 395 | tracked.forEach(function(item) { 396 | play(item.node, item.name, timeline.time); 397 | }); 398 | } 399 | 400 | function createTimeline() { 401 | var time = 0; 402 | var lastPosition = position; 403 | var lastKeyTime = 0; 404 | var lastDiff = 0; 405 | var decelOrigin; 406 | var decelDelta; 407 | var decelStep = 0; 408 | var decelTime; 409 | // var enterVelocity; 410 | var keyframes = []; 411 | 412 | if (paginated) { 413 | // When finger is released, decide whether to jump to next/previous page 414 | // or to snap back to the current page 415 | if (Math.abs(position - max) > pageLimit || Math.abs(velocity) > kPageEscapeVelocity) { 416 | if (position > max) { 417 | if (max != absMax) { 418 | max += viewport+pageSpacing; 419 | min += viewport+pageSpacing; 420 | 421 | // XXXjoe Only difference between this and code below is -viewport. Merge 'em! 422 | var totalSpacing = min % viewport; 423 | var page = -Math.round((position+viewport-totalSpacing)/(viewport+pageSpacing)); 424 | dispatch("scrollability-page", animator.node, {page: page}); 425 | } 426 | } else { 427 | if (min != absMin) { 428 | max -= viewport+pageSpacing; 429 | min -= viewport+pageSpacing; 430 | 431 | var totalSpacing = min % viewport; 432 | var page = -Math.round((position-viewport-totalSpacing)/(viewport+pageSpacing)); 433 | dispatch("scrollability-page", animator.node, {page: page}); 434 | } 435 | } 436 | } 437 | } 438 | 439 | var continues = true; 440 | while (continues) { 441 | if (position > max && constrained) { 442 | if (velocity > 0) { 443 | // Slowing down 444 | var excess = position - max; 445 | var elasticity = (1.0 - excess / bounceLimit); 446 | velocity = Math.max(velocity - kBounceDecelRate, 0) * elasticity; 447 | // D&&D('slowing down', velocity); 448 | position += velocity; 449 | } else { 450 | // Bouncing back 451 | if (!decelStep) { 452 | decelOrigin = position; 453 | decelDelta = max - position; 454 | } 455 | // D&&D('bouncing back'); 456 | position = easeOutExpo(decelStep, decelOrigin, decelDelta, bounceTime); 457 | continues = ++decelStep <= bounceTime && Math.floor(Math.abs(position)) > max; 458 | } 459 | } else if (position < min && constrained) { 460 | if (velocity < 0) { 461 | // if (!enterVelocity) { 462 | // enterVelocity = velocity; 463 | // } 464 | // Slowing down 465 | var excess = min - position; 466 | var elasticity = (1.0 - excess / bounceLimit); 467 | velocity = Math.min(velocity + kBounceDecelRate, 0) * elasticity; 468 | position += velocity; 469 | } else { 470 | // Bouncing back 471 | if (!decelStep) { 472 | decelOrigin = position; 473 | decelDelta = min - position; 474 | // XXXjoe Record velocity when going past limit, use to shrink bounceTime 475 | // decelTime = bounceTime * (-enterVelocity / 10); 476 | // D&&D(decelTime); 477 | } 478 | position = easeOutExpo(decelStep, decelOrigin, decelDelta, bounceTime); 479 | continues = ++decelStep <= bounceTime && Math.ceil(position) < min; 480 | } 481 | } else { 482 | continues = Math.floor(Math.abs(velocity)*10) > 0; 483 | if (!continues) 484 | break; 485 | 486 | velocity *= kFriction; 487 | position += velocity; 488 | } 489 | 490 | saveKeyframe(!continues); 491 | time += kAnimationStep; 492 | } 493 | 494 | if (paginated) { 495 | var pageIndex = Math.round(position/(viewport+pageSpacing)); 496 | position = pageIndex * (viewport+pageSpacing); 497 | saveKeyframe(true); 498 | } else if (position > max && constrained) { 499 | position = max; 500 | saveKeyframe(true); 501 | } else if (position < min && constrained) { 502 | position = min; 503 | saveKeyframe(true); 504 | } 505 | 506 | var totalTime = keyframes.length ? keyframes[keyframes.length-1].time : 0; 507 | 508 | var name = "scrollability" + (animationIndex++); 509 | var css = generateCSSKeyframes(animator, keyframes, name, totalTime, offset); 510 | 511 | return {time: totalTime, position: position, keyframes: keyframes, name: name, css: css}; 512 | 513 | function saveKeyframe(force) { 514 | var diff = position - lastPosition; 515 | // Add a new frame when we've changed direction, or passed the prescribed granularity 516 | if (force || (time-lastKeyTime >= kKeyframeIncrement || (lastDiff < 0 != diff < 0))) { 517 | keyframes.push({position: position, time: time}); 518 | 519 | tracked.forEach(function(item) { 520 | item.keyframes.push({time: time, css: item.callback(position)}); 521 | }); 522 | 523 | lastDiff = diff; 524 | lastPosition = position; 525 | lastKeyTime = time; 526 | } 527 | } 528 | } 529 | 530 | function update(pos) { 531 | if (!dispatch("scrollability-scroll", node, 532 | {direction: animator.direction, position: pos, max: max, min: min})) { 533 | return; 534 | } 535 | 536 | reposition(pos); 537 | 538 | if (scrollbar && touchMoved) { 539 | fadeIn(scrollbar); 540 | } 541 | } 542 | 543 | function reposition(pos) { 544 | // D&&D('move to', pos, offset); 545 | node.style.webkitTransform = animator.update(pos+offset); 546 | node.scrollableOffset = offset; 547 | 548 | tracked.forEach(function(item) { 549 | item.node.style.webkitTransform = item.callback(pos); 550 | }); 551 | } 552 | 553 | function terminex(showScrollbar) { 554 | if (scrollbar) { 555 | if (showScrollbar) { 556 | fadeIn(scrollbar); 557 | } else { 558 | scrollbar.style.opacity = '0'; 559 | scrollbar.style.webkitTransition = 'opacity 0.33s linear'; 560 | } 561 | } 562 | 563 | node.removeEventListener("webkitAnimationEnd", node.normalEnd, false); 564 | 565 | delete node.earlyEnd; 566 | delete node.normalEnd; 567 | 568 | if (!animator.mute) { 569 | dispatch("scrollability-end", node); 570 | } 571 | 572 | } 573 | 574 | function terminate() { 575 | terminex(); 576 | } 577 | } 578 | 579 | // ************************************************************************************************* 580 | 581 | function getTouchAnimators(node, touchX, touchY, startTime) { 582 | var animators = []; 583 | 584 | // Get universally scrollable elements 585 | var candidates = document.querySelectorAll('.scrollable.universal'); 586 | for (var j = 0; j < candidates.length; ++j) { 587 | findAnimators(candidates[j], animators, touchX, touchY, startTime); 588 | } 589 | 590 | if (!candidates.length) { 591 | // Find scrollable nodes that were directly touched 592 | findAnimators(node, animators, touchX, touchY, startTime); 593 | } 594 | 595 | return animators; 596 | } 597 | 598 | function findAnimators(element, animators, touchX, touchY, startTime) { 599 | while (element) { 600 | if (element.nodeType == 1) { 601 | var animator = createAnimatorForElement(element, touchX, touchY, startTime); 602 | if (animator) { 603 | // Look out for duplicates 604 | var exists = false; 605 | for (var j = 0; j < animators.length; ++j) { 606 | if (animators[j].node == element) { 607 | exists = true; 608 | break; 609 | } 610 | } 611 | if (!exists) { 612 | animator = wrapAnimator(animator, touchX, touchY, startTime); 613 | if (animator) { 614 | animators.push(animator); 615 | } 616 | } 617 | } 618 | } 619 | element = element.parentNode; 620 | } 621 | } 622 | 623 | function createAnimatorForElement(element, touchX, touchY, startTime) { 624 | var classes = element.className.split(' '); 625 | if (classes.indexOf("scrollable") == -1) 626 | return; 627 | 628 | for (var i = 0; i < classes.length; ++i) { 629 | var name = classes[i]; 630 | if (directions[name]) { 631 | var animator = directions[name](element); 632 | animator.direction = name; 633 | animator.paginated = classes.indexOf('paginated') != -1; 634 | return animator; 635 | } 636 | } 637 | } 638 | 639 | function generateCSSKeyframes(animator, keyframes, name, time, offset) { 640 | var lines = ['@-webkit-keyframes ' + name + ' {']; 641 | 642 | keyframes.forEach(function(keyframe) { 643 | var percent = (keyframe.time / time) * 100; 644 | var frame = Math.floor(percent) + '% {' 645 | + '-webkit-transform: ' + (keyframe.css || animator.update(keyframe.position+offset)) + ';' 646 | + '}'; 647 | // D&&D(frame); 648 | lines.push(frame); 649 | }); 650 | 651 | lines.push('}'); 652 | 653 | return lines.join('\n'); 654 | } 655 | 656 | function setTouched(target) { 657 | var touched = []; 658 | for (var n = target; n; n = n.parentNode) { 659 | if (n.nodeType == 1) { 660 | n.className = (n.className ? n.className + ' ' : '') + 'touched'; 661 | touched.push(n); 662 | } 663 | } 664 | return touched; 665 | } 666 | 667 | function releaseTouched(touched) { 668 | for (var i = 0; i < touched.length; ++i) { 669 | var n = touched[i]; 670 | n.className = n.className.replace('touched', ''); 671 | } 672 | } 673 | 674 | function initScrollbar(element) { 675 | if (!element.scrollableScrollbar) { 676 | var scrollbar = element.scrollableScrollbar = document.createElement('div'); 677 | scrollbar.className = 'scrollability-scrollbar'; 678 | } 679 | return element.scrollableScrollbar; 680 | } 681 | 682 | function easeOutExpo(t, b, c, d) { 683 | return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; 684 | } 685 | 686 | // ************************************************************************************************* 687 | 688 | function createXDirection(node) { 689 | var parent = node.parentNode; 690 | var clipper = node.querySelector(".scrollable > .clipper") || node; 691 | 692 | // Necessary to pause animation in order to get correct transform value 693 | if (node.style.webkitAnimation) { 694 | node.style.webkitAnimationPlayState = "paused"; 695 | } 696 | var transform = getComputedStyle(node).webkitTransform; 697 | var position = new WebKitCSSMatrix(transform).m41 - (node.scrollableOffset||0); 698 | 699 | return { 700 | node: node, 701 | min: -clipper.offsetWidth + parent.offsetWidth, 702 | max: 0, 703 | position: position, 704 | viewport: parent.offsetWidth, 705 | bounce: parent.offsetWidth * kBounceLimit, 706 | constrained: true, 707 | 708 | filter: function(x, y) { 709 | return x; 710 | }, 711 | 712 | disable: function (x, y, startX, startY) { 713 | var dx = Math.abs(x - startX); 714 | var dy = Math.abs(y - startY); 715 | if (dy > dx && dy > kLockThreshold) { 716 | return true; 717 | } 718 | }, 719 | 720 | update: function(position) { 721 | return 'translate3d(' + Math.round(position) + 'px, 0, 0)'; 722 | } 723 | }; 724 | } 725 | 726 | function createYDirection(node) { 727 | var parent = node.parentNode; 728 | var clipper = node.querySelector(".scrollable > .clipper") || node; 729 | 730 | // Necessary to pause animation in order to get correct transform value 731 | if (node.style.webkitAnimation) { 732 | node.style.webkitAnimationPlayState = "paused"; 733 | } 734 | 735 | var transform = getComputedStyle(node).webkitTransform; 736 | var position = new WebKitCSSMatrix(transform).m42; 737 | // D&&D('start ' + position); 738 | 739 | return { 740 | node: node, 741 | scrollbar: initScrollbar(node), 742 | position: position, 743 | min: -clipper.offsetHeight + parent.offsetHeight, 744 | max: 0, 745 | viewport: parent.offsetHeight, 746 | bounce: parent.offsetHeight * kBounceLimit, 747 | constrained: true, 748 | 749 | filter: function(x, y) { 750 | return y; 751 | }, 752 | 753 | disable: function(x, y, startX, startY) { 754 | var dx = Math.abs(x - startX); 755 | var dy = Math.abs(y - startY); 756 | if (dx > dy && dx > kLockThreshold) { 757 | return true; 758 | } 759 | }, 760 | 761 | update: function(position) { 762 | return 'translate3d(0, ' + Math.round(position) + 'px, 0)'; 763 | } 764 | }; 765 | } 766 | 767 | function play(node, name, time) { 768 | if (name) { 769 | node.style.webkitAnimation = name + " " + time + "ms linear both"; 770 | } 771 | node.style.webkitAnimationPlayState = name ? "running" : "paused"; 772 | } 773 | 774 | function fadeIn(node) { 775 | node.style.webkitTransition = ''; 776 | node.style.opacity = '1'; 777 | } 778 | 779 | function dispatch(name, target, props) { 780 | var e = document.createEvent("Events"); 781 | e.initEvent(name, false, true); 782 | 783 | if (props) { 784 | for (var name in props) { 785 | e[name] = props[name]; 786 | } 787 | } 788 | 789 | return target.dispatchEvent(e); 790 | } 791 | -------------------------------------------------------------------------------- /static/examples/pages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scrollability 6 | 7 | 8 | 9 | 10 | 44 | 45 | 46 | 47 | 91 | 92 |
93 | 94 | -------------------------------------------------------------------------------- /static/examples/s1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/examples/s1.jpg -------------------------------------------------------------------------------- /static/examples/s2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/examples/s2.jpg -------------------------------------------------------------------------------- /static/examples/s3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/examples/s3.jpg -------------------------------------------------------------------------------- /static/examples/s4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/examples/s4.jpg -------------------------------------------------------------------------------- /static/examples/s5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/examples/s5.jpg -------------------------------------------------------------------------------- /static/examples/tableview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scrollability 6 | 7 | 8 | 9 | 10 | 81 | 82 | 83 | 84 | 164 | 165 | 166 | 167 |
Scrollability Demo
168 |
Footer
169 |
170 |
171 |
172 | 173 | 174 | -------------------------------------------------------------------------------- /static/scrollbar-btm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/scrollbar-btm.png -------------------------------------------------------------------------------- /static/scrollbar-mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/scrollbar-mid.png -------------------------------------------------------------------------------- /static/scrollbar-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/scrollbar-top.png -------------------------------------------------------------------------------- /static/scrollbar.css: -------------------------------------------------------------------------------- 1 | 2 | .scrollable { 3 | -webkit-transform: translate3d(0,0,0); 4 | } 5 | 6 | .scrollability-scrollbar { 7 | position: absolute; 8 | top: 0; 9 | right: 2px; 10 | width: 7px; 11 | height: 1px; 12 | z-index: 2147483647; 13 | opacity: 0; 14 | -webkit-transform: translate3d(0,0,0); 15 | -webkit-box-sizing: border-box; 16 | -webkit-transform-origin: top left; 17 | /*-webkit-border-image: url(scrollbar.png) 6 2 6 2 / 3px 1px 3px 1px round round;*/ 18 | background: url(scrollbar-mid.png) no-repeat; 19 | } 20 | -------------------------------------------------------------------------------- /static/scrollbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joehewitt/scrollability/fed4b023d10401a527c4797461c84b43f8797000/static/scrollbar.png --------------------------------------------------------------------------------