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
15 nov. 2022 14:05
Xmas Coding is a fun way to learn and share Christmas spirit. It is a game that helps you to learn about coding, programming, and other computer skills. You can also use Xmas Coding as a tool to help others learn about the same things. Try this Cybersecurity Cape Girardeau for best reviews. Xmas Coding was created by the developers of Code Academy, an educational resource which provides learning material for students and teachers alike.
19 nov. 2022 12:43
25 puzzles are created each year and pre-tested by Advent of Code founder Eric Wastl. It will be published daily from December 1st to December 25th at midnight EST. There are many good self-taught like australianwritings.comprogrammers. ‍Even the founder, Josh Teng, he taught himself how to code for two years, so we may be biased. But yes, it's quite possible that you're self-taught.
03 jul. 2023 07:42
Learn coding basics with this tutorial by helping the Grinch steal Christmas. Each level has a new objective to complete. Check Here
04 jul. 2023 13:32
Coding is an intricate art of transforming ideas into functional software, where every line of code serves as a vital building block. From creating innovative applications to solving complex problems, coding empowers individuals to craft digital solutions. Whether it's mastering programming languages, designing algorithms, or debugging errors, coders navigate through the intricacies of syntax and logic, 글자수세기 and symbols with precision to bring their visions to life. In this ever-evolving digital landscape, the art of coding continues to shape the world we live in, revolutionizing industries, and driving technological advancements that push boundaries and unlock new possibilities.
11 jul. 2023 06:31
This information is just exciting and new. I've been trying to decide where to go to school, and this has helped me in one way fnaf
28 jul. 2023 07:53
Great information, I will recommend it to my friends for them to check out. Thanks for sharing! If you have more time, please visit: run 3
04 aug. 2023 10:49
Sure, here is an attractive comment in 200 words about the passage you provided:

The article "How to Create an Interactive 3D New Year's Greeting Card with Blend4Web" is a great resource for anyone who wants to learn how to create interactive 3D applications. The author does a great job of explaining the concepts in a clear and concise way, and the examples are easy to follow. You can also read more about coding and websites on 기술 매뉴얼
15 sep. 2023 06:25
Thank you for your useful information, see more similar topics, I will visit you often moto x3m
16 nov. 2023 10:18
It's clear that there is a lot to learn about this. I think you made some good points about features age of war
Please register or log in to leave a reply.