Новогоднее программирование
2015-02-03
Сегодня мы рассмотрим программную реализацию приложения, с помощью которого пользователь может передать новогоднее поздравление своим друзьям и близким.
В этой статье мы покажем, как использовать такие возможности движка, как Canvas-текстуры и видео-текстуры, которые могут применяться для создания множества интересных эффектов. Поддержка Canvas-текстур в Blend4Web позволяет реализовать любые возможности HTML5 Canvas, например, динамически рисовать на трёхмерных текстурах.
Структура приложения
Вы можете посмотреть подробное описание по подготовке, экспорту 3D-сцен, использованию менеджера проектов, а так же по инициализации, загрузке приложения в статье "Урок: создание интерактивного веб-приложения".
Приведем основной код целиком:
"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("&", "&");
}
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();
Для создания простого экрана загрузки приложения в функцию инициализации необходимо передать несколько параметров:
bg_color – цвет фона экрана загрузки приложения
bar_color – цвет индикатора этапов загрузки
background_container_id – идентификатор контейнера экрана загрузки приложения
canvas_container_id – идентификатор основного Canvas-элемента приложения
preloader_fadeout – параметр, указывающий тип перехода от загрузки к работе приложения (плавный или резкий)
Интерактивность объектов
Ряд объектов в приложении сделаны интерактивными.
Рассмотрим подробно функцию load_cb, вызываемую после загрузки сцены.
function load_cb(data_id) {
...
m_mouse.enable_mouse_hover_outline();
load_data();
}
Здесь мы выполняем ряд важных подготовительных действий:
- разрешаем подсветку объектов, на которые указывает курсор мыши, (для этого необходимо выставить на объекте в Blender'e свойство Object > Selection and Outlining > Selectable) используя аддон mouse.js. Подсвечивание объекта сообщает пользователю о том, что он может нажать на объект, чтобы проиграть его анимацию.
-вызываем функцию load_data, в которой вызывается функция prepare_objects_anim. В prepare_objects_anim мы подготавливаем анимацию объектов. Об этом можно прочитать более подробно в статье ”Создаем игру. Часть 1. Персонаж”.
При нажатии мышки вызывается функция main_canvas_down:
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;
}
}
Для запуска анимации объектов используются функции play_letter_box_anim, play_confetti_box_anim, play_monkey_box_anim, play_bear_anim. Для идентификации объектов используется их имя, заданное в Blender'e.
В приложении для вывода поздравительного сообщения используется Canvas-текстура. За управление текстурой отвечает функция print_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");
}
}
Отображение напечатанного текста в текстуре осуществляет функция update_canvas_texture_context из модуля textures.js.
Напечатанный текст кодируется и сохраняется в URL. При открытии приложения происходит декодирование URL и печать поздравительного текста в Canvas-текстуру.
Кодирование и декодирование текстового сообщения осуществляют функции encode_message и decode_message.
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;
}
Рассмотрим функцию, вызывающую анимацию письма.
Для экономии ресурсов все объекты в коробках не отображаются пока не будет вызвана функция, проигрывающая их анимацию. Поэтому перед стартом анимации необходимо вызвать функцию set_letter_objs_visibility, чтобы включить отображение объекта.
function play_letter_box_anim() {
...
set_letter_objs_visibility();
...
}
Поскольку в приложении используется обратная анимация для всех объектов, то перед запуском звука необходимо убедиться, что этот же звук, запущенный ранее, остановлен. Для этого вызываются функции speaker_stop и speaker_play из модуля sfx.js.
function play_letter_box_anim() {
...
m_sfx.speaker_stop(speaker);
m_sfx.speaker_play(speaker);
...
Следующим шагом запрещаются любые пользовательские действия через глобальную переменную _disable_interaction. Затем отключается glow-эффект выделенных курсором мыши объектов. После этого вызывается функция calc_camera_sensor_data, в которой вычисляется текущее положение камеры и записывается в глобальные переменные, и disable_camera_controls из модуля app.js, запрещающая управление камерой пользователю.
function play_letter_box_anim() {
...
_disable_interaction = true;
calc_camera_sensor_data();
m_app.disable_camera_controls();
...
}
Потом устанавливается скорость анимации равная единице через функцию set_speed из модуля animation.js и запускается анимация объектов через функцию play из того же самого модуля. При этом по завершению анимации объекта obj_letter (самого письма) будет вызвана функция start.
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);
}
Интерфейс приложения
По завершению анимации письма, будет отображен HTML-интерфейс в виде управляющих кнопок. При нажатии на кнопку "Изменить текст" (open_button) будет произведена очистка Canvas-текстуры функцией prepare_canvas, а также будет скрыта часть интерфейса, не связанная с вводом поздравительного текста. При этом будут отображены элементы, предназначенные для ввода текста.
function start() {
...
open_button.addEventListener("click", function() {
...
prepare_canvas();
show_textarea();
}, false);
...
}
При желании пользователя оставить все как есть, он может нажать на кнопку "Закрыть". При этом работа приложения будет возвращена в начальное состояние (пользовательские действия разрешены, подсветка выделенных курсором мыши объектов и т.д.), а так же будет запущена обратная анимация объектов письма, коробки и арматуры письма.
function start() {
...
close_button.addEventListener("click", function() {
_disable_interaction = false;
m_mouse.enable_mouse_hover_glow()
...
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);
...
}
После ввода поздравительного текста пользователь может нажать на кнопку "Сохранить", после чего будет произведен вызов функции send_button_click_cb:
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-элементов, предназначенных для ввода текста, отображение элементов управления, а так же печать текста в Canvas-текстуру функцией print_text и запись закодированного сообщения в URL.
Анимация камеры
Одновременно с анимацией письма при помощи API производится анимация движения камеры. Рассмотрим анимацию камеры.
При нажатии на коробку с письмом производится плавное перемещение камеры с текущей позиции на заданную. Для этого создаются сенсоры в функции create_sensors, которая вызывается сразу после загрузки приложения в 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);
}
Создаются два различных сенсора: t_sensor и e_sensor. t_sensor служит для определения общего времени движения камеры, а e_sensor - для определения покадрового смещения в пределах общего времени смещения камеры. Более подробно об использовании сенсоров читайте в статье "Меблируем комнату. Часть 2: интерактивность и физика".
Использование видео-текстуры
Поскольку в движке используется раздельное применение аудио и видео дорожек, на сцену был добавлен объект speaker. Для управления телевизором и синхронизации запуска видео и аудио дорожек используется функция tv_play:
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;
}
Управление видео-текстурой осуществляется функциями pause_video, reset_video
и play_video из модуля textures.js. За управление звуком отвечает функции speaker_stop и speaker_play из модуля sfx.js.Заключение
Мы рассмотрели создание интерактивного приложения, использующего 3D-движок Blend4Web, в котором активно использовались такие вещи как экран загрузки приложения, Canvas-текстура, видео-текстура. Подобный функционал дает разработчикам новые инструменты в создании интерактивных приложений.
Запустить приложение в отдельном окне
Исходные файлы приложения и сцены включены в состав бесплатного дистрибутива Blend4Web SDK.
Изменения
[2015-02-03] Изначальная публикация.
[2015-10-01] Небольшие изменения в статье.