// // NewFleshApp.js // // Created by David Back, Sam Gondelman, and Parker Richard on 10/26/18. // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function() { // BEGIN LOCAL_SCOPE const APP_BASE = "NewFleshApp.html"; const APP_URL = Script.resolvePath(APP_BASE); const APP_LABEL = "NEW FLESH"; const APP_SORT_ORDER = 12; const ENTITY_NAME = "hackathon2018.newflesh"; const LEFT_FOREARM_JOINT = "LeftForeArm"; const LEFT_HAND_JOINT = "LeftHand"; const RIGHT_HAND_JOINT = "RightHand"; const WATCH_Z_OFFSET = -0.05; const WATCH_FOREARM_HAND_DISTANCE = 0.8; // Images by Parker Richard const RESET_URL = Script.resolvePath("textures/reset.png"); const SKULL_URL = Script.resolvePath("textures/skull-effect.png"); const GHOST_URL = Script.resolvePath("textures/ghost-effect.png"); const PARTICLE_URL = Script.resolvePath("textures/particle-effect.png"); const DARKNESS_URL = Script.resolvePath("textures/darkness-effect.png"); const PARTICLE_TEXTURE = Script.resolvePath("textures/glow-particle.png"); // https://poly.google.com/view/dqkji8vm0-s const WATCH_MODEL = Script.resolvePath("models/watchModel.fbx"); // https://www.cgbookcase.com/textures/rock-12 const MATERIAL_JSON = Script.resolvePath("materials/HackathonMaterial.json"); // https://poly.google.com/view/3SRO1k1Brxe const SKULL_MODEL = Script.resolvePath("models/skull.obj"); const APP_ICON_ACTIVE = Script.resolvePath("newflesh-a.svg"); const APP_ICON_INACTIVE = Script.resolvePath("newflesh-i.svg"); // Cleanup in case of crash before function cleanupFromBefore() { const ENTITY_SEARCH_DISTANCE = 1000; var toDelete = Entities.findEntitiesByName(ENTITY_NAME, MyAvatar.position, ENTITY_SEARCH_DISTANCE); for (var i = 0; i < toDelete.length; i++) { Entities.deleteEntity(toDelete[i]); } } cleanupFromBefore(); var entities = []; var appEntities = []; var overlays = []; var watchEntity = null; var projectionEffect = null; var background = null; var isActive = false; var watchOpen = false; var currentSelectionIndex = -1; var t = 0; const NUM_BUTTONS = 5; const BUTTON_WIDTH = 0.1; const BUTTON_HEIGHT = 0.15; var buttons = [ { url: RESET_URL }, { url: SKULL_URL, open: function() { appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "Model", modelURL: SKULL_MODEL, parentID: MyAvatar.SELF_ID, dimensions: { x: 0.6, y: 0.6, z: 0.6 }, parentJointIndex: MyAvatar.getJointIndex("Head"), localPosition: { x: 0, y: 0.5 * Vec3.distance(MyAvatar.getJointPosition("HeadTop_End"), MyAvatar.getJointPosition("Head")), z: 0 }, localRotation: Quat.fromPitchYawRollDegrees(0, -20, 0), userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("HeadTop_End"), textures: PARTICLE_TEXTURE, maxParticles: 100, emitRate: 100, lifespan: 1.0, emitSpeed: 0.3, speedSpread: 0, particleRadius: 0.03, radiusStart: 0, radiusFinish: 0.05, color: { red: 255, blue: 0, green: 0}, colorSpread: { red: 100, blue: 100, green: 100}, colorStart: { red: 200, blue: 200, green: 200}, colorFinish: { red: 255, blue: 0, green: 255}, emitAcceleration: {x: 0, y: 0.5, z: 0}, alpha: 1, alphaStart: 0, alphaFinish: 0, polarFinish: 3.141592653589793, emitterShouldTrail: false, isEmitting: true, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); } }, { url: GHOST_URL, open: function() { t = 0; var model = Graphics.getModel(MyAvatar.SELF_ID); if (model) { var partID = 0; for (var i = 0; i < model.meshes.length; i++) { var mesh = model.meshes[i]; for (var j = 0; j < mesh.parts.length; j++) { appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "Material", position: MyAvatar.position, materialURL: MATERIAL_JSON + "?ghost", priority: 1, parentID: MyAvatar.SELF_ID, parentMaterialName: partID, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); partID++; } } } }, update: function(dt) { for (var i = 0; i < appEntities.length; i++) { Entities.editEntity(appEntities[i], { materialMappingPos: { x: Math.cos(t * 0.03), y: Math.sin(t * 0.02) }, materialMappingScale: {x: 0.1 * Math.sin(t * 0.12) + 0.3, y: 0.1 * Math.sin(t * 0.09) + 0.3 } }); } t = t + dt; } }, { url: PARTICLE_URL, open: function() { // Hide my avatar var model = Graphics.getModel(MyAvatar.SELF_ID); if (model) { var partID = 0; for (var i = 0; i < model.meshes.length; i++) { var mesh = model.meshes[i]; for (var j = 0; j < mesh.parts.length; j++) { appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "Material", position: MyAvatar.position, materialURL: MATERIAL_JSON + "?invisible", priority: 1, parentID: MyAvatar.SELF_ID, parentMaterialName: partID, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); partID++; } } } const HALF_PARTICLE_EFFECT_COUNT = 15; var jointNames = MyAvatar.getJointNames(); for (var i = 0; i < HALF_PARTICLE_EFFECT_COUNT; i++) { if (jointNames.length == 0) { break; } var index = Math.floor(Math.random() * jointNames.length); var jointIndex = MyAvatar.getJointIndex(jointNames[index]); jointNames.splice(index, 1); appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, parentJointIndex: jointIndex, textures: PARTICLE_TEXTURE, maxParticles: 25, emitRate: 10, lifespan: 2.5, emitSpeed: 0.0, speedSpread: 0.0, particleRadius: 0.025, radiusStart: 0.0, radiusFinish: 0.0, alpha: 0.5, alphaStart: 0.0, alphaFinish: 0.0, color: { red: 255, green: 255, blue: 255 }, colorSpread: { red: 100, green: 100, blue: 100 }, emitAcceleration: { x: 0, y: 0, z: 0 }, emitDimensions: { x: 0.1, y: 0.1, z: 0.5 }, polarFinish: Math.PI / 2.0, emitterShouldTrail: true, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); index = Math.floor(Math.random() * jointNames.length); jointIndex = MyAvatar.getJointIndex(jointNames[index]); jointNames.splice(index, 1); appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, parentJointIndex: jointIndex, textures: PARTICLE_TEXTURE, maxParticles: 25, emitRate: 10, lifespan: 2.5, emitSpeed: 0.0, speedSpread: 0.0, particleRadius: 0.025, radiusStart: 0.0, radiusFinish: 0.0, alpha: 0.5, alphaStart: 0.0, alphaFinish: 0.0, color: { red: 255, green: 255, blue: 255 }, colorSpread: { red: 100, green: 100, blue: 100 }, emitAcceleration: { x: 0, y: 0, z: 0 }, emitDimensions: { x: 0.1, y: 0.1, z: 0.5 }, polarFinish: Math.PI / 2.0, emitterShouldTrail: false, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); } appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("LeftEye"), textures: PARTICLE_TEXTURE, maxParticles: 100, emitRate: 0, lifespan: 1.0, emitSpeed: 0.6, speedSpread: 0, emitDimensions: { x: 0.5, y: 0.5, z: 0.01 }, emitOrientation: Quat.fromPitchYawRollDegrees(10, 0, 90), particleRadius: 0.03, radiusStart: 0, radiusFinish: 0.05, color: { red: 255, blue: 0, green: 0}, colorSpread: { red: 100, blue: 100, green: 100}, colorStart: { red: 200, blue: 200, green: 200}, colorFinish: { red: 255, blue: 0, green: 255}, emitAcceleration: {x: 0, y: 0, z: 0}, alpha: 1, alphaStart: 0, alphaFinish: 0, polarFinish: 0.0872664600610733, emitterShouldTrail: false, isEmitting: false, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("RightEye"), textures: PARTICLE_TEXTURE, maxParticles: 100, emitRate: 0, lifespan: 1.0, emitSpeed: 0.6, speedSpread: 0, emitDimensions: { x: 0.5, y: 0.5, z: 0.01 }, emitOrientation: Quat.fromPitchYawRollDegrees(-10, 0, 90), particleRadius: 0.03, radiusStart: 0, radiusFinish: 0.05, color: { red: 255, blue: 0, green: 0}, colorSpread: { red: 100, blue: 100, green: 100}, colorStart: { red: 200, blue: 200, green: 200}, colorFinish: { red: 255, blue: 0, green: 255}, emitAcceleration: {x: 0, y: 0, z: 0}, alpha: 1, alphaStart: 0, alphaFinish: 0, polarFinish: 0.0872664600610733, emitterShouldTrail: false, isEmitting: false, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); }, update: function() { const PARTICLE_AUDIO_THRESHOLD = 0.01; const MAX_PARTICLE_EMIT_RATE = 70; Entities.editEntity(appEntities[appEntities.length - 2], { isEmitting: Audio.inputLevel > PARTICLE_AUDIO_THRESHOLD, emitRate: MAX_PARTICLE_EMIT_RATE * Audio.inputLevel }); Entities.editEntity(appEntities[appEntities.length - 1], { isEmitting: Audio.inputLevel > PARTICLE_AUDIO_THRESHOLD, emitRate: MAX_PARTICLE_EMIT_RATE * Audio.inputLevel }); } }, { url: DARKNESS_URL, open: function() { t = 0; appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "Zone", parentID: MyAvatar.SELF_ID, dimensions: { x: 50.0, y: 50.0, z: 50.0 }, hazeMode: "enabled", haze: { hazeColor: { red: 0, green: 0, blue: 0 }, hazeRange: 1.0 }, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); appEntities.push(Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: MyAvatar.SELF_ID, textures: PARTICLE_TEXTURE, maxParticles: 100, emitRate: 10, lifespan: 10, emitSpeed: 0.0, speedSpread: 0.0, particleRadius: 5, radiusSpread: 2, alpha: 1.0, alphaStart: 0.0, alphaFinish: 0.0, color: { red: 255, green: 0, blue: 0 }, colorSpread: { red: 100, green: 0, blue: 0 }, spinSpread: 2.0 * Math.PI, emitAcceleration: { x: 0, y: 0, z: 0 }, accelerationSpread: { x: 0.5, y: 0.5, z: 0.5 }, emitDimensions: { x: 50.0, y: 25.0, z: 50.0 }, polarFinish: Math.PI, emitterShouldTrail: true, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true)); }, update: function(dt) { Entities.editEntity(appEntities[0], { haze: { hazeRange: 1.0 + 3.0 * Math.sin(t * 0.5) * Math.sin(t * 0.5) } }); t = t + dt; } } ]; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var appButton = tablet.addButton({ text: APP_LABEL, icon: APP_ICON_INACTIVE, activeIcon: APP_ICON_ACTIVE, sortOrder: APP_SORT_ORDER }); function onClicked() { if (isActive) { tablet.gotoHomeScreen(); } else { isActive = true; tablet.gotoWebScreen(APP_URL); } } function onScreenChanged(type, url) { isActive = type === "Web" && (url.indexOf(APP_BASE) === url.length - APP_BASE.length); appButton.editProperties({ isActive: isActive }); } // Handle the events we're receiving from the web UI function onWebEventReceived(event) { // Converts the event to a JavasScript Object if (typeof event === "string") { event = JSON.parse(event); } if (event.type === "activate") { activateApp(); } else if (event.type === "deactivate") { deactivateApp(); } } appButton.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); tablet.webEventReceived.connect(onWebEventReceived); function activateApp() { deactivateApp(); watchEntity = Entities.addEntity({ name: ENTITY_NAME, type: "Model", modelURL: WATCH_MODEL, dimensions: { x: 0.1, y: 0.1, z: 0.02 }, parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex(LEFT_FOREARM_JOINT), localPosition: { x: WATCH_Z_OFFSET, y: WATCH_FOREARM_HAND_DISTANCE * Vec3.distance(MyAvatar.getJointPosition(LEFT_FOREARM_JOINT), MyAvatar.getJointPosition(LEFT_HAND_JOINT)), z: WATCH_Z_OFFSET }, localRotation: Quat.fromPitchYawRollDegrees(0, 45, 0), userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true); entities.push(watchEntity); projectionEffect = Entities.addEntity({ name: ENTITY_NAME, type: "ParticleEffect", parentID: watchEntity, textures: PARTICLE_TEXTURE, maxParticles: 10, lifespan: 0.4, emitRate: 10, emitSpeed: 0.5, speedSpread: 0, alpha: 0, alphaFinish: 0, alphaStart: 0.5, particleRadius: 0.15, radiusFinish: 0.15, radiusStart: 0, color: { red: 18, green: 164, blue: 222 }, colorSpread: { red: 100, green: 50, blue: 50 }, emitAcceleration: { x: 0, y: 0.5, z: 0 }, accelerationSpread: { x: 0.5, y: 0, z: 0.5 }, emitOrientation: Quat.fromPitchYawRollDegrees(-180, 0, 0), isEmitting: false, userData: JSON.stringify({ grabbableKey: { grabbable: false } }) }, true); entities.push(projectionEffect); background = Overlays.addOverlay("cube", { dimensions: { x: (NUM_BUTTONS - 1) * BUTTON_WIDTH, y: BUTTON_HEIGHT, z: 0.01 }, alpha: 0.0, visible: true, solid: true, localPosition: { x: 0, y: 0., z: -0.15 }, localOrientation: Quat.fromPitchYawRollDegrees(-100, 0, 120), parentID: watchEntity, ignorePickIntersection: true }); overlays.push(background); for (var i = 0; i < NUM_BUTTONS; i++) { var buttonInfo = getButtonInfo(i); overlays.push(Overlays.addOverlay("image3d", { url: buttons[buttonInfo.index].url, dimensions: { x: BUTTON_WIDTH, y: BUTTON_HEIGHT, z: 0.01 }, alpha: 0.0, visible: true, localPosition: buttonInfo.pos, localOrientation: Quat.fromPitchYawRollDegrees(0, 0, 0), parentID: background, ignorePickIntersection: true })); } } function deactivateApp() { if (currentSelectionIndex !== -1) { deactivateEffect(currentSelectionIndex); } for (var i = 0; i < entities.length; i++) { Entities.deleteEntity(entities[i]); } for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } entities = []; overlays = []; watchEntity = null; projectionEffect = null; background = null; watchOpen = false; } function openWatch() { Overlays.editOverlay(background, { ignorePickIntersection: false }); Entities.editEntity(projectionEffect, { isEmitting: true }); watchOpen = true; } function closeWatch() { Overlays.editOverlay(background, { ignorePickIntersection: true }); for (var i = 0; i < NUM_BUTTONS; i++) { Overlays.editOverlay(overlays[i + 1], { alpha: 0.0 }); } Entities.editEntity(projectionEffect, { isEmitting: false }); watchOpen = false; } function checkWatchActivation() { var watchPosition = Entities.getEntityProperties(watchEntity, ['position']).position; var rightHandPosition = MyAvatar.getJointPosition(RIGHT_HAND_JOINT); var distance = Vec3.distance(watchPosition, rightHandPosition); const ACTIVATION_DISTANCE = 10; const DEACTIVATION_DISTANCE = 20; if (watchOpen && distance > DEACTIVATION_DISTANCE) { closeWatch(); } else if (!watchOpen && distance < ACTIVATION_DISTANCE) { openWatch(); } } function activateEffect(i) { if (buttons[i].open !== undefined) { buttons[i].open(); } } function updateEffect(i, dt) { if (buttons[i].update !== undefined) { buttons[i].update(dt); } } function deactivateEffect(i) { if (buttons[i].close !== undefined) { buttons[i].close(); } for (var i = 0; i < appEntities.length; i++) { Entities.deleteEntity(appEntities[i]); } appEntities = []; } var scroll = 0.0; var scrollRate = 0; var slowScrolling = true; function getButtonInfo(i) { var index = ((buttons.length - 1) + i) % buttons.length; var posOffset = BUTTON_WIDTH * (i + scroll); if (posOffset < 0.0) { posOffset = posOffset + NUM_BUTTONS * Math.abs(Math.floor(posOffset / BUTTON_WIDTH)); } posOffset = posOffset % (BUTTON_WIDTH * NUM_BUTTONS); var HALF_NAX_OFFSET = NUM_BUTTONS / 2 * BUTTON_WIDTH; var alpha = Math.abs(posOffset - HALF_NAX_OFFSET) / HALF_NAX_OFFSET; alpha = alpha * 1.2; alpha = Math.min(1.0, Math.max(0.0, alpha)); return { index: index, alpha: 1.0 - alpha, pos: { x: -NUM_BUTTONS * BUTTON_WIDTH / 2 + posOffset, y: 0, z: 0 } } } function update(dt) { checkWatchActivation(); if (currentSelectionIndex !== -1) { updateEffect(currentSelectionIndex, dt); } scroll = scroll + scrollRate * dt; if (watchOpen) { for (var i = 0; i < NUM_BUTTONS; i++) { var buttonInfo = getButtonInfo(i); Overlays.editOverlay(overlays[i + 1], { //url: buttons[buttonInfo.index].url, alpha: buttonInfo.alpha, localPosition: buttonInfo.pos }); } } if (slowScrolling) { scrollRate = 0.9 * scrollRate; } } Script.update.connect(update); var startPress; var prevPos2D = undefined; var avgScroll = 0; function mousePressOnOverlay(id, event) { if (Pointers.isRightHand(event.id) && id === background && event.isPrimaryButton) { startPress = new Date(); prevPos2D = event.pos2D; scrollRate = 0.0; slowScrolling = true; avgScroll = 0; } } Overlays.mousePressOnOverlay.connect(mousePressOnOverlay); function mouseReleaseOnOverlay(id, event) { if (Pointers.isRightHand(event.id) && id === background && event.isPrimaryButton) { var endPress = new Date(); if (endPress - startPress < 200 && avgScroll < 0.01) { var index = prevPos2D.x / BUTTON_WIDTH - scroll; if (index < 0.0) { index = index + NUM_BUTTONS * Math.abs(Math.floor(index / NUM_BUTTONS)); } index = Math.floor(index % NUM_BUTTONS); if (currentSelectionIndex !== -1) { deactivateEffect(currentSelectionIndex); } currentSelectionIndex = index; activateEffect(currentSelectionIndex); } scrollRate = 1000.0 * avgScroll; prevPos2D = undefined; avgScroll = 0; } } Overlays.mouseReleaseOnOverlay.connect(mouseReleaseOnOverlay); function mouseMoveOnOverlay(id, event) { if (Pointers.isRightHand(event.id) && id === background && prevPos2D !== undefined) { var diff = event.pos2D.x - prevPos2D.x; scroll = scroll + diff / BUTTON_WIDTH; if (avgScroll == 0) { avgScroll = diff; } else { avgScroll = 0.9 * avgScroll + 0.1 * diff; } prevPos2D = event.pos2D; } } Overlays.mouseMoveOnOverlay.connect(mouseMoveOnOverlay); function skeletonModelURLChanged() { if (currentSelectionIndex !== -1) { deactivateEffect(currentSelectionIndex); activateEffect(currentSelectionIndex); } } MyAvatar.skeletonModelURLChanged.connect(skeletonModelURLChanged); function shutdown() { deactivateApp(); tablet.removeButton(appButton); } Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE