Source: ui/flow.reel/flow-translate-composer.js

var TranslateComposer = require("../../composer/translate-composer").TranslateComposer,
    Point = require("../../core/geometry/point").Point,
    convertPointFromPageToNode = Point.convertPointFromPageToNode;

/**
 * @class FlowTranslateComposer
 * @extends TranslateComposer
 */
var FlowTranslateComposer = exports.FlowTranslateComposer = TranslateComposer.specialize( /** @lends FlowTranslateComposer# */ {

    constructor: {
        value: function FlowTranslateComposer() {
            this.handleMousewheel = this.handleWheel;
        }
    },

    stealChildrenPointer: {
        value: true
    },

    _linearScrollingVector: {
        value: [-300, 0]
    },

    /**
     * A constant 2d vector used to transform a drag vector into a scroll vector
     */
    linearScrollingVector: {
        get: function () {
            return this._linearScrollingVector;
        },
        set: function (value) {
            this._linearScrollingVector = value;
        }
    },

    /**
     *  How fast the cursor has to be moving before translating starts. Only
     *  applied when another component has claimed the pointer.
     *  @type {number}
     *  @default 500
     */
    startTranslateSpeed: {
        value: 500
    },

    startTranslateRadius: {
        value: 8
    },

    // TODO doc
    /**
     */
    _startPageX: {
        value: null
    },

    // TODO doc
    /**
     */
    _startPageY: {
        value: null
    },

    // TODO doc
    /**
     */
    _pageX: {
        value: null
    },

    // TODO doc
    /**
     */
    _pageY: {
        value: null
    },

    // TODO doc
    /**
     */
    _pointerStartX: {
        value: null
    },

    // TODO doc
    /**
     */
    _pointerStartY: {
        value: null
    },

    // TODO doc
    /**
     */
    _contentOffsetX: {
        value: null
    },

    // TODO doc
    /**
     */
    _contentOffsetY: {
        value: null
    },

    _superStart: {
        value: TranslateComposer.prototype._start
    },

    // TODO doc
    /**
     */
    _start: {
        value: function (x, y, target, timeStamp) {
            this._superStart(x, y, target, timeStamp);

            // TODO: Review using getComputedStyle outside draw cycle
            var computedStyle = window.getComputedStyle(this._element, null),
                borderLeft = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("border-left-width")),
                borderTop = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("border-top-width")),
                paddingLeft = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("padding-left")),
                paddingTop = this.convertCssPixelsPropertyStringToNumber(computedStyle.getPropertyValue("padding-top")),
                point = convertPointFromPageToNode(this._element, new Point().init(x, y));

            this._pointerStartX = this._pointerX = point.x - borderLeft - paddingLeft;
            this._pointerStartY = this._pointerY = point.y - borderTop - paddingTop;
            this._contentOffsetX = this._startPageX - this._pointerStartX;
            this._contentOffsetY = this._startPageY - this._pointerStartY;
            this._computePointedElement();
            this._startPageX = this._pageX = x;
            this._startPageY = this._pageY = y;
            this._startScroll = this._scroll;
            this._previousScrollDelta = 0;
            this._scrollEnd = null;
        }
    },

    // TODO doc
    /**
     */
    _analyzeMovement: {
        value: function (event) {
            var velocity = event.velocity,
                speed, dX, dY;

            if (!velocity) {
                return;
            }
            speed = velocity.speed;
            if (speed >= this.startTranslateSpeed) {
                this._stealPointer();
            } else {
                dX = this.startPageX - event.pageX;
                dY = this.startPageY - event.pageY;
                if (dX * dX + dY * dY > this.startTranslateRadius * this.startTranslateRadius) {
                    this._stealPointer();
                }
            }
        }
    },

    // TODO doc
    /**
     */
    _dispatchTranslateStart: {
        value: function (x, y) {
            var translateStartEvent = document.createEvent("CustomEvent");

            translateStartEvent.initCustomEvent("translateStart", true, true, null);
            translateStartEvent.scroll = this._scroll;
            translateStartEvent.translateX = 0;
            translateStartEvent.translateY = 0;
            translateStartEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateStartEvent);
        }
    },

    // TODO doc
    /**
     */
    _dispatchTranslateEnd: {
        value: function () {
            var translateEndEvent = document.createEvent("CustomEvent");

            translateEndEvent.initCustomEvent("translateEnd", true, true, null);
            translateEndEvent.scroll = this._scroll;
            translateEndEvent.translateX = 0;
            translateEndEvent.translateY = 0;
            translateEndEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateEndEvent);
        }
    },

    _dispatchTranslateCancel: {
        value: function () {
            var translateCancelEvent = document.createEvent("CustomEvent");

            translateCancelEvent.initCustomEvent("translateCancel", true, true, null);
            translateCancelEvent.scroll = this._scroll;
            translateCancelEvent.translateX = 0;
            translateCancelEvent.translateY = 0;
            translateCancelEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateCancelEvent);
        }
    },

    // TODO doc
    /**
     */
    _dispatchTranslate: {
        value: function () {
            var translateEvent = document.createEvent("CustomEvent");
            translateEvent.initCustomEvent("translate", true, true, null);
            translateEvent.scroll = this._scroll;
            translateEvent.translateX = 0;
            translateEvent.translateY = 0;
            translateEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateEvent);
        }
    },

    // TODO doc
    /**
     */
    _move: {
        value: function (x, y) {
            var pointerDelta;

            if (this._isFirstMove) {
                this._dispatchTranslateStart();
                this._isFirstMove = false;
            }
            this._pageX = x;
            this._pageY = y;
            this._updateLinearScroll();
            if (this._shouldDispatchTranslate) {
                this._dispatchTranslate();
            }
        }
    },

    // TODO doc
    /**
     */
    _end: {
        value: function (event) {
            /*this.startTime = Date.now();
            if (!this._isFirstMove) {
                this._dispatchTranslateEnd();
            }
            this._releaseInterest();*/
            if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
                this.startTime = Date.now();
                this.endX = this.startX = this._pageX;
                this.endY = this.startY = this._pageY;

                var velocity = event.velocity;

                if ((this._hasMomentum) && ((velocity.speed>40) || this.translateStrideX)) {
                    if (this._axis !== "vertical") {
                        this.momentumX = velocity.x * this._pointerSpeedMultiplier * (this._invertXAxis ? 1 : -1);
                    } else {
                        this.momentumX = 0;
                    }
                    if (this._axis !== "horizontal") {
                        this.momentumY = velocity.y * this._pointerSpeedMultiplier * (this._invertYAxis ? 1 : -1);
                    } else {
                        this.momentumY=0;
                    }
                    this.endX = this.startX + (this.momentumX * this.__momentumDuration / 2000);
                    this.endY = this.startY + (this.momentumY * this.__momentumDuration / 2000);
                    this.startStrideXTime = null;
                    this.startStrideYTime = null;
                    this.animateMomentum = true;
                } else {
                    this.animateMomentum = false;
                }

                if (this.animateMomentum) {
                    this._animationInterval();
                } else if (!this._isFirstMove) {
                    // Only dispatch a translateEnd if a translate start has occured
                    this._dispatchTranslateEnd();
                }
            }
            this._releaseInterest();
        }
    },

    _translateEndTimeout: {
        value: null
    },

    _mousewheelStrideTimeout: {
        value: null
    },

    _previousDelta: {
        value: 0
    },

    _listenToWheelEvent: {
        value: true
    },

    captureWheel: {
        value: function () {
            if (!this.eventManager.componentClaimingPointer(this._WHEEL_POINTER)) {
                this.eventManager.claimPointer(this._WHEEL_POINTER, this.component);
            }
        }
    },

    // TODO Add wheel event listener for Firefox
    // TODO doc
    /**
     */
    handleWheel: {
        value: function (event) {
            var self = this;

            // If this composers' component is claiming the "wheel" pointer then handle the event
            if (this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this.component)) {
                var oldScroll = this._scroll,
                    deltaX = event.wheelDeltaX || -event.deltaX || 0,
                    deltaY = event.wheelDeltaY || -event.deltaY || 0,
                    delta;

                if (this.translateStrideX) {
                    clearTimeout(this._mousewheelStrideTimeout);
                    if (Math.abs(this._linearScrollingVector[0]) > Math.abs(this._linearScrollingVector[1])) {
                        if (Math.abs(deltaX) > Math.abs(deltaY)) {
                            delta = this._linearScrollingVector[0] * -deltaX / Math.abs(this._linearScrollingVector[0]);
                        } else {
                            delta = 0;
                        }
                    } else {
                        if (Math.abs(deltaX) > Math.abs(deltaY)) {
                            delta = 0;
                        } else {
                            delta = this._linearScrollingVector[1] * -deltaY / Math.abs(this._linearScrollingVector[1]);
                        }
                    }
                    if ((this._mousewheelStrideTimeout === null) || (Math.abs(delta) > Math.abs(this._previousDelta * (this._mousewheelStrideTimeout === null ? 2 : 4)))) {
                        if (delta > 1) {
                            this.callDelegateMethod("previousStride", this);
                        } else {
                            if (delta < -1) {
                                this.callDelegateMethod("nextStride", this);
                            }
                        }
                    }
                    this._mousewheelStrideTimeout = setTimeout(function () {
                        self._mousewheelStrideTimeout = null;
                        self._previousDelta = 0;
                    }, 70);
                    self._previousDelta = delta;
                    if (delta !== 0 && this._shouldPreventDefault(event)) {
                        event.preventDefault();
                    }
                } else {
                    if (this._translateEndTimeout === null) {
                        this._dispatchTranslateStart();
                    }
                    this._pageX = this._pageX + ((deltaX * 20) / 100);
                    this._pageY = this._pageY + ((deltaY * 20) / 100);
                    this._updateScroll();
                    this._dispatchTranslate();
                    clearTimeout(this._translateEndTimeout);
                    this._translateEndTimeout = setTimeout(function () {
                        self._dispatchTranslateEnd();
                        self._translateEndTimeout = null;
                    }, 400);

                    // If we're not at one of the extremes (i.e. the scroll actually
                    // changed the translate) then we want to preventDefault to stop
                    // the page scrolling.
                    if (oldScroll !== this._scroll && this._shouldPreventDefault(event)) {
                        event.preventDefault();
                    }
                }
                this.eventManager.forfeitPointer(this._WHEEL_POINTER, this.component);
            }
        }
    },

    // TODO doc
    /**
     */
    _scroll: {
        value: 0
    },

    // TODO doc
    /**
     */
    scroll: {
        get: function () {
            return this._scroll;
        },
        set: function (value) {
            if ((this.minScroll !== null) && (value < this.minScroll)) {
                value = this.minScroll;
            }
            if ((this.maxScroll !== null) && (value > this.maxScroll)) {
                value = this.maxScroll;
            }
            this._scroll = value;
        }
    },

    // TODO doc
    /**
     */
    minScroll: {
        value: null
    },

    // TODO doc
    /**
     */
    maxScroll: {
        value: null
    },

    // TODO doc
    /**
     */
    _flow: {
        value: null
    },

    // TODO doc
    /**
     */
    flow: {
        get: function () {
            return this._flow;
        },
        set: function (value) {
            this._flow = value;
            this.component = value;
        }
    },

    // TODO doc
    /**
     */
    _updateScroll: {
        value: function () {
            this._updateLinearScroll();
        }
    },

    // TODO doc
    /**
     */
    _updateLinearScroll: {
        value: function () {
            var flow = this._flow,
                ratio = flow.isCameraEnabled ? 500 / flow._height : 1,
                x = ((this._pageX - this._startPageX) * this._linearScrollingVector[0] * ratio * flow._sceneScaleX.denominator) / flow._sceneScaleX.numerator,
                y = ((this._pageY - this._startPageY) * this._linearScrollingVector[1] * ratio * flow._sceneScaleY.denominator) / flow._sceneScaleY.numerator,
                squaredMagnitude = this._linearScrollingVector[0] * this._linearScrollingVector[0] + this._linearScrollingVector[1] * this._linearScrollingVector[1],
                scroll = (x + y) / squaredMagnitude;

            this.scroll += scroll - this._previousScrollDelta;
            this._previousScrollDelta = scroll;
        }
    },

    // TODO doc
    /**
     */
    frame: {
        value: function (timestamp) {
            if (this.isAnimating) {
                this._animationInterval();
            }
        }
    },

    // TODO doc
    /**
     */
    convertCssPixelsPropertyStringToNumber: {
        value: function (property) {
            if (typeof property === "string") {
                if (property.substr(-2) === "px") {
                    return property.substr(0, property.length - 2) * 1;
                } else {
                    return 0;
                }
            } else {
                return 0;
            }
        }
    },

    _rayPointDistance: {
        value: function (rayVector, point) {
            var dotProduct,
                magnitude,
                x, y, z;

            dotProduct = rayVector[0] * point[0] + rayVector[1] * point[1] + rayVector[2] * point[2];
            if (dotProduct >= 0) {
                magnitude = rayVector[0] * rayVector[0] + rayVector[1] * rayVector[1] + rayVector[2] * rayVector[2];
                dotProduct /= magnitude;
                x = rayVector[0] * dotProduct - point[0];
                y = rayVector[1] * dotProduct - point[1];
                z = rayVector[2] * dotProduct - point[2];
                return Math.sqrt(x * x + y * y + z * z);
            } else {
                // behind ray
                return false;
            }
        }
    },

    // TODO doc
    /**
     */
    _closerIndex: {
        value: null
    },

    _computePointedElement_spline: {
        value: []
    },

    // TODO doc
    /**
     */
    _computePointedElement: {
        value: function () {
            var splinePaths = this._flow._splinePaths,
                pathsLength = splinePaths.length;

            if (pathsLength) {
                var flow = this._flow,
                    vX = flow._viewpointTargetPoint[0] - flow._viewpointPosition[0],
                    vZ = flow._viewpointTargetPoint[2] - flow._viewpointPosition[2],
                    yAngle = Math.atan2(vX, vZ),
                    tmpZ = vZ * Math.cos(-yAngle) - vX * Math.sin(-yAngle),
                    xAngle = Math.atan2(flow._viewpointTargetPoint[1] - flow._viewpointPosition[1], tmpZ),
                    x2 = this._element.clientWidth * 0.5 - this._pointerX,
                    y2 = this._pointerY - this._element.clientHeight * 0.5,
                    perspective = (this._element.offsetHeight * 0.5) / Math.tan((flow._viewpointFov * flow._doublePI) * (1 / 720)),
                    z2, tmp,
                    splines = this._computePointedElement_spline,
                    visibleIndexes = flow._visibleIndexes,
                    length = visibleIndexes.length,
                    pathIndex,
                    slideIndex,
                    slideTime,
                    scale = flow._sceneScale,
                    closerIndex = null,
                    closerTime = null,
                    minDistance = 1e100,
                    distance,
                    indexTime,
                    rotation,
                    corner,
                    edge1,
                    edge2,
                    rayVector,
                    offset,
                    i;

                tmp = perspective * Math.cos(xAngle) - y2 * Math.sin(xAngle);
                y2 = perspective * Math.sin(xAngle) + y2 * Math.cos(xAngle);
                z2 = tmp * Math.cos(yAngle) - x2 * Math.sin(yAngle);
                x2 = tmp * Math.sin(yAngle) + x2 * Math.cos(yAngle);
                rayVector = [x2, y2, z2];
                for (i = 0; i < splinePaths.length; i++) {
                    for (i = 0; i < splinePaths.length; i++) {
                        splines[i] = splinePaths[i].transform([
                            scale.x.numerator / scale.x.denominator, 0, 0, 0,
                            0, scale.y.numerator / scale.y.denominator, 0, 0,
                            0, 0, scale.z.numerator / scale.z.denominator, 0,
                            -flow._viewpointPosition[0] + flow._firstIterationWidth * 0.5 + flow._firstIterationOffsetLeft,
                            -flow._viewpointPosition[1] + flow._firstIterationHeight * 0.5 + flow._firstIterationOffsetTop,
                            -flow._viewpointPosition[2],
                            1
                        ]);
                    }
                }
                for (i = 0; i < length; i++) {
                    offset = this._flow.offset(visibleIndexes[i]);
                    pathIndex = offset.pathIndex;
                    slideTime = offset.slideTime;
                    indexTime = splines[pathIndex]._convertSplineTimeToBezierIndexTime(slideTime);
                    if (indexTime !== null) {
                        var pos = splines[pathIndex].getPositionAtIndexTime(indexTime);
                        distance = this._rayPointDistance(rayVector, pos);
                        if (distance !== false) {
                            if (distance < minDistance) {
                                minDistance = distance;
                                closerIndex = visibleIndexes[i];
                            }
                        }
                    }
                }
                this._closerIndex = closerIndex;
                splines.length = 0;
            }
        }
    },

    // TODO
    /**
     */
    _previousScrollDelta: {
        value: 0
    },

    // TODO
    /**
     */
    _startScroll: {
        value: 0
    },

    // TODO doc
    /**
     */
    _translateStride: {
        value: null
    },

    // TODO doc
    /**
     */
    translateStride: {
        get: function () {
            return this._translateStride;
        },
        set: function (value) {
            this._translateStride = value;
            this.translateStrideX = value;
        }
    },

    // TODO doc
    /**
     */
    _scrollEnd: {
        value: null
    },

    // TODO doc
    /**
     */
    _scrollStart: {
        value: null
    },

    // TODO doc
    /**
     */
    _hasMomentum: {
        value: true
    },

    isLimitedToSingleStride: {
        value: false
    },

    // TODO doc
    /**
     */
    _animationInterval: {
        value: function () {
            var time = Date.now(), t, t2, tmp, tmpX, tmpY, animateStride = false, scroll, min, max;

            min = this.minScroll;
            max = this.maxScroll;
            this.minScroll = null;
            this.maxScroll = null;
            if (this._scrollEnd === null) {
                this._scrollStart = this.scroll;
                this._pageX = this.endX;
                this._pageY = this.endY;
                this._updateScroll();
                this._scrollEnd = this.scroll;
                if (this.isLimitedToSingleStride && this.translateStrideX) {
                    if (this._scrollEnd > Math.floor(this._scrollStart) + this.translateStrideX) {
                        this._scrollEnd = Math.floor(this._scrollStart) + this.translateStrideX;
                    }
                    if (this._scrollEnd < Math.ceil(this._scrollStart) - this.translateStrideX) {
                        this._scrollEnd = Math.ceil(this._scrollStart) - this.translateStrideX;
                    }
                }
                this._pageX = this.startX;
                this._pageY = this.startY;
                this._updateScroll();
            }
            if (this.animateMomentum) {
                t = time - this.startTime;
                if (t < this.__momentumDuration) {
                    this._pageX = this.startX + ((this.momentumX+this.momentumX*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                    this._pageY = this.startY + ((this.momentumY+this.momentumY*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                    this._updateScroll();
                    if (this.translateStrideX && (this.startStrideXTime === null) && ((this.__momentumDuration - t < this.translateStrideDuration) || (Math.abs(this.scroll - this._scrollEnd) < this.translateStrideX * 0.75))) {
                        this.startStrideXTime = time;
                        this._strideStartScroll = this._scroll;
                    }
                } else {
                    this.animateMomentum = false;
                }
            } else {
                if (this.startStrideXTime === null) {
                    this.startStrideXTime = this.startTime;
                    this._strideStartScroll = this._scrollStart;
                }
            }
            scroll = this.scroll;
            if (this.startStrideXTime && (time - this.startStrideXTime > 0)) {
                tmp = Math.round(this._scrollEnd / this.translateStrideX);
                if (time - this.startStrideXTime < this.translateStrideDuration) {
                    t = this._bezierTValue((time - this.startStrideXTime) / this.translateStrideDuration, 0.275, 0, 0.275, 1);
                    t2 = (time - this.startStrideXTime) / this.translateStrideDuration;
                    scroll = scroll * (1 - t2) + ((tmp *  this.translateStrideX) * t + (this._strideStartScroll) * (1 - t)) * t2;
                    animateStride = true;
                } else {
                    scroll = tmp * this.translateStrideX;
                    this.animateMomentum = false;
                }
            }
            this.minScroll = min;
            this.maxScroll = max;
            if (scroll < min) {
                scroll = min;
                this.animateMomentum = false;
                animateStride = false;
            }
            if (scroll > max) {
                scroll = max;
                this.animateMomentum = false;
                animateStride = false;
            }
            this.scroll = scroll;
            this.isAnimating = this.animateMomentum || animateStride;
            if (this.isAnimating) {
                this.needsFrame=true;
            } else {
                this._dispatchTranslateEnd();
                this._scrollEnd = null;
            }
        }
    }

});