Source: ext/main.js

"use strict";

/**
 * Main Blend4Web module.
 * Implements methods to initialize and change the global params of the engine.
 * @module main
 * @local LoopCallback
 * @local RenderCallback
 * @local FPSCallback
 */
b4w.module["main"] = function(exports, require) {

/**
 * Loop callback.
 * @callback LoopCallback
 * @param {Number} timeline Timeline
 * @param {Number} delta Delta
 */

/**
 * Rendering callback.
 * @callback RenderCallback
 * @param {Number} delta Delta
 * @param {Number} timeline Timeline
 */

var m_anchors   = require("__anchors");
var m_anim      = require("__animation");
var m_assets    = require("__assets");
var m_cfg       = require("__config");
var m_compat    = require("__compat");
var m_cont      = require("__container");
var m_ctl       = require("__controls");
var m_data      = require("__data");
var m_debug     = require("__debug");
var m_ext       = require("__extensions");
var m_geom      = require("__geometry");
var m_input     = require("__input");
var m_hud       = require("__hud");
var m_nla       = require("__nla");
var m_lnodes    = require("__logic_nodes")
var m_obj       = require("__objects");
var m_phy       = require("__physics");
var m_print     = require("__print");
var m_render    = require("__renderer");
var m_scenes    = require("__scenes");
var m_sfx       = require("__sfx");
var m_shaders   = require("__shaders");
var m_textures  = require("__textures");
var m_time      = require("__time");
var m_trans     = require("__transform");
var m_util      = require("__util");
var m_version   = require("__version");
var m_particles = require("__particles");

var cfg_ctx = m_cfg.context;
var cfg_def = m_cfg.defaults;

var _elem_canvas_webgl = null;
var _elem_canvas_hud = null;

var _last_abs_time = 0;
var _pause_time = 0;
var _resume_time = 0;
var _loop_cb = [];

/**
 * FPS callback
 * @callback FPSCallback
 * @param {Number} fps_avg Averaged rendering FPS.
 * @param {Number} phy_fps_avg Averaged physics FPS.
 */
var _fps_callback = function() {};

var _fps_counter = function() {};

var _render_callback = function() {};
var _canvas_data_url_params = {
    callback: null,
    format: "image/png",
    quality: 1.0,
    blob_url: "",
    last_auto_revoke: false,
    curr_auto_revoke: false
};

var WEBGL_CTX_IDS = ["webgl", "experimental-webgl"];
var WEBGL2_CTX_IDS = ["webgl2", "experimental-webgl2"];

var _gl = null;

/**
 * NOTE: According to the spec, this function takes only one param
 */
var _requestAnimFrame = (function() {
  return window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
         window.oRequestAnimationFrame ||
         window.msRequestAnimationFrame ||
         function(callback) {return window.setTimeout(callback,
             1000/cfg_def.max_fps);};
})();

// public enums

/**
 * Create the WebGL context and initialize the engine.
 * @method module:main.init
 * @param {HTMLCanvasElement} elem_canvas_webgl Canvas element for WebGL
 * @param {HTMLCanvasElement} [elem_canvas_hud] Canvas element for HUD
 * @returns {WebGLRenderingContext|Null} WebGL context or null
 */
exports.init = function(elem_canvas_webgl, elem_canvas_hud) {
    m_cfg.set_paths();

    // NOTE: for debug purposes
    // works in chrome with --enable-memory-info --js-flags="--expose-gc"
    //window.setInterval(function() {window.gc();}, 1000);

    m_print.set_verbose(cfg_def.console_verbose);

    var ver_str = m_version.version_str() + " " + m_version.type() +
            " (" + m_version.date_str() + ")";
    m_print.log("%cINIT ENGINE", "color: #00a", ver_str);
    m_print.log("%cUSER AGENT:", "color: #00a", navigator.userAgent);

    // check gl context and performance.now()
    if (!window["WebGLRenderingContext"])
        return null;

    setup_clock();

    _elem_canvas_webgl = elem_canvas_webgl;
    if (elem_canvas_hud) {
        m_hud.init(elem_canvas_hud);
        _elem_canvas_hud = elem_canvas_hud;
    } else {
        // disable features which depend on HUD
        m_cfg.defaults.show_hud_debug_info = false;
        m_cfg.sfx.mix_mode = false;
    }

    m_compat.apply_context_alpha_hack();

    // allow WebGL 2 only in Chrome and Firefox
    if (!(m_compat.check_user_agent("Chrome") ||
                m_compat.check_user_agent("Firefox")))
        cfg_def.webgl2 = false;

    var gl = get_context(elem_canvas_webgl, cfg_def.webgl2);

    // fallback to WebGL 1
    if (!gl && cfg_def.webgl2) {
        cfg_def.webgl2 = false;
        gl = get_context(elem_canvas_webgl, false);
    }

    if (!gl)
        return null;

    m_print.log("%cINIT WEBGL " + (cfg_def.webgl2 ? "2" : "1"), "color: #00a");

    _gl = gl;

    init_context(_elem_canvas_webgl, _elem_canvas_hud, gl);
    m_cfg.apply_quality();
    m_compat.set_hardware_defaults(gl, true);

    m_shaders.load_shaders();

    if (cfg_def.ie11_edge_touchscreen_hack)
        elem_canvas_webgl.style["touch-action"] = "none";

    m_print.log("%cSET PRECISION:", "color: #00a", cfg_def.precision);

    return gl;
}

function setup_clock() {
    if (!window.performance) {
        m_print.log("Apply performance workaround");
        window.performance = {};
    }

    var curr_time = Date.now();

    if (!window.performance.now) {
        m_print.log("Apply performance.now() workaround");

        //cfg_def.no_phy_interp_hack = true;

        window.performance.now = function() {
            return Date.now() - curr_time;
        }
    }

    m_time.set_timeline(0);
}

function get_context(canvas, init_webgl2) {

    var ctx = null;
    
    var ctx_ids = init_webgl2 ? WEBGL2_CTX_IDS : WEBGL_CTX_IDS;

    for (var i = 0; i < ctx_ids.length; i++) {
        var name = ctx_ids[i];

        try {
            ctx = canvas.getContext(name, cfg_ctx);
        } catch(e) {
            // nothing
        }

        if (ctx)
            break;
    }

    if (ctx)
        m_compat.detect_tegra_invalid_enum_issue(ctx);

    return ctx;
}

function init_context(canvas, canvas_hud, gl) {
    canvas.addEventListener("webglcontextlost",
            function(event) {
                event.preventDefault();

                m_print.error("WebGL context lost");

                // at least prevent freeze
                pause();

            }, false);

    m_ext.setup_context(gl);

    var rinfo = m_ext.get_renderer_info();
    if (rinfo)
        m_print.log("%cRENDERER INFO:", "color: #00a",
            gl.getParameter(rinfo.UNMASKED_VENDOR_WEBGL) + ", " +
            gl.getParameter(rinfo.UNMASKED_RENDERER_WEBGL));

    m_render.setup_context(gl);
    m_geom.setup_context(gl);
    m_textures.setup_context(gl);
    m_shaders.setup_context(gl);
    m_debug.setup_context(gl);
    m_cont.setup_context(gl);
    m_data.setup_canvas(canvas);
    m_cont.init(canvas, canvas_hud);

    m_scenes.setup_dim(canvas.width, canvas.height, 1);

    m_sfx.init();

    _fps_counter = init_fps_counter();

    loop();
}

/**
 * Resize the rendering canvas.
 * @method module:main.resize
 * @param {Number} width New canvas width
 * @param {Number} height New canvas height
 * @param {Boolean} [update_canvas_css=true] Change canvas CSS width/height
 * @deprecated Use {@link module:container.resize|container.resize} instead
 */
exports.resize = resize;
function resize(width, height, update_canvas_css) {
    m_print.error_deprecated("main.resize", "container.resize");
    
    m_cont.resize(width, height, update_canvas_css);
}


/**
 * Set the callback for the FPS counter
 * @method module:main.set_fps_callback
 * @param {FPSCallback} fps_cb FPS callback
 */
exports.set_fps_callback = function(fps_cb) {
    _fps_callback = fps_cb;
}
/**
 * Remove the callback for the FPS counter
 * @method module:main.clear_fps_callback
 */
exports.clear_fps_callback = function() {
    _fps_callback = function() {};
}


/**
 * Set the rendering callback which is executed for every frame just before the
 * rendering. Only one callback is allowed.
 * @method module:main.set_render_callback
 * @param {RenderCallback} callback Render callback
 */
exports.set_render_callback = function(callback) {
    set_render_callback(callback);
}
function set_render_callback(callback) {
    _render_callback = callback;
}

/**
 * Remove the rendering callback.
 * @method module:main.clear_render_callback
 */
exports.clear_render_callback = function() {
    clear_render_callback();
}
function clear_render_callback() {
    _render_callback = function() {};
}




/**
 * Return the engine's global timeline value
 * @method module:main.global_timeline
 * @returns {Number} Floating-point number of seconds elapsed since the engine start-up
 * @deprecated Use {@link module:time.get_timeline|time.get_timeline} instead
 */
exports.global_timeline = function() {
    m_print.error_deprecated("main.global_timeline", "time.get_timeline");
    return m_time.get_timeline();
}

exports.pause = pause;
/**
 * Pause the engine
 * @method module:main.pause
 */
function pause() {
    if (is_paused())
        return;

    _pause_time = performance.now() / 1000;
    m_sfx.pause();
    m_phy.pause();
    m_textures.pause();
    m_anchors.pause();
}

/**
 * Resume the engine (after pausing)
 * @method module:main.resume
 */
exports.resume = function() {
    if (!is_paused())
        return;

    _resume_time = performance.now() / 1000;
    m_sfx.resume();
    m_phy.resume();
    m_textures.play(true);
    m_anchors.resume();
}

/**
 * Check if the engine is paused
 * @method module:main.is_paused
 * @returns {Boolean} Paused flag
 */
exports.is_paused = is_paused;
function is_paused() {
    return (_resume_time < _pause_time);
}

function loop() {
    var vr_display = cfg_def.stereo === "HMD" && m_input.get_webvr_display();
    if (vr_display)
        vr_display.requestAnimationFrame(loop);
    else
        _requestAnimFrame(loop);

    // float sec
    var abstime = performance.now() / 1000;

    if (!_last_abs_time)
        _last_abs_time = abstime;

    var delta = abstime - _last_abs_time;

    // do not render short frames
    if (delta < 1/cfg_def.max_fps)
        return;

    var timeline = m_time.get_timeline();

    for (var i = 0; i < _loop_cb.length; i++)
        _loop_cb[i](timeline, delta);

    if (!is_paused()) {
        // correct delta if resume occured since last frame
        if (_resume_time > _last_abs_time)
            delta -= (_resume_time - Math.max(_pause_time, _last_abs_time));

        timeline += delta;
        m_time.set_timeline(timeline);

        m_debug.update();

        m_assets.update();
        m_data.update();
        frame(timeline, delta);

        _fps_counter(delta);
    }

    _last_abs_time = abstime;

    if (vr_display && vr_display.isPresenting)
        vr_display.submitFrame();
}

function to_blob(callback, type, quality) {
    if (!_elem_canvas_webgl)
        return;

    if (_elem_canvas_webgl.toBlob)
        _elem_canvas_webgl.toBlob(callback, type, quality);
    else {
        var binStr = atob(_elem_canvas_webgl.toDataURL(type, quality).split(',')[1]);
        var data = new Uint8Array(binStr.length);

        for (var i = 0; i < binStr.length; i++)
            data[i] = binStr.charCodeAt(i);

        callback(new Blob([data], {type: type || 'image/png'}));
    }
}

function frame(timeline, delta) {
    // possible unload between frames
    if (!m_data.is_primary_loaded())
        return;

    m_hud.reset();

    m_trans.update(delta);

    m_lnodes.update(timeline, delta)

    m_nla.update(timeline, delta);

    // sound
    m_sfx.update(timeline, delta);

    // animation
    if (delta)
        m_anim.update(delta);

    // possible unload in animation callbacks
    if (!m_data.is_primary_loaded())
        return;

    m_phy.update(timeline, delta);

    // possible unload in physics callbacks
    if (!m_data.is_primary_loaded())
        return;

    //inputs should be updated before controls
    m_input.update(timeline);
    // controls
    m_ctl.update(timeline, delta);

    // possible unload in controls callbacks
    if (!m_data.is_primary_loaded())
        return;

    // anchors
    m_anchors.update(false);

    // objects
    m_obj.update(timeline, delta);

    // particles
    m_particles.update();

    // user callback
    _render_callback(delta, timeline);

    // possible unload in render callback
    if (!m_data.is_primary_loaded())
        return;

    // rendering
    m_scenes.update(timeline, delta);

    // anchors
    m_anchors.update_visibility();

    var cb = _canvas_data_url_params.callback;
    if (cb) {
        if (_canvas_data_url_params.last_auto_revoke)
            URL.revokeObjectURL(_canvas_data_url_params.blob_url);

        to_blob(function(blob) {
            _canvas_data_url_params.blob_url = URL.createObjectURL(blob);
            cb(_canvas_data_url_params.blob_url);
        }, _canvas_data_url_params.format, _canvas_data_url_params.quality);

        _canvas_data_url_params.callback = null;
        _canvas_data_url_params.format = "image/png";
        _canvas_data_url_params.quality = 1.0;
    }
}

function init_fps_counter() {
    var fps_avg = 60;       // decent default value

    var fps_frame_counter = 0;
    var interval = cfg_def.fps_measurement_interval;
    var interval_cb = cfg_def.fps_callback_interval;

    var fps_counter = function(delta) {
        // NOTE: fixes issues when delta=0
        if (delta < 1/cfg_def.max_fps)
            return;

        fps_avg = m_util.smooth(1/delta, fps_avg, delta, interval);

        // stays zero for disabled physics/FPS calculation
        var phy_fps_avg = m_phy.get_fps();

        fps_frame_counter = (fps_frame_counter + 1) % interval_cb;
        if (fps_frame_counter == 0) {
            _fps_callback(Math.round(fps_avg), phy_fps_avg);
        }
    }

    return fps_counter;
}

/**
 * Reset the engine.
 * Unloads the scene and releases the engine's resources.
 * @method module:main.reset
 */
exports.reset = function() {
    m_data.unload(0);

    m_data.reset();
    m_ext.reset();
    m_render.reset();
    m_geom.reset();
    m_textures.reset_mod();
    m_shaders.reset();
    m_debug.reset();
    m_cont.reset();
    m_data.reset();
    m_cont.reset();
    m_time.reset();
    m_sfx.reset();

    _elem_canvas_webgl = null;
    _elem_canvas_hud = null;

    _last_abs_time = 0;

    _pause_time = 0;
    _resume_time = 0;

    _fps_callback = function() {};
    _fps_counter = function() {};

    _render_callback = function() {};

    _loop_cb.length = 0;

    _gl = null;
}

/**
 * Register one-time callback to return DataURL of rendered canvas element.
 * @param {BlobURLCallback} callback BlobURL callback.
 * @param {String} [format="image/png"] The image format ("image/png", "image/jpeg",
 * "image/webp" and so on).
 * @param {Number} [quality=1.0] Number between 0 and 1 for types: "image/jpeg",
 * "image/webp".
 * @param {Boolean} [auto_revoke=true] Automatically revoke blob object.
 * If auto_revoke is false then application must revoke blob URL via the following call {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL| URL.revokeObjectURL(blobURL)}.
 */
exports.canvas_data_url = function(callback, format, quality, auto_revoke) {
    _canvas_data_url_params.curr_auto_revoke = typeof auto_revoke === "undefined" ?
            auto_revoke: true;

    _canvas_data_url_params.last_auto_revoke = _canvas_data_url_params.curr_auto_revoke;
    _canvas_data_url_params.callback = callback;
    _canvas_data_url_params.format = format || _canvas_data_url_params.format;
    _canvas_data_url_params.quality = quality || _canvas_data_url_params.quality;
}

/**
 * Return the main canvas element.
 * @method module:main.get_canvas_elem
 * @returns {HTMLCanvasElement} Canvas element
 * @deprecated Use {@link module:container.get_canvas|container.get_canvas} instead
 */
exports.get_canvas_elem = function() {
    m_print.error_deprecated("main.get_canvas_elem", "container.get_canvas");
    return _elem_canvas_webgl;
}
/**
 * Check using device.
 * @method module:main.detect_mobile
 * @returns {Boolean} Checking result.
 */
exports.detect_mobile = function() {
    return m_compat.detect_mobile();
}
/**
 * Append a callback to be executed every frame
 * (even if the rendering is paused). Its purpose is to perform actions 
 * non-related to the actual rendering, e.g html/css manipulation.
 * This method allows registration of multiple callbacks.
 * @method module:main.append_loop_cb
 * @param {LoopCallback} callback Callback
 */
exports.append_loop_cb = function(callback) {
    for (var i = 0; i < _loop_cb.length; i++)
        if (_loop_cb[i] == callback)
            return;
    _loop_cb.push(callback);
}
/**
 * Remove loop callback.
 * @method module:main.remove_loop_cb
 * @param {LoopCallback} callback Callback
 */
exports.remove_loop_cb = function(callback) {
    for (var i = 0; i < _loop_cb.length; i++)
        if (_loop_cb[i] == callback) {
            _loop_cb.splice(i, 1);
            break;
        }
}

/**
 * Return renderer info.
 * @method module:main.get_renderer_info
 * @returns {RendererInfo|Null} Renderer info.
 */
exports.get_renderer_info = function() {
    var rinfo = m_ext.get_renderer_info();

    if (!rinfo)
        return null;

    var vendor = _gl.getParameter(rinfo.UNMASKED_VENDOR_WEBGL);
    var renderer = _gl.getParameter(rinfo.UNMASKED_RENDERER_WEBGL);

    return {"vendor": vendor, "renderer": renderer};
}

}