import * as THREE from 'three';
import { EVENTS } from '../Constants';
/**
* @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: EVENTS.RETICLE.RETICLE_RIPPLE_END } );
}
material.map.needsUpdate = true;
};
/**
* Reticle ripple start event
* @type {object}
* @event Reticle#reticle-ripple-start
*/
this.dispatchEvent( { type: EVENTS.RETICLE.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: EVENTS.RETICLE.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: EVENTS.RETICLE.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: EVENTS.RETICLE.RETICLE_UPDATE, progress } );
if ( progress >= 1.0 ) {
window.cancelAnimationFrame( this.timerId );
if ( this.callback ) { this.callback(); }
this.end();
this.ripple();
}
}
} );
export { Reticle };