/* global GlobalDebugger */ (function () { // Modules Script.include(Script.resolvePath("./resources/modules/flow.js?v12")); var AppUi = Script.require("appUi"), URL = Script.resolvePath("./resources/avatarCustomization101_ui.html?v12344555"), CONFIG = Script.require(Script.resolvePath("./resources/config.js?v12345678919")), BLENDSHAPE_DATA = Script.require(Script.resolvePath("./resources/modules/blendshapes.js?v1")), MATERIAL_DATA = Script.require(Script.resolvePath("./resources/modules/materials.js")), AVATAR_URL = Script.resolvePath("./resources/avatar/avatar.fst"); var DEBUG = true; // #region UTILITY FUNCTIONS function deepCopy(objectToCopy) { var newObject; try { newObject = JSON.parse(JSON.stringify(objectToCopy)); } catch (e) { console.error("Error with deepCopy utility method" + e); } return newObject; } // #endregion UTILITY FUNCTIONS // #region MIRROR FUNCTIONS var MIRROR_DISTANCE_M = 0.5; var MIRROR_DISTANCE_BLENDSHAPES_M = 0.3; // mirror is closer when looking at your face var mirrorCubeID; var mirrorZoneID; function spawnMirror() { // create mirrror parent to avatar var position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.5, z: -MIRROR_DISTANCE_M })); mirrorCubeID = Entities.addEntity({ type: "Box", name: "mirror", dimensions: { "x": 0.6, "y": 0.7, "z": 0.001 }, position: position, rotation: MyAvatar.orientation, // Quat.cancelOutRollAndPitch(Quat.lookAt(position, MyAvatar.position, Vec3.UNIT_Y)), userData: "{\"grabbableKey\":{\"grabbable\":false}}", collisionless: true, script: Script.resolvePath("./resources/modules/mirrorClient.js") }, "domain"); } function deleteMirror() { // Delete mirror entity // set mirrorID to null Entities.deleteEntity(mirrorCubeID); Entities.deleteEntity(mirrorZoneID); mirrorCubeID = null; mirrorZoneID = null; } // #endregion MIRROR FUNCTIONS // #region AVATAR FUNCTIONS var STRING_BOOKMARK_NAME = CONFIG.STRING_BOOKMARK_NAME; function bookmarkAvatar() { AvatarBookmarks.addBookmark(STRING_BOOKMARK_NAME); } function saveAvatarAndChangeToAvi() { bookmarkAvatar(); changeAvatarToAvi(); // getAvatarInfo // Save Avatar information via Settings // !idea if there's already avatar information in the Settings (and different than curr) // should we have a prompt that asks if they want to overwrite this avatar info // Change Avatar to Avi () } function restoreAvatar() { var bookmarksObject = AvatarBookmarks.getBookmarks(); if (bookmarksObject[STRING_BOOKMARK_NAME]) { AvatarBookmarks.loadBookmark(STRING_BOOKMARK_NAME); AvatarBookmarks.removeBookmark(STRING_BOOKMARK_NAME); setIsAviEnabledFalse(); } else { Window.alert("No bookmark was saved in the avatar app."); } } function changeAvatarToAvi() { // Set avatar to Avi.fst MyAvatar.useFullAvatarURL(AVATAR_URL); MyAvatar.setAttachmentsVariant([]); setIsAviEnabledTrue(); } function isAviYourCurrentAvatar() { return MyAvatar.skeletonModelURL === AVATAR_URL; } // Contains all steps to set the app state to isAviEnabled = true function setIsAviEnabledTrue() { dynamicData.state.isAviEnabled = true; spawnMirror(); updateUI(STRING_STATE); } // Contains all steps to set the app state to isAviEnabled = false function setIsAviEnabledFalse() { dynamicData.state.isAviEnabled = false; dynamicData.state.activeTabName = STRING_INFO; updateUI(STRING_STATE); } // #endregion AVATAR FUNCTIONS // #region MATERIAL var materialID; var materialProperties; var STRING_GLASS_MAT = "glass"; var STRING_CHAIN_MAT = "chainmail"; var STRING_DISCO_MAT = "disco"; var STRING_DEFAULT_MAT = "default"; var STRING_RED_MAT = "red"; var STRING_TEXTURE_MAT = "texture"; var MATERIAL_DEFAULT = MATERIAL_DATA.defaults; var MATERIAL_GLASS = MATERIAL_DATA.glass; var MATERIAL_CHAINMAIL = MATERIAL_DATA.chainmail; var MATERIAL_DISCO = MATERIAL_DATA.disco; var MATERIAL_RED = MATERIAL_DATA.red; var MATERIAL_TEXTURE = MATERIAL_DATA.texture; // @args updatesObject name [string] function updateMaterial(newMaterialDataToApply, isNamed, isPBR) { // Applies to all materials materialProperties = { type: "Material", name: "Avatar101-Material", parentID: MyAvatar.sessionUUID, description: newMaterialDataToApply.description, materialURL: "materialData", priority: 1, materialMappingScale: { x: newMaterialDataToApply.materialMappingScale.x, y: newMaterialDataToApply.materialMappingScale.y }, parentMaterialName: newMaterialDataToApply.parentMaterialName, materialData: JSON.stringify({ materialVersion: 1, materials: newMaterialDataToApply.materials }) }; if (!isNamed) { dynamicData[STRING_MATERIAL].selectedMaterial = ""; } var type = isPBR ? "pbr" : "shadeless"; for (var property in newMaterialDataToApply.materials) { // take all properties in materials and put inside dynamic data dynamicData[STRING_MATERIAL][type][property] = newMaterialDataToApply.materials[property]; } if (materialID) { // exists print("Material exists, Entities.editEntity time to update!"); Entities.editEntity(materialID, materialProperties); } else { // needs to be created print("Material must be created! Entities.addEntity"); materialID = Entities.addEntity(materialProperties, "avatar"); } } // presets function applyNamedMaterial(materialName) { switch (materialName){ case STRING_DEFAULT_MAT: // updateMaterial materialName, isNamed, isPBR updateMaterial(MATERIAL_DEFAULT, true, true); break; case STRING_GLASS_MAT: updateMaterial(MATERIAL_GLASS, true, true); break; case STRING_CHAIN_MAT: updateMaterial(MATERIAL_CHAINMAIL, true, true); break; case STRING_DISCO_MAT: updateMaterial(MATERIAL_DISCO, true, true); break; case STRING_RED_MAT: updateMaterial(MATERIAL_RED, true, false); break; case STRING_TEXTURE_MAT: updateMaterial(MATERIAL_TEXTURE, true, false); break; } } // #endregion MATERIAL // #region BLENDSHAPES var TRANSITION_TIME_SECONDS = 0.25; var STRING_AWE = "awe"; var STRING_ANGRY = "angry"; var STRING_DEFAULT = "default"; var STRING_LAUGH = "laugh"; var BLENDSHAPES_DEFAULT = BLENDSHAPE_DATA.defaults; var BLENDSHAPES_AWE = BLENDSHAPE_DATA.awe; var BLENDSHAPES_LAUGH = BLENDSHAPE_DATA.laugh; var BLENDSHAPES_ANGRY = BLENDSHAPE_DATA.angry; function mixValue(valueA, valueB, percentage) { return valueA + ((valueB - valueA) * percentage); } var lastEmotionUsed = BLENDSHAPES_DEFAULT; var emotion = BLENDSHAPES_DEFAULT; var isChangingEmotion = false; var changingEmotionPercentage = 0.0; Script.update.connect(function(deltaTime) { if (!isChangingEmotion) { return; } changingEmotionPercentage += deltaTime / TRANSITION_TIME_SECONDS; if (changingEmotionPercentage >= 1.0) { changingEmotionPercentage = 1.0; isChangingEmotion = false; if (emotion === BLENDSHAPES_DEFAULT) { MyAvatar.hasScriptedBlendshapes = false; } } for (var blendshape in emotion) { MyAvatar.setBlendshape(blendshape, mixValue(lastEmotionUsed[blendshape], emotion[blendshape], changingEmotionPercentage)); } }); function updateBlendshapes(newBlendshapeDataToApply, isName) { if (DEBUG) { print("New blendshape data", JSON.stringify(newBlendshapeDataToApply)); } if (!isName) { // is not named blendshape, ensure last blendshape is not selected dynamicData[STRING_BLENDSHAPES].selected = ""; } if (emotion !== lastEmotionUsed) { lastEmotionUsed = emotion; } if (newBlendshapeDataToApply !== lastEmotionUsed) { changingEmotionPercentage = 0.0; emotion = newBlendshapeDataToApply; isChangingEmotion = true; MyAvatar.hasScriptedBlendshapes = true; // All properties in emotion set to the blendshapes in dynamic data for(var property in emotion) { dynamicData[STRING_BLENDSHAPES].updatedProperties[property] = emotion[property]; } } } // presets function applyNamedBlendshapes(blendshapeName) { // switch statement that matches the blendshape name // "smile" -> updateBlendshapes(BLEND_SMILE); switch (blendshapeName){ case STRING_DEFAULT: updateBlendshapes(BLENDSHAPES_DEFAULT, true); break; case STRING_AWE: updateBlendshapes(BLENDSHAPES_AWE, true); break; case STRING_LAUGH: updateBlendshapes(BLENDSHAPES_LAUGH, true); break; case STRING_ANGRY: updateBlendshapes(BLENDSHAPES_ANGRY, true); break; } dynamicData[STRING_BLENDSHAPES].selected = blendshapeName; } // #endregion BLENDSHAPES // #region FLOW // Subtype event strings var STRING_DEBUG_TOGGLE = CONFIG.FLOW_EVENTS_SUBTYPE.STRING_DEBUG_TOGGLE, STRING_COLLISIONS_TOGGLE = CONFIG.FLOW_EVENTS_SUBTYPE.STRING_COLLISIONS_TOGGLE, STRING_HAIR = CONFIG.FLOW_EVENTS_SUBTYPE.STRING_HAIR, STRING_JOINTS = CONFIG.FLOW_EVENTS_SUBTYPE.STRING_JOINTS; // Called when user navigates to flow tab function addRemoveFlowDebugSpheres(isEnabled) { // draw debug circles on the joints var flowSettings = GlobalDebugger.getDisplayData(); // the state of flow is the opposite of what we want if (flowSettings.debug !== isEnabled) { GlobalDebugger.toggleDebugShapes(); dynamicData[STRING_FLOW].showDebug = isEnabled; } } function addRemoveCollisions(isEnabled) { // draw debug circles on the joints var flowSettings = GlobalDebugger.getDisplayData(); // the state of flow is the opposite of what we want if (flowSettings.collisions !== isEnabled) { GlobalDebugger.toggleCollisions(); dynamicData[STRING_FLOW].enableCollisions = isEnabled; } } function updateFlow(newFlowDataToApply, subtype) { if (DEBUG) { print("updating flow: ", subtype, JSON.stringify(newFlowDataToApply)) } // propertyName is the key and value is the new propety value // for example newFlowDataToApply = { stiffness: 0.5 } for (var propertyName in newFlowDataToApply) { var newValue = newFlowDataToApply[propertyName]; if (subtype === STRING_HAIR) { GlobalDebugger.setJointDataValue("leaf", propertyName, newValue); dynamicData[STRING_FLOW].hairFlowOptions[propertyName] = newValue; } else if (subtype === STRING_JOINTS) { GlobalDebugger.setCollisionDataValue("HeadTop_End", propertyName, newValue); dynamicData[STRING_FLOW].jointFlowOptions[propertyName] = newValue; } } } // #endregion FLOW // #region ANIMATION var ANIMATION_1_DATA = { startframe: null, endframe: null, duration: null }; var STRING_DEFAULT_WALK = "defaultWalk"; // todo check on string function loadAnimationsIntoCache() { // loop through all animations and add them to the AnimationCache } function updateAnimation(animationName) { // update default walk to animation } // #endregion // #region APP // App variables var UPDATE_UI = CONFIG.UPDATE_UI; var BUTTON_NAME = CONFIG.BUTTON_NAME; var APP_NAME = CONFIG.APP_NAME; // Static strings var STRING_MATERIAL = CONFIG.STRING_MATERIAL, STRING_BLENDSHAPES = CONFIG.STRING_BLENDSHAPES, STRING_ANIMATION = CONFIG.STRING_ANIMATION, STRING_FLOW = CONFIG.STRING_FLOW, STRING_INFO = CONFIG.STRING_INFO, STRING_STATE = CONFIG.STRING_STATE; // UI variables var ui; var dynamicData = deepCopy(CONFIG.INITIAL_DYNAMIC_DATA); // Tab dynamic variables var currentTab; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, home: URL, onMessage: onMessage, // graphicsDirectory: Script.resolvePath("./resources/icons/"), onOpened: onOpened, onClosed: onClosed }); // check avatar, if avatar is Avi.fst (or fbx) then set APP_AVI_ENABLED state // if not Avi avatar, dynamicData.aviEnabled is false // loadAnimationsIntoCache(); Script.scriptEnding.connect(unload); } function onClosed() { deleteMirror(); // save lastTab that the user was on dynamicData.state.activeTabName = currentTab; } function onOpened() { if (DEBUG) { print("ACA101 onOpened: isAviEnabled ", isAviYourCurrentAvatar()); print("ACA101 onOpened: activeTabName is ", dynamicData.state.activeTabName); } if (isAviYourCurrentAvatar()) { setIsAviEnabledTrue(); // if your last closed tab has extra setup functionality // ensure you have the correct view for the current tab switchTabs(dynamicData.state.activeTabName); } else { setIsAviEnabledFalse(); } updateUI(); } function switchTabs(tabName) { // if tabName === STRING_BLENDSHAPES // setMirrorDistanceToBlendshapes(); // if currentTab === STRING_BLENDSHAPES && tabName !== STRING_BLENDSHAPES // setMirrorDistanceToDefault(); print("Switch tabs"); if (tabName === STRING_FLOW && dynamicData[STRING_FLOW].showDebug){ addRemoveFlowDebugSpheres(true); } if (currentTab === STRING_FLOW && tabName !== STRING_FLOW){ addRemoveFlowDebugSpheres(false); } currentTab = tabName; } function unload() { deleteMirror(); if (materialID) { Entities.deleteEntity(materialID); materialID = null; } // deleteFlowDebugSpheres(); // removeAvi as avatar and restore old avatar // if no old avatar in Settings setAvatar to Woody? } // #endregion APP // #region EVENTS var EVENT_BRIDGE_OPEN_MESSAGE = CONFIG.EVENT_BRIDGE_OPEN_MESSAGE; var EVENT_CHANGE_AVATAR_TO_AVI_AND_SAVE_AVATAR = CONFIG.EVENT_CHANGE_AVATAR_TO_AVI_AND_SAVE_AVATAR; var EVENT_RESTORE_SAVED_AVATAR = CONFIG.EVENT_RESTORE_SAVED_AVATAR; var EVENT_CHANGE_AVATAR_TO_AVI_WITHOUT_SAVING_AVATAR = CONFIG.EVENT_CHANGE_AVATAR_TO_AVI_WITHOUT_SAVING_AVATAR; var EVENT_UPDATE_MATERIAL = CONFIG.EVENT_UPDATE_MATERIAL; var EVENT_UPDATE_BLENDSHAPE = CONFIG.EVENT_UPDATE_BLENDSHAPE; var EVENT_UPDATE_FLOW = CONFIG.EVENT_UPDATE_FLOW; var EVENT_UPDATE_ANIMATION = CONFIG.EVENT_UPDATE_ANIMATION; var EVENT_CHANGE_TAB = CONFIG.EVENT_CHANGE_TAB; var EVENT_UPDATE_AVATAR = CONFIG.EVENT_UPDATE_AVATAR; var DEBUG_EVENTS = true; // Handles events recieved from the UI function onMessage(data) { // EventBridge message from HTML script. // Check against EVENT_NAME to ensure we're getting the correct messages from the correct app if (!data.type || data.type.indexOf(APP_NAME) === -1) { if (DEBUG_EVENTS) { print("Event type event name index check: ", !data.type, data.type.indexOf(APP_NAME) === -1); } return; } data.type = data.type.replace(APP_NAME, ""); if (DEBUG_EVENTS) { print("onMessage: ", data.type); print("subtype: ", data.subtype); } switch (data.type) { case EVENT_BRIDGE_OPEN_MESSAGE: updateUI(); break; case EVENT_UPDATE_AVATAR: switch (data.subtype) { case EVENT_CHANGE_AVATAR_TO_AVI_AND_SAVE_AVATAR: saveAvatarAndChangeToAvi(); break; case EVENT_RESTORE_SAVED_AVATAR: restoreAvatar(); break; case EVENT_CHANGE_AVATAR_TO_AVI_WITHOUT_SAVING_AVATAR: changeAvatarToAvi(); break; default: break; } break; case EVENT_CHANGE_TAB: switchTabs(data.value); break; case EVENT_UPDATE_MATERIAL: // delegates the method depending on if // event has name property or updates property if (data.name) { applyNamedMaterial(data.name); } else { updateMaterial(data.updates, false, true); // *** todo add if pbr or shadeless } updateUI(STRING_MATERIAL); break; case EVENT_UPDATE_BLENDSHAPE: if (data.name) { applyNamedBlendshapes(data.name); } else { updateBlendshapes(data.updates); } updateUI(STRING_BLENDSHAPES); break; case EVENT_UPDATE_FLOW: switch (data.subtype) { case STRING_DEBUG_TOGGLE: print("TOGGLE DEBUG SPHERES ", data.updates) addRemoveFlowDebugSpheres(data.updates); break; case STRING_COLLISIONS_TOGGLE: addRemoveCollisions(data.updates); break; case STRING_HAIR: updateFlow(data.updates, STRING_HAIR); break; case STRING_JOINTS: updateFlow(data.updates, STRING_JOINTS); break; default: console.error("Flow recieved no matching subtype"); break; } updateUI(STRING_FLOW); break; case EVENT_UPDATE_ANIMATION: // updateAnimation(data.name); break; default: break; } } function updateUI(type) { var messageObject = { type: UPDATE_UI, subtype: type ? type : "", value: type ? dynamicData[type] : dynamicData }; if (DEBUG_EVENTS) { print("Update UI", type); } ui.sendToHtml(messageObject); } // #endregion EVENTS startup(); }());