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
01 dec. 2023 08:03
Thank you for providing valuable information on overseas soccer broadcasting and MLB broadcasting 무료스포츠중계
30 jan. 2024 10:17
Thanks for giving me this information. What you’ve written on your blog is great. You wrote a very helpful and fun blog post that you let people read. Online Games
30 jan. 2024 21:09
In the published article, we discussed the creation of a simple interactive application. In this application, actively used innovations through Albany internet marketing such as Canvas-texture, . Here you can ask a question or express your opinion on the application.
27 feb. 2024 09:31
I’m quite certain I will learn a lot of new stuff right here! Best of luck for the next! Senior Home Care Services
28 feb. 2024 12:07
I wanted to thanks on your time for this glorious learn!! I definitely having fun with every little bit of it and I have you bookmarked to check out new stuff you blog pos Answering Toronto
28 feb. 2024 18:39
Telegram @mrdonald2012 Sell Fullz Info SSN DOB + DL Scan Front Back,CVV,Cash App Transfer 2024

-Hello all buyer

Happy New Year 2024 Everybody!

I sell Fresh - Fast and Good price.

And I need good buyer for business long-term.

Im good seller, best tools, sell online 24h.

Let's have fun cooperation and make money together.

Work long term and trust each other.

-Please contact us now to receive preferential prices:

—Buy 100 get 20 free for all services—


Telegram: https://t.me/mrdonald2012 @mrdonald2012 (Mr Donald )


ICQ: 6682668 (Mr Donald )

This is my service:


.FULLZ INFO SSN DOB + DL (Driver's License) 100% New for All states ( FL,CA,UT,TX,MN,PA,OH…etc)

- Format Fullz : Fullz Name DOB SSN Address City of State Zip + DL

- We provide fullz with 100% new quality and do not resell

- You can use it to open an account, etc., depending on your level and working style

. FULLZ INFO SSN DOB + CREDIT POINTS ( 700-800+ ) NO DL

***************************

. Original DL Scan for sale Front, Back + USA SSN number (front and back + SSN number), selfie.. 4.

We have EU Country ID Card and DL SCAN: Netherlands, Spain, Sweden, France, Romania, Sweden, Poland, UK, Latvia Lithuania…ETC (front and back), selfie. .

- Claim your country! sex…ETC

- 100% fresh quality not resold

- Can be used to open an account, etc., depending on your level and working style

- Take advantage!!!

*************************

. Sell Dead Fullz Uk + NIN + DL / US All Bank

- Fomat: 1966-06-05 KK 35 27 69 B Michaela Fulton 19 South Lea Durham DH7 6RN 447899785929 missfulton@hotmail.co.uk (no account number, sort code) if you need it!

6. Sell Fullz CA + SIN

- Format: GUS ARIZA 26 BURLINGTON WAY WINNIPEG MB R3Y 1B5 brian436.ariza@live.ca 02/03/55 307 829 143

**************************

7. Sell Cvv worldwide US, UK, CA, AUS, INTER.. Fresh And Live 95%

- Quality is our top priority for customers

- Me has daily new update with 95% valid rate.

- All cards are confirmed before shipment 100% alive and well

- List price Cvv Credit/Debit card

United States: $25
CA : 35$
Australia : 40$
New Zealand : 35$
Brazil : 20$
France : 35$
Germany : 35$
Sweden : 35$
Spain: 35$
Denmark : 35$
Ireland : 35$
Mexico : 25$
Spain : 35$
Denmark : 25$
Asia : 35$


FOMAT CVV: Card Number Cvv2 Exp Name Address City Zip State Country sometimes phone and email numbers are also provided


–you can choose the Box you need when you buy CC from me

– The replacement time is now 6 hours

– You will work with cvv within 6 hours and let me know the result

– I do not refund

– I will replace if cvv dies

____Contact me now for good business (online 24/7)

-ICQ:6682668 ( Mr Donald)
-Telegram: https://t.me/mrdonald2012 @mrdonald2012 (Mr Donald )


Payment methods: BTC(Bitcoin) OR USDT(Tether)

- I am looking for more good customers for long-term cooperation

-Thank you for read !
28 feb. 2024 21:19
I want to join occult for Money ritual +2349027025197

+2349027025197 %##HOW TO JOIN SECRET OCCULT SOICETY FOR MONEY RITUAL IN OWERRI ITALY

+2349027025197 DO YOU WANT TO JOIN STAUNCH SECRET ILLUMINATI OCCULT CIRCLE FOR MONEY RITUAL CALL 📱☎ +2349027025197 COME JOIN HOME OF RICHES, Greatorldrado ILLUMINATI OCCULT FOR MONEY RITUAL IN AFRICA INDONESIA DUBAI MALAYSIA ITALY GERMANY TURKEY AUSTRALIA OWERRI ABUJA KANO JOS MAKURDI PORT-HARCOURT LAGOS INDONESIA THAILAND MILWAUKEE ANAMBRA UMUAHIA LAGOS DUBAI GERMANY ITALY NIGERIA AUSTRALIA QATAR INDONESIA CALL NOW +2349027025197 This page is for those who are seriously interested in Greatorldrado Brotherhood occult fraternity. People with prejudices and the mob should stay away from here: they would only toddle in darkness and be highly indignant. The described black magic rituals are not without danger and are consequently unsuitable for people who are not mentally in good constitution. Take heed to follow all instructions the way they are described. Without the necessary precautions every ritual will turn to your disadvantage, confusion and total destruction. On the contrary, by following the instructions with precision, you will achieve a complete success in all your enterprises. +2349027025197 Many today are seeking to join a secret society, the one that will give them back their hope and help them to achieve all the things they have wanted in life. They realize that they have lost their dreams and their ambitions. They have settled for a life of mediocrity. Sadly, many are disappointed, for real secret societies are rare, hard to find and even more difficult to join. The more well known have, over time, lost their own secrets and present merely a facade of mystical mumbo-jumbo without possessing any real substance.+2349027025197 There are no accidents and it is no coincidence that you have been led to The TEMPLE OF GREATORLDRADO BROTHERHOOD. The Brotherhood reaches out to help you and to offer the hand of friendship and hope. Contact spiritual Grandmaster now for your enquiries +234902720197 #JOIN GREATORLDRADO BROTHERHOOD FOR WEALTH FAME POWER PROTECTION BUSINESS-GROWTH POLITICAL-ASSIGNMENT WORK-PROMOTION E.T.C CONTACT GRANDMASTER NOW FOR ENQUIRIES +2349027025197 GREATORLDRADO Brotherhood knows that everyone has great potential but often they have lost their self-esteem and their desire for a better life. All of us know that we can be better than we are. We are not living up to our potential. Yet we have this fear to take chances, to venture into unfamiliar enterprises and territories. +2349027025197 Membership into the kingdom of GREATORLDRADO BROTHERHOOD has given many the courage, the confidence, the knowledge and the power they needed to change their life for the better. Becoming a Member of the Brotherhood can give your life a new meaning and direction, and show you how to be happier and more fulfilled. +2349027025197 The GREATORLDRADO Brotherhood offers simple solutions, a helping hand, and answers. It teaches the secret techniques and methods to become anything you wish to be and to attain all the riches of life. Every Member of The GREATORLDRADO Brotherhood is given the knowledge and the power to achieve all the success and riches of life that they have dreamed about. We invite anyone who wishes to forge the spirit of friendship in a true brotherhood of power to join us, to unite our power to your own. Join us and share in the Brotherhood’s closely-guarded secrets and circles of power. +2349027025197 Together we can achieve great things that we could not achieve alone. Joining GREATORLDRADO Brotherhood will start you on the path to a new life filled with money, friends, health and the fulfillment of all your dreams. Beginning today, you can change your life and your fortune. Learn how you can have, do and be anything you want in life. +2349027025197 WEALTH #POWER #PROTECTION. +2349027025197 OFTEN MANY HAS VENTURED INTO SPIRITUAL MEANS OF ACQUIRING LUXURY BUT ONLY FEW GET TO THE PEAK WHEN IT COMES TO SPIRITUAL WEALTH & LUXURY NOT BECAUSE THEY ARE SPIRITUALLY INFECTED OR UNWORHTY BUT IT'S ON CLEARER VIEW TO THE HUMAN WORLD NOW TO ACTUALLY KNOW THAT IT TAKES A GREAT SACRIFICE AND OFFERING TO PIERCE THE HEART OF THE SPIRITUAL WORLD IN ORDER TO MAKE A RFEQUEST NOT JUST TO MAKE A REQUEST BUT TO GET ANSWERS AND POSITIVE POSITIVE RESPONSE FOR THE SPIRITUAL GUARDIANS OF AGE. GREATORLDRADO OCCULT knows that everyone has great potential but often many have lost their self-esteem and their desire for a better life. All of us know that we can be better than we are. We are not living up to our potential. Yet we have this fear to take chances, to venture into unfamiliar enterprises and territories. Contact spiritual Grandmaster now for your enquiries +2349027025197 Image for post D GREAT BROTHERHOOD has given many the courage, the confidence, the knowledge and the power they needed to change their life for the better.
01 mar. 2024 08:44
certainly a lot to find out about this topic.I love all the points you’ve made phun celeb extra
03 mar. 2024 16:35
Thank you for the information it’s really very good and also very easy for us to win therefore you can try it Landscape Supplies
Please register or log in to leave a reply.