/** * @author Inateno / http://inateno.com / http://dreamirl.com */ /** * @constructor Camera * @class the eyes used to see in your scenes :)<br> * you have to append it to a Render and have to give it a scene to look in<br> * you can move your camera inside a render as you want<br> * also you can make more than one camera in a Render or looking in a scene<br> * <br><br> * example: if you want to make a mini-map, you can make a camera with big sizes (FHD), but little scale(0.2) * and maybe override the render method to call custom rendering for mini-map<br> * then you got two camera, these two are looking at the same scene, and are in the same Render * your "mini-map" camera is over the first carmera * <br><br> * example2: on a split-screen multiplayer game, you can make one camera by player, with input for each cameras * @example Game.camera = new DE.Camera( 1920, 1080, 0, 0, { "name": "mainGame", "backgroundColor": "green" } ); * @param {Int} width initial width inside the render * @param {Int} height initial height inside the render * @param {Int} x position in the Render * @param {Int} y position in the Render * @param {Object} [params] optional parameters * @property {String} [name="noname"] name your camera * @property {String} [tag="none"] assign tags if it's can be useful for you * @property {Scene} [scene=null] you can give a scene on creation, or later **/ define( [ 'DE.CONFIG', 'DE.Sizes', 'DE.Vector2', 'DE.CanvasBuffer', 'DE.CollisionSystem', 'DE.ImageManager', 'DE.Event' ], function( CONFIG, Sizes, Vector2, CanvasBuffer, CollisionSystem, ImageManager, Event ) { function Camera( width, height, x, y, params ) { params = params || {}; this.name = params.name || ""; this.tag = params.tag || ""; this.scene = params.scene || null; this.gui = undefined; this.renderSizes = new Sizes( width, height, params.scale || params.scaleX || 1 , params.scale || params.scaleY || 1 ); this.fieldSizes = new Sizes( width, height, params.scale || params.scaleX || 1 , params.scale || params.scaleY || 1 ); // position in the render (canvas) this.renderPosition= new Vector2( x + width * 0.5, y + height * 0.5, params.z || -10 ); this.savedPosition = new Vector2( x + width * 0.5, y + height * 0.5, params.z || -10 ); // position inside the sceneworld this.scenePosition = new Vector2( params.realx || x, params.realy || y , params.realz || params.z || -10 ); this.limits = { minX: params.minX != undefined ? params.minX : undefined ,maxX: params.maxX != undefined ? params.maxX : undefined ,minY: params.minY != undefined ? params.minY : undefined ,maxY: params.maxY != undefined ? params.maxY : undefined }; this.opacity = params.opacity || 1; this.backgroundColor = params.backgroundColor || null; this.backgroundImage = params.backgroundImage || null; this.cameras = new Array(); this.maxCameras = 0; this.freeze = false; this.sleep = false; this.startX = 0 this.startY = 0 this._buffer = new CanvasBuffer( this.renderSizes.width, this.renderSizes.height ); // this._gameObjects = []; // sotre last active objects this._visibleGameObjects = []; this.indexMouseOver = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; // 20 touches max ? this.lastPointersPos = {}; /**** * store between two event types if you asked to prevent some events * @private */ this._propagationEvent = [ {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} ]; // add Events components on camera Event.addEventComponents( this ); } /**** * getBuffer@void previously the _buffer was private - let to maximise retro-version (but I'll disapear, don't user it) you shouldn't get this buffer and do action on, but it's usefull for the engine */ Camera.prototype.getBuffer = function(){ return this._buffer; } /**** * render@void( ctx@CanvasContext, drawRatio@float, physicRatio@float ) renderise the camera inside the given ctx you shouldn't change this method */ Camera.prototype.render = function( ctx, drawRatio, physicRatio ) { if ( this.sleep ) { return; } var _buffer = this._buffer; if ( !this.freeze ) { _buffer.ctx.globalAlpha = this.opacity; if ( this.backgroundColor != null ) { _buffer.ctx.fillStyle = this.backgroundColor; _buffer.ctx.fillRect( 0, 0, this.renderSizes.width, this.renderSizes.height ); } if ( this.backgroundImage != null ) { _buffer.ctx.drawImage( ImageManager.images[ this.backgroundImage ], 0, 0, this.renderSizes.width, this.renderSizes.height ); } _buffer.ctx.save(); // renderize here game objects if ( this.scene ) { var _gameObjects /*= this._gameObjects*/ = this.scene.gameObjects; this._visibleGameObjects = []; for ( var i = 0, t = _gameObjects.length, g,ratioz; i < t; i++ ) { g = _gameObjects[ i ]; if ( g && g.enable && g.position.z > this.scenePosition.z && ( g.position.x + g.biggerOffset.width >= this.scenePosition.x && g.position.x - g.biggerOffset.width <= this.scenePosition.x + this.fieldSizes.width ) && ( g.position.y + g.biggerOffset.height >= this.scenePosition.y && g.position.y - g.biggerOffset.height <= this.scenePosition.y + this.fieldSizes.height ) ) { this._visibleGameObjects.push( g ); // distance from 10 between object and camera is ratio 1 ratioz = 10 / ( g.position.z - this.scenePosition.z ); g.render( _buffer.ctx, physicRatio, ratioz, this.scenePosition, this.renderSizes ); } } } else { _buffer.ctx.textAlign = "center"; _buffer.ctx.fillStyle = "white"; _buffer.ctx.fillText( "No scene affiliated :(", this.renderSizes.width * 0.5, this.renderSizes.height * 0.5 ); } _buffer.ctx.restore(); _buffer.ctx.globalAlpha = this.opacity; // debuging - display name and stroke the camera if ( CONFIG.DEBUG ) { _buffer.ctx.fillStyle = "white"; _buffer.ctx.textAlign = "left"; _buffer.ctx.fillText( "Camera " + this.name, 10, 20); _buffer.ctx.strokeStyle = "red"; _buffer.ctx.strokeRect( 0, 0, this.renderSizes.width >> 0 , this.renderSizes.height >> 0 ); _buffer.ctx.fillStyle = "yellow"; _buffer.ctx.fillRect( this.renderSizes.width - 10, this.renderSizes.height - 10, 10, 10 ); _buffer.ctx.fillRect( this.renderSizes.width - 10, 0, 10, 10 ); _buffer.ctx.fillRect( 0, this.renderSizes.height - 10, 10, 10 ); _buffer.ctx.fillRect( 0, 0, 10, 10 ); _buffer.ctx.fillRect( this.renderSizes.width * 0.5 ,this.renderSizes.height * 0.5 , 20, 5 ); _buffer.ctx.fillRect( this.renderSizes.width * 0.5 ,this.renderSizes.height * 0.5 , 5, 20 ); } this.getFocus(); this.checkLimits(); this.applyShake(); } ctx.translate( this.renderPosition.x * drawRatio >> 0 , this.renderPosition.y * drawRatio >> 0 ); ctx.rotate( this.renderPosition.rotation ); ctx.drawImage( _buffer.canvas , -this.renderSizes.width * this.renderSizes.scaleX * drawRatio * 0.5 >> 0 , -this.renderSizes.height * this.renderSizes.scaleY * drawRatio * 0.5 >> 0 , this.renderSizes.width * this.renderSizes.scaleX * drawRatio >> 0 , this.renderSizes.height * this.renderSizes.scaleY * drawRatio >> 0 ); // the GUI will totally change with DOM components, try to not use it // prefer using GameObjects in your scene if ( this.gui ) { this.gui.render( ctx, this.renderPosition, this.renderSizes, drawRatio, physicRatio ); } ctx.rotate( -this.renderPosition.rotation ); ctx.translate( -this.renderPosition.x * drawRatio >> 0 , -this.renderPosition.y * drawRatio >> 0 ); } /**** * check camera limits */ Camera.prototype.checkLimits = function() { var limits = this.limits; if ( limits.minX != undefined && this.scenePosition.x < limits.minX ) this.scenePosition.x = limits.minX; else if ( limits.maxX != undefined && this.scenePosition.x + this.renderSizes.width > limits.maxX ) this.scenePosition.x = limits.maxX - this.renderSizes.width; if ( limits.minY != undefined && this.scenePosition.y < limits.minY ) this.scenePosition.y = limits.minY; else if ( limits.maxY != undefined && this.scenePosition.y + this.renderSizes.height > limits.maxY ) this.scenePosition.y = limits.maxY - this.renderSizes.height; } /*** * screenChangedSizeIndex@void( physicRatio@float ) when the engine, or you change quality TODO - remove newSizes if this isn't used / useful */ Camera.prototype.screenChangedSizeIndex = function( physicRatio, newSizes ) { this.renderSizes.width = this.fieldSizes.width * physicRatio >> 0; this.renderSizes.height = this.fieldSizes.height * physicRatio >> 0; this.renderPosition.x = this.savedPosition.x * physicRatio >> 0; this.renderPosition.y = this.savedPosition.y * physicRatio >> 0; this._buffer.canvas.width = this.renderSizes.width; this._buffer.canvas.height = this.renderSizes.height; } /**** * add@void( camera@Camera ) childs possible with camera, not often usefull but could be TODO - I think there is problems with this, I just tried it quickly */ Camera.prototype.add = function( camera ) { this.cameras.push( camera ); ++this.maxCameras; } /**** * remove@void( camera@Camera ) remove a camera affilied in this one */ Camera.prototype.remove = function( camera ) { var pos = this.cameras.indexOf( camera ); if ( pos == -1 ) { CONFIG.debug.log( "%cRemove camera not found ", 1, "color:orange", camera ); return; } this.cameras.splice( pos, 1 ); this.maxCameras--; } /**** * create a shake with given range you can only have one at a time */ Camera.prototype.shake = function( xRange, yRange, duration ) { this.shakeData = { "startedAt" : Date.now() ,"duration" : duration ,"xRange" : xRange ,"yRange" : yRange ,"prevX" : this.shakeData ? this.shakeData.prevX : 0 ,"prevY" : this.shakeData ? this.shakeData.prevY : 0 }; } /**** * make the shake happen */ Camera.prototype.applyShake = function() { if ( !this.shakeData ) return; var shake = this.shakeData; // restore previous shake this.scenePosition.x -= shake.prevX; this.scenePosition.y -= shake.prevY; if ( Date.now() - this.shakeData.startedAt > this.shakeData.duration ) { delete this.shakeData; return; } shake.prevX = - ( Math.random() * shake.xRange ) + ( Math.random() * shake.xRange ) >> 0; shake.prevY = - ( Math.random() * shake.yRange ) + ( Math.random() * shake.yRange ) >> 0; this.scenePosition.x += shake.prevX; this.scenePosition.y += shake.prevY; } /**** * focus@void( gameObject@GameObject, offsets@Vector2 ) give a target to this camera, then camera will focus it until you changed or removed it you can lock independent axes */ Camera.prototype.focus = function( gameObject, lock ) { this.target = gameObject; this.focusLocks = lock; } /**** * getFocus@void apply focus on target if there is one */ Camera.prototype.getFocus = function() { if ( !this.target ) return; var pos = this.target.getPos(); var ratioz = ( Math.abs( this.scenePosition.z + pos.z ) - 10 ) * 0.1 + 1; if ( !this.focusLocks.x ) this.scenePosition.x = pos.x - ( this.fieldSizes.width * 0.5 ) / ratioz; if ( !this.focusLocks.y ) this.scenePosition.y = pos.y - ( this.fieldSizes.height * 0.5 ) / ratioz; } /**** convertMousePos@MouseVector2( mouse@MouseVector2, physicRatio@float ) * convert mouse pos with harmonics and ratio */ Camera.prototype.convertMousePos = function( mouse, physicRatio ) { var harmonics = this.renderPosition.getHarmonics(); if ( harmonics.sin == 0 && harmonics.cos == 0 ) { if ( this.renderSizes.scaleX != 1 || this.renderSizes.scaleY != 1 ) { return { x: ( mouse.x / physicRatio - ( ( this.renderSizes.width - this.renderSizes.width * this.renderSizes.scaleX ) / 2 ) ) / this.renderSizes.scaleX >> 0 , y: ( mouse.y / physicRatio - ( ( this.renderSizes.height - this.renderSizes.height * this.renderSizes.scaleY ) / 2 ) ) / this.renderSizes.scaleY >> 0 , "index": mouse.index }; } return { x: mouse.x / physicRatio >> 0 , y: mouse.y / physicRatio >> 0 , "index": mouse.index }; } var x = ( this.renderSizes.width / 2 - mouse.x ); var y = ( this.renderSizes.height / 2 - mouse.y ); if ( this.renderSizes.scaleX != 1 || this.renderSizes.scaleY != 1 ) { return { "x": ( ( -(x * harmonics.cos + y * harmonics.sin ) + this.renderSizes.width / 2 ) / physicRatio - ( ( this.renderSizes.width - this.renderSizes.width * this.renderSizes.scaleX ) / 2 ) ) / this.renderSizes.scaleX >> 0 , "y": ( ( (x * harmonics.sin + y * -harmonics.cos ) + this.renderSizes.height / 2 ) / physicRatio - ( ( this.renderSizes.height - this.renderSizes.height * this.renderSizes.scaleY ) / 2 ) ) / this.renderSizes.scaleY >> 0 , "index": mouse.index , "isDown": mouse.isDown }; } return { "x": ( -(x * harmonics.cos + y * harmonics.sin ) + this.renderSizes.width / 2 ) / physicRatio >> 0 , "y": ( (x * harmonics.sin + y * -harmonics.cos ) + this.renderSizes.height / 2 ) / physicRatio >> 0 , "index": mouse.index, "isDown": mouse.isDown }; } /*** * custom events return true inside these functions if you want stop propagation (not propaging in GameObjects) */ Camera.prototype.onMouseDown = function(){}; Camera.prototype.onMouseUp = function(){}; Camera.prototype.onMouseMove = function(){}; Camera.prototype.onMouseEnter = function(){}; Camera.prototype.onMouseLeave = function(){}; Camera.prototype.onMouseClick = function(){}; /* last event, called after all very usefull when you want to do something only if you didn't click on anything (just stopPropagation and this won't be called) */ Camera.prototype.onLastMouseMove = function(){}; Camera.prototype.onLastMouseDown = function(){}; Camera.prototype.onLastMouseUp = function(){}; Camera.prototype.onLastMouseClick = function(){}; /*** * onMouseDown@void( mouse@MouseVector2, physicRatio@float ) */ Camera.prototype.oOnMouseDown = function( mouse, physicRatio ) { mouse.isDown = true; mouse.date = Date.now(); this._propagationEvent[ mouse.index ] = mouse; this.mouseDetectorHandler( "Down", mouse, physicRatio ); } /*** * onMouseUp@void( mouse@MouseVector2, physicRatio@float ) */ Camera.prototype.oOnMouseUp = function( mouse, physicRatio ) { if ( Date.now() < this._propagationEvent[ mouse.index ].date + CONFIG.CLICK_DELAY ) this.mouseDetectorHandler( "Click", mouse, physicRatio ); this.mouseDetectorHandler( "Up", mouse, physicRatio ); this._propagationEvent[ mouse.index ] = {}; } /*** * onMouseMove@void( mouse@MouseVector2, physicRatio@float ) */ Camera.prototype.oOnMouseMove = function( mouse, physicRatio ) { if ( !this._propagationEvent[ mouse.index ].index ) this._propagationEvent[ mouse.index ] = mouse; this.lastPointersPos[ mouse.index ] = mouse; this.indexMouseOver[ mouse.index ] = true; this.mouseDetectorHandler( "Move", mouse, physicRatio ); } /**** * onMouseEnter@void( mouse@MouseVector2, physicRatio@float ) * when the mouse enter in the camera field this isn't called by the main mouse handler because it's a custom event over the native */ Camera.prototype.oOnMouseEnter = function( mouse, physicRatio ) { mouse = this.convertMousePos( mouse, physicRatio ); this.onMouseEnter( mouse ); } /**** * onMouseLeave@void( mouse@MouseVector2, physicRatio@float ) * when the mouse leave the camera field (same than previously) */ Camera.prototype.oOnMouseLeave = function( mouse, physicRatio ) { this.indexMouseOver[ mouse.index ] = false; mouse = this.convertMousePos( mouse, physicRatio ); this.onMouseLeave( mouse ); } /**** * mouseDetectorHandler@bool( eventType@string, mouse@MouseVector2, physicRatio@float ) main mouse handler */ Camera.prototype.mouseDetectorHandler = function( eventType, mouse, physicRatio ) { if ( this.freeze || this.sleep || this._propagationEvent[ mouse.index ][ "prevent" + eventType ] ) return; mouse = this.convertMousePos( mouse, physicRatio ); for ( var i in this.scene[ "onGlobalMouse" + eventType ] ) { if ( this.scene[ "onGlobalMouse" + eventType ][ i ].enable ) if ( this.scene[ "onGlobalMouse" + eventType ][ i ][ "onGlobalMouse" + eventType ]( mouse, this._propagationEvent[ mouse.index ] ) ) return; } this.trigger( "mouse" + eventType, mouse ); if ( this[ "onMouse" + eventType ]( mouse, this._propagationEvent[ mouse.index ] ) || mouse.stopPropagation ) return; if ( this.gui && this.gui[ "onMouse" + eventType ]( mouse, this._propagationEvent[ mouse.index ] ) || mouse.stopPropagation ) return; mouse.x += this.scenePosition.x; mouse.y += this.scenePosition.y; for ( var i = this._visibleGameObjects.length - 1, g; i >= 0; --i ) { g = this._visibleGameObjects[ i ]; if ( _gameObjectMouseEvent( eventType, g, mouse, this._propagationEvent[ mouse.index ] ) ) return; } if ( !mouse.stopPropagation && !this._propagationEvent[ mouse.index ][ "preventLast" + eventType ] ) { this.trigger( "onLastMouse" + eventType, mouse, this._propagationEvent[ mouse.index ] ); this[ "onLastMouse" + eventType ]( mouse, this._propagationEvent[ mouse.index ] ); for ( var i in this.scene[ "onLastGlobalMouse" + eventType ] ) { if ( this.scene[ "onLastGlobalMouse" + eventType ][ i ].enable ) this.scene[ "onLastGlobalMouse" + eventType ][ i ][ "onLastGlobalMouse" + eventType ]( mouse, this._propagationEvent[ mouse.index ] ); } } } /**** * _gameObjectMouseEvent@bool( eventType@string, g@GameObject, mouse@MouseVector2 ) */ function _gameObjectMouseEvent( eventType, g, mouse, propagationEvent ) { if ( !g.enable ) return false; if ( g.collider && ( g[ "onMouse" + eventType ] || g.onMouseLeave || g.onMouseEnter ) ) { var wasOver = g.indexMouseOver[ mouse.index ]; if ( CollisionSystem.checkCollisionWith( mouse, g.collider ) ) { // mouseEnter event occurs here g.indexMouseOver[ mouse.index ] = true; if ( !wasOver && g.onMouseEnter && g.onMouseEnter( mouse, propagationEvent ) ) return true; if ( g[ "onMouse" + eventType ] && g[ "onMouse" + eventType ]( mouse, propagationEvent ) ) return true; } // no collision but was over last frame, there is a leave event else if ( wasOver && g.onMouseLeave ) { g.indexMouseOver[ mouse.index ] = false; if ( g.onMouseLeave( mouse, propagationEvent ) ) return true; } } for ( var c = g.childrens.length - 1, co; c >= 0; --c ) { if ( _gameObjectMouseEvent( eventType, g.childrens[ c ], mouse, propagationEvent ) ) return true; } } Event.addEventCapabilities( Camera ); Camera.prototype.DEName = "Camera"; CONFIG.debug.log( "Camera loaded", 3 ); return Camera; } );