API Docs for: 0.1.0
Show:

File: source/js/app/astronomical_object.js

/**
 * @module AstronomicalObject
 */
define(['gl', 'glMatrix', 'shaders', 'buffers'], function (gl, glMatrix, shaderProgram, buffers) {

    /**
     * AstronomicalObject is a class that represents Planets, Moons, the Sun, Galaxy, and Saturn's Rings.
     * 
     * @class AstronomicalObject
     * @constructor
     * @param {Object}  config                  The config object.
     * @param {String}  config.name             Name of the Astronomical Body. Useful for debugging, but also used in generating the instructions for the keyboard shortcuts.
     * @param {Array}   config.origin           X, Y, Z co-ordinates of the origin of the body in space.
     * @param {int}     config.orbitDistance    (in miles) from whatever it is orbiting. This is then automatically reduced for presen tation purposes.
     * @param {float}   config.orbitalPeriod    Number of days to make a full orbit.
     * @param {float}   config.spinPeriod       Number of days to rotate fully on its axis.
     * @param {int}     config.radius           (in miles). This is then automatically increased for presentation purposes.
     * @param {float}   config.axis             Rotational axis (in degrees).
     * @param {String}  config.texture          Url pointing to the texture image to be mapped to the object.
     * @param {String}  config.specularTexture  Url pointing to the specular map to be mapped to the object.
     * @param {String}  config.shortcutKey      The key that when pressed should make the camera snap to the object.
     * @param {boolean} config.spins            Determines whether or not the object should spin on its axis.
     * @param {boolean} config.spinsClockwise   Defaults to false. Determines spin direction.
     * @param {boolean} config.useLighting      Determines whether or not the object should be affected by Phong shading.
     * @param {boolean} config.spherical        Determines which buffers to initialise and draw the object with (cuboidal or spherical).
     */
    var AstronomicalObject = function (config) {
        this.setAttributes(config);
        this.setOrigin(config.origin);
        this.setRandomStartingOrbit();
        this.initMatrix();
        this.initTextures();
        buffers.initBuffers(this);
    };

    AstronomicalObject.prototype = {

        /**
         * Sets the attributes of the object instance based on the passed config object.
         * @method setAttributes
         * @param {Object} config The config object.
         */
        setAttributes: function (config) {
            this.name                 = config.name            || 'name not set';
            this.orbits               = config.orbits          || false;
            this.orbitDistance        = config.orbitDistance   || 0;
            this.orbitalPeriod        = config.orbitalPeriod   || 1;
            this.spinPeriod           = config.spinPeriod      || 1;
            this.radius               = config.radius          || 10;
            this.textureImage         = config.texture         || 'textures/moon.gif';
            this.specularTextureImage = config.specularTexture || false;
            this.spherical            = this.getBoolean(config.spherical);
            this.useLighting          = this.getBoolean(config.useLighting);
            this.spins                = this.getBoolean(config.spins);
            this.spinsClockwise       = this.getBoolean(config.spinsClockwise, false);
            this.shortcutKey          = config.shortcutKey;
            this.setAxis(config.axis);
            this.normalise();
            this.prepareSpecialCases();
        },

        /**
         * Sets the origin of the object, using the passed value if there is one, or calculating based on the orbited object if there isn't.
         * @method setOrigin
         * @param {Array} origin Three-value array representing the origin, or null.
         */
        setOrigin: function (origin) {
            if (origin) {
                this.origin = origin;
            }
            else if (this.orbits) {
                this.origin = [];
                this.origin[0] = this.orbits.origin[0];
                this.origin[1] = this.orbits.origin[1];
                this.origin[2] = this.orbits.origin[2] - this.distanceFromBodyWeAreOrbiting;
            }
            else {
                this.origin = [0, 0, 0];
            }
        },

        /**
         * Called on initialisation - this moves the object to a position in its orbit, randomised to prevent all objects starting off in a long straight line.
         * @method setRandomStartingOrbit
         */
        setRandomStartingOrbit: function () {
            this.lastSpinAngle = 0;
            this.lastOrbitAngle = 0;
            this.cumulativeOrbitAngle = 0;

            if (this.name === 'Saturn\'s Rings') {
                // angle rings towards the Sun
                var saturnsRingsAngle = this.degreesToRadians(90);
                this.lastOrbitAngle = saturnsRingsAngle;
                this.cumulativeOrbitAngle = saturnsRingsAngle;
            }
            else if (this.orbits) {
                var randomStartingOrbit = (Math.PI * 2) / Math.random();
                if (this.spinsClockwise) {
                    randomStartingOrbit *= -1;
                }
                this.lastOrbitAngle = randomStartingOrbit;
                this.cumulativeOrbitAngle = randomStartingOrbit;
            }
        },

        /**
         * Initialises the model view matrix.
         * @method initMatrix
         */
        initMatrix: function () {
            this.modelViewMatrix = glMatrix.mat4.create();

            if (this.orbits.orbits) {
                glMatrix.mat4.rotate(this.modelViewMatrix, this.modelViewMatrix, this.orbits.lastOrbitAngle, [0, 1, 0]);
                glMatrix.mat4.translate(this.modelViewMatrix, this.modelViewMatrix, this.orbits.origin);
            }

            glMatrix.mat4.rotate(this.modelViewMatrix, this.modelViewMatrix, this.lastOrbitAngle, [0, 1, 0]);
            glMatrix.mat4.translate(this.modelViewMatrix, this.modelViewMatrix, [0, 0, -this.distanceFromBodyWeAreOrbiting]);
        },

        /**
         * Initialises the textures for the object.
         * @method initTextures
         */
        initTextures: function () {
            if (this.textureImage) {
                this.initTexture(this.textureImage, 'texture');
            }
            if (this.specularTextureImage) {
                this.initTexture(this.specularTextureImage, 'specularTexture');
            }
        },

        /**
         * Initialises a texture for the object.
         * @method initTexture
         * @param {String} imageSrc         URL pointing to the texture image.
         * @param {String} imageProperty    The property to set texture to on this object.
         */
        initTexture: function (imageSrc, imageProperty) {
            var texture = gl.createTexture();
            texture.image = new Image();
            texture.image.crossOrigin = 'anonymous';

            var self = this;

            texture.image.onload = function () {
                self.handleLoadedTexture(texture, imageProperty);
            };

            texture.image.src = imageSrc;
        },

        /**
         * Handle the image texture once it has downloaded.
         * @method handleLoadedTexture
         * @param {Object} texture A WebGL TEXTURE_2D object.
         * @param {String} imageProperty The property to set texture to on this object.
         */
        handleLoadedTexture: function (texture, imageProperty) {
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
            gl.generateMipmap(gl.TEXTURE_2D);
            gl.bindTexture(gl.TEXTURE_2D, null);
            this[imageProperty] = texture;
            this.isReady = true;
        },

        /**
         * False until all assets for the Astronomical Object have been downloaded (i.e. the texture maps).
         * @property isReady
         * @type {Boolean}
         * @default false
         */
        isReady: false,

        /**
         * Sets the axis of the object.
         * @method setAxis
         * @param {int} axis Axis (in degrees) that the object rotates on.
         */
        setAxis: function (axis) {
            axis = axis || 0;
            this.axis = this.degreesToRadians(axis);
            this.axisArray = [
                this.axis / this.degreesToRadians(90),
                1 - (this.axis / this.degreesToRadians(90)),
                0
            ];
        },

        /**
         * Adjusts the scales and radiuses for aesthetic reasons.
         * @method normalise
         */
        normalise: function () {
            this.orbitDistance /= 50000;
            this.radius /= 100;
        },

        /**
         * Executes code specific to individual entities, e.g. the Sun/Saturn's Rings. In future, this could be extracted out into a subclass.
         * @method prepareSpecialCases
         */
        prepareSpecialCases: function () {
            this.distanceFromBodyWeAreOrbiting = 0;
            if (this.orbits) {
                this.distanceFromBodyWeAreOrbiting = this.radius + this.orbitDistance + this.orbits.radius;
            }
            
            if (this.name === 'Sun') {
                this.radius /= 10;
            }
            else if (this.name === 'Saturn\'s Rings') {
                this.distanceFromBodyWeAreOrbiting = 0;
                this.orbitalPeriod = this.orbits.orbitalPeriod;
                this.spinPeriod    = this.orbits.spinPeriod;
            }
        },

        /**
         * Converts degrees to radians.
         * @method degreesToRadians
         * @param  {int} celsius Value in degrees.
         * @return {float}       Converted value in radians.
         */
        degreesToRadians: function (celsius) {
            return celsius * (Math.PI / 180);
        },

        /**
         * Converts the given value into a boolean.
         * @method getBoolean
         * @param  {Object} attribute Value to convert (typically a boolean or null)
         * @param {boolean} defaultValue Value to default to if one is not specified.
         * @return {boolean}          The boolean value.
         */
        getBoolean: function (attribute, defaultValue) {
            if (defaultValue === undefined) {
                defaultValue = true;
            }
            return attribute === undefined ? defaultValue : attribute;
        },

        /**
         * Draws the object, relative to a projection matrix handles by the Camera object.
         * @method draw
         * @param  {array} projectionMatrix glMatrix object (mat4) representing projection of the camera.
         */
        draw: function (projectionMatrix) {
            this.setupLighting(projectionMatrix);
            this.setupTexture();
            buffers.drawElements(this);
        },

        /**
         * Initialises the shader variables for lighting.
         * @method setupLighting
         * @param  {array} projectionMatrix glMatrix object (mat4) representing projection of the camera.
         */
        setupLighting: function (projectionMatrix) {
            var normalMatrix = glMatrix.mat3.create();
            gl.uniform1i(shaderProgram.showSpecularHighlightsUniform, true);
            gl.uniform1i(shaderProgram.useLightingUniform, this.useLighting);
            gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, projectionMatrix);
            gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, this.modelViewMatrix);
            glMatrix.mat3.normalFromMat4(normalMatrix, this.modelViewMatrix);
            gl.uniformMatrix3fv(shaderProgram.nMatrixUniform, false, normalMatrix);
        },

        /**
         * Sets up the texture.
         * @method setupTexture
         */
        setupTexture: function () {
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, this.texture);
            gl.uniform1i(shaderProgram.useTexturesUniform, true);
            gl.uniform1i(shaderProgram.samplerUniform, 0);
            gl.uniform1i(shaderProgram.showSpecularSamplerUniform, false);
            
            if (this.specularTextureImage) {
                gl.uniform1i(shaderProgram.showSpecularSamplerUniform, true);
                gl.activeTexture(gl.TEXTURE1);
                gl.bindTexture(gl.TEXTURE_2D, this.specularTexture);
                gl.uniform1i(shaderProgram.specularSamplerUniform, 1);
            }
        },

        /**
         * Performs the calculations necessary for the object to orbit and spin on its axis, if applicable.
         * @method animate
         * @param {float} millisecondsPerDay The number of milliseconds that represent a day - this is integral in some of the calculations of the animation.
         * @param {float} millisecondsSinceLastFrame The number of milliseconds since the last frame was rendered.
         */
        animate: function (millisecondsPerDay, millisecondsSinceLastFrame) {

            var orbitAmount = this.calculatePortionOf(this.orbitalPeriod, millisecondsSinceLastFrame, millisecondsPerDay),
                spinAmount  = this.calculatePortionOf(this.spinPeriod, millisecondsSinceLastFrame, millisecondsPerDay);

            if (this.orbits) {
                var translationMatrix = glMatrix.mat4.create();

                this.beforeOrbit(translationMatrix);

                // NORMAL PLANETS
                if (!this.orbits.orbits) {
                    // 3. move to origin of body we're orbiting
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, this.distanceFromBodyWeAreOrbiting]);

                    // 4. rotate by extra orbit angle
                    glMatrix.mat4.rotate(translationMatrix, translationMatrix, orbitAmount, [0, 1, 0]);

                    // 5. move back out to orbit space
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, -this.distanceFromBodyWeAreOrbiting]);
                }
                // MOONS etc
                else {
                    // 1. move to center of earth
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, this.distanceFromBodyWeAreOrbiting]);
                    
                    // 2. rotate by the moon's CUMULATIVE orbit amount
                    glMatrix.mat4.rotate(translationMatrix, translationMatrix, -this.cumulativeOrbitAngle, [0, 1, 0]);
                    
                    // 3. move to center of sun
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, this.orbits.distanceFromBodyWeAreOrbiting]);

                    // 4. rotate by earth's LAST orbit angle
                    glMatrix.mat4.rotate(translationMatrix, translationMatrix, this.orbits.lastOrbitAngle, [0, 1, 0]);

                    // 5. move back out by earth's distance
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, -this.orbits.distanceFromBodyWeAreOrbiting]);

                    // 6. rotate by the moon's cumulative orbit amount PLUS the new orbit
                    glMatrix.mat4.rotate(translationMatrix, translationMatrix, this.cumulativeOrbitAngle + orbitAmount, [0, 1, 0]);

                    // 7. move back out to orbit space (away from earth)
                    glMatrix.mat4.translate(translationMatrix, translationMatrix, [0, 0, -this.distanceFromBodyWeAreOrbiting]);
                }

                // move the planet according to its orbit matrix
                glMatrix.mat4.multiply(this.modelViewMatrix, this.modelViewMatrix, translationMatrix);

                this.afterOrbit(spinAmount);
            }
            else if (this.spins) {
                glMatrix.mat4.rotate(this.modelViewMatrix, this.modelViewMatrix, spinAmount, [0, 1, 0]);
            }
            
            this.updateAttributes(orbitAmount, spinAmount);
        },

        /**
         * Rotation to perform before orbit.
         * @method beforeOrbit
         * @param  {Object} translationMatrix glMatrix to multiply by modelViewMatrix
         */
        beforeOrbit: function (translationMatrix) {
            // unspin
            if (this.isNotFirstAnimationFrame) {
                var angle = this.spinsClockwise ? this.lastSpinAngle : -this.lastSpinAngle;
                glMatrix.mat4.rotate(translationMatrix, translationMatrix, angle, [0, 1, 0]);
                glMatrix.mat4.rotate(translationMatrix, translationMatrix, -1, this.axisArray);
            }
        },

        /**
         * Rotation to perform after orbit.
         * @method afterOrbit
         * @param  {float} spinAmount Spin amount to take into account.
         */
        afterOrbit: function (spinAmount) {
            // perform spin
            var angle = this.spinsClockwise ? (-this.lastSpinAngle - spinAmount) : (this.lastSpinAngle + spinAmount);
            glMatrix.mat4.rotate(this.modelViewMatrix, this.modelViewMatrix, 1, this.axisArray);
            glMatrix.mat4.rotate(this.modelViewMatrix, this.modelViewMatrix, angle, [0, 1, 0]);
            this.isNotFirstAnimationFrame = true;
        },

        /**
         * Calculates the portion of a given attribute, based on the number of milliseconds since the last frame and the number of milliseconds which represents a day.
         * @method calculatePortionOf
         * @param  {int} attribute A property of the current object, e.g. orbitalPeriod
         * @param  {float} millisecondsSinceLastFrame    Number of milliseconds since last frame.
         * @param  {float} millisecondsPerDay The number of milliseconds that represent a day - this is integral in some of the calculations of the animation.
         * @return {float}           Angle (in radians) that should be moved by.
         */
        calculatePortionOf: function (attribute, millisecondsSinceLastFrame, millisecondsPerDay) {
            var proportion = millisecondsSinceLastFrame / (millisecondsPerDay * attribute),
                proportionInRadians = (Math.PI * 2) * proportion;
            return proportionInRadians;
        },

        /**
         * Updates the object's attributes concerning angles.
         * @method updateAttributes
         * @param  {float} orbitAmount Last orbit amount travelled
         * @param  {float} spinAmount  Last spin amount spun
         */
        updateAttributes: function (orbitAmount, spinAmount) {
            this.lastOrbitAngle = orbitAmount;
            this.cumulativeOrbitAngle += this.lastOrbitAngle;
            this.lastSpinAngle += spinAmount;
        }
    };

    return AstronomicalObject;
});