509 lines · 24.2 KB · JavaScript
RawDownload
////  Created by Luis Cuenca on 11/14/19//  Copyright 2019 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() {    var ZoomStatus = {        "zoomingIn" : 0, // The camera is moving towards the selected entity        "zoomingOut" : 1, // The camera is moving away from the selected entity        "zoomedIn" : 2, // The camera is locked looking at the selected entity        "zoomedOut" : 4, // The camera is on its initial position and mode        "consumed" : 5 // The zooming loop has been completed    }     var FocusType = {        "avatar" : 0,        "entity" : 1    }     var EasingFunctions = {        easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },        // accelerating from zero velocity        easeInCubic: function (t) { return t*t*t },        // decelerating to zero velocity        easeOutCubic: function (t) { return (--t)*t*t+1 },        // acceleration until halfway, then deceleration        easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },        // accelerating from zero velocity        easeInQuart: function (t) { return t*t*t*t },        // decelerating to zero velocity        easeOutQuart: function (t) { return 1-(--t)*t*t*t },        // acceleration until halfway, then deceleration        easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },        // accelerating from zero velocity        easeInQuint: function (t) { return t*t*t*t*t },        // decelerating to zero velocity        easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },        // acceleration until halfway, then deceleration        easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }    };    // Class that manages the zoom effect    var ZoomData = function(type, lookAt, focusNormal, focusDimensions, velocity, maxDuration) {        var self = this;         this.focusType = type;        this.lookAt = lookAt; // Look at 3d point in world coordinates        this.focusDimensions = focusDimensions; // 2d dimensions of the bounding box projection approximation of the selected entity        this.focusNormal = focusNormal; // The normal vector provided by the ray intersection used to initialize the zoom        this.velocity = velocity; // Max velocity of the camera zoom effect        this.maxDuration =  maxDuration; // Max duration of the camera zoom effect         this.initialPos = Camera.getPosition();        this.initialRot = Camera.getOrientation();        this.interpolatedPos = this.initialPos;        this.interpolatedRot = this.initialRot;        this.initialMode = Camera.mode;        this.initialOffset = Vec3.distance(self.initialPos, MyAvatar.getDefaultEyePosition()); // Save the offset distance from the camera to the avatar eyes (boom)         this.finalPos = Vec3.ZERO;        this.finalRot = Quat.IDENTITY;        this.direction = Vec3.ZERO; // Direction vector for the camera path        this.distance = Vec3.ZERO; // Total distance that the camera needs to cover to zoom in along the path        this.totalTime = 0.0; // Total time needed to cover the zoom path        this.elapsedTime = 0.0;         var MAX_ZOOM_IN_AMOUNT = 0.6; // Maximum percentage of camera proximity to the selected entity from the look at point        var MAX_ZOOM_OUT_AMOUNT = 0.2; // Maximum percentage of camera remoteness to the selected entity from the look at point         this.maxZoomInAmount = MAX_ZOOM_IN_AMOUNT;        this.maxZoomOutAmount = MAX_ZOOM_OUT_AMOUNT;        this.currentZoomAmount = 0.0;         this.zoomPanOffset = {x: 0.5 * Window.innerWidth, y: 0.5 * Window.innerHeight};        this.zoomPanDelta = {x: 0.0, y: 0.0};         this.status = ZoomStatus.zoomedOut;         var OWN_CAMERA_CHANGE_MAX_FRAMES = 30;        this.ownCameraChangeElapseTime = 0.0; // Variables used to track if the camera mode change is triggered by this script        this.ownCameraChange = false;         // or by user input, since the later needs to abort the zoom loop          // Function called when the user release the mouse while panning the camera when using the secondary zoom        this.applyZoomPan = function() {            self.zoomPanOffset.x += self.zoomPanDelta.x;            self.zoomPanOffset.y += self.zoomPanDelta.y;        }         // Function called when the user moves the mouse while panning the camera when using the secondary zoom        this.setZoomPanDelta = function(x, y) {            self.zoomPanDelta.x = x;            self.zoomPanDelta.y = y;            var totalX = self.zoomPanOffset.x + self.zoomPanDelta.x;            var totalY = self.zoomPanOffset.y + self.zoomPanDelta.y;            totalX = Math.min(Math.max(totalX, 0.0), Window.innerWidth);            totalY = Math.min(Math.max(totalY, 0.0), Window.innerHeight);            self.zoomPanDelta.x = totalX - self.zoomPanOffset.x;            self.zoomPanDelta.y = totalY - self.zoomPanOffset.y;            self.updateSuperPan(totalX, totalY);        }         // This function computes the correct distance to the selected entity so it ends up fitted on the screen        this.getFocusDistance = function(zoomDims) {            var objAspect = zoomDims.x / zoomDims.y;            var camAspect = Camera.frustum.aspectRatio;            var m = 0.0;            if (objAspect < camAspect) {                m = zoomDims.y;            } else {                m = zoomDims.x / camAspect;            }            var DEGREE_TO_RADIAN = 0.0174533;            var fov = DEGREE_TO_RADIAN * Camera.frustum.fieldOfView;            return (0.5 * m) / Math.tan(0.5 * fov);        }         // This function computes the final camera position and rotation        this.computeFinalForm = function() {            self.finalPos = Vec3.sum(self.lookAt, Vec3.multiply(self.getFocusDistance(self.focusDimensions), self.focusNormal));            self.finalRot = Quat.lookAtSimple(self.finalPos, self.lookAt);        }         // Function that computes the time and path information for the camera zoom in        this.computeRouteIn = function() {            var railVector = Vec3.subtract(self.finalPos, self.initialPos);            self.direction = Vec3.normalize(railVector);            self.distance = Vec3.length(railVector);            self.totalTime = self.distance / self.velocity;            self.totalTime = self.totalTime > self.maxDuration ? self.maxDuration : self.totalTime;        }         // Function that computes the time and path information for the camera zoom out        // Since the camera orientation and position gets reset when changing camera modes,        // the camera path while zooming out is different from the path zooming in.        this.computeRouteOut = function() {            self.finalPos = Camera.getPosition();            var camOffset = Vec3.ZERO;            var myAvatarRotation = MyAvatar.orientation;            if (self.initialMode.indexOf("first") != -1) {                self.initialRot = myAvatarRotation;            } else {                var lookAtPoint = MyAvatar.getDefaultEyePosition();                var lookFromSign = self.initialMode.indexOf("selfie") != -1 ? 1 : -1;                var lookFromPoint = Vec3.sum(lookAtPoint, Vec3.multiply(self.initialOffset * lookFromSign, Quat.getFront(myAvatarRotation)));                self.initialRot = Quat.lookAtSimple(lookFromPoint, lookAtPoint);                self.initialPos = lookFromPoint;            }            self.computeRouteIn();        }         // This function initiate the camera zoom in        this.initZoomIn = function() {            if (self.status === ZoomStatus.zoomedOut) {                self.computeRouteIn();                self.status = ZoomStatus.zoomingIn;                self.changeCameraMode("independent");                self.elapsedTime = 0.0;            }        }         // This function initiate the camera zoom in        this.initZoomOut = function() {            if (self.status === ZoomStatus.zoomedIn) {                self.computeRouteOut();                self.status = ZoomStatus.zoomingOut;                self.changeCameraMode("independent");                self.elapsedTime = 0.0;            }        }         // This function checks if the zoom loop needs update        this.needsUpdate = function() {            return self.status === ZoomStatus.zoomingIn || self.status === ZoomStatus.zoomingOut;        }         // This function connects to the update signal only when zoom in or zoom out is needed        this.updateZoom = function(deltaTime) {            if (self.ownCameraChange) {                self.ownCameraChange = self.ownCameraChangeElapseTime < OWN_CAMERA_CHANGE_MAX_FRAMES * deltaTime;                self.ownCameraChangeElapseTime += deltaTime;            }            if (!self.needsUpdate) {                return;            }            if (self.elapsedTime < self.totalTime) {                var ratio = EasingFunctions.easeInOutQuart(self.elapsedTime / self.totalTime);                 if (self.status === ZoomStatus.zoomingIn) {                    var curDist = self.distance * ratio;                    var addition = Vec3.multiply(curDist, self.direction);                    self.interpolatedPos = Vec3.sum(self.initialPos, addition);                    self.interpolatedRot = Quat.mix(self.initialRot, self.finalRot, ratio);                } else if (self.status === ZoomStatus.zoomingOut) {                    self.interpolatedPos = Vec3.sum(self.finalPos, Vec3.multiply(self.distance * ratio, Vec3.multiply(-1, self.direction)));                    self.interpolatedRot = Quat.mix(self.finalRot, self.initialRot, ratio);                }                self.elapsedTime += deltaTime;                Camera.setPosition(self.interpolatedPos);                Camera.setOrientation(self.interpolatedRot);            } else {                Camera.setPosition(self.finalPos);                Camera.setOrientation(self.finalRot);                if (self.status === ZoomStatus.zoomingIn) {                    self.status = ZoomStatus.zoomedIn;                } else if (self.status === ZoomStatus.zoomingOut) {                    self.status = ZoomStatus.consumed;                    self.changeCameraMode(self.initialMode);                }            }        }         // This function recomputes the camera distance to the selected item in order to fit it on the screen        this.resetZoomAspect = function() {            self.computeFinalForm();            self.computeRouteIn();            Camera.setPosition(self.finalPos);        }         // This function updates the secondary zoom distance to the selected entity according to the mouse wheel event        this.updateSuperZoom = function(delta) {            var ZOOM_STEP = 0.1;            self.currentZoomAmount = self.currentZoomAmount + (delta < 0.0 ? -1 : 1) * ZOOM_STEP;            self.currentZoomAmount = Math.min(Math.max(self.currentZoomAmount, -self.maxZoomOutAmount), self.maxZoomInAmount);            self.updateSuperPan(self.zoomPanOffset.x, self.zoomPanOffset.y);        }         // This function updates the secondary zoom panning according to the mouse move event        this.updateSuperPan = function(x, y) {            var zoomOffset = Vec3.multiply(self.currentZoomAmount, Vec3.subtract(self.lookAt, self.finalPos));            var xRatio = 0.5 - x / Window.innerWidth;            var yRatio = 0.5 - y / Window.innerHeight;            var cameraOrientation = Camera.getOrientation();            var cameraY = Quat.getUp(cameraOrientation);            var cameraX = Vec3.multiply(-1, Quat.getRight(cameraOrientation));            var xOffset = Vec3.multiply(xRatio * self.focusDimensions.x, cameraX);            var yOffset = Vec3.multiply(yRatio * self.focusDimensions.y, cameraY);            zoomOffset = Vec3.sum(zoomOffset, xOffset);            zoomOffset = Vec3.sum(zoomOffset, yOffset);            Camera.setPosition(Vec3.sum(self.finalPos, zoomOffset));        }         // This function aborts the zoom loop when the user changes the camera mode        this.abort = function() {            self.changeCameraMode(self.initialMode);        }         // This function changes the camera mode        this.changeCameraMode = function(mode) {            self.ownCameraChange = true;            self.ownCameraChangeElapseTime = 0.0;            Camera.mode = mode;        }         this.computeFinalForm();    }     // Class that manages the mouse events and computes the 3d structure data of the selected entity or avatar    var ZoomOnAnything = function() {        var self = this;        this.zoomEntityData; // The selected entity's data        this.zoomCameraPos;        var ZOOM_MAX_VELOCITY = 15.0; // meters per second        var ZOOM_MAX_DURATION = 1.0; // seconds        this.zoomDelta = {x: 0.0, y: 0.0}; // mouse move delta while panning        this.isPanning = false;        this.screenPointRef = {x: 0.0, y: 0.0}; // Screen pixels reference used to compute the zoomDelta        this.connected = false;         // This function estimates the projection axis and the dimensions along that axis of the selected entity        this.getEntityDimsFromNormal = function(dims, rot, normal) {            var zoomXNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_X);            var zoomYNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Y);            var zoomZNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Z);            var affinities = [                {axis: "x", normal: zoomXNormal, affin: Math.abs(Vec3.dot(zoomXNormal, normal)), dims: {x: dims.z, y: dims.y}},                {axis: "y", normal: zoomYNormal, affin: Math.abs(Vec3.dot(zoomYNormal, normal)), dims: {x: dims.x, y: dims.z}},                {axis: "z", normal: zoomZNormal, affin: Math.abs(Vec3.dot(zoomZNormal, normal)), dims: {x: dims.x, y: dims.y}}            ];            affinities.sort(function(a, b) {                return b.affin - a.affin;            });            return affinities[0];        }         // This function estimates the appropriate focus point when the selected entity is an avatar        this.getAvatarFocusPoint = function(avatar) {            var rEyeIndex = avatar.getJointIndex("RightEye");            var lEyeIndex = avatar.getJointIndex("LeftEye");            var headIndex = avatar.getJointIndex("Head");            var focusPoint = Vec3.ZERO;            var validPoint = false;            var count = 0;            if (rEyeIndex != -1) {                focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(rEyeIndex));                validPoint = true;                count++;            }            if (lEyeIndex != -1) {                var leftEyePos = avatar.getJointPosition(lEyeIndex);                var NORMAL_EYE_DISTANCE = 0.1;                focusPoint = Vec3.sum(focusPoint, leftEyePos);                validPoint = true;                count++;            }            if (headIndex != -1) {                focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(headIndex));                validPoint = true;                count++;            }            if (!validPoint) {                focusPoint = avatar.getJointPosition("Hips");                count++;            }            return Vec3.multiply(1.0/count, focusPoint);        }         // This function sets the data needed to zoom in on an avatar        this.getZoomDataFromAvatar = function(avatarID, skinToBoneDist, zoomVelocity, maxDuration) {            var headDiam = 2.0 * skinToBoneDist; // Head diameter is estimated based on the distance to the bone            headDiam = headDiam < 0.5 ? 0.5 : headDiam;            var avatar = AvatarList.getAvatar(avatarID);            var focusPoint = self.getAvatarFocusPoint(avatar);            var focusDims = {x: headDiam, y: headDiam};            var focusNormal = Quat.getFront(avatar.orientation);            var zoomData = new ZoomData(FocusType.avatar, focusPoint, focusNormal, focusDims, zoomVelocity, maxDuration);            return zoomData;        }         // This function sets the data needed to zoom in on an entity        this.getZoomDataFromEntity = function(intersection, objectProps, zoomVelocity, maxDuration) {            var position = objectProps.position;            var dimensions = objectProps.dimensions;            var rotation = objectProps.rotation;            var focusNormal = intersection.surfaceNormal;            var dimsResult = self.getEntityDimsFromNormal(dimensions, rotation, focusNormal);            var focusDims = dimsResult.dims;            var focusDepth = Vec3.dot(Vec3.subtract(intersection.intersection, position), dimsResult.normal);            var newPosition = Vec3.sum(position, Vec3.multiply(focusDepth, dimsResult.normal));            var zoomData = new ZoomData(FocusType.entity, newPosition, focusNormal, focusDims, zoomVelocity, maxDuration);            return zoomData;        }         // This function initiate the zoom loop on the selected entity        this.zoomOnEntity = function(intersection, objectProps) {            self.zoomEntityData = self.getZoomDataFromEntity(intersection, objectProps, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);            self.zoomEntityData.initZoomIn();        }         // This function initiates the zoom loop on the selected avatar        this.zoomOnAvatar = function(avatarID, skinToBoneDist) {            self.zoomEntityData = self.getZoomDataFromAvatar(avatarID, skinToBoneDist, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);            self.zoomEntityData.initZoomIn();        }         // This function connects to the Script.update signal        this.connect = function() {            self.connected = true;            Script.update.connect(self.updateZoom);        }         // This function disconnects to the Script.update signal        this.disconnect = function() {            self.connected = false;            Script.update.disconnect(self.updateZoom);        }         // This function get connected to the Script.update signal        this.updateZoom = function(deltaTime) {            if (self.zoomEntityData) {                if (self.zoomEntityData.needsUpdate()) {                    self.zoomEntityData.updateZoom(deltaTime);                    if (self.zoomEntityData.status === ZoomStatus.consumed) {                        self.disconnect();                        self.zoomEntityData = undefined;                    }                } else {                    self.disconnect();                }            } else if (self.connected) {                self.disconnect();            }        }         // This function listen to the mouse double click that evaluates the selected entity and the intersection data that initiates the zoom loop        this.mouseDoublePressEvent = function(event) {            if (event.isLeftButton) {                self.connect();                if (!self.zoomEntityData) {                    var pickRay = Camera.computePickRay(event.x, event.y);                    var intersection = AvatarManager.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, [], [MyAvatar.sessionUUID], false);                    if (!intersection.intersects) {                        intersection = Entities.findRayIntersection({ origin: pickRay.origin, direction: pickRay.direction }, Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_AVATAR_ENTITIES);                        var entityProps = Entities.getEntityProperties(intersection.entityID);                        if (entityProps.type === "Shape") {                            var FIND_SHAPES_DISTANCE = 10.0;                            var shapes = Entities.findEntitiesByType("Shape", intersection.intersection, FIND_SHAPES_DISTANCE);                            intersection = Entities.findRayIntersection({ origin: pickRay.origin, direction: pickRay.direction }, Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_AVATAR_ENTITIES, [], shapes);                            entityProps = Entities.getEntityProperties(intersection.entityID);                        }                        if (!entityProps.dimensions) {                            return;                        }                        self.zoomOnEntity(intersection, entityProps);                    } else {                        var avatar = AvatarList.getAvatar(intersection.avatarID);                        var skinToBoneDist = Vec3.distance(intersection.intersection, avatar.getJointPosition(intersection.jointIndex));                        self.zoomOnAvatar(intersection.avatarID, skinToBoneDist);                    }                } else if (!self.zoomEntityData.needsUpdate()){                    self.zoomEntityData.initZoomOut();                }            }        }         // This function listen to the mouse press event to initiate panning while on secondary zoom        this.mousePressEvent = function(event) {            if (event.isRightButton) {                self.isPanning = true;                self.screenPointRef = {x: event.x, y: event.y};            }        }         // This function listen to the mouse release event to apply the pan delta while on secondary zoom        this.mouseReleaseEvent = function(event) {            if (event.isRightButton) {                if (self.zoomEntityData) {                    self.zoomEntityData.applyZoomPan();                    self.isPanning = false;                    self.screenPointRef = {x: 0, y: 0};                }            }        }         // This function listen to the mouse move event to modify the pan delta coordenates while on secondary zoom        this.mouseMoveEvent = function(event) {            if (event.isRightButton) {                if (self.isPanning && self.zoomEntityData) {                    self.zoomEntityData.setZoomPanDelta(event.x - self.screenPointRef.x, event.y - self.screenPointRef.y);                }            }        }         // This function listen to the mouse wheel event to modify the ammount of secondary zoom        this.mouseWheel = function(event) {            if (self.zoomEntityData) {                self.zoomEntityData.updateSuperZoom(event.delta);            }        }         // This function aborts the zoom loop        this.abort = function() {            self.zoomEntityData.abort();            self.zoomEntityData = undefined;            if (self.connected) {                self.disconnect();            }        }         // This function refits the selected entity on the screen when the screen dimensions change        this.geometryChanged = function() {            if (self.zoomEntityData !== undefined){                self.zoomEntityData.resetZoomAspect();            }        }         // This function aborts the zoom loop if the camera mode is set by the user        this.modeUpdated = function(mode) {            if (self.zoomEntityData && !self.zoomEntityData.ownCameraChange) {                self.abort();            }        }         // This function initiate the connections        this.init = function() {            // Connect signals            Window.geometryChanged.connect(self.geometryChanged);            Camera.modeUpdated.connect(self.modeUpdated);            Controller.mousePressEvent.connect(self.mousePressEvent);            Controller.mouseDoublePressEvent.connect(self.mouseDoublePressEvent);            Controller.mouseMoveEvent.connect(self.mouseMoveEvent);            Controller.mouseReleaseEvent.connect(self.mouseReleaseEvent);            Controller.wheelEvent.connect(self.mouseWheel);        }         // This function finish the connections        this.scriptEnding = function() {            // Disconnect on exit            Window.geometryChanged.disconnect(self.geometryChanged);            Camera.modeUpdated.disconnect(self.modeUpdated);            Controller.mousePressEvent.disconnect(self.mousePressEvent);            Controller.mouseDoublePressEvent.disconnect(self.mouseDoublePressEvent);            Controller.mouseMoveEvent.disconnect(self.mouseMoveEvent);            Controller.mouseReleaseEvent.disconnect(self.mouseReleaseEvent);            Controller.wheelEvent.disconnect(self.mouseWheel);            if (self.zoomEntityData) {                self.abort();

Lines 1–500 of 509.