Skip to content

Commit

Permalink
* Clean up some of the slop - still sloppy, but physics feel good now
Browse files Browse the repository at this point in the history
  • Loading branch information
joehewitt committed Sep 17, 2011
1 parent 848f3c1 commit bdf5c68
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 129 deletions.
162 changes: 35 additions & 127 deletions scrollability.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -25,20 +25,24 @@ var isTouch = "ontouchstart" in window;


var kAnimationStep = 4; var kAnimationStep = 4;


var kFrameGranularity = 96;

var kFriction = 0.99;

// Number of pixels finger must move to determine horizontal or vertical motion // Number of pixels finger must move to determine horizontal or vertical motion
var kLockThreshold = 10; var kLockThreshold = 10;


// Maximum velocity for motion after user releases finger // Maximum velocity for motion after user releases finger
var kMaxVelocity = 12 / (window.devicePixelRatio||1); var kMaxVelocity = 9935;


// Percentage of the page which content can be overscrolled before it must bounce back // Percentage of the page which content can be overscrolled before it must bounce back
var kBounceLimit = 0.5; var kBounceLimit = 0.6;


// Rate of deceleration when content has overscrolled and is slowing down before bouncing back // Rate of deceleration when content has overscrolled and is slowing down before bouncing back
var kBounceDecelRate = 0.01; var kBounceDecelRate = 0.01;


// Duration of animation when bouncing back // Duration of animation when bouncing back
var kBounceTime = 200; var kBounceTime = 220;
var kPageBounceTime = 60; var kPageBounceTime = 60;


// Percentage of viewport which must be scrolled past in order to snap to the next page // Percentage of viewport which must be scrolled past in order to snap to the next page
Expand Down Expand Up @@ -83,7 +87,6 @@ exports.scrollToTop = function() {
exports.scrollTo(scrollable, 0, 0, kScrollToTopTime); exports.scrollTo(scrollable, 0, 0, kScrollToTopTime);
} }
} }

} }


exports.scrollTo = function(element, x, y, animationTime) { exports.scrollTo = function(element, x, y, animationTime) {
Expand Down Expand Up @@ -138,54 +141,39 @@ function onOrientationChange(event) {
} }


function onTouchStart(event) { function onTouchStart(event) {
D&&D(event.timeStamp);
stopAnimation(); stopAnimation();


var touchCandidate = event.target;
var touch = event.touches[0]; var touch = event.touches[0];
var touched = null; var touched = null;
var startTime = new Date().getTime();


touchX = startX = touch.clientX; touchX = startX = touch.clientX;
touchY = startY = touch.clientY; touchY = startY = touch.clientY;
touchDown = true; touchDown = true;
touchMoved = false; touchMoved = false;
D&&D('touch', touchY);


touchAnimators = getTouchAnimators(event.target, touchX, touchY, startTime); touchAnimators = getTouchAnimators(event.target, touchX, touchY, event.timeStamp);
if (!touchAnimators.length && !exports.globalScrolling) { if (!touchAnimators.length && !exports.globalScrolling) {
return true; return true;
} }


var holdTimeout = setTimeout(function() {
holdTimeout = 0;
touched = setTouched(touchCandidate);
}, 50);

var d = document; var d = document;
d.addEventListener('touchmove', onTouchMove, false); d.addEventListener('touchmove', onTouchMove, false);
d.addEventListener('touchend', onTouchEnd, false); d.addEventListener('touchend', onTouchEnd, false);


animationInterval = setInterval(touchAnimation, 0); // XXXjoe DEBUG! REMOVE ME!
event.preventDefault(); if (D) event.preventDefault();


function onTouchMove(event) { function onTouchMove(event) {
D&&D(event.timeStamp);
event.preventDefault(); event.preventDefault();
touchMoved = true; touchMoved = true;


if (holdTimeout) {
clearTimeout(holdTimeout);
holdTimeout = 0;
}
if (touched) { if (touched) {
releaseTouched(touched); releaseTouched(touched);
touched = null; touched = null;
} }
var touch = event.touches[0]; var touch = event.touches[0];
touchX = touch.clientX; touchX = touch.clientX;
touchY = touch.clientY; touchY = touch.clientY;
D&&D('touch', touchY);


// Reduce the candidates down to the one whose axis follows the finger most closely // Reduce the candidates down to the one whose axis follows the finger most closely
if (touchAnimators.length > 1) { if (touchAnimators.length > 1) {
Expand All @@ -198,14 +186,11 @@ function onTouchStart(event) {
} }
} }
} }

touchAnimation(event.timeStamp);
} }


function onTouchEnd(event) { function onTouchEnd(event) {
if (holdTimeout) {
clearTimeout(holdTimeout);
holdTimeout = 0;
}

// Simulate a click event when releasing the finger // Simulate a click event when releasing the finger
if (touched) { if (touched) {
var evt = document.createEvent('MouseEvents'); var evt = document.createEvent('MouseEvents');
Expand All @@ -217,14 +202,13 @@ function onTouchStart(event) {
d.removeEventListener('touchmove', onTouchMove, false); d.removeEventListener('touchmove', onTouchMove, false);
d.removeEventListener('touchend', onTouchEnd, false); d.removeEventListener('touchend', onTouchEnd, false);
touchDown = false; touchDown = false;

touchAnimation(event.timeStamp);
} }
} }


function wrapAnimator(animator, startX, startY, startTime) { function wrapAnimator(animator, startX, startY, startTime) {
if (animator.node.cleanup) { animator.node.style.webkitAnimationPlayState = "paused";
animator.node.style.webkitAnimationPlayState = "paused";
// animator.node.cleanup(null, true);
}


var trans = getComputedStyle(animator.node).webkitTransform; var trans = getComputedStyle(animator.node).webkitTransform;
var y = new WebKitCSSMatrix(trans).m42; var y = new WebKitCSSMatrix(trans).m42;
Expand All @@ -249,11 +233,11 @@ function wrapAnimator(animator, startX, startY, startTime) {
var pageLimit = viewport * kPageLimit; var pageLimit = viewport * kPageLimit;
var lastTouch = startTouch = animator.filter(startX, startY); var lastTouch = startTouch = animator.filter(startX, startY);
var lastTime = startTime; var lastTime = startTime;
var lastStep = 0;
var stillTime = 0; var stillTime = 0;
var stillThreshold = 20; var stillThreshold = 20;
var snapped = false; var snapped = false;
var locked = false; var locked = false;
var deltas = [];


var startPosition = position; var startPosition = position;


Expand All @@ -279,43 +263,19 @@ function wrapAnimator(animator, startX, startY, startTime) {
} }


function animate(touch, time) { function animate(touch, time) {
var lastLastTime = lastTime; lastStep = time - lastTime;
var timeStep = time - lastTime;
var deltaTime = 1 / timeStep;
lastTime = time; lastTime = time;

var continues = true; var continues = true;
if (touchDown) { if (touchDown) {
var delta = touch - lastTouch; velocity = touch - lastTouch;

// if (!delta) {
// // Heuristics to prevent out delta=0 changes from making velocity=0 and
// // stopping all motion in its tracks. We need to distinguish when the finger
// // has actually stopped moving from when the timer fired too quickly.
// if (!stillTime) {
// stillTime = time;
// }
// if (time - stillTime < stillThreshold) {
// // D&&D('ignore', time-stillTime, stillThreshold)
// // lastTime = lastLastTime;
// return true;
// }// else {
// // D&&D('hmm...', time-stillTime, stillThreshold)
// // }
// } else {
// stillTime = 0;
// }


if (!locked && Math.abs(touch - startTouch) > kLockThreshold) { if (!locked && Math.abs(touch - startTouch) > kLockThreshold) {
locked = true; locked = true;
dispatch("scrollability-lock", animator.node, {direction: animator.direction}); dispatch("scrollability-lock", animator.node, {direction: animator.direction});
} }


lastTouch = touch; lastTouch = touch;
velocity = delta / deltaTime;
if (delta) {
deltas.push({delta: delta, time: timeStep});
}


// Apply resistance along the edges // Apply resistance along the edges
if (position > max && absMax == max && constrained) { if (position > max && absMax == max && constrained) {
Expand All @@ -326,72 +286,33 @@ function wrapAnimator(animator, startX, startY, startTime) {
velocity *= (1.0 - excess / bounceLimit); velocity *= (1.0 - excess / bounceLimit);
} }


position += velocity * deltaTime; position += velocity;
D&&D('move', velocity * deltaTime, timeStep);
sync(position, continues); sync(position, continues);
animator.node.style.webkitAnimationName = ''; animator.node.style.webkitAnimationName = '';
return continues; return continues;
} else { } else {
var delta = touch - lastTouch; velocity = (velocity/lastStep) * kAnimationStep;
if (delta) {
deltas.push({delta: delta, time: timeStep});
}
// var tx = new Date();
// D&&D('start velocity', velocity * deltaTime, t, position - startPosition, time - startTime,
// (position - startPosition) / (time - startTime));
// deltas.forEach(function(item, i) {
// D&&D('delta', i, item.delta, 'time', item.time);
// });

// var delta = touch - lastTouch;
// if (delta) {
// deltas.push({delta: delta, time: timeStep})
// }
// var tots = 0, totsi = 0, averaged = 0;
// for (var i = deltas.length-1; i >= 0 && averaged < 10; --i) {
// var item = deltas[i];
// // D&&D('delta', i, deltas.length, item.delta, 'time', item.time);
// // if (i == deltas.length-1 && Math.abs(item.delta) <= Math.abs(deltas[i-1].delta)) {
// // D&&D('skip', item.delta, deltas[i-1].delta);
// // continue;
// // }
// tots += item.delta;
// totsi += item.time;
// ++averaged;
// }
velocity = (deltas[deltas.length-1].delta/deltas[deltas.length-1].time) * kAnimationStep;
// velocity = (tots/totsi) * kAnimationStep;
// D&&D('total', tots, 'time', totsi, averaged, velocity);
// delta
// velocity = (delta / deltaTime) * kAnimationStep;
deltaTime = 1 / kAnimationStep;
// D&&D('delta', delta, 'velocity', velocity, 'deltaTime', deltaTime);


var timeline = createKeyframes(); var timeline = createKeyframes();
var ss = document.styleSheets[0]; var ss = document.styleSheets[0];
var index = ss.rules.length; var index = ss.rules.length;
var rule = ss.insertRule(timeline.css, index); var rule = ss.insertRule(timeline.css, index);
var scrollingRule = index; var scrollingRule = index;

var oldCleanup = animator.node.cleanup; var oldCleanup = animator.node.cleanup;
var cleanup = animator.node.cleanup = function(event, noSync) { var cleanup = animator.node.cleanup = function(event, noSync) {
delete animator.node.cleanup; delete animator.node.cleanup;
// animator.node.removeEventListener("webkitAnimationEnd", cleanup, false);
// if (!noSync) {
// sync(timeline.position);
// }
// animator.node.style.webkitAnimationName = '';
ss.deleteRule(scrollingRule); ss.deleteRule(scrollingRule);
} }


// animator.node.addEventListener("webkitAnimationEnd", cleanup, false); if (timeline.time) {
// D&&D('total', timeline.time); animator.node.style.webkitAnimation = timeline.name + " " + timeline.time + "ms 0 1 linear both";
animator.node.style.webkitAnimation = timeline.name + " " + timeline.time + "ms 0 1 linear both"; animator.node.style.webkitAnimationPlayState = "running";
animator.node.style.webkitAnimationPlayState = "running"; } else {
animator.node.style.webkitAnimation = '';
}
if (oldCleanup) { if (oldCleanup) {
oldCleanup(); oldCleanup();
} }
// D&&D('took', new Date().getTime() - tx.getTime());
return false; return false;
} }
} }
Expand Down Expand Up @@ -419,19 +340,18 @@ function wrapAnimator(animator, startX, startY, startTime) {
var name = "scrollability" + (animationIndex++); var name = "scrollability" + (animationIndex++);
var cssKeyframes = ['@-webkit-keyframes ' + name + ' {']; var cssKeyframes = ['@-webkit-keyframes ' + name + ' {'];


var lastPos; // var lastPos;
// keyframes.forEach(function(keyframe) { // keyframes.forEach(function(keyframe) {
var l = keyframes.length; var l = keyframes.length;
for (var i = 0; i < l; ++i) { for (var i = 0; i < l; ++i) {
var keyframe = keyframes[i]; var keyframe = keyframes[i];
var percent = Math.round((keyframe.time / time) * 100); var percent = Math.round((keyframe.time / time) * 100);
var pos = Math.round(keyframe.position); var pos = Math.round(keyframe.position);
// var frame = percent == 0 ? '0%' : percent + '%'; var frame = percent == 0 ? '0%' : percent + '%';
// if (pos != lastPos || percent == 100) { // if (pos != lastPos || percent == 100) {
var keyframe = percent + '% { -webkit-transform: translate3d(0, ' + pos + 'px, 0) }'; var keyframe = percent + '% { -webkit-transform: translate3d(0, ' + pos + 'px, 0) }';
cssKeyframes.push(keyframe); cssKeyframes.push(keyframe);
lastPos = pos; // lastPos = pos;
D&&D(keyframe);
// } // }
} }
// }); // });
Expand Down Expand Up @@ -488,7 +408,7 @@ function wrapAnimator(animator, startX, startY, startTime) {
decelOrigin = velocity; decelOrigin = velocity;
} }


velocity = velocity * 0.99; velocity = velocity * kFriction;


if (Math.floor(Math.abs(velocity)*100) == 0) { if (Math.floor(Math.abs(velocity)*100) == 0) {
continues = false; continues = false;
Expand All @@ -501,7 +421,7 @@ function wrapAnimator(animator, startX, startY, startTime) {


function saveKeyframe(pos, continues) { function saveKeyframe(pos, continues) {
var diff = position - lastPosition; var diff = position - lastPosition;
if (time-lastSyncTime >= 24 || (lastDiff < 0 != diff < 0)) { if (time-lastSyncTime >= kFrameGranularity || (lastDiff < 0 != diff < 0)) {
keyframes.push({position: position, time: time}); keyframes.push({position: position, time: time});


lastDiff = diff; lastDiff = diff;
Expand Down Expand Up @@ -553,16 +473,6 @@ function wrapAnimator(animator, startX, startY, startTime) {
} }


function terminate() { function terminate() {
// Snap to the integer endpoint, since position may be a subpixel value while animating
// if (paginated) {
// var pageIndex = Math.round(position/viewport);
// sync(pageIndex * (viewport+pageSpacing));
// } else if (position > max && constrained) {
// sync(max);
// } else if (position < min && constrained) {
// sync(min);
// }

// // Hide the scrollbar // // Hide the scrollbar
// if (scrollbar) { // if (scrollbar) {
// scrollbar.style.opacity = '0'; // scrollbar.style.opacity = '0';
Expand All @@ -579,9 +489,7 @@ function wrapAnimator(animator, startX, startY, startTime) {
return animator; return animator;
} }


function touchAnimation() { function touchAnimation(time) {
var time = new Date().getTime();

// Animate each of the animators // Animate each of the animators
for (var i = 0; i < touchAnimators.length; ++i) { for (var i = 0; i < touchAnimators.length; ++i) {
var animator = touchAnimators[i]; var animator = touchAnimators[i];
Expand Down Expand Up @@ -747,7 +655,7 @@ function createXDirection(element) {


function createYDirection(element) { function createYDirection(element) {
var parent = element.parentNode; var parent = element.parentNode;
var baseline = 0;//isiOS5 ? (element.scrollable_vertical||0) : 0; var baseline = isiOS5 ? (element.scrollable_vertical||0) : 0;


return { return {
node: element, node: element,
Expand Down
4 changes: 2 additions & 2 deletions static/examples/tableview.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
left: 0; left: 0;
top: 24px; top: 24px;
bottom: 24px; bottom: 24px;
width: 100%; width: 320px;
background: #fff; background: #fff;
overflow: hidden; overflow: hidden;
/* overflow: scroll; /* overflow: scroll;
Expand Down Expand Up @@ -144,7 +144,7 @@
item.className = 'item'; item.className = 'item';
item.innerHTML = names[i%names.length]; item.innerHTML = names[i%names.length];
page.appendChild(item); page.appendChild(item);
if (page.offsetHeight > container.offsetHeight*6) { if (page.offsetHeight > container.offsetHeight*1) {
break; break;
} }
} }
Expand Down

0 comments on commit bdf5c68

Please sign in to comment.