/*global require, exports, console, MontageElement */
/**
* @module "montage/ui/flow.reel"
*/
var Component = require("../component").Component,
observeProperty = require("frb/observers").observeProperty,
FlowBezierSpline = require("./flow-bezier-spline").FlowBezierSpline,
RangeController = require("../../core/range-controller").RangeController;
var PARSE_MS_PATTERN = /^(\d+)ms$/,
PARSE_SEC_PATTERN = /^(\d+)s$/;
/**
* @class Flow
* @extends Component
*/
var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ {
/**
* @constructs Flow
*/
constructor: {
value: function Flow() {
// The template has a binding from these visibleIndexes to
// the frustum controller's visibleIndexes. We manage the
// array within the flow and use it also in the flow
// translate composer.
this._paths = [];
this._visibleIndexes = [];
this._needsClearVisibleIndexes = true;
// Tracks the elastic scrolling offsets relative to their
// corresponding non-elastic scrolling positions on their
// FlowBezierSpline.
this._slideOffsets = {};
this.defineBinding("_numberOfIterations", {
"<-": "contentController.content.length"
});
// dispatches handle_numberOfIterationsChange
this.addOwnPropertyChangeListener("_numberOfIterations", this);
window.addEventListener("resize", this, false);
this._newVisibleIndexes = [];
}
},
/**
* An optional component or element to place inside the camera's
* perspective shared by the slides.
*
* This will likely be replaced with a template parameter in a future
* version.
*/
slotContent: {
serializable: true,
value: null
},
__flowTranslateComposer: {
value: null
},
_flowTranslateComposer: {
get: function () {
return this.__flowTranslateComposer;
},
set: function (value) {
if (this.__flowTranslateComposer) {
this.__flowTranslateComposer.removeEventListener("translateStart", this, false);
this.__flowTranslateComposer.removeEventListener("translateEnd", this, false);
}
this.__flowTranslateComposer = value;
this.__flowTranslateComposer.addEventListener("translateStart", this, false);
this.__flowTranslateComposer.addEventListener("translateEnd", this, false);
}
},
__firstIteration: {
value: null
},
_firstIteration: {
get: function () {
return this.__firstIteration;
},
set: function (value) {
this.__firstIteration = value;
this.needsDraw = true;
}
},
handleTranslateStart: {
value: function () {
this.callDelegateMethod("didTranslateStart", this);
}
},
handleTranslateEnd: {
value: function () {
this.callDelegateMethod("didTranslateEnd", this);
}
},
_scrollingMode: {
value: "linear"
},
_transform: {
value: null
},
_transformCss: {
value: null
},
_transformPerspective: {
value: null
},
/**
* One of "linear" or "drag".
*
* Drag mode is an experiment to preserve the dragged slide's
* position relative to the gesture pointer. Since this feature is
* not yet ready, "linear" is the default.
*
* Used by the corresponding `FlowTranslateComposer` and
* communicated by way of a binding to the property of the same
* name.
*/
scrollingMode: {
serializable: true,
get: function () {
return this._scrollingMode;
},
set: function (value) {
switch (value) {
case "linear":
case "drag":
this._scrollingMode = value;
break;
}
}
},
_linearScrollingVector: {
value: [-300, 0]
},
/**
* A constant 2d vector used to transform a drag vector into a
* scroll vector, applicable only in the "linear"
* `scrollingMode`.
*
* Used by the corresponding `FlowTranslateComposer` and
* communicated by way of a binding to the property of the same
* name.
*/
linearScrollingVector: {
seriazable: true,
get: function () {
return this._linearScrollingVector;
},
set: function (value) {
this._linearScrollingVector = value;
}
},
/**
* The repetition that manages the slides. The repetition is
* instantiated and bound by the template.
*/
_repetition: {
value: null
},
/**
* The amount of time in miliseconds after a gesture has ended until
* the flow's scroll inertia disipates.
*/
momentumDuration: {
serializable: true,
value: 650
},
_splinePaths: {
value: null
},
// TODO doc
/**
* An internal representation of the paths that slides will follow.
* The paths are taken from the serialization, transformed, and
* stored here. Each path is a `FlowBezierSpline`.
* @private
*/
splinePaths: {
enumerable: false,
get: function () {
if (!this._splinePaths) {
this._splinePaths = [];
}
return this._splinePaths;
},
set: function (value) {
this._splinePaths = value;
}
},
/**
* Creates a `FlowBezierSpline` with data from a path in
* the serialization and appends it to `splinePaths`
* array.
*
* The public interface for modifying the paths of a
* `Flow` is to set the `paths` propery.
*
* @private
*/
_appendPath: {
value: function (path) {
var splinePath = new FlowBezierSpline(),
pathKnots = path.knots,
length = path.knots.length,
knots = [],
nextHandlers = [],
previousHandlers = [],
densities = [],
i, j,
splinePathParameters,
pathUnits = path.units,
units = pathUnits ? Object.keys(pathUnits) : void 0,
unit,
iPathKnot;
splinePath._parameterKeys = units;
splinePathParameters = splinePath.parameters = {};
if(units) {
for (i=0;(unit = units[i]);i++) {
splinePathParameters[unit] = {
data: [],
units: pathUnits[unit]
};
}
}
for (i = 0; i < length; i++) {
iPathKnot = pathKnots[i];
knots[i] = iPathKnot.knotPosition;
previousHandlers[i] = iPathKnot.previousHandlerPosition;
nextHandlers[i] = iPathKnot.nextHandlerPosition;
densities[i] = iPathKnot.previousDensity; // TODO: implement previous/next density
for (j in pathUnits) {
if (pathUnits.hasOwnProperty(j)) {
splinePathParameters[j].data.push(iPathKnot[j]);
}
}
}
splinePath.knots = knots;
splinePath.previousHandlers = previousHandlers;
splinePath.nextHandlers = nextHandlers;
splinePath.densities = densities;
splinePath._computeDensitySummation();
this.splinePaths.push(splinePath);
if (!path.hasOwnProperty("headOffset")) {
path.headOffset = 0;
}
if (!path.hasOwnProperty("tailOffset")) {
path.tailOffset = 0;
}
this._paths.push(path);
this._updateLength();
}
},
_paths: {
value: null
},
/**
* The paths that slides will follow in the flow, as represented in
* the serialization of a Flow. Each path is an object with a
* "knots" array, "headOffset", "tailOffset", and "units"
* descriptor. Each "knot" has "knotPosition",
* "previousHandlerPosition", "previousDensity", and "nextDensity"
* properties. The positions are 3d vectors represented as arrays.
* Densities are describe slide gravitation relative to other knots,
* where equal densities represent linear distribution of slides
* from knot to knot.
*
* The paths property is a getter and setter. The Flow converts the
* paths to and from an internal `splinePaths`
* representation.
*/
// TODO document the meaning of offsets
paths: { // TODO: listen for changes?
get: function () {
var length = this.splinePaths.length,
paths = [],
path,
pathLength,
parametersLength,
knot,
i, j, k,
iSplinePath;
for (i = 0; (iSplinePath = this.splinePaths[i]); i++) {
pathLength = iSplinePath.knots.length;
path = {
knots: [],
units: {}
};
for (j = 0; j < pathLength; j++) {
knot = {
knotPosition: iSplinePath.knots[j]
};
if (iSplinePath.nextHandlers && iSplinePath.nextHandlers[j]) {
knot.nextHandlerPosition = iSplinePath.nextHandlers[j];
}
if (iSplinePath.previousHandlers && iSplinePath.previousHandlers[j]) {
knot.previousHandlerPosition = iSplinePath.previousHandlers[j];
}
// TODO implememnt previous/next densities
if (iSplinePath.densities && iSplinePath.densities[j]) {
knot.previousDensity = iSplinePath.densities[j];
knot.nextDensity = iSplinePath.densities[j];
}
path.knots.push(knot);
}
for (j in iSplinePath.parameters) {
if (iSplinePath.parameters.hasOwnProperty(j)) {
path.units[j] = iSplinePath.parameters[j].units;
parametersLength = iSplinePath.parameters[j].data.length;
for (k = 0; k < parametersLength; k++) {
path.knots[k][j] = iSplinePath.parameters[j].data[k];
}
}
}
if (this._paths[i].hasOwnProperty("headOffset")) {
path.headOffset = this._paths[i].headOffset;
} else {
path.headOffset = 0;
}
if (this._paths[i].hasOwnProperty("tailOffset")) {
path.tailOffset = this._paths[i].tailOffset;
} else {
path.tailOffset = 0;
}
paths.push(path);
}
return paths;
},
set: function (value) {
var length = value.length,
i;
this._splinePaths = [];
this._paths = [];
for (i = 0; i < length; i++) {
this._appendPath(value[i]);
}
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
_isCameraEnabled: {
value: true
},
isCameraEnabled: {
get: function () {
return this._isCameraEnabled;
},
set: function (value) {
var enabled = !!value;
if (this._isCameraEnabled !== enabled) {
this._isCameraEnabled = enabled;
this._isCameraUpdated = true;
this._needsComputeVisibleRange = true;
this.needsDraw = true;
}
}
},
/**
* CSS perspective value in pixels used when camera is disabled
*/
_perspective: {
value: 500
},
perspective: {
get: function () {
return this._perspective;
},
set: function (value) {
var perspective = parseFloat(value);
if (!isNaN(perspective) && (this._perspective !== perspective)) {
this._perspective = perspective;
this._isCameraUpdated = true;
this._needsComputeVisibleRange = true;
this.needsDraw = true;
}
}
},
/**
* The camera elements is the DOM element that contains the
* repetition and on which the flow applies 3d transforms.
*/
_cameraElement: {
value: null
},
_cameraPosition: {
value: [0, 0, 800]
},
/**
* An [x, y, z] array representing the 3d vector describing the
* position of the virtual camera.
*/
cameraPosition: {
get: function () {
return this._cameraPosition;
},
set: function (value) {
this._cameraPosition = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
_viewpointPosition: {
get: function () {
if (this._isCameraEnabled) {
return this.cameraPosition;
} else {
return [
(50 - this._sceneOffsetLeft) * 0.01 * this._width,
(50 - this._sceneOffsetTop) * 0.01 * this._height,
this._perspective
];
}
}
},
_cameraTargetPoint: {
value: [0, 0, 0]
},
/**
* An [x, y, z] array representing the 3d vector describing the
* position of one of the points in the center of the camera's view.
*/
cameraTargetPoint: {
get: function () {
return this._cameraTargetPoint;
},
set: function (value) {
this._cameraTargetPoint = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
_viewpointTargetPoint: {
get: function () {
if (this._isCameraEnabled) {
return this.cameraTargetPoint;
} else {
return [
(50 - this._sceneOffsetLeft) * 0.01 * this._width,
(50 - this._sceneOffsetTop) * 0.01 * this._height,
0
];
}
}
},
_cameraFov: {
value: 50
},
/**
* The "field of view" of the camera, measured in degrees away from
* the ray from the camera position to the camera target point.
*/
cameraFov: {
get: function () {
return this._cameraFov;
},
set: function (value) {
this._cameraFov = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
_viewpointFov: {
get: function () {
if (this._isCameraEnabled) {
return this.cameraFov;
} else {
return ((Math.PI / 2) - Math.atan2(this._perspective, this._height / 2)) * 360 / Math.PI;
}
}
},
// TODO: Implement camera roll
_cameraRoll: {
value: 0
},
/**
* The angle that the camera is rotated about the ray from the
* camera position to the camera target point, away from vertical.
*
* This feature is not presently implemented.
*/
cameraRoll: {
get: function () {
return this._cameraRoll;
},
set: function (value) {
this._cameraRoll = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
/**
* Scene's vertical offset relative to the viewport.
* Expressed as a percentage of the viewport's height.
* Only in use when camera is disabled.
*/
_sceneOffsetTop: {
value: 50
},
sceneOffsetTop: {
get: function () {
return this._sceneOffsetTop;
},
set: function (value) {
this._sceneOffsetTop = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
/**
* Scene's horizontal offset relative to the viewport.
* Expressed as a percentage of the viewport's width.
* Only in use when camera is disabled.
*/
_sceneOffsetLeft: {
value: 50
},
sceneOffsetLeft: {
get: function () {
return this._sceneOffsetLeft;
},
set: function (value) {
this._sceneOffsetLeft = value;
this._isCameraUpdated = true;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
},
/**
* Scene's scale for each axis x/y/z
* Expressed as an object representing a fraction with numerator and denominator properties
* Defaults as 1:1, original size
*/
_sceneScaleX: {
value: {
numerator: 1,
denominator: 1
}
},
_sceneScaleY: {
value: {
numerator: 1,
denominator: 1
}
},
_sceneScaleZ: {
value: {
numerator: 1,
denominator: 1
}
},
_sceneScale: {
value: {
x: {numerator: 1, denominator: 1},
y: {numerator: 1, denominator: 1},
z: {numerator: 1, denominator: 1}
},
},
_updateSceneScale: {
value: function () {
this._sceneScale = {
x: this._sceneScaleX,
y: this._sceneScaleY,
z: this._sceneScaleZ
};
}
},
sceneScaleX: {
get: function () {
return this._sceneScaleX;
},
set: function (value) {
if ((typeof value === "object") &&
(typeof value.numerator !== "undefined") &&
(typeof value.denominator !== "undefined") &&
(!isNaN(value.numerator)) &&
(!isNaN(value.denominator)) &&
(value.denominator !== 0)) {
this._sceneScaleX = value;
this._updateSceneScale();
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
}
},
sceneScaleY: {
get: function () {
return this._sceneScaleY;
},
set: function (value) {
if ((typeof value === "object") &&
(typeof value.numerator !== "undefined") &&
(typeof value.denominator !== "undefined") &&
(!isNaN(value.numerator)) &&
(!isNaN(value.denominator)) &&
(value.denominator !== 0)) {
this._sceneScaleY = value;
this._updateSceneScale();
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
}
},
sceneScaleZ: {
get: function () {
return this._sceneScaleZ;
},
set: function (value) {
if ((typeof value === "object") &&
(typeof value.numerator !== "undefined") &&
(typeof value.denominator !== "undefined") &&
(!isNaN(value.numerator)) &&
(!isNaN(value.denominator)) &&
(value.denominator !== 0)) {
this._sceneScaleZ = value;
this._updateSceneScale();
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
}
},
_stride: {
value: 0
},
/**
* The interval between snap points along the paths, to which slides
* wil gravitate. The default value of "0" disables this feature.
*/
stride: {
get: function () {
return this._stride;
},
set: function (value) {
this._stride = value;
}
},
/**
* An internal cache of `scrollingTransitionDuration` in
* units of miliseconds.
*/
_scrollingTransitionDurationMiliseconds: {
value: 500
},
/**
* An internal cache of `scrollingTransitionDuration` as
* a string suitable for CSS.
*/
_scrollingTransitionDuration: {
value: "500ms"
},
/**
* The configurable duration of scrolling animation in miliseconds.
* It may be set to a number, or a CSS duration like "1s" or
* "500ms". Getting the property always reports the number. The
* default is 500.
*/
scrollingTransitionDuration: { // TODO: think about using the Date Converter
get: function () {
return this._scrollingTransitionDuration;
},
set: function (duration) {
var durationString = duration + "",
length = durationString.length,
value,
match;
// TODO parseInt ?
if ((match = PARSE_MS_PATTERN.exec(durationString))) {
value = +match[1];
} else if ((match = PARSE_SEC_PATTERN.exec(durationString))) {
value = +match[1] * 1000;
} else {
value = +durationString;
durationString += "ms";
}
if (!isNaN(value) && (this._scrollingTransitionDurationMiliseconds !== value)) {
this._scrollingTransitionDurationMiliseconds = value;
this._scrollingTransitionDuration = durationString;
}
}
},
// TODO doc
/**
*/
hasSelectedIndexScrolling: {
value: false
},
// TODO doc
/**
*/
selectedIndexScrollingOffset: {
value: 0
},
// TODO doc
/**
*/
_handleSelectedIndexesChange: {
value: function (plus, minus, index) {
if (this.hasSelectedIndexScrolling && plus[0]) {
this.startScrollingIndexToOffset(
Math.floor(this.contentController.content.indexOf(plus[0].object) / this._paths.length),
this.selectedIndexScrollingOffset
);
}
}
},
/**
* Internal lookup table of CSS timing functions to their
* corresponding cubic bezier parameters. This is used by the
* `scrollingTransitionTimingFunction` setter to
* translate <em>named</em> transitions like "ease" to their
* corresponding cubic bezier internal representation.
*/
_timingFunctions: {
value: {
"ease": [0.25, 0.1, 0.25, 1],
"linear": [0, 0, 1, 1],
"ease-in": [0.42, 0, 1, 1],
"ease-out": [0, 0, 0.58, 1],
"ease-in-out": [0.42, 0, 0.58, 1]
}
},
/**
* Internal representation of the CSS timing function as an array of
* numbers for scroll transitions, the format of CSS timing
* functions.
*
* This is produced by setting
* `scrollingTransitionTimingFunction`. Note the absence
* of the `Bezier` qualifier.
*/
_scrollingTransitionTimingFunctionBezier: {
value: [0.25, 0.1, 0.25, 1]
},
/**
* Internal cache of the transition timing function.
*/
_scrollingTransitionTimingFunction: {
value: "ease"
},
/**
* The CSS timing function, "ease" by default, used for smooth
* scroll transitions. Supports named timing functions "ease",
* "linear", "ease-in", "ease-out", "ease-in-out", and the tuple
* `cubic-bezier(0, 0, 1, 1)` format as well.
*/
scrollingTransitionTimingFunction: {
get: function () {
return this._scrollingTransitionTimingFunction;
},
set: function (timingFunction) {
var string = timingFunction + "",
bezier,
i;
if (this._timingFunctions.hasOwnProperty(string)) {
this._scrollingTransitionTimingFunction = string;
this._scrollingTransitionTimingFunctionBezier = this._timingFunctions[string];
} else {
if ((string.substr(0, 13) === "cubic-bezier(") && (string.substr(string.length - 1, 1) === ")")) {
bezier = string.substr(13, string.length - 14).split(",");
if (bezier.length === 4) {
for (i = 0; i < 4; i++) {
bezier[i] -= 0;
if (isNaN(bezier[i])) {
return;
}
}
if (bezier[0] < 0) {
bezier[0] = 0;
} else {
if (bezier[0] > 1) {
bezier[0] = 1;
}
}
if (bezier[2] < 0) {
bezier[2] = 0;
} else {
if (bezier[2] > 1) {
bezier[2] = 1;
}
}
// TODO: check it is not the same bezier
this._scrollingTransitionTimingFunction = "cubic-bezier(" + bezier + ")";
this._scrollingTransitionTimingFunctionBezier = bezier;
}
}
}
}
},
/**
* Used in scrolling transitions to compute the interpolation values
* in a cubic bezier curve in the same way as CSS transitions.
*/
_computeCssCubicBezierValue: {
value: function (x, bezier) {
var t = 0.5,
step = 0.25,
t2,
k,
i;
for (i = 0; i < 20; i++) { // TODO: optimize with Newton's method or similar
t2 = t * t;
k = 1 - t;
if ((3 * (k * k * t * bezier[0] + k * t2 * bezier[2]) + t2 * t) > x) {
t -= step;
} else {
t += step;
}
step *= 0.5;
}
t2 = t * t;
k = 1 - t;
return 3 * (k * k * t * bezier[1] + k * t2 * bezier[3]) + t2 * t;
}
},
/**
* A flag that indicates that scrolling animation is in progress.
*/
_isTransitioningScroll: {
value: false
},
/**
* Stops scrolling animation.
*/
stopScrolling: {
value: function () {
this._isTransitioningScroll = false;
// TODO: Fire scrollingTransitionCancel event
}
},
/**
* Starts an scrolling animation from the given slide index to the
* given offset position.
*/
// TODO document the range of slide indexes and what they correspond
// to, relative to the actual content, organized content, visible
// content, or order on the document
// TODO document the units of the offset position
startScrollingIndexToOffset: { // TODO: Fire scrollingTransitionStart event
value: function (index, offset) {
this._scrollingOrigin = this.scroll;
this._scrollingDestination = index - offset;
if (this._scrollingDestination > this._length) {
this._scrollingDestination = this._length;
} else {
if (this._scrollingDestination < 0) {
this._scrollingDestination = 0;
}
}
this._isScrolling = true;
this._scrollingStartTime = Date.now();
this._isTransitioningScroll = true;
this.needsDraw = true;
this.callDelegateMethod("didTranslateStart", this);
}
},
/**
* A flag that informs the `draw` method that the camera
* properties were changed.
*/
_isCameraUpdated: {
value: true
},
// TODO doc
/**
*/
_width: {
value: 0
},
// TODO doc
/**
*/
_height: {
value: 0
},
// TODO: bounding box is working as bounding rectangle only. Update it to work with boxes
// TODO doc
/**
*/
_boundingBoxSize: {
value: null
},
// TODO doc
/**
*/
boundingBoxSize: {
serializable: true,
get: function () {
return this._boundingBoxSize;
},
set: function (value) {
this._boundingBoxSize = value;
this.elementsBoundingSphereRadius = Math.sqrt(value[0] * value[0] + value[1] * value[1] + value[2] * value[2]) * 0.5;
this._needsComputeVisibleRange = true;
}
},
// TODO doc
/**
*/
_elementsBoundingSphereRadius: {
value: 1
},
// TODO doc
/**
*/
elementsBoundingSphereRadius: {
get: function () {
return this._elementsBoundingSphereRadius;
},
set: function (value) {
if (this._elementsBoundingSphereRadius !== value) {
this._elementsBoundingSphereRadius = value;
this.needsDraw = true;
this._needsComputeVisibleRange = true;
}
}
},
_halfPI: {
value: Math.PI * 0.5
},
_doublePI: {
value: Math.PI * 2
},
// TODO doc
/**
*/
_computeFrustumNormals: {
value: function () {
var angle = ((this._viewpointFov * 0.5) * this._doublePI) / 360,
y = Math.sin(angle),
z = Math.cos(angle),
x = (y * this._width) / this._height,
viewpointPosition = this._viewpointPosition,
viewpointTargetPoint = this._viewpointTargetPoint,
vX = viewpointTargetPoint[0] - viewpointPosition[0],
vY = viewpointTargetPoint[1] - viewpointPosition[1],
vZ = viewpointTargetPoint[2] - viewpointPosition[2],
yAngle = this._halfPI - Math.atan2(vZ, vX),
tmpZ = vX * Math.sin(yAngle) + vZ * Math.cos(yAngle),
rX, rY, rZ,
rX2, rY2, rZ2,
xAngle = this._halfPI - Math.atan2(tmpZ, vY),
invLength,
vectors = [[z, 0, x], [-z, 0, x], [0, z, y], [0, -z, y]],
iVector,
out = [],
i;
for (i = 0; i < 4; i++) {
iVector = vectors[i];
rX = iVector[0];
rY = iVector[1] * Math.cos(-xAngle) - iVector[2] * Math.sin(-xAngle);
rZ = iVector[1] * Math.sin(-xAngle) + iVector[2] * Math.cos(-xAngle);
rX2 = rX * Math.cos(-yAngle) - rZ * Math.sin(-yAngle);
rY2 = rY;
rZ2 = rX * Math.sin(-yAngle) + rZ * Math.cos(-yAngle);
invLength = 1 / Math.sqrt(rX2 * rX2 + rY2 * rY2 + rZ2 * rZ2);
out.push([rX2 * invLength, rY2 * invLength, rZ2 * invLength]);
}
return out;
}
},
// TODO doc
/**
*/
_segmentsIntersection: {
value: function (segment1, segment2) {
var n = 0,
m = 0,
start,
end,
result = [];
while ((n < segment1.length) && (m < segment2.length)) {
if (segment1[n][0] >= segment2[m][1]) {
m++;
} else {
if (segment1[n][1] <= segment2[m][0]) {
n++;
} else {
if (segment1[n][0] >= segment2[m][0]) {
start = segment1[n][0];
} else {
start = segment2[m][0];
}
if (segment1[n][1] <= segment2[m][1]) {
end = segment1[n][1];
} else {
end = segment2[m][1];
}
result.push([start, end]);
if (segment1[n][1] < segment2[m][1]) {
n++;
} else {
if (segment1[n][1] > segment2[m][1]) {
m++;
} else {
n++;
m++;
}
}
}
}
}
return result;
}
},
// TODO doc
/**
*/
_computeVisibleRange: { // TODO: make it a loop, optimize
value: function (spline) {
var splineLength = spline._knots.length - 1,
viewpointPosition = this._viewpointPosition,
planeOrigin0 = viewpointPosition[0],
planeOrigin1 = viewpointPosition[1],
planeOrigin2 = viewpointPosition[2],
normals = this._computeFrustumNormals(),
mod,
r = [], r2 = [], r3 = [], tmp,
i, j,
elementsBoundingSphereRadius = this._elementsBoundingSphereRadius,
splineKnots = spline.getScaledKnots(this._sceneScale),
splineNextHandlers = spline.getScaledNextHandlers(this._sceneScale),
splinePreviousHandlers = spline.getScaledPreviousHandlers(this._sceneScale),
out = [];
for (i = 0; i < splineLength; i++) {
mod = normals[0];
r = spline.directedPlaneBezierIntersection(
planeOrigin0 - mod[0] * elementsBoundingSphereRadius,
planeOrigin1 - mod[1] * elementsBoundingSphereRadius,
planeOrigin2 - mod[2] * elementsBoundingSphereRadius,
normals[0],
splineKnots[i],
splineNextHandlers[i],
splinePreviousHandlers[i + 1],
splineKnots[i + 1]
);
if (r.length) {
mod = normals[1];
r2 = spline.directedPlaneBezierIntersection(
planeOrigin0 - mod[0] * elementsBoundingSphereRadius,
planeOrigin1 - mod[1] * elementsBoundingSphereRadius,
planeOrigin2 - mod[2] * elementsBoundingSphereRadius,
normals[1],
splineKnots[i],
splineNextHandlers[i],
splinePreviousHandlers[i + 1],
splineKnots[i + 1]
);
if (r2.length) {
tmp = this._segmentsIntersection(r, r2);
if (tmp.length) {
mod = normals[2];
r = spline.directedPlaneBezierIntersection(
planeOrigin0 - mod[0] * elementsBoundingSphereRadius,
planeOrigin1 - mod[1] * elementsBoundingSphereRadius,
planeOrigin2 - mod[2] * elementsBoundingSphereRadius,
normals[2],
splineKnots[i],
splineNextHandlers[i],
splinePreviousHandlers[i + 1],
splineKnots[i + 1]
);
tmp = this._segmentsIntersection(r, tmp);
if (tmp.length) {
mod = normals[3];
r = spline.directedPlaneBezierIntersection(
planeOrigin0 - mod[0] * elementsBoundingSphereRadius,
planeOrigin1 - mod[1] * elementsBoundingSphereRadius,
planeOrigin2 - mod[2] * elementsBoundingSphereRadius,
normals[3],
splineKnots[i],
splineNextHandlers[i],
splinePreviousHandlers[i + 1],
splineKnots[i + 1]
);
tmp = this._segmentsIntersection(r, tmp);
for (j = 0; j < tmp.length; j++) {
r3.push([i, tmp[j][0], tmp[j][1]]);
}
}
}
}
}
}
var densities = spline._densities, d1, d2, dS, p1, p2, t1, t2;
for (i = 0; i < r3.length; i++) {
d1 = densities[r3[i][0]];
d2 = densities[r3[i][0] + 1];
dS = r3[i][0] ? spline._densitySummation[r3[i][0]-1] : 0;
p1 = r3[i][1];
p2 = r3[i][2];
t1 = (d2 - d1) * p1 * p1 * 0.5 + p1 * d1 + dS;
t2 = (d2 - d1) * p2 * p2 * 0.5 + p2 * d1 + dS;
out.push([t1, t2]);
}
return out;
}
},
_determineCssPrefixedProperties: {
value: function () {
if("webkitTransform" in this.element.style) {
this._transform = "webkitTransform";
this._transformCss = "-webkit-transform";
this._transformPerspective = "webkitPerspective";
} else if("MozTransform" in this.element.style) {
this._transform = "MozTransform";
this._transformCss = "-moz-transform";
this._transformPerspective = "MozPerspective";
} else if("msTransform" in this.element.style) {
this._transform = "msTransform";
this._transformCss = "-ms-transform";
this._transformPerspective = "msPerspective";
} else {
this._transform = "transform";
this._transformPerspective = "perspective";
}
}
},
_isListeningToResize: {
value: true
},
isListeningToResize: {
get: function () {
return this._isListeningToResize;
},
set: function (value) {
var _value = !!value;
if (this._isListeningToResize !== _value) {
this._isListeningToResize = _value;
if (this._isListeningToResize) {
window.addEventListener("resize", this, false);
} else {
window.removeEventListener("resize", this, false);
}
}
}
},
_needsClearVisibleIndexes: {
value: false
},
handleResize: {
value: function () {
this._isCameraUpdated = true;
this._needsComputeVisibleRange = true;
this.needsDraw = true;
this._needsClearVisibleIndexes = true;
}
},
enterDocument: {
value: function (firstTime) {
if (firstTime) {
var self = this;
this._determineCssPrefixedProperties();
this._repetition.addRangeAtPathChangeListener("selectedIterations", this, "_handleSelectedIndexesChange");
// TODO remove event listener
}
}
},
/**
* In order to prevent jitter and minimize thrashing on the DOM, the Flow
* attempts to reuse every iteration, favoring moving them around with CSS
* transforms over moving them around on the document, and favoring
* rebinding over removing an iteration that is leaving and injecting an
* iteration for content entering.
*
* To those ends, this algorithm takes the idealized array of new visible
* indexes, as computed by willDraw, and transforms the current visible
* indexes array without moving any content that is in both
* the old and new arrays. Instead, it finds all of the positions that
* have content that is leaving the Flow, and fills those "holes" with
* content entering the Flow.
*
* To accomplish this, the algorithm takes as input the
* `newVisibleIndexes` and its inverse-lookup table,
* `newContentIndexes`. It uses the content indexes so that it
* can triangulate whether the content at any particular visible index will
* be retained in the new visible indexes at any position. Otherwise,
* there will be a hole at that index.
*
* Conceptually there are two domains of indexes: content indexes and
* visible indexes. The visible indexes correspond to positions within the
* repetition. The content indexes correspond to where the content exists
* within the backing organized content array. There are both new and old
* forms of both indexes. We use the <em>new, visible</em> indexes, the
* <em>new, content</em> indexes, and the <em>old, content` indexes
* to triangulate.
*
* <pre>
* OVI -> OCI
* ^ v ?
* NVI NCI
* </pre>
*/
_updateVisibleIndexes: {
value: function (newVisibleIndexes, newContentIndexes) {
var oldVisibleIndexes = this._visibleIndexes,
oldIndexesLength = oldVisibleIndexes && !isNaN(oldVisibleIndexes.length) ? oldVisibleIndexes.length : 0,
holes,
j,
i;
if (this._needsClearVisibleIndexes) {
this._visibleIndexes.splice(newVisibleIndexes.length, Infinity);
this._needsClearVisibleIndexes = false;
}
// Search for viable holes, leave content at the same visible index
// whenever possible.
for (i = 0; i < oldIndexesLength; i++) {
// # Legend
// _: the previously defined expression in this legend
// oldVisibleIndexes[i]: the index of the content that was at
// visible index "i".
// newContentIndexes[_]: the position that the content should
// be in now, or undefined if the content is no longer visible.
// newVisibleIndexes[_]: knowing that the content index that
// was at visible index "i" in oldVisibleIndexes is now at
// index "_" in newVisibleIndexes, set this to null as a
// sentinel indicating "no change in position"
// The likelyhood that newContentIndexes had a
// number-turned-to-string property that wasn't his own is
// pretty slim as it's provided internally.
// if (newContentIndexes.hasOwnProperty(oldVisibleIndexes[i])) {
if (typeof newContentIndexes[oldVisibleIndexes[i]] === "number") {
newVisibleIndexes[newContentIndexes[oldVisibleIndexes[i]]] = null;
} else {
(holes || (holes = [])).push(i);
}
}
// Fill the holes
if(holes) {
for (i = j = 0; (j < holes.length) && (i < newVisibleIndexes.length); i++) {
if (newVisibleIndexes[i] !== null) {
oldVisibleIndexes.set(holes[j], newVisibleIndexes[i]);
j++;
}
}
}
// Add new values to the end if the visible indexes have grown
for (j = oldIndexesLength; i < newVisibleIndexes.length; i++) {
if (newVisibleIndexes[i] !== null) {
oldVisibleIndexes.set(j, newVisibleIndexes[i]);
j++;
}
}
// Don't bother triming the excess. We just make them invisible and
// leave them on the origin.
}
},
_needsComputeVisibleRange: {
value: true
},
_previousVisibleRanges: {
value: null
},
viewportWidth: {
get: function () {
return this._width;
},
set: function (value) {
if (this._width !== value) {
this._width = value;
this._needsComputeVisibleRange = true;
}
}
},
viewportHeight: {
get: function () {
return this._height;
},
set: function (value) {
if (this._height !== value) {
this._height = value;
this._needsComputeVisibleRange = true;
}
}
},
_firstIterationWidth: {
value: 1
},
_firstIterationHeight: {
value: 1
},
firstIterationWidth: {
get: function () {
return this._firstIterationWidth;
},
set: function (value) {
if (value !== this._firstIterationWidth) {
this._firstIterationWidth = value;
this._needsComputeVisibleRange = true;
this._needsClearVisibleIndexes = true;
}
}
},
firstIterationHeight: {
get: function () {
return this._firstIterationHeight;
},
set: function (value) {
if (value !== this._firstIterationHeight) {
this._firstIterationHeight = value;
this._needsComputeVisibleRange = true;
this._needsClearVisibleIndexes = true;
}
}
},
_firstIterationOffsetLeft: {
value: 0
},
_firstIterationOffsetTop: {
value: 0
},
willDraw: {
value: function Flow_willDraw(timestamp) {
var intersections,
index,
i,
countI,
j,
k,
offset,
startIndex,
endIndex,
mod,
div,
iterations,
newVisibleIndexes = this._newVisibleIndexes,
// newContentIndexes is a reverse-lookup hash of
// newVisibleIndexes, which we keep in sync manually.
newContentIndexes = {},
time,
interpolant,
paths = this._paths,
pathsLength = paths.length,
splinePaths = this.splinePaths;
this.viewportWidth = this._element.clientWidth;
this.viewportHeight = this._element.clientHeight;
if (this.__firstIteration) {
var element = this.__firstIteration.firstElement.children[0];
if ((element.offsetWidth !== 0) && (element.offsetHeight !== 0)) {
this.firstIterationWidth = element.offsetWidth;
this.firstIterationHeight = element.offsetHeight;
this._firstIterationOffsetLeft = element.offsetLeft;
this._firstIterationOffsetTop = element.offsetTop;
if (!this._boundingBoxSize) {
var x = Math.max(
Math.abs(this._firstIterationWidth + this._firstIterationOffsetLeft),
Math.abs(this._firstIterationOffsetLeft)
),
y = Math.max(
Math.abs(this._firstIterationHeight + this._firstIterationOffsetTop),
Math.abs(this._firstIterationOffsetTop)
);
this._elementsBoundingSphereRadius = Math.sqrt(x * x + y * y);
}
}
}
// Manage scroll animation
if (this._isTransitioningScroll) {
time = (Date.now() - this._scrollingStartTime) / this._scrollingTransitionDurationMiliseconds; // TODO: division by zero
interpolant = this._computeCssCubicBezierValue(time, this._scrollingTransitionTimingFunctionBezier);
if (time < 1) {
this.scroll = this._scrollingOrigin + (this._scrollingDestination - this._scrollingOrigin) * interpolant;
} else {
this.scroll = this._scrollingDestination;
this._isTransitioningScroll = false;
this._needsToCallDidTranslateEndDelegate = true;
}
}
// "iterations" is the number of iterations for the numerical methods
// integration of elastic scrolling. The higher the iterations, the more
// precise it is, but slower to compute. Setting it to 6 provides
// a good balance between precision and performance.
time = timestamp;
iterations = 6;
var interval1 = this.lastDrawTime ? (time - this.lastDrawTime) * 0.018 * this._elasticScrollingSpeed : 0,
interval = 1 - (interval1 / iterations),
offset1, offset2, resultOffset,
min = this._minSlideOffsetIndex,
max = this._maxSlideOffsetIndex,
position,
step;
this.lastDrawTime = time;
if (this._hasElasticScrolling) {
for (j = 0; j < iterations; j++) {
for (i = this._draggedSlideIndex - 1; i >= min; i--) {
offset1 = this._getSlideOffset(i);
offset2 = this._getSlideOffset(i + 1);
resultOffset = (offset1 - offset2) * interval + offset2;
if (resultOffset > 0) {
resultOffset = 0;
}
this._updateSlideOffset(i, resultOffset);
}
for (i = this._draggedSlideIndex + 1; i <= max; i++) {
offset1 = this._getSlideOffset(i);
offset2 = this._getSlideOffset(i - 1);
resultOffset = (offset1 - offset2) * interval + offset2;
if (resultOffset < 0) {
resultOffset = 0;
}
this._updateSlideOffset(i, resultOffset);
}
}
}
// Compute which slides are in view
if (splinePaths.length) {
mod = this._numberOfIterations % pathsLength;
div = (this._numberOfIterations - mod) / pathsLength;
if (this._needsComputeVisibleRange) {
this._previousVisibleRanges = [];
}
for (k = 0; k < pathsLength; k++) {
iterations = div + ((k < mod) ? 1 : 0);
if (this._needsComputeVisibleRange) {
intersections = this._computeVisibleRange(splinePaths[k]);
this._previousVisibleRanges[k] = intersections;
splinePaths[k]._computeDensitySummation();
} else {
intersections = this._previousVisibleRanges[k];
}
offset = this._scroll - paths[k].headOffset;
for (i = 0, countI = intersections.length; i < countI; i++) {
step = iterations / 2;
j = step;
while (step >= 1) {
index = (j|0) * pathsLength + k;
position = (j|0) + this._getSlideOffset(index) - offset;
step /= 2;
if (position >= intersections[i][0]) {
j -= step;
} else {
j += step;
}
}
j = (j - 1) | 0;
if (j < 0) {
j = 0;
}
do {
index = j * pathsLength + k;
position = j + this._getSlideOffset(index) - offset;
if ((position >= intersections[i][0]) && (position <= intersections[i][1]) && ( newContentIndexes[index] === void 0)) {
newContentIndexes[index] = newVisibleIndexes.length;
newVisibleIndexes[newVisibleIndexes.length] = index;
}
j++;
} while ((position <= intersections[i][1]) && (j < iterations));
}
}
this._needsComputeVisibleRange = false;
}
this._updateVisibleIndexes(newVisibleIndexes, newContentIndexes);
newVisibleIndexes.length = 0;
}
},
draw: {
value: function (timestamp) {
var i,
length = this._repetition._drawnIterations.length,
iteration,
element,
elementChildren,
pos,
perspective,
visibleIndexes = this._visibleIndexes,
viewpointPosition = this._viewpointPosition,
viewpointTargetPoint = this._viewpointTargetPoint,
indexTime,
rotation,
offset,
_splinePaths = this._splinePaths,
iSplinePath,
cssText;
if (this._isTransitioningScroll) {
this.needsDraw = true;
}
if (this._isCameraUpdated) {
if (this._isCameraEnabled) {
perspective = Math.tan(((90 - this._viewpointFov * 0.5) * this._doublePI) / 360) * this._height * 0.5;
var vX = viewpointTargetPoint[0] - viewpointPosition[0],
vY = viewpointTargetPoint[1] - viewpointPosition[1],
vZ = viewpointTargetPoint[2] - viewpointPosition[2],
yAngle = Math.atan2(-vX, -vZ), // TODO: Review this
tmpZ,
xAngle;
tmpZ = vX * -Math.sin(-yAngle) + vZ * Math.cos(-yAngle);
xAngle = Math.atan2(-vY, -tmpZ);
this._element.style[this._transformPerspective]= perspective + "px";
cssText = "translate3d(0,0,";
cssText += perspective;
cssText += "px)rotateX(";
cssText += xAngle;
cssText += "rad)rotateY(";
cssText += (-yAngle);
cssText += "rad)";
cssText += "translate3d(";
cssText += (-viewpointPosition[0]);
cssText += "px,";
cssText += (-viewpointPosition[1]);
cssText += "px,";
cssText += (-viewpointPosition[2]);
cssText += "px)";
this._cameraElement.style[this._transform] = cssText;
this._element.classList.remove("camera-disabled");
} else {
this._element.style[this._transformPerspective]= this._perspective + "px";
cssText = "translate3d(" ;
cssText += (0.5 * this._width - viewpointPosition[0]);
cssText += "px, ";
cssText += (0.5 * this._height - viewpointPosition[1]);
cssText += "px,0)";
this._cameraElement.style[this._transform] = cssText;
this._element.classList.add("camera-disabled");
}
this._isCameraUpdated = false;
}
if (_splinePaths.length) {
for (i = 0; i < length; i++) {
offset = this.offset(visibleIndexes[i]);
iSplinePath = _splinePaths[offset.pathIndex];
indexTime = iSplinePath._convertSplineTimeToBezierIndexTime(offset.slideTime);
iteration = this._repetition._drawnIterations[i];
element = iteration.cachedFirstElement || iteration.firstElement;
if (indexTime !== null) {
if ((elementChildren = element.children[0])) {
if (element.classList.contains("selected")) {
elementChildren.classList.add("selected");
} else {
elementChildren.classList.remove("selected");
}
if (element.classList.contains("active")) {
elementChildren.classList.add("active");
} else {
elementChildren.classList.remove("active");
}
}
pos = iSplinePath.getPositionAtIndexTime(indexTime, this._sceneScale);
rotation = iSplinePath.getRotationAtIndexTime(indexTime);
cssText = this._transformCss;
cssText += ":translate3d(";
cssText += (((pos[0] * 100000) >> 0) * 0.00001);
cssText += "px,";
cssText += (((pos[1] * 100000) >> 0) * 0.00001);
cssText += "px,";
cssText += (((pos[2] * 100000) >> 0) * 0.00001) ;
cssText += "px)";
cssText += (rotation[2] ? "rotateZ(" + (((rotation[2] * 100000) >> 0) * 0.00001) + "rad)" : "");
cssText += (rotation[1] ? "rotateY(" + (((rotation[1] * 100000) >> 0) * 0.00001) + "rad)" : "");
cssText += (rotation[0] ? "rotateX(" + (((rotation[0] * 100000) >> 0) * 0.00001) + "rad)" : "");
cssText += ";";
cssText += iSplinePath.getStyleAtIndexTime(indexTime);
element.setAttribute("style",cssText);
} else {
element.setAttribute("style", "display:none");
}
}
} else {
for (i = 0; i < length; i++) {
iteration = this._repetition._drawnIterations[i];
element = iteration.cachedFirstElement || iteration.firstElement;
element.setAttribute("style", "display:none");
}
}
// Continue animation during elastic scrolling
if (this._slideOffsetsLength) {
this.needsDraw = true;
}
if (this._needsToCallDidTranslateEndDelegate) {
this._needsToCallDidTranslateEndDelegate = false;
this.callDelegateMethod("didTranslateEnd", this);
}
}
},
didDraw: {
value: function () {
if (!this.viewportHeight || !this.viewportWidth) {
this.needsDraw = true;
}
}
},
// TODO doc
/**
*/
_updateLength: {
value: function _updateLength() {
if (this._paths) {
var iPath,
pathsLength = this._paths.length,
iterations,
iLength,
maxLength = 0,
div, mod,
i;
if (pathsLength > 0) {
mod = this._numberOfIterations % pathsLength;
div = (this._numberOfIterations - mod) / pathsLength;
for (i = 0; i < pathsLength; i++) {
iPath = this._paths[i];
iterations = div + ((i < mod) ? 1 : 0);
iLength = iterations - iPath.tailOffset + iPath.headOffset - 1;
if (iLength > maxLength) {
maxLength = iLength;
}
}
this.length = maxLength;
}
this.needsDraw = true;
}
}
},
/**
* `numberOfIterations` represents the number of iterations that
* would be visible without culling the ones that are outside the field of
* view. It is the same as the number of values from the content after
* filters are applied by the content controller.
*
* The content length is managed by the flow. Do not set this property.
* It is however safe to read and observe changes to the property.
*/
_numberOfIterations: {
value: 0
},
handle_numberOfIterationsChange: {
value: function () {
this._updateLength();
}
},
content: {
get: function () {
return this.getPath("contentController.content");
},
set: function (content) {
this.contentController = new RangeController().initWithContent(content);
}
},
// TODO doc
/**
*/
contentController: {
value: null
},
// TODO doc
/**
*/
isSelectionEnabled: {
value: null
},
// TODO doc
/**
*/
selectedIndexes: {
serializable: false,
value: null
},
// TODO doc
/**
*/
activeIndexes: {
serializable: false,
value: null
},
observeProperty: {
value: function (key, emit, scope) {
return observeProperty(this, key, emit, scope);
}
},
// TODO remove, will be obsoleted by inner template, provided we have a way
// to redraft the innter template with a wrapper node.
templateDidLoad: {
value: function () {
var self = this;
// TODO consider a two-way binding on needsDraw
this._repetition.willDraw = function () {
self.needsDraw = true;
};
}
},
_length: {
value: 0
},
/**
* The number of visible iterations after frustum culling, which is
* subject to variation depending on the scroll offset.
*
* <pre>
* +----+ +----+ +----+ +----+ +----+
* | | | | | | | | | | length = 5
* +----+ +----+ +----+ +----+ +----+
* --+ +----+ +----+ +----+ +----+ +-
* | | | | | | | | | | length = 6
* --+ +----+ +----+ +----+ +----+ +-
* </pre>
*/
length: {
get: function () {
return this._length;
},
set: function (value) {
if (value < 0) {
this._length = 0;
} else {
this._length = value;
}
}
},
// TODO doc
/*
*/
_scroll: {
value: 0
},
// TODO doc
/*
*/
_elasticScrollingRange: {
value: 20
},
_hasElasticScrolling: {
value: false
},
// TODO doc
/**
*/
hasElasticScrolling: {
get: function () {
return this._hasElasticScrolling;
},
set: function (value) {
if (value) {
this._hasElasticScrolling = true;
} else {
this._hasElasticScrolling = false;
}
}
},
// TODO doc
/**
*/
_slideOffsets: {
value: null
},
// TODO doc
/*
*/
_slideOffsetsLength: {
value: 0
},
// TODO doc
/*
*/
_maxSlideOffsetIndex: {
value: -1
},
// TODO doc
/*
*/
_minSlideOffsetIndex: {
value: 2e9
},
// TODO doc
/*
*/
_updateSlideOffset: {
value: function (index, value) {
var epsilon = 1e-4;
if (index >= 0) {
if ((value < -epsilon) || (value > epsilon)) {
if (typeof this._slideOffsets[index] === "undefined") {
this._slideOffsetsLength++;
if (index < this._minSlideOffsetIndex) {
this._minSlideOffsetIndex = index;
}
if (index > this._maxSlideOffsetIndex) {
this._maxSlideOffsetIndex = index;
}
}
this._slideOffsets[index] = value;
} else {
this._removeSlideOffset(index);
}
}
}
},
// TODO doc
/*
*/
_incrementSlideOffset: {
value: function (index, value) {
this._updateSlideOffset(index, this._getSlideOffset(index) + value);
}
},
// TODO doc
/*
*/
_removeSlideOffset: {
value: function (index) {
if (typeof this._slideOffsets[index] !== "undefined") {
var keys, i, integerKey, keysLength;
delete this._slideOffsets[index];
this._slideOffsetsLength--;
if (index === this._minSlideOffsetIndex) {
keys = Object.keys(this._slideOffsets);
this._minSlideOffsetIndex = 2e9;
for (i = 0, keysLength = keys.length; i < keysLength; i++) {
integerKey = keys[i] | 0;
if (integerKey < this._minSlideOffsetIndex) {
this._minSlideOffsetIndex = integerKey;
}
}
}
if (index === this._maxSlideOffsetIndex) {
if (typeof keys === "undefined") {
keys = Object.keys(this._slideOffsets);
keysLength = keys.length;
}
this._maxSlideOffsetIndex = -1;
for (i = 0; i < keysLength; i++) {
integerKey = keys[i] | 0;
if (integerKey > this._maxSlideOffsetIndex) {
this._maxSlideOffsetIndex = integerKey;
}
}
}
}
}
},
// TODO doc
/*
*/
_getSlideOffset: {
value: function (index) {
if (index < this._minSlideOffsetIndex) {
if (this._minSlideOffsetIndex > this._draggedSlideIndex) {
index = this._draggedSlideIndex;
} else {
index = this._minSlideOffsetIndex;
}
} else {
if (index > this._maxSlideOffsetIndex) {
if (this._maxSlideOffsetIndex < this._draggedSlideIndex) {
index = this._draggedSlideIndex;
} else {
index = this._maxSlideOffsetIndex;
}
}
}
if (typeof this._slideOffsets[index] !== "undefined") {
return this._slideOffsets[index];
} else {
return 0;
}
}
},
// TODO doc
/**
*/
scroll: {
get: function () {
return this._scroll;
},
set: function (value) {
if (value < 0) {
value = 0;
}
if (value > this.length) {
value = this.length;
}
if (this._hasElasticScrolling && (this._draggedSlideIndex !== null)) {
var i,
n,
min = this._draggedSlideIndex - this._elasticScrollingRange,
max = this._draggedSlideIndex + this._elasticScrollingRange,
tmp,
j,
x;
if (min > this._minSlideOffsetIndex) {
min = this._minSlideOffsetIndex;
}
if (max < this._maxSlideOffsetIndex) {
max = this._maxSlideOffsetIndex;
}
tmp = value - this._scroll;
if (min < 0) {
min = 0;
}
for (i = min; i <= max; i++) {
if (i !== this._draggedSlideIndex) {
this._incrementSlideOffset(i, tmp);
} else {
this._removeSlideOffset(i);
}
}
this._scroll = value;
} else {
this._scroll = value;
}
this.needsDraw = true;
}
},
previousStride: {
value: function () {
var currentPosition = Math.round(this.scroll / this.stride),
moveTo = (currentPosition - 1) * this.stride;
this.startScrollingIndexToOffset(0, -moveTo);
}
},
nextStride: {
value: function () {
var currentPosition = Math.round(this.scroll / this.stride),
moveTo = (currentPosition + 1) * this.stride;
this.startScrollingIndexToOffset(0, -moveTo);
}
},
_isInputEnabled: { // TODO: Replace by pointerBehavior
value: true
},
// TODO doc
/**
*/
isInputEnabled: {
get: function () {
return this._isInputEnabled;
},
set: function (value) {
if (value) {
this._isInputEnabled = true;
} else {
this._isInputEnabled = false;
}
}
},
// TODO doc
/**
*/
_draggedSlideIndex: {
value: 0
},
// TODO doc
/**
*/
draggedSlideIndex: {
get: function () {
return this._draggedSlideIndex;
},
set: function (value) {
if (value !== this._draggedSlideIndex) {
if (value !== null) {
var offset = this._getSlideOffset(value),
min = this._minSlideOffsetIndex,
max = this._maxSlideOffsetIndex,
i;
this._incrementSlideOffset(this._draggedSlideIndex, -offset);
for (i = min; i <= max; i++) {
if (i !== this._draggedSlideIndex) {
this._incrementSlideOffset(i, -offset);
}
}
this._removeSlideOffset(value);
this._scroll -= offset;
this._flowTranslateComposer._scroll = this._scroll;
}
this._draggedSlideIndex = value;
this.needsDraw = true;
}
}
},
// TODO doc
/**
*/
_elasticScrollingSpeed: {
value: 1
},
elasticScrollingSpeed: {
get: function () {
return this._elasticScrollingSpeed;
},
set: function (value) {
this._elasticScrollingSpeed = parseFloat(value);
}
},
// TODO doc
/**
*/
lastDrawTime: {
value: null
},
// TODO doc
/**
*/
offset: {
enumerable: false,
value: function (slideIndex) {
var pathsLength = this._paths.length,
pathIndex = slideIndex % pathsLength,
slideTime = Math.floor(slideIndex / pathsLength) - this._scroll + this._paths[pathIndex].headOffset;
return {
pathIndex: pathIndex,
slideTime: slideTime + this._getSlideOffset(slideIndex)
};
}
},
serializeSelf: {
value: function (serializer) {
serializer.setAllValues();
// TODO: we need a way to add nodes to the serialization... we only
// have methods to serialize components.
// HACK: we're only going to serialize components if their DOM
// element is a direct child of the flow, since we don't have a way
// to add elements to the serialization there's really no point in
// doing anyelse reliably.
var originalContent = this.originalContent;
for (var i = 0, node; (node = originalContent[i]); i++) {
if (node.component) {
serializer.addObject(node.component);
}
}
}
}
});
if (window.MontageElement) {
MontageElement.define("montage-flow", Flow);
}