Blog

Xmas Coding

2015-02-03

Regarding the programming of interactive 3D apps, we'll take a look at how the logic behind the New Year's greeting card was implemented.

In this article, we'll show you how to use such features of the engine as Canvas and video textures which can be applied to create many amazing effects. Blend4Web's Canvas textures leverage HTML5 Canvas power to make dynamic drawing on textures in 3D space possible.

Application Structure

You can look at the detailed description on preparing and exporting 3D scenes, project management system usage, as well as initialization and loading of an application in the article Tutorial: Creating an Interactive Web Application.

We are listing the code of the application in full:

"use strict";

b4w.register("new_year_main", function(exports, require) {

var m_tex       = require("textures");
var m_data      = require("data");
var m_app       = require("app");
var m_main      = require("main");
var m_version   = require("version");
var m_scenes    = require("scenes");
var m_anim      = require("animation");
var m_cam       = require("camera");
var m_vec3      = require("vec3");
var m_controls  = require("controls");
var m_trans     = require("transform");
var m_utils     = require("util");
var m_sfx       = require("sfx");
var m_mouse     = require("mouse");
var m_lights    = require("lights");
var m_preloader = require("preloader");
var mc_lang     = require("new_year_language");
var m_cfg      = require("config");

var _assets_dir;
var DEBUG = (m_version.type() === "DEBUG");

var PRELOADING = true;

var CANVAS_BKG_ALPHA_CLIP = 0.95;
var MAX_TEXT_ROW_LENGTH = 515;
var MARGIN_LEFT = 235;
var MARGIN_TOP = 185;
var LINE_SPACING = 1.25;
var MAX_INDEX_OF_LETTERS = 300;
var NUMBER_OF_END_ROW = 12;
var SPLITTERS = " ,.-+!?";

var _default_cam_eye, _current_cam_dist, _default_cam_target, _default_cam_dist, _default_cam_angles;
var _vec3_tmp, _vec3_tmp2 = new Float32Array(3);
var _current_cam_angles = new Float32Array(2);
var _timeline = 0;
var LETTER_ANIM_TIME = 25/24;

var _objs_confetti = [];
var _trigger_confetti_box = false;
var _trigger_monkey_box = false;
var _trigger_bear = false;
var _video_started = false;
var _lamp_params;

var _disable_interaction = false;

exports.init = function() {
    set_quality_config();
    m_app.init({
        canvas_container_id: "canvas3d",
        callback: init_cb,
        pause_invisible: false,
        physics_enabled: false,
        key_pause_enabled: false,
        assets_dds_available: !DEBUG,
        assets_min50_available: !DEBUG,
        console_verbose: DEBUG,
        gl_debug: DEBUG
    });
}

function init_cb(canvas_elem, success) {

    if(!success) {
        console.log("b4w init failure");
        return;
    }
    m_app.enable_controls();

    if (PRELOADING)
        m_preloader.create_simple_preloader({
            bg_color:"#00000000",
            bar_color:"#FFF",
            background_container_id: "background_image_container",
            canvas_container_id: "canvas3d",
            preloader_fadeout: true});

    if (!m_main.detect_mobile())
       canvas_elem.addEventListener("mousedown", main_canvas_down);
    canvas_elem.addEventListener("touchstart", main_canvas_down);

    window.onresize = on_resize;
    on_resize();
    load();
}

function load() {
    if (DEBUG)
        _assets_dir =  "../../deploy/assets/new_year/";
    else
        _assets_dir = "../../assets/new_year/";
    var p_cb = PRELOADING ? preloader_cb : null;
    m_data.load( _assets_dir + "christmas_tree.json", load_cb, p_cb, true);
}

function fix_yandex_share_href() {
    var links = document.getElementsByTagName("a");
    for (var i =0; i < links.length; i++)
        links[i].href = links[i].href.replace("&amp;", "&");
}

function load_cb(data_id) {
    if (window.Ya) {
        var ya_share = new Ya.share({
            element: 'yandex_icons',
            elementStyle: {
                "type": "none",
                "quickServices": ["facebook" ,"twitter", "vkontakte", "odnoklassniki", 
                "gplus", "moimir"]
            },
            onready: function(instance){
                    set_language();
                    var send_button = document.getElementById("send_button");
                    send_button.onclick = function(){
                        send_button_click_cb();
                        instance.updateShareLink(window.location.href + " via Blend4Web", mc_lang.get_translation("title"));
                        fix_yandex_share_href();
                    }
                    instance.updateShareLink(window.location.href + " via Blend4Web", mc_lang.get_translation("title"));
                    fix_yandex_share_href();                  
                }
            });
    } else {
        var send_button = document.getElementById("send_button");
        send_button.onclick = function() {
            send_button_click_cb();
        }
    }
    var mail_button = document.getElementById("mail_to");
    mail_button.onclick = function(){
        this.href = onclick="mailto:yourfriends?subject=" +
            mc_lang.get_translation("title") + "&body=" +
            window.location.href.replace('&', '%26');
    }
    m_app.enable_camera_controls();
    load_data();
    create_sensors();
    m_mouse.enable_mouse_hover_outline();
}

function load_data() {
    
    prepare_cam_and_lamp_params();
    prepare_objects_anim();

    var param = m_app.get_url_params();

    if (param && param["lang"] && param["lang"] == "ru") {
        mc_lang.set_language(param["lang"]);
        document.body.className = "lang_ru";
    } else
        document.body.className = "lang_en";

    if (param && param["text"])
        var message = param["text"];
    else
        var message = null;

    prepare_canvas();
    process_message(message);
}
function set_quality_config() {
    if (m_main.detect_mobile())
        m_cfg.set("quality", m_cfg.P_LOW);
}

function set_language() {
    var param = m_app.get_url_params();
    if (param && param["lang"] && param["lang"] == "ru")
        mc_lang.set_language(param["lang"]);
}

function prepare_canvas() {
    var ctx_image = m_tex.get_canvas_texture_context("my_letter");
    if (ctx_image) {
        ctx_image.clearRect(0, 0, ctx_image.canvas.width, ctx_image.canvas.height);
        ctx_image.globalAlpha = CANVAS_BKG_ALPHA_CLIP;
        ctx_image.globalAlpha = 1.0;
        ctx_image.font = "44px congratulatory_font, 'URW Chancery L', cursive";
        ctx_image.fillStyle = "#ffffff";
        m_tex.update_canvas_texture_context("my_letter");
    }
}

function prepare_objects_anim() {
    var obj_letter = m_scenes.get_object_by_name("letter");
    var obj_arm = m_scenes.get_object_by_dupli_name("letter", "armature_letter");
    var obj_letter_gift = m_scenes.get_object_by_dupli_name("gift", "Armature.001");

    m_anim.apply(obj_letter, "letter_fly_group");
    m_anim.apply(obj_letter_gift, "cap_fly");
    m_anim.apply(obj_arm, "letter_fly_fin");

    m_anim.set_behavior(obj_letter, m_anim.AB_FINISH_STOP);
    m_anim.set_behavior(obj_arm, m_anim.AB_FINISH_STOP);
    m_anim.set_behavior(obj_letter_gift, m_anim.AB_FINISH_STOP);

    var obj_monkey_box = m_scenes.get_object_by_dupli_name("gift_monkey", "Armature_gift_monkey");
    var obj_monkey = m_scenes.get_object_by_dupli_name("gift_monkey.001", "Armature");
     m_anim.apply(obj_monkey_box, "cap_fly");
     m_anim.set_behavior(obj_monkey_box, m_anim.AB_FINISH_STOP);
    m_anim.set_first_frame(obj_monkey);
    m_anim.apply(obj_monkey, "jump_B4W_BAKED");
    m_anim.set_behavior(obj_monkey, m_anim.AB_FINISH_STOP);

    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder"));
    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder.001"));
    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder.002"));
    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder.003"));
    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder.004"));
    _objs_confetti.push(m_scenes.get_object_by_dupli_name("confetti", "Cylinder.005"));
    var obj_confetti_box = m_scenes.get_object_by_dupli_name("gift_5", "Armature_gift_5");

    m_anim.apply(obj_confetti_box, "cap_fly");
    m_anim.set_behavior(obj_confetti_box, m_anim.AB_FINISH_STOP);

    for (var i = 0; i < _objs_confetti.length; i++) {
        m_anim.apply(_objs_confetti[i], "ParticleSystem 3");
        m_anim.set_behavior(_objs_confetti[i], m_anim.AB_FINISH_STOP);
    }

    var confetti_ribbons_above = m_scenes.get_object_by_dupli_name("confetti_ribbons", "ribbons_flom_above");
    var confetti_ribbons_below = m_scenes.get_object_by_dupli_name("confetti", "ribbons_from_below");

    m_anim.apply(confetti_ribbons_above, "Shader NodetreeAction.004");
    m_anim.set_behavior(confetti_ribbons_above, m_anim.AB_FINISH_STOP);

    m_anim.apply(confetti_ribbons_below, "Shader NodetreeAction");
    m_anim.set_behavior(confetti_ribbons_below, m_anim.AB_FINISH_STOP);

    var obj_bear = m_scenes.get_object_by_dupli_name("bear", "bear");
    m_anim.apply(obj_bear, "bear_wiggle");
    m_anim.set_behavior(obj_bear, m_anim.AB_FINISH_STOP);
    m_anim.set_first_frame(obj_bear);

    set_letter_objs_visibility(true);
    set_monkey_objs_visibility(true);
    set_confetti_objs_visibility(true);
}

function set_letter_objs_visibility(visibility) {
    var obj_letter_paper = m_scenes.get_object_by_dupli_name("letter", "letter");
    var obj_letter_seal = m_scenes.get_object_by_dupli_name("letter", "wax_seal_rope");
    if (!visibility) {
        m_scenes.show_object(obj_letter_paper);
        m_scenes.show_object(obj_letter_seal);
    } else {
        m_scenes.hide_object(obj_letter_paper);
        m_scenes.hide_object(obj_letter_seal);
    }
}

function set_monkey_objs_visibility(visibility) {
    var obj_monkey_head = m_scenes.get_object_by_dupli_name("gift_monkey.001", "monkey");
    var obj_monkey_neck = m_scenes.get_object_by_dupli_name("gift_monkey.001", "monkey.001");
    if (!visibility) {
        m_scenes.show_object(obj_monkey_head);
        m_scenes.show_object(obj_monkey_neck);
    } else {
        m_scenes.hide_object(obj_monkey_head);
        m_scenes.hide_object(obj_monkey_neck);
    }
}

function set_confetti_objs_visibility(visibility) {
    var confetti_ribbons_above = m_scenes.get_object_by_dupli_name("confetti_ribbons", "ribbons_flom_above");
    var confetti_ribbons_below = m_scenes.get_object_by_dupli_name("confetti", "ribbons_from_below");
    if (!visibility) {
        m_scenes.show_object(confetti_ribbons_above);
        m_scenes.show_object(confetti_ribbons_below);
        for (var i = 0; i < _objs_confetti.length; i++)
            m_scenes.show_object(_objs_confetti[i]);
    } else {
        m_scenes.hide_object(confetti_ribbons_above);
        m_scenes.hide_object(confetti_ribbons_below);
        for (var i = 0; i < _objs_confetti.length; i++)
            m_scenes.hide_object(_objs_confetti[i]);
    }
}

function prepare_cam_and_lamp_params() {

    var cam_obj = m_scenes.get_active_camera();
    _default_cam_eye = m_cam.get_eye(cam_obj);
    _default_cam_target = m_cam.get_pivot(cam_obj);

    var cam_pivot = new Float32Array(3);
    m_cam.get_pivot(cam_obj, cam_pivot);
    _default_cam_dist = m_vec3.dist(cam_pivot, _default_cam_eye);
    _default_cam_angles = m_cam.get_camera_angles(cam_obj);
}

function process_message(message) {
    var text_area = document.getElementById("text_element");
    text_area.oninput = function() {
        if (text_area.value.length > MAX_INDEX_OF_LETTERS)
            text_area.value = text_area.value.substr(0, MAX_INDEX_OF_LETTERS);
    }
    var ctx_image = m_tex.get_canvas_texture_context("my_letter");
    if (message)
        text_area.value = decode_message(message);
    else
        text_area.value = mc_lang.get_translation("default_text");
    var text_message = prepare_text(text_area.value, ctx_image);
    print_text(text_message);
}

function start() {
    var container  = document.getElementById("container");
    var open_button = document.getElementById("open_button");
    var close_button = document.getElementById("close_button");
    var text_container = document.getElementById("text_container");

    var icons = document.getElementById("icons");
    icons.style.visibility = "visible";

    open_button.addEventListener("click", function() {
        text_container.style.visibility = "visible";
        close_button.style.visibility = "hidden";
        open_button.style.visibility = "hidden";
        prepare_canvas();
        show_textarea();
    }, false);

    close_button.addEventListener("click", function() {
        _disable_interaction = false;
        m_mouse.enable_mouse_hover_outline()
        container.style.visibility = "hidden";
        text_container.style.visibility = "hidden";
        icons.style.visibility = "hidden";

        var obj_letter = m_scenes.get_object_by_name("letter");
        var obj_arm = m_scenes.get_object_by_dupli_name("letter", "armature_letter");
        var obj_letter_gift = m_scenes.get_object_by_dupli_name("gift", "Armature.001");

        m_anim.set_speed(obj_letter, -2);
        m_anim.set_speed(obj_letter_gift, -2);
        m_anim.set_speed(obj_arm, -2);
        
        m_anim.play(obj_letter);
        m_anim.play(obj_letter_gift, set_letter_objs_visibility);
        m_anim.play(obj_arm);

        m_app.enable_camera_controls();
    }, false);

    container.style.visibility = "visible";
}

function show_textarea() {

    var open_button = document.getElementById("open_button");
    var text_area = document.getElementById("text_element");
    var text_container = document.getElementById("text_container");

    text_area.disabled = false;
    open_button.style.visibility = "hidden";
    text_container.style.visibility = "visible";
}

function send_button_click_cb() {
    var text_area = document.getElementById("text_element");
    var text_container = document.getElementById("text_container");
    var open_button = document.getElementById("open_button");
    var close_button = document.getElementById("close_button");
    var message = text_area.value;
    var ctx_image = m_tex.get_canvas_texture_context("my_letter");
    var text = prepare_text(message, ctx_image);
    print_text(text);

    text_container.style.visibility = "hidden";
    open_button.style.visibility = "inherit";
    close_button.style.visibility = "inherit";

    var message_text;
    
    message_text = encode_message(message);

    window.history.pushState("", "", "?lang=" + mc_lang.get_language() + "&text=" + message_text);

}

function on_resize() {

    m_app.resize_to_container();

    var h = window.innerHeight;
    var w = window.innerWidth;

    var text_element = document.getElementById("text_element");
    text_element.style.fontSize = (0.025 * h).toString() + "px";

    var html = document.getElementsByTagName("html")[0];
    html.style.height = h.toString() + "px";
    html.style.width = w.toString() + "px";

    var bkg_img = document.getElementById("background_image_container");
    if (bkg_img) {
        bkg_img.style.height = h.toString() + "px";
        bkg_img.style.width = w.toString() + "px";
    }
    var preloader = document.getElementById("simple_preloader_container");
    if (preloader) {
        preloader.style.height = h.toString() + "px";
        preloader.style.width = w.toString() + "px";
    }

    var container = document.getElementById("container");

    container.style.width = (0.5 * h).toString() + "px";
    container.style.height = (0.6 * h).toString() + "px";
    container.style.top = (0.03 * h).toString() + "px";

}

function prepare_text(message, context) {

    var letters = message.split("");

    var row = "";
    var word = "";
    var counter = 0;
    var text = [];

    for (var i = 0; i < letters.length; i++) {
        if (i >= MAX_INDEX_OF_LETTERS) {
            word += "...";
            break;
        }
        if (letters[i] == "\n") {
            row += word;
            text.push(row);
            row = "";
            word = "";
            continue;
        }
        if (SPLITTERS.indexOf(letters[i]) > -1) {
            if (context.measureText(row + word).width > MAX_TEXT_ROW_LENGTH) {
                text.push(row);
                row = "";
                row += word;
            } else
                row += word;
            word = "";
            row += letters[i];
        } else {
            word += letters[i];
            if (context.measureText(word).width > MAX_TEXT_ROW_LENGTH) {
                row += word;
                text.push(row);
                word = "";
                row = "";
            }
        }
    }

    row += word;
    text.push(row);
    if (text.length > NUMBER_OF_END_ROW) {
        text.length = NUMBER_OF_END_ROW;
        text.push("...");
    }
    return text;
}

function print_text(text) {

    if (text) {
        var ctx_image = m_tex.get_canvas_texture_context("my_letter");
        var font = ctx_image.font.split("px");
        var font_height = parseInt(font[0]);
        for (var i = 0; i < text.length; i++)
            ctx_image.fillText(text[i], MARGIN_LEFT, Math.round(LINE_SPACING * font_height * i + MARGIN_TOP));
        m_tex.update_canvas_texture_context("my_letter");
    }

}

function encode_message(message) {
    var code, dif, message_text = "";
    var len = message.length > MAX_INDEX_OF_LETTERS ? MAX_INDEX_OF_LETTERS : message.length;
    for (var i = 0; i < len; i++) {
        code = message[i].charCodeAt(0).toString(16);
        dif = 4 - code.length;

        for (var j = 0; j < dif; j++)
            code = "0" + code;
        message_text += code;
    }
    return message_text;
}

function decode_message(message) {
    var bit = "";
    var text = "";

    for (var i = 0; i < message.length; i = i + 4) {
        bit += message[i] + message[i + 1] + message[i + 2] + message[i + 3];
        text += String.fromCharCode(parseInt(bit, 16));
        bit = "";
    }
    return text;
}

function main_canvas_down(e) {
    if (_disable_interaction)
        return;

    if (e.preventDefault)
        e.preventDefault();

    var x = m_mouse.get_coords_x(e);
    var y = m_mouse.get_coords_y(e);

    var obj = m_scenes.pick_object(x, y);
    if (obj)
        switch(m_scenes.get_object_name(obj)) {
        case "box":
            play_letter_box_anim();
            break;
        case "box_5":
            play_confetti_box_anim();
            break;
        case "box_6":
            play_monkey_box_anim();
            break;
        case "tv":
            tv_play();
            break;
        case "bear":
            play_bear_anim();
            break;
        }
}

function play_letter_box_anim() {
    var obj_letter = m_scenes.get_object_by_name("letter");
    var obj_arm = m_scenes.get_object_by_dupli_name("letter", "armature_letter");
    var obj_letter_gift = m_scenes.get_object_by_dupli_name("gift", "Armature.001");
    var speaker = m_scenes.get_object_by_dupli_name("gift", "letter");

    set_letter_objs_visibility();

    m_sfx.stop(speaker);
    m_sfx.play_def(speaker);

    _disable_interaction = true;
    m_mouse.disable_mouse_hover_outline();

    calc_camera_sensor_data();

    m_app.disable_camera_controls();

    m_anim.set_speed(obj_letter, 1);
    m_anim.set_speed(obj_letter_gift, 1);
    m_anim.set_speed(obj_arm, 1);

    m_anim.play(obj_letter, start);
    m_anim.play(obj_letter_gift);
    m_anim.play(obj_arm);
}

function play_bear_anim() {
    var obj_bear = m_scenes.get_object_by_dupli_name("bear", "bear");
    var speaker = m_scenes.get_object_by_dupli_name("bear", "spk_bear");
    m_sfx.stop(speaker);
    m_sfx.play_def(speaker);
    if (!_trigger_bear) {
        m_anim.set_speed(obj_bear, 1);
        m_anim.play(obj_bear);      

    } else {
        m_anim.set_speed(obj_bear, -1);
        m_anim.play(obj_bear);
    }
    _trigger_bear = !_trigger_bear;
}

function tv_play() {
    var lamp = m_scenes.get_object_by_name("lamp");
    var speaker = m_scenes.get_object_by_dupli_name("TV", "speaker");

    if (_video_started) {
        m_tex.pause_video("Texture");
        m_tex.reset_video("Texture");
        m_sfx.stop(speaker);
    } else {
        m_tex.play_video("Texture");
        m_sfx.play_def(speaker);
    }
    _video_started = !_video_started;
}

function play_monkey_box_anim() {
    var obj_monkey_box = m_scenes.get_object_by_dupli_name("gift_monkey", "Armature_gift_monkey");
    var obj_monkey = m_scenes.get_object_by_dupli_name("gift_monkey.001", "Armature");
    var speaker = m_scenes.get_object_by_dupli_name("gift_monkey", "monkey");
    m_sfx.stop(speaker);
    m_sfx.play_def(speaker);
    if (!_trigger_monkey_box) {
        set_monkey_objs_visibility();
        m_anim.set_speed(obj_monkey_box, 1);
        m_anim.play(obj_monkey_box);

        m_anim.set_speed(obj_monkey, 1);
        m_anim.play(obj_monkey);

    } else {
        m_anim.set_speed(obj_monkey_box, -1.7);
        m_anim.play(obj_monkey_box, set_monkey_objs_visibility);

        m_anim.set_speed(obj_monkey, -3);
        m_anim.play(obj_monkey);
    }
    _trigger_monkey_box = !_trigger_monkey_box;
}

function play_confetti_box_anim() {
    var obj_confetti_box = m_scenes.get_object_by_dupli_name("gift_5", "Armature_gift_5");
    var confetti_ribbons_below = m_scenes.get_object_by_dupli_name("confetti", "ribbons_from_below");
    var confetti_ribbons_above = m_scenes.get_object_by_dupli_name("confetti_ribbons", "ribbons_flom_above");
    var speaker = m_scenes.get_object_by_dupli_name("gift_5", "fireworks");
    m_sfx.stop(speaker);

    if (!_trigger_confetti_box) {
        set_confetti_objs_visibility();
        m_anim.set_speed(obj_confetti_box, 1);
        m_anim.play(obj_confetti_box);
        m_anim.play(confetti_ribbons_below, play_confetti_ribbons_above);
        m_sfx.play_def(speaker);

        for (var i = 0; i < _objs_confetti.length; i++)
            m_anim.play(_objs_confetti[i]);
    } else {
        m_anim.set_speed(obj_confetti_box, -2);
        m_anim.play(obj_confetti_box);

        for (var i = 0; i < _objs_confetti.length; i++) {
            m_anim.stop(_objs_confetti[i]);
            var obj_name = m_scenes.get_object_name(_objs_confetti[i]);
            if (obj_name == "Cylinder" || obj_name == "Cylinder.001"
                    || obj_name == "Cylinder.002")
                m_anim.set_frame(_objs_confetti[i], 0);
            else
                m_anim.set_first_frame(_objs_confetti[i]);
        }
        m_anim.stop(confetti_ribbons_below);
        m_anim.set_first_frame(confetti_ribbons_below);
        m_anim.stop(confetti_ribbons_above);
        m_anim.set_first_frame(confetti_ribbons_above);
        set_confetti_objs_visibility(true);
    }
    _trigger_confetti_box = !_trigger_confetti_box;
}

function play_confetti_ribbons_above() {
    var confetti_ribbons_above = m_scenes.get_object_by_dupli_name("confetti_ribbons", "ribbons_flom_above");
    m_anim.play(confetti_ribbons_above, set_confetti_objs_visibility);
}

function calc_camera_sensor_data() {
    _timeline = m_main.global_timeline();

    var cam_obj = m_scenes.get_active_camera();
    var cam_pivot = m_cam.get_pivot(cam_obj, _vec3_tmp);
    var cam_eye = m_cam.get_eye(cam_obj, _vec3_tmp2);
    _current_cam_dist = m_vec3.dist(cam_pivot, cam_eye);
    m_cam.get_camera_angles(cam_obj, _current_cam_angles);
    if (_current_cam_angles[0] > Math.PI)
        _current_cam_angles[0] -= 2 * Math.PI;
}

function create_sensors() {
    var cam_obj = m_scenes.get_active_camera();

    var t_sensor = m_controls.create_timeline_sensor();
    var e_sensor = m_controls.create_elapsed_sensor();

    var logic_func = function(s) {
        return s[0] - _timeline < LETTER_ANIM_TIME;
    }

    var cam_move_cb = function(cam_obj, id, pulse) {
        if (pulse > 0) {
            var elapsed = m_controls.get_sensor_value(cam_obj, id, 1);
            var delta_distance = (_default_cam_dist - _current_cam_dist) * (elapsed/LETTER_ANIM_TIME);
            var delta_horisontal_angle = (_default_cam_angles[0] - _current_cam_angles[0]) * (elapsed/LETTER_ANIM_TIME);
            var delta_vertical_angle = (_default_cam_angles[1] - _current_cam_angles[1]) * (elapsed/LETTER_ANIM_TIME);
            m_trans.move_local(cam_obj, 0, delta_distance, 0);
            m_cam.rotate_target_camera(cam_obj, delta_horisontal_angle, delta_vertical_angle);
        } else {
            m_cam.set_look_at(cam_obj, _default_cam_eye, _default_cam_target, m_utils.AXIS_Y);
        }
    }

    m_controls.create_sensor_manifold(cam_obj, "CAMERA_MOVE", 
            m_controls.CT_CONTINUOUS, [t_sensor, e_sensor], logic_func, 
            cam_move_cb);
}

function preloader_cb(percentage) {
    m_preloader.update_preloader(percentage);
}

});
b4w.require("new_year_main").init();

Loading Screen

In this app a rather simple loading screen is used. It is initialized in the init_cb() function by using create_simple_preloader() from the preloader.js add-on.

m_preloader.create_simple_preloader({
            bg_color:"#00000000",
            bar_color:"#FFF",
            background_container_id: "background_image_container",
            canvas_container_id: "canvas3d",
            preloader_fadeout: true});

To create a simple loading screen we need to pass several parameters to the initialization function:

bg_color – background color of the loading screen

bar_color – color of the preloader bar

background_container_id – identifer of the preloader container element

canvas_container_id – identifier of the main Canvas element of the app

preloader_fadeout – parameter to indicate the type of the transition between the preloader and the app (smooth or sharp)

Interactivity of Objects

Some objects are made interactive in this app.

Lets look at the load_cb() function in detail, which is called after the scene is loaded.

function load_cb(data_id) {
    ...
    m_mouse.enable_mouse_hover_outline();
    load_data();
}

Here we carry out a set of important preparatory actions:

- permitting outlining of objects under the mouse cursor (to make it possible it is necessary to enable the Object > Selection and Outlining > Selectable option) using the mouse.js add-on. The outlining of objects tells the user that he or she can click on the object to play back its animation.

-calling the load_data() function, in which yet another function prepare_objects_anim() is executed. In prepare_objects_anim() we prepare object animation. You can read about it in more detail in the ”Making a Game Part 1. The Character” article.

Upon clicking the mouse the main_canvas_down() function is called:

function main_canvas_down(e) {
        ...
        switch(m_scenes.get_object_name(obj)) {
        case "gift*box":
            play_letter_box_anim();
            break;
        case "gift_5*box_5":
            play_confetti_box_anim();
            break;
        case "gift_monkey*box_6":
            play_monkey_box_anim();
            break;
        case "TV*tv":
            tv_play();
            break;
        case "bear*bear":
            play_bear_anim();
            break;
        }
}

The play_letter_box_anim(), play_confetti_box_anim(), play_monkey_box_anim(), play_bear_anim() functions are used for launching object animation. In order to identify objects we use their names as specified in Blender.

We use a Canvas texture to print the greeting message in the app. The print_text() function is used to control this texture:

function print_text(text) {
    if (text) {
        var ctx_image = m_tex.get_canvas_texture_context("my_letter");
        var font = ctx_image.font.split("px");
        var font_height = parseInt(font[0]);
        for (var i = 0; i < text.length; i++)
            ctx_image.fillText(text[i], MARGIN_LEFT, Math.round(LINE_SPACING * font_height * i + MARGIN_TOP));
        m_tex.update_canvas_texture_context("my_letter");
    }
}

The update_canvas_texture_context() function from the textures.js module displays the typed text in the texture.

The input text is encoded and saved to the URL. When the app is started, it decodes the URL and prints the greeting text on the Canvas texture.

The encoding and decoding of the message is performed by the encode_message() and decode_message() functions.

function encode_message(message) {
    var code, dif, message_text = "";
    var len = message.length > MAX_INDEX_OF_LETTERS ? MAX_INDEX_OF_LETTERS : message.length;
    for (var i = 0; i < len; i++) {
        code = message[i].charCodeAt(0).toString(16);
        dif = 4 - code.length;

        for (var j = 0; j < dif; j++)
            code = "0" + code;
        message_text += code;
    }
    return message_text;
}

function decode_message(message) {
    var bit = "";
    var text = "";

    for (var i = 0; i < message.length; i = i + 4) {
        bit += message[i] + message[i + 1] + message[i + 2] + message[i + 3];
        text += String.fromCharCode(parseInt(bit, 16));
        bit = "";
    }
    return text;
}

Let's take a look at the function which triggers animation of the letter.

To conserve resources, all objects in the boxes are not rendered until a function, which plays back their animation, is called. That's why it is neccessary to call the set_letter_objs_visibility() function in order to make an object appear.

function play_letter_box_anim() {
    ...
    set_letter_objs_visibility();
   ...
}

Because we are using reverse animation for all objects in this app, we must ensure the sound has been stopped before starting any new sound. In order to do this we call the speaker_stop() and speaker_play() functions from the sfx.js module.

function play_letter_box_anim() {
    ...
    m_sfx.speaker_stop(speaker);
    m_sfx.speaker_play(speaker);
    ...

Our next step is to block any user interaction by using the _disable_interaction global variable. Then, glow effect is disabled for the selected objects. After which the calc_camera_sensor_data() function is called, in which the current camera position is calculated and saved to the global variables. Lastly, the disable_camera_controls() function from the app.js module is called to disable user control over the camera.

function play_letter_box_anim() {
    ...
    _disable_interaction = true;
    calc_camera_sensor_data();
    m_app.disable_camera_controls();
    ...
}

In the next step we set the animation speed to one using the set_speed() function from the animation.js module and launch object animation by using the play() function from the same module. After obj_letter object animation ends (we mean the letter itself), the start() function will be called.

function play_letter_box_anim() {
    ...
    m_anim.set_speed(obj_letter, 1);
    m_anim.set_speed(obj_letter_gift, 1);
    m_anim.set_speed(obj_arm, 1);

    m_anim.play(obj_letter, start);
    m_anim.play(obj_letter_gift);
    m_anim.play(obj_arm);
}

User Interface

An HTML interface with control buttons will appear when the letter finishes its animation. Upon clicking the "edit text" button (open_button variable) the Canvas texture is cleared by the prepare_canvas() function and also the part of the interface which is not connected to the greeting message input becomes hidden. On the other hand, the elements for typing text will be revealed.

 function start() {
    ...
    open_button.addEventListener("click", function() {
        ...
        prepare_canvas();
        show_textarea();
    }, false);
    ...
 }

If the user wishes to keep the default message how it is, he or she can just close the letter. If so, the app will return to its initial state (that is, user interaction becomes possible including outlining objects using the mouse and others). Also, reverse animation of the letter objects, box and letter armature will be launched.

 function start() {
    ...
    close_button.addEventListener("click", function() {
        _disable_interaction = false;
        m_mouse.enable_mouse_hover_outline()
        ...
        m_anim.set_speed(obj_letter, -2);
        m_anim.set_speed(obj_letter_gift, -2);
        m_anim.set_speed(obj_arm, -2);
        
        m_anim.play(obj_letter);
        m_anim.play(obj_letter_gift, set_letter_objs_visibility);
        m_anim.play(obj_arm);

        m_app.enable_camera_controls();
    }, false);
    ...
}

After inputting a greeting, the user can click the "save" button causing the send_button_click_cb() function to be called:

 function send_button_click_cb() {
    ...
    print_text(text);
    ...
    message_text = encode_message(message);

    window.history.pushState("", "", "?lang=" + mc_lang.get_language() + "&text=" + message_text);
}

HTML elements for text input become hidden, control elements appear, the text is rendered on the Canvas texture using the print_text() function and the message is encoded in the URL.

Camera Animation

The camera is moved via API at the same time as the letter performs its animation. Let's take a look at the camera's animation.

Upon clicking on the box with the letter, the camera smoothly moves from its current position to the required one. In order to make this happen, sensors are created in the create_sensors() function, which is called upon loading the app in load_cb().

function create_sensors() {
    ...
    var t_sensor = m_controls.create_timeline_sensor();
    var e_sensor = m_controls.create_elapsed_sensor();

    var logic_func = function(s) {
        return s[0] - _timeline < LETTER_ANIM_TIME;
    }

    var cam_move_cb = function(cam_obj, id, pulse) {
        if (pulse > 0) {
            var elapsed = m_controls.get_sensor_value(cam_obj, id, 1);
            var delta_distance = (_default_cam_dist - _current_cam_dist) * (elapsed/LETTER_ANIM_TIME);
            var delta_horisontal_angle = (_default_cam_angles[0] - _current_cam_angles[0]) * (elapsed/LETTER_ANIM_TIME);
            var delta_vertical_angle = (_default_cam_angles[1] - _current_cam_angles[1]) * (elapsed/LETTER_ANIM_TIME);
            m_trans.move_local(cam_obj, 0, delta_distance, 0);
            m_cam.rotate_pivot(cam_obj, delta_horisontal_angle, delta_vertical_angle);
        } else {
            m_cam.set_look_at(cam_obj, _default_cam_eye, _default_cam_target, m_utils.AXIS_Y);
        }
    }

    m_controls.create_sensor_manifold(cam_obj, "CAMERA_MOVE", 
            m_controls.CT_CONTINUOUS, [t_sensor, e_sensor], logic_func, 
            cam_move_cb);
}

Two different sensors are created: t_sensor and e_sensor. t_sensor serves to track the time of camera movement, while e_sensor is needed to calculate the shift of the camera between frames while moving. Yoyu can read further about using of sensors in this article - "Furnishing a Room. Part 2: Interactivity and Physics".

Using Video Textures

Since the engine uses separate audio and video tracks, a speaker object has been added to the scene. The tv_play() function controls the TV set and syncronizes video and audio tracks:

function tv_play() {
    var speaker = m_scenes.get_object_by_dupli_name("TV", "speaker");

    if (_video_started) {
        m_tex.pause_video("Texture");
        m_tex.reset_video("Texture");
        m_sfx.speaker_stop(speaker);
    } else {
        m_tex.play_video("Texture");
        m_sfx.speaker_play(speaker);
    }
    _video_started = !_video_started;
}

Video texture is controlled by the pause_video(), reset_video() and play_video() methods from the textures.js module. Audio is controlled by the following functions: speaker_stop() and speaker_play() from the sfx.js module.

Conclusion

We have looked at creating an interactive app based on the Blend4Web engine, in which features were actively used such as preloader, canvas and video textures. This functionality gives developers handy tools for creating interactive applications.

Run the app in a separate tab

The source files of the app and the scene are included in the free Blend4Web SDK distribution.

Changelog

[2015-02-03] Initial release.

[2015-10-01] Minor changes in article.

Comments
09 nov. 2016 11:07
Loaded to 94% and stopped on iPhone Firefox and Safari
Please register or log in to leave a reply.