ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
311 lines (309 loc) • 9.04 kB
JavaScript
/**
_enyo.ScrollMath_ implements a scrolling dynamics simulation. It is a helper
kind used by other scroller kinds, such as
<a href="#enyo.TouchScrollStrategy">enyo.TouchScrollStrategy</a>.
_enyo.ScrollMath_ is not typically created in application code.
*/
enyo.kind({
name: "enyo.ScrollMath",
kind: "enyo.Component",
published: {
//* True if vertical scrolling is enabled
vertical: true,
//* True if horizontal scrolling is enabled
horizontal: true
},
events: {
//* Fires when scroll action starts.
onScrollStart: "",
//* Fires while scroll action is in progress.
onScroll: "",
//* Fires when scroll action stops.
onScrollStop: ""
},
//* 'Spring' damping returns the scroll position to a value inside the
//* boundaries. Lower values provide _faster_ snapback.
kSpringDamping: 0.93,
//* 'Drag' damping resists dragging the scroll position beyond the
//* boundaries. Lower values provide _more_ resistance.
kDragDamping: 0.5,
//* 'Friction' damping reduces momentum over time. Lower values provide
//* _more_ friction.
kFrictionDamping: 0.97,
//* Additional 'friction' damping applied when momentum carries the viewport
//* into overscroll. Lower values provide _more_ friction.
kSnapFriction: 0.9,
//* Scalar applied to 'flick' event velocity
kFlickScalar: 15,
//* Limits the maximum allowable flick. On Android > 2, we limit this to
//* prevent compositing artifacts.
kMaxFlick: enyo.platform.android > 2 ? 2 : 1e9,
//* The value used in friction() to determine if the delta (e.g., y - y0) is
//* close enough to zero to consider as zero
kFrictionEpsilon: 1e-2,
//* Top snap boundary, generally 0
topBoundary: 0,
//* Right snap boundary, generally (viewport width - content width)
rightBoundary: 0,
//* Bottom snap boundary, generally (viewport height - content height)
bottomBoundary: 0,
//* Left snap boundary, generally 0
leftBoundary: 0,
//* Animation time step
interval: 20,
//* Flag to enable frame-based animation; if false, time-based animation is used
fixedTime: true,
//* @protected
// simulation state
x0: 0,
x: 0,
y0: 0,
y: 0,
destroy: enyo.inherit(function (sup) {
return function() {
this.stop();
sup.apply(this, arguments);
};
}),
/**
Simple Verlet integrator for simulating Newtonian motion.
*/
verlet: function() {
var x = this.x;
this.x += x - this.x0;
this.x0 = x;
//
var y = this.y;
this.y += y - this.y0;
this.y0 = y;
},
/**
Boundary damping function.
Returns damped 'value' based on 'coeff' on one side of 'origin'.
*/
damping: function(value, origin, coeff, sign) {
var kEpsilon = 0.5;
//
// this is basically just value *= coeff (generally, coeff < 1)
//
// 'sign' and the conditional is to force the damping to only occur
// on one side of the origin.
//
var dv = value - origin;
// Force close-to-zero to zero
if (Math.abs(dv) < kEpsilon) {
return origin;
}
return value*sign > origin*sign ? coeff * dv + origin : value;
},
/**
Dual-boundary damping function.
Returns damped 'value' based on 'coeff' when exceeding either boundary.
*/
boundaryDamping: function(value, aBoundary, bBoundary, coeff) {
return this.damping(this.damping(value, aBoundary, coeff, 1), bBoundary, coeff, -1);
},
/**
Simulation constraints (spring damping occurs here)
*/
constrain: function() {
var y = this.boundaryDamping(this.y, this.topBoundary, this.bottomBoundary, this.kSpringDamping);
if (y != this.y) {
// ensure snapping introduces no velocity, add additional friction
this.y0 = y - (this.y - this.y0) * this.kSnapFriction;
this.y = y;
}
var x = this.boundaryDamping(this.x, this.leftBoundary, this.rightBoundary, this.kSpringDamping);
if (x != this.x) {
this.x0 = x - (this.x - this.x0) * this.kSnapFriction;
this.x = x;
}
},
/**
The friction function
*/
friction: function(inEx, inEx0, inCoeff) {
// implicit velocity
var dp = this[inEx] - this[inEx0];
// let close-to-zero collapse to zero (i.e. smaller than epsilon is considered zero)
var c = Math.abs(dp) > this.kFrictionEpsilon ? inCoeff : 0;
// reposition using damped velocity
this[inEx] = this[inEx0] + c * dp;
},
// one unit of time for simulation
frame: 10,
// piece-wise constraint simulation
simulate: function(t) {
while (t >= this.frame) {
t -= this.frame;
if (!this.dragging) {
this.constrain();
}
this.verlet();
this.friction('y', 'y0', this.kFrictionDamping);
this.friction('x', 'x0', this.kFrictionDamping);
}
return t;
},
animate: function() {
this.stop();
// time tracking
var t0 = enyo.now(), t = 0;
// delta tracking
var x0, y0;
// animation handler
var fn = this.bindSafely(function() {
// wall-clock time
var t1 = enyo.now();
// schedule next frame
this.job = enyo.requestAnimationFrame(fn);
// delta from last wall clock time
var dt = t1 - t0;
// record the time for next delta
t0 = t1;
// user drags override animation
if (this.dragging) {
this.y0 = this.y = this.uy;
this.x0 = this.x = this.ux;
}
// frame-time accumulator
// min acceptable time is 16ms (60fps)
t += Math.max(16, dt);
// alternate fixed-time step strategy:
if (this.fixedTime && !this.isInOverScroll()) {
t = this.interval;
}
// consume some t in simulation
t = this.simulate(t);
// scroll if we have moved, otherwise the animation is stalled and we can stop
if (y0 != this.y || x0 != this.x) {
//this.log(this.y, y0);
this.scroll();
} else if (!this.dragging) {
this.stop(true);
this.scroll();
}
y0 = this.y;
x0 = this.x;
});
this.job = enyo.requestAnimationFrame(fn);
},
//* @protected
start: function() {
if (!this.job) {
this.animate();
this.doScrollStart();
}
},
stop: function(inFireEvent) {
var job = this.job;
if (job) {
this.job = enyo.cancelRequestAnimationFrame(job);
}
if (inFireEvent) {
this.doScrollStop();
}
},
stabilize: function() {
this.start();
var y = Math.min(this.topBoundary, Math.max(this.bottomBoundary, this.y));
var x = Math.min(this.leftBoundary, Math.max(this.rightBoundary, this.x));
this.y = this.y0 = y;
this.x = this.x0 = x;
this.scroll();
this.stop(true);
},
startDrag: function(e) {
this.dragging = true;
//
this.my = e.pageY;
this.py = this.uy = this.y;
//
this.mx = e.pageX;
this.px = this.ux = this.x;
},
drag: function(e) {
if (this.dragging) {
var dy = this.vertical ? e.pageY - this.my : 0;
this.uy = dy + this.py;
// provides resistance against dragging into overscroll
this.uy = this.boundaryDamping(this.uy, this.topBoundary, this.bottomBoundary, this.kDragDamping);
//
var dx = this.horizontal ? e.pageX - this.mx : 0;
this.ux = dx + this.px;
// provides resistance against dragging into overscroll
this.ux = this.boundaryDamping(this.ux, this.leftBoundary, this.rightBoundary, this.kDragDamping);
//
this.start();
return true;
}
},
dragDrop: function() {
if (this.dragging && !window.PalmSystem) {
var kSimulatedFlickScalar = 0.5;
this.y = this.uy;
this.y0 = this.y - (this.y - this.y0) * kSimulatedFlickScalar;
this.x = this.ux;
this.x0 = this.x - (this.x - this.x0) * kSimulatedFlickScalar;
}
this.dragFinish();
},
dragFinish: function() {
this.dragging = false;
},
flick: function(e) {
var v;
if (this.vertical) {
v = e.yVelocity > 0 ? Math.min(this.kMaxFlick, e.yVelocity) : Math.max(-this.kMaxFlick, e.yVelocity);
this.y = this.y0 + v * this.kFlickScalar;
}
if (this.horizontal) {
v = e.xVelocity > 0 ? Math.min(this.kMaxFlick, e.xVelocity) : Math.max(-this.kMaxFlick, e.xVelocity);
this.x = this.x0 + v * this.kFlickScalar;
}
this.start();
},
mousewheel: function(e) {
var dy = this.vertical ? e.wheelDeltaY || e.wheelDelta: 0;
if ((dy > 0 && this.y < this.topBoundary) || (dy < 0 && this.y > this.bottomBoundary)) {
this.stop(true);
this.y = this.y0 = this.y0 + dy;
this.start();
return true;
}
},
scroll: function() {
this.doScroll();
},
// NOTE: Yip/Orvell method for determining scroller instantaneous velocity
// FIXME: incorrect if called when scroller is in overscroll region
// because does not account for additional overscroll damping.
/**
Animates a scroll to the specified position.
*/
scrollTo: function(inX, inY) {
if (inY !== null) {
this.y = this.y0 - (inY + this.y0) * (1 - this.kFrictionDamping);
}
if (inX !== null) {
this.x = this.x0 - (inX + this.x0) * (1 - this.kFrictionDamping);
}
this.start();
},
setScrollX: function(inX) {
this.x = this.x0 = inX;
},
setScrollY: function(inY) {
this.y = this.y0 = inY;
},
setScrollPosition: function(inPosition) {
this.setScrollY(inPosition);
},
isScrolling: function() {
return Boolean(this.job);
},
isInOverScroll: function() {
return this.job && (this.x > this.leftBoundary || this.x < this.rightBoundary ||
this.y > this.topBoundary || this.y < this.bottomBoundary);
}
});