Show:
'use strict';
(function(name, myClass) {
    let instance = new myClass();
    // This is only for unit test.
    if (typeof module !== 'undefined') {
        module['exports'] = instance;
    } else {
        window[name] = instance;
        instance.createCanvas();
    }
    return instance;

}) ('yeah', function(global) {
    let tracking = global.tracking || {};

    const MAX_TRACKING_FAST_THRESHOLD = 100;
    const MAX_SENSITIVITY = 100;
    const MIN_SENSITIVITY = 0;
    const DEFAULT_SENSITIVITY = 60;
    const MIN_CAPTURE_INTERVAL = 250;
    const DEFAULT_CAPTURE_INTERVAL = 1000;
    const DELAY_TO_DECENTRALIZE_CPU_USAGE = 200;
    const CAPTURE_OFFSET_TOP = 0;
    const CAPTURE_OFFSET_LEFT = 0;
    const CANVAS_HEIGHT_FOR_CALCULATION = 128;
    const DEFAULT_DELAY_FOR_VIDEO_INIT = 1000;
    const DEFAULT_MARKER_SIZE = 4;
    const DEFAULT_OPACITY = 1;
    const OVER_WRAPPED_OPACITY = 0.5;
    const HIDDEN_OPACITY = 0;

    const MIN_DATA_TO_CALC_YEAH = 2;

    const MIN_SENSITIVITY_AUTO_ADJUSTMENT = 2;
    const MAX_MODERATE_CORNER_COUNT = 400;
    const MIN_MODERATE_CORNER_COUNT = 250;
    const MAX_SENSITIVITY_ADJUST_DENOMINATOR = 500;
    const MIN_SENSITIVITY_ADJUST_DENOMINATOR = 100;
    const MAX_MATCH_RATE = 100;
    const MAX_YEAH_SCORE = 100;
    const MIN_YEAH_SCORE = 0;

    const INVERSE_BASE = 1;

    /**
     * Get flag for video playing status
     * http://www.w3schools.com/TagS/ref_av_dom.asp
     *
     * @private
     * @method isVideoPaused
     * @param {Object} videoElm Video element
     * @return {Boolean} Flag for video playing status (stop: true, playing: false)
     */
    function isVideoPaused(videoElm) {
        return (videoElm !== undefined) ? videoElm.paused : false;
    }

    /**
     * Promise to make delay
     *
     * @private
     * @method delayTimer
     * @param {Number} delay Delay time for resolve timer (ms)
     * @return {Object} Promise object to make delay like setTimeout
     */
    function delayTimer(delay) {
        return new Promise((resolve) => {
            setTimeout(resolve, delay);
        });
    }

    /**
     * Promise to wrap processing
     *
     * @private
     * @method pEmit
     * @param {Function} func Function to be wrapped by Promise
     * @return {Object} Promise object for some processing
     */
    function pEmit(func) {
        return new Promise((resolve, reject) => {
            try {
                let result = func();
                resolve(result);
            } catch (error) {
                reject(error);
            }
        });
    }

    /**
     * Fill marker on tracked corners and matched points
     *
     * @private
     * @method fillTrackedPointsOnCanvas
     * @param {Object} canvasContext Canvas element
     * @param {Object} trackedData Tracked data points
     * @param {Number} scaleInverted Inverted scale
     * @param {Object} options Options of Yeah class
     */
    function fillTrackedPointsOnCanvas(canvasContext, trackedData, scaleInverted, options) {
        if (!options.isShowCapturePanel) {
            return;
        }
        canvasContext.fillStyle = '#f00';
        let loopCnt = trackedData.cornerList.length;
        // eslint-disable-next-line
        while ((loopCnt-=2)>=0) {
            canvasContext.fillRect(
                trackedData.cornerList[loopCnt] * scaleInverted,
                // eslint-disable-next-line
                trackedData.cornerList[loopCnt + 1] * scaleInverted,
                options.markerSize, options.markerSize
            );
        }
        canvasContext.fillStyle = '#0f0';
        loopCnt = trackedData.matchList.length;
        let match;
        // eslint-disable-next-line
        while ((loopCnt-=2)>=0) { // Skip drawing the half of them.
            match = trackedData.matchList[loopCnt].keypoint1;
            canvasContext.fillRect(
                match[0] * scaleInverted, match[1] * scaleInverted,
                options.markerSize, options.markerSize
            );
        }
    }

    /**
     * A library to convert video to its excitement score.
     *
     * @class Yeah
     */
    class Yeah {
        /**
         * constructor of yeah.js class
         *
         * @constructor
         */
        constructor() {
            this.videoElm;
            this.userCanvas;
            this.userCtx;
            this.calcCanvas;
            this.calcCtx;
            this.options = {
                captureInterval:         DEFAULT_CAPTURE_INTERVAL,
                sensitivity:             DEFAULT_SENSITIVITY,
                isShowCapturePanel:      false,
                isAutoAdjustSensitivity: true,
                markerSize:              DEFAULT_MARKER_SIZE
            };
            this.startTime;
            this.scale;
            this.scaleInverted;
            this.customYeahCalculator;
            this.lastCornerList;
            this.lastDescriptorList;
            this.tsDataList = new Array();
            this.captureIntervalTimer;
        }

        /**
         * Create canvas
         *
         * @method createCanvas
         */
        createCanvas() {
            if (this.userCanvas === undefined) {
                this.userCanvas = document.createElement('canvas');
                this.userCtx = this.userCanvas.getContext('2d');
                this.calcCanvas = document.createElement('canvas');
                this.calcCtx = this.calcCanvas.getContext('2d');
            }
        }

        /**
         * Set options
         *
         * @method SetOptions
         * @param {Object} options Options to be set
         */
        setOptions(options) {
            if (options.captureInterval !== undefined) {
                this.setCaptureInterval(options.captureInterval);
            }
            if (options.sensitivity !== undefined) {
                this.setSensitivity(options.sensitivity);
            }
            if (options.isShowCapturePanel !== undefined) {
                this.setIsShowCapturePanel(options.isShowCapturePanel);
            }
            if (options.isAutoAdjustSensitivity !== undefined) {
                this.setIsAutoAdjustSensitivity(options.isAutoAdjustSensitivity);
            }
            if (options.markerSize !== undefined) {
                this.setMarkerSize(options.markerSize);
            }
        }

        /**
         * Get options
         *
         * @method getOptions
         * @return {Object} Options set to Yeah class
         */
        getOptions() {
            return this.options;
        }

        /**
         * Set video element
         *
         * @method setVideoElement
         * @param {Object} videoElm A video element as a capture target
         */
        setVideoElement(videoElm) {
            if (this.videoElm === undefined) {
                this.videoElm = videoElm;
                this.videoElm.parentNode.insertBefore(this.userCanvas, this.videoElm.nextSibling);
            }
        }

        /**
         * Set video src
         *
         * @method setVideoSrc
         * @param {String} src Video source url
         */
        setVideoSrc(src) {
            this.videoElm.src = src;
            this.clearLastCaptureInfo();
        }

        /**
         * Get capture interval
         *
         * @method getCaptureInterval
         * @return {Number} Capture interval
         */
        getCaptureInterval() {
            return this.options.captureInterval;
        }

        /**
         * Set capture interval
         *
         * @method setCaptureInterval
         * @param {Number} captureInterval Capture interval to be set
         */
        setCaptureInterval(captureInterval) {
            this.options.captureInterval = Math.max(MIN_CAPTURE_INTERVAL, captureInterval);
        }

        /**
         * Get sensitivity
         *
         * @method getSensitivity
         * @return {Number} Sensitivity
         */
        getSensitivity() {
            return this.options.sensitivity;
        }

        /**
         * Update sensitivity
         *
         * @method setSensitivity
         * @param {Number} sensitivity Sensitivity to be set
         */
        setSensitivity(sensitivity) {
            sensitivity = Math.min(MAX_SENSITIVITY, sensitivity);
            sensitivity = Math.max(MIN_SENSITIVITY, sensitivity);
            this.options.sensitivity = sensitivity;
        }

        /**
         * Get flag for showing capture panel
         *
         * @method isShowCapturePanel
         * @return {Boolean} Flag for show capture panel
         */
        isShowCapturePanel() {
            return this.options.isShowCapturePanel;
        }

        /**
         * Set flag for showing capture panel
         *
         * @method setIsShowCapturePanel
         * @param {Boolean} bool Flag to be set
         */
        setIsShowCapturePanel(bool) {
            this.options.isShowCapturePanel = bool;
            this.videoElm.style.opacity = bool ? OVER_WRAPPED_OPACITY : DEFAULT_OPACITY;
        }

        /**
         * Get flag for auto sensitivity adjustment
         *
         * @method isAutoAdjustSensitivity
         * @return {Boolean} Flag for auto sensitivity adjustment
         */
        isAutoAdjustSensitivity() {
            return this.options.isAutoAdjustSensitivity;
        }

        /**
         * Set flag for auto sensitivity adjustment
         *
         * @method setIsAutoAdjustSensitivity
         * @param {Boolean} bool Flag to be set
         */
        setIsAutoAdjustSensitivity(bool) {
            this.options.isAutoAdjustSensitivity = bool;
        }

        /**
         * Get marker size
         *
         * @method getMarkerSize
         * @return {Number} Marker size
         */
        getMarkerSize() {
            return this.options.markerSize;
        }

        /**
         * Set marker size
         *
         * @method setMarkerSize
         * @param {Number} markerSize Marker size to be set
         */
        setMarkerSize(markerSize) {
            this.options.markerSize = markerSize;
        }

        /**
         * play video and initialize canvas
         *
         * @method playVideo
         * @param {String} src Video src
         * @param {Number} delay Delay for copying height and width from video element
         * @return {Object} Promise object
         */
        playVideo(src, delay) {
            this.setVideoSrc(src);

            delay = delay || DEFAULT_DELAY_FOR_VIDEO_INIT;
            this.videoElm.style.opacity = HIDDEN_OPACITY;
            this.userCanvas.style.opacity = HIDDEN_OPACITY;

            return delayTimer(delay)
            .then(() => {
                this.userCanvas.width = this.videoElm.clientWidth;
                this.userCanvas.height = this.videoElm.clientHeight;
                this.scale = CANVAS_HEIGHT_FOR_CALCULATION / this.userCanvas.height;
                this.scaleInverted = INVERSE_BASE / this.scale;
                this.calcCanvas.width = this.userCanvas.width * this.scale;
                this.calcCanvas.height = this.userCanvas.height * this.scale;
                this.videoElm.style.opacity = this.isShowCapturePanel() ? OVER_WRAPPED_OPACITY : DEFAULT_OPACITY;
                this.userCanvas.style.opacity = DEFAULT_OPACITY;
            });
        }

        /**
         * Stop capturing video element
         *
         * @method stopCaptureVideo
         */
        stopCaptureVideo() {
            if (this.captureIntervalTimer) {
                clearInterval(this.captureIntervalTimer);
            }
        }

        /**
         * Start capturing video element to canvas
         *
         * @method startCaptureVideo
         * @param {Function} successCallback Callback after getting capture data
         * @param {Function} failureCallback Callback for getting error for each capture loop
         */
        startCaptureVideo(successCallback, failureCallback) {
            this.stopCaptureVideo();
            this.captureIntervalTimer = setInterval(() => {
                if (!isVideoPaused(this.videoElm)) {
                    if (this.isShowCapturePanel()) {
                        this.userCtx.drawImage(this.videoElm, CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP, this.videoElm.clientWidth, this.videoElm.clientHeight);
                    }
                    this.calcCtx.drawImage(this.videoElm, CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP, this.calcCanvas.width, this.calcCanvas.height);
                } else {
                    this.clearLastCaptureInfo();
                }
                delayTimer(DELAY_TO_DECENTRALIZE_CPU_USAGE)
                .then(() => this.findFeatures())
                .then((data) => pEmit(() => {
                    data.time = data.now - this.startTime;
                    data.yeah = null;
                    if (!isVideoPaused(this.videoElm)) {
                        this.setSensitivity(this.calcAdjustedSensitivity(data.cornerList.length, this.options));
                        data.yeah = this.calcYeah(data.matchRate, this.tsDataList);
                        fillTrackedPointsOnCanvas(this.userCtx, data, this.scaleInverted, this.options);
                        this.tsDataList.push(data);
                        if (this.tsDataList.length > MIN_DATA_TO_CALC_YEAH) {
                            this.tsDataList.shift();
                        }
                    }
                    data.sensitivity = this.getSensitivity();
                    return data;
                })).then((data) => {
                    successCallback(data);
                }).catch((error) => {
                    if (failureCallback !== undefined) {
                        failureCallback(error);
                    } else {
                        throw new Error('yeah.js: caught an error in captureIntervalTimer.');
                    }
                });
            }, this.getCaptureInterval());
        }

        /**
         * Set custom yeah calculator
         *
         * @method setCustomYeahCalculator
         * @param {Function} func Customized yeah calculator
         */
        setCustomYeahCalculator(func) {
            this.customYeahCalculator = func;
        }

        /**
         * Calculate yeah score from time series data set
         *
         * @method calcYeah
         * @param {Number} currentMatchRate Current match rate score
         * @param {Array} tsDataList List of corner detection and yeah score history data
         * @return {Number} Yeah score
         */
        calcYeah(currentMatchRate, tsDataList) {
            if (this.customYeahCalculator !== undefined) {
                return this.customYeahCalculator(currentMatchRate, tsDataList);
            }

            let yeahScore;
            let tsDataCnt = tsDataList.length;
            if (tsDataCnt >= MIN_DATA_TO_CALC_YEAH) {
                /* eslint-disable */
                yeahScore = (Math.abs(currentMatchRate - tsDataList[tsDataCnt - 1].matchRate) * 2
                    + Math.abs(tsDataList[tsDataCnt - 1].matchRate - tsDataList[tsDataCnt - 2].matchRate)) / 3;
                /* eslint-enable */
                yeahScore = Math.min(MAX_YEAH_SCORE, yeahScore);
                yeahScore = Math.max(MIN_YEAH_SCORE, yeahScore);
            } else {
                yeahScore = null;
            }
            return yeahScore;
        }

        /**
         * Clear last capture info
         *
         * @method clearLastCaptureInfo
         */
        clearLastCaptureInfo() {
            this.tsDataList = new Array();
            this.lastCornerList = undefined;
            this.lastDescriptorList = undefined;
        }

        /**
         * Find features from captured image on canvas
         *
         * @method findFeatures
         * @return {Object} Promise object to find features
         */
        findFeatures() {
            return pEmit(() => {
                let now = (new Date()).getTime();
                if (this.startTime === undefined) {
                    this.startTime = now;
                }
                let matchRate = null;
                if (isVideoPaused(this.videoElm)) {
                    return {
                        now:        now,
                        cornerList: [],
                        matchList:  [],
                        matchRate:  matchRate
                    };
                }

                tracking.Fast.THRESHOLD = MAX_TRACKING_FAST_THRESHOLD - this.getSensitivity();
                let imageData = this.calcCtx.getImageData(
                    CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP,
                    this.calcCtx.canvas.width, this.calcCtx.canvas.height
                );
                let gray = tracking.Image.grayscale(imageData.data, imageData.width, imageData.height);
                let cornerList = tracking.Fast.findCorners(gray, imageData.width, imageData.height);

                let matchList = new Array();
                let descriptorList = tracking.Brief.getDescriptors(gray, imageData.width, cornerList);
                if (cornerList.length && this.lastDescriptorList && this.lastCornerList) {
                    // tracking.Brief.reciprocalMatch is more accurate but takes higher cpu cost.
                    matchList = tracking.Brief.match(this.lastCornerList, this.lastDescriptorList, cornerList, descriptorList);
                    matchRate = Math.min(MAX_MATCH_RATE, matchList.length / cornerList.length * MAX_MATCH_RATE);
                }

                this.lastDescriptorList = descriptorList;
                this.lastCornerList = cornerList;

                return {
                    now:        now,
                    cornerList: cornerList,
                    matchList:  matchList,
                    matchRate:  matchRate
                };
            });
        }

        /**
         * Calculate adjusted sensitivity with found corner size
         *
         * @method calcAdjustedSensitivity
         * @param {Number} cornerCnt Found corner size
         * @param {Object} options Options of Yeah class
         * @return {Number} Calculated sensitivity
         */
        calcAdjustedSensitivity(cornerCnt, options) {
            let adjustedSensitivity = options.sensitivity;
            if (!options.isAutoAdjustSensitivity) {
                return adjustedSensitivity;
            }
            if (cornerCnt > MAX_MODERATE_CORNER_COUNT && options.sensitivity > MIN_SENSITIVITY) {
                adjustedSensitivity = adjustedSensitivity - Math.max(
                    MIN_SENSITIVITY_AUTO_ADJUSTMENT,
                    Math.ceil((cornerCnt - MAX_MODERATE_CORNER_COUNT) / MAX_SENSITIVITY_ADJUST_DENOMINATOR)
                );
                adjustedSensitivity = Math.max(MIN_SENSITIVITY, adjustedSensitivity);
            } else if (cornerCnt < MIN_MODERATE_CORNER_COUNT && options.sensitivity < MAX_SENSITIVITY) {
                adjustedSensitivity = adjustedSensitivity + Math.max(
                    MIN_SENSITIVITY_AUTO_ADJUSTMENT,
                    Math.ceil((MIN_MODERATE_CORNER_COUNT - cornerCnt) / MIN_SENSITIVITY_ADJUST_DENOMINATOR)
                );
                adjustedSensitivity = Math.min(MAX_SENSITIVITY, adjustedSensitivity);
            }
            return adjustedSensitivity;
        }

    }
    return Yeah;
} (this.self || global));