// wand.js // part of bubblewand // // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // // Makes bubbles when you wave the object around, or hold it near your mouth and make noise into the microphone. // // For the example, it's attached to a wand -- but you can attach it to whatever entity you want. I dream of BubbleBees :) bzzzz...pop! // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html 'use strict' function convertRange(value, r1, r2) { return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]; } (function() { Script.include("http://hifi-public.s3.amazonaws.com/scripts/utilities.js"); Script.include("http://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); // Script.include("../../utilities.js"); // Script.include("../../libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); // var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); //for local testing var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop1.wav"), SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop2.wav"), SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop3.wav") ] var overlays = false; //debug overlays for hand position to detect when wand is near avatar head var TARGET_SIZE = 0.5; var TARGET_COLOR = { red: 128, green: 128, blue: 128 }; var TARGET_COLOR_HIT = { red: 0, green: 255, blue: 0 }; var HAND_SIZE = 0.25; if (overlays) { var leftCubePosition = MyAvatar.getLeftPalmPosition(); var rightCubePosition = MyAvatar.getRightPalmPosition(); var leftHand = Overlays.addOverlay("cube", { position: leftCubePosition, size: HAND_SIZE, color: { red: 0, green: 0, blue: 255 }, alpha: 1, solid: false }); var rightHand = Overlays.addOverlay("cube", { position: rightCubePosition, size: HAND_SIZE, color: { red: 255, green: 0, blue: 0 }, alpha: 1, solid: false }); var gustZoneOverlay = Overlays.addOverlay("cube", { position: getGustDetectorPosition(), size: TARGET_SIZE, color: TARGET_COLOR, alpha: 1, solid: false }); } function getGustDetectorPosition() { //put the zone in front of your avatar's face var DISTANCE_IN_FRONT = 0.2; var DISTANCE_UP = 0.5; var DISTANCE_TO_SIDE = 0.0; var up = Quat.getUp(MyAvatar.orientation); var front = Quat.getFront(MyAvatar.orientation); var right = Quat.getRight(MyAvatar.orientation); var upOffset = Vec3.multiply(up, DISTANCE_UP); var rightOffset = Vec3.multiply(right, DISTANCE_TO_SIDE); var frontOffset = Vec3.multiply(front, DISTANCE_IN_FRONT); var offset = Vec3.sum(Vec3.sum(rightOffset, frontOffset), upOffset); var position = Vec3.sum(MyAvatar.position, offset); return position; } var BUBBLE_GRAVITY = { x: 0, y: -0.1, z: 0 } var BUBBLE_PARTICLE_COLOR = { red: 0, blue: 255, green: 40 } var wandEntity = this; this.preload = function(entityID) { // print('PRELOAD') this.entityID = entityID; this.properties = Entities.getEntityProperties(this.entityID); BubbleWand.originalProperties = this.properties; BubbleWand.init(); print('initial position' + JSON.stringify(BubbleWand.originalProperties.position)); } this.unload = function(entityID) { if (overlays) { Overlays.deleteOverlay(leftHand); Overlays.deleteOverlay(rightHand); Overlays.deleteOverlay(gustZoneOverlay); } Entities.editEntity(entityID, { name: "" }); Script.update.disconnect(BubbleWand.update); Entities.deleteEntity(BubbleWand.currentBubble); while (BubbleWand.bubbles.length > 0) { Entities.deleteEntity(BubbleWand.bubbles.pop()); } }; var BubbleWand = { bubbles: [], timeSinceMoved: 0, resetAtTime: 2, currentBubble: null, atOriginalLocation: true, update: function(dt) { BubbleWand.internalUpdate(dt); }, internalUpdate: function(dt) { var _t = this; //get the current position of the wand var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; //if the wand is in the gust detector, activate mouth mode and change the overlay color var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) var velocity = Vec3.subtract(wandPosition, _t.lastPosition) // print('VELOCITY:'+JSON.stringify(velocity)) var velocityStrength = Vec3.length(velocity) * 100; var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); var upOffset = Vec3.multiply(upVector, 0.2); var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; var mouthMode; if (hitTargetWithWand) { mouthMode = true; } else { mouthMode = false; } //print('velocityStrength'+velocityStrength) //we want to reset the object to its original position if its been a while since it has moved // print('wand position ' + JSON.stringify(wandPosition)) // print('last position ' + JSON.stringify(_t.lastPosition)) // print('at original location? ' + _t.atOriginalLocation) if (velocityStrength < 0.01) { velocityStrength = 0 } var isMoving; if (velocityStrength === 0) { isMoving = false; } else { isMoving = true; } if (isMoving === true) { // print('MOVING') // print('velocityStrength ' + velocityStrength) _t.timeSinceMoved = 0; _t.atOriginalLocation = false; } else { _t.timeSinceMoved = _t.timeSinceMoved + dt; } if (isMoving === false && _t.atOriginalLocation === false) { if (_t.timeSinceMoved > _t.resetAtTime) { _t.timeSinceMoved = 0; _t.returnToOriginalLocation(); } } //debug overlays for mouth mode if (overlays) { var leftHandPos = MyAvatar.getLeftPalmPosition(); var rightHandPos = MyAvatar.getRightPalmPosition(); Overlays.editOverlay(leftHand, { position: leftHandPos }); Overlays.editOverlay(rightHand, { position: rightHandPos }); } if (mouthMode === true && overlays === true) { Overlays.editOverlay(gustZoneOverlay, { position: getGustDetectorPosition(), color: TARGET_COLOR_HIT }) } else if (overlays) { Overlays.editOverlay(gustZoneOverlay, { position: getGustDetectorPosition(), color: TARGET_COLOR }) } var volumeLevel = MyAvatar.audioAverageLoudness; //volume numbers are pretty large, so lets scale them down. var convertedVolume = convertRange(volumeLevel, [0, 5000], [0, 10]); // default is 'wave mode', where waving the object around grows the bubbles //store the last position of the wand for velocity calculations _t.lastPosition = wandPosition; if (velocityStrength > 10) { velocityStrength = 10 } //actually grow the bubble var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; var avatarFront = Quat.getFront(MyAvatar.orientation); var forwardOffset = Vec3.multiply(avatarFront, 0.1); if (velocityStrength > 1 || convertedVolume > 1) { //add some variation in bubble sizes var bubbleSize = randInt(1, 5); bubbleSize = bubbleSize / 50; //release the bubble if its dimensions are bigger than the bubble size if (dimensions.x > bubbleSize) { //bubbles pop after existing for a bit -- so set a random lifetime var lifetime = randInt(3, 8); // var angularVelocity = Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip); Entities.editEntity(_t.currentBubble, { // collisionsWillMove:true, // ignoreForCollisions:false, velocity: mouthMode ? avatarFront : velocity, // angularVelocity: Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip), lifetime: lifetime }); _t.lastBubble = _t.currentBubble; //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble BubbleWand.spawnBubble(); return } else { if (mouthMode) { dimensions.x += 0.005 * convertedVolume; dimensions.y += 0.005 * convertedVolume; dimensions.z += 0.005 * convertedVolume; } else { dimensions.x += 0.005 * velocityStrength; dimensions.y += 0.005 * velocityStrength; dimensions.z += 0.005 * velocityStrength; } } } else { if (dimensions.x >= 0.02) { dimensions.x -= 0.001; dimensions.y -= 0.001; dimensions.z -= 0.001; } } //update the bubble to stay with the wand tip Entities.editEntity(_t.currentBubble, { position: _t.wandTipPosition, dimensions: dimensions }); }, spawnBubble: function() { var _t = this; //create a new bubble at the tip of the wand //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); var upOffset = Vec3.multiply(upVector, 0.2); var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; //store the position of the tip on spawn for use in velocity calculations _t.lastPosition = wandPosition; //create a bubble at the wand tip _t.currentBubble = Entities.addEntity({ type: 'Model', modelURL: bubbleModel, position: wandTipPosition, dimensions: { x: 0.01, y: 0.01, z: 0.01 }, collisionsWillMove: true, //true ignoreForCollisions: false, //false gravity: BUBBLE_GRAVITY, collisionSoundURL: POP_SOUNDS[randInt(0, 4)], shapeType: "sphere", script: bubbleScript, }); //print('spawnbubble position' + JSON.stringify(wandTipPosition)); //add this bubble to an array of bubbles so we can keep track of them _t.bubbles.push(_t.currentBubble) }, returnToOriginalLocation: function() { var _t = this; _t.atOriginalLocation = true; Script.update.disconnect(BubbleWand.update) Entities.deleteEntity(_t.currentBubble); Entities.editEntity(wandEntity.entityID, _t.originalProperties) _t.spawnBubble(); Script.update.connect(BubbleWand.update); }, init: function() { var _t = this; this.spawnBubble(); Script.update.connect(BubbleWand.update); } } })