# interface/Reticle.js

    import * as THREE from 'three';

    /**
     * @classdesc Reticle 3D Sprite
     * @constructor
     * @param {THREE.Color} [color=0xffffff] - Color of the reticle sprite
     * @param {boolean} [autoSelect=true] - Auto selection
     * @param {number} [dwellTime=1500] - Duration for dwelling sequence to complete
     */

    function Reticle ( color = 0xffffff, autoSelect = true, dwellTime = 1500 ) {

        this.dpr = window.devicePixelRatio;

        const { canvas, context } = this.createCanvas();
        const material = new THREE.SpriteMaterial( { color, map: this.createCanvasTexture( canvas ) } );

        THREE.Sprite.call( this, material );

        this.canvasWidth = canvas.width;
        this.canvasHeight = canvas.height;
        this.context = context;
        this.color = color instanceof THREE.Color ? color : new THREE.Color( color );

        this.autoSelect = autoSelect;
        this.dwellTime = dwellTime;
        this.rippleDuration = 500;
        this.position.z = -10;
        this.center.set( 0.5, 0.5 );
        this.scale.set( 0.5, 0.5, 1 );

        this.startTimestamp = null;
        this.timerId = null;
        this.callback = null;

        this.frustumCulled = false;

        this.updateCanvasArcByProgress( 0 );

    };

    Reticle.prototype = Object.assign( Object.create( THREE.Sprite.prototype ), {

        constructor: Reticle,

        /**
         * Set material color
         * @param {THREE.Color} color
         * @memberOf Reticle
         * @instance
         */
        setColor: function ( color ) {

            this.material.color.copy( color instanceof THREE.Color ? color : new THREE.Color( color ) );

        },

        /**
         * Create canvas texture
         * @param {HTMLCanvasElement} canvas
         * @memberOf Reticle
         * @instance
         * @returns {THREE.CanvasTexture}
         */
        createCanvasTexture: function ( canvas ) {

            const texture = new THREE.CanvasTexture( canvas );
            texture.minFilter = THREE.LinearFilter;
            texture.magFilter = THREE.LinearFilter;
            texture.generateMipmaps = false;

            return texture;

        },

        /**
         * Create canvas element
         * @memberOf Reticle
         * @instance
         * @returns {object} object
         * @returns {HTMLCanvasElement} object.canvas
         * @returns {CanvasRenderingContext2D} object.context
         */
        createCanvas: function () {

            const width = 32;
            const height = 32;
            const canvas = document.createElement( 'canvas' );
            const context = canvas.getContext( '2d' );
            const dpr = this.dpr;

            canvas.width = width * dpr;
            canvas.height = height * dpr;
            context.scale( dpr, dpr );

            context.shadowBlur = 5;
            context.shadowColor = 'rgba(200,200,200,0.9)';

            return { canvas, context };

        },

        /**
         * Update canvas arc by progress
         * @param {number} progress
         * @memberOf Reticle
         * @instance
         */
        updateCanvasArcByProgress: function ( progress ) {

            const context = this.context;
            const { canvasWidth, canvasHeight, material } = this;
            const dpr = this.dpr;
            const degree = progress * Math.PI * 2;
            const color = this.color.getStyle();
            const x = canvasWidth * 0.5 / dpr;
            const y = canvasHeight * 0.5 / dpr;
            const lineWidth = 3;

            context.clearRect( 0, 0, canvasWidth, canvasHeight );
            context.beginPath();

            if ( progress === 0 ) {
                context.arc( x, y, canvasWidth / 16, 0, 2 * Math.PI );
                context.fillStyle = color;
                context.fill();
            } else {
                context.arc( x, y, canvasWidth / 4 - lineWidth, -Math.PI / 2, -Math.PI / 2 + degree );
                context.strokeStyle = color;
                context.lineWidth = lineWidth;
                context.stroke();
            }

            context.closePath();

            material.map.needsUpdate = true;

        },

        /**
         * Ripple effect
         * @memberOf Reticle
         * @instance
         * @fires Reticle#reticle-ripple-start
         * @fires Reticle#reticle-ripple-end
         */
        ripple: function () {

            const context = this.context;
            const { canvasWidth, canvasHeight, material } = this;
            const duration = this.rippleDuration;
            const timestamp = performance.now();
            const color = this.color;
            const dpr = this.dpr;
            const x = canvasWidth * 0.5 / dpr;
            const y = canvasHeight * 0.5 / dpr;

            const update = () => {

                const timerId = window.requestAnimationFrame( update );
                const elapsed = performance.now() - timestamp;
                const progress = elapsed / duration;
                const opacity = 1.0 - progress > 0 ? 1.0 - progress : 0;
                const radius = progress * canvasWidth * 0.5 / dpr;

                context.clearRect( 0, 0, canvasWidth, canvasHeight );
                context.beginPath();
                context.arc( x, y, radius, 0, Math.PI * 2 );
                context.fillStyle = `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${opacity})`;
                context.fill();
                context.closePath();

                if ( progress >= 1.0 ) {

                    window.cancelAnimationFrame( timerId );
                    this.updateCanvasArcByProgress( 0 );

                    /**
                     * Reticle ripple end event
                     * @type {object}
                     * @event Reticle#reticle-ripple-end
                     */
                    this.dispatchEvent( { type: 'reticle-ripple-end' } );

                }

                material.map.needsUpdate = true;

            };

            /**
             * Reticle ripple start event
             * @type {object}
             * @event Reticle#reticle-ripple-start
             */
            this.dispatchEvent( { type: 'reticle-ripple-start' } );

            update();

        },

        /**
         * Make reticle visible
         * @memberOf Reticle
         * @instance
         */
        show: function () {

            this.visible = true;

        },

        /**
         * Make reticle invisible
         * @memberOf Reticle
         * @instance
         */
        hide: function () {

            this.visible = false;

        },

        /**
         * Start dwelling
         * @param {function} callback
         * @memberOf Reticle
         * @instance
         * @fires Reticle#reticle-start
         */
        start: function ( callback ) {

            if ( !this.autoSelect ) {

                return;

            }

            /**
             * Reticle start event
             * @type {object}
             * @event Reticle#reticle-start
             */
            this.dispatchEvent( { type: 'reticle-start' } );

            this.startTimestamp = performance.now();
            this.callback = callback;
            this.update();

        },

        /**
         * End dwelling
         * @memberOf Reticle
         * @instance
         * @fires Reticle#reticle-end
         */
        end: function(){

            if ( !this.startTimestamp ) { return; }

            window.cancelAnimationFrame( this.timerId );

            this.updateCanvasArcByProgress( 0 );
            this.callback = null;
            this.timerId = null;
            this.startTimestamp = null;

            /**
             * Reticle end event
             * @type {object}
             * @event Reticle#reticle-end
             */
            this.dispatchEvent( { type: 'reticle-end' } );

        },

        /**
         * Update dwelling
         * @memberOf Reticle
         * @instance
         * @fires Reticle#reticle-update
         */
        update: function () {

            this.timerId = window.requestAnimationFrame( this.update.bind( this ) );

            const elapsed = performance.now() - this.startTimestamp;
            const progress = elapsed / this.dwellTime;

            this.updateCanvasArcByProgress( progress );

            /**
             * Reticle update event
             * @type {object}
             * @event Reticle#reticle-update
             */
            this.dispatchEvent( { type: 'reticle-update', progress } );

            if ( progress >= 1.0 ) {

                window.cancelAnimationFrame( this.timerId );
                if ( this.callback ) { this.callback(); }
                this.end();
                this.ripple();

            }

        }

    } );

    export { Reticle };