overte/examples/libraries/entityCameraTool.js

678 lines
21 KiB
JavaScript

//
// entityCameraTool.js
// examples
//
// Created by Ryan Huffman on 10/14/14.
// Copyright 2014 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
//
var MOUSE_SENSITIVITY = 0.9;
var SCROLL_SENSITIVITY = 0.05;
var PAN_ZOOM_SCALE_RATIO = 0.4;
// Scaling applied based on the size of the object being focused
var FOCUS_ZOOM_SCALE = 1.3;
// Minimum zoom level when focusing on an object
var FOCUS_MIN_ZOOM = 0.5;
// Scaling applied based on the current zoom level
var ZOOM_SCALING = 0.02;
var MIN_ZOOM_DISTANCE = 0.01;
var MAX_ZOOM_DISTANCE = 200;
var MODE_INACTIVE = 'inactive';
var MODE_ORBIT = 'orbit';
var MODE_PAN = 'pan';
var EASING_MULTIPLIER = 8;
var INITIAL_ZOOM_DISTANCE = 2;
var INITIAL_ZOOM_DISTANCE_FIRST_PERSON = 3;
var easeOutCubic = function(t) {
t--;
return t * t * t + 1;
};
EASE_TIME = 0.5;
CameraManager = function() {
var that = {};
that.enabled = false;
that.mode = MODE_INACTIVE;
that.zoomDistance = INITIAL_ZOOM_DISTANCE;
that.targetZoomDistance = INITIAL_ZOOM_DISTANCE;
that.yaw = 0;
that.pitch = 0;
that.targetYaw = 0;
that.targetPitch = 0;
that.focalPoint = { x: 0, y: 0, z: 0 };
that.targetFocalPoint = { x: 0, y: 0, z: 0 };
easing = false;
easingTime = 0;
startOrientation = Quat.fromPitchYawRollDegrees(0, 0, 0);
that.previousCameraMode = null;
that.lastMousePosition = { x: 0, y: 0 };
that.enable = function() {
if (that.enabled) return;
that.enabled = true;
that.mode = MODE_INACTIVE;
// Pick a point INITIAL_ZOOM_DISTANCE in front of the camera to use as a focal point
that.zoomDistance = INITIAL_ZOOM_DISTANCE;
that.targetZoomDistance = that.zoomDistance;
var focalPoint = Vec3.sum(Camera.getPosition(),
Vec3.multiply(that.zoomDistance, Quat.getFront(Camera.getOrientation())));
// Determine the correct yaw and pitch to keep the camera in the same location
var dPos = Vec3.subtract(focalPoint, Camera.getPosition());
var xzDist = Math.sqrt(dPos.x * dPos.x + dPos.z * dPos.z);
that.targetPitch = -Math.atan2(dPos.y, xzDist) * 180 / Math.PI;
that.targetYaw = Math.atan2(dPos.x, dPos.z) * 180 / Math.PI;
that.pitch = that.targetPitch;
that.yaw = that.targetYaw;
that.focalPoint = focalPoint;
that.setFocalPoint(focalPoint);
that.previousCameraMode = Camera.mode;
Camera.mode = "independent";
that.updateCamera();
cameraTool.setVisible(false);
}
that.disable = function(ignoreCamera) {
if (!that.enabled) return;
that.enabled = false;
that.mode = MODE_INACTIVE;
if (!ignoreCamera) {
Camera.mode = that.previousCameraMode;
}
cameraTool.setVisible(false);
}
that.focus = function(position, dimensions, easeOrientation) {
if (dimensions) {
var size = Math.max(dimensions.x, Math.max(dimensions.y, dimensions.z));
that.targetZoomDistance = Math.max(size * FOCUS_ZOOM_SCALE, FOCUS_MIN_ZOOM);
} else {
that.targetZoomDistance = Vec3.length(Vec3.subtract(Camera.getPosition(), position));
}
if (easeOrientation) {
// Do eased turning towards target
that.focalPoint = that.targetFocalPoint = position;
that.zoomDistance = that.targetZoomDistance = Vec3.length(Vec3.subtract(Camera.getPosition(), position));
var dPos = Vec3.subtract(that.focalPoint, Camera.getPosition());
var xzDist = Math.sqrt(dPos.x * dPos.x + dPos.z * dPos.z);
that.targetPitch = -Math.atan2(dPos.y, xzDist) * 180 / Math.PI;
that.targetYaw = Math.atan2(dPos.x, dPos.z) * 180 / Math.PI;
that.pitch = that.targetPitch;
that.yaw = that.targetYaw;
startOrientation = Camera.getOrientation();
easing = true;
easingTime = 0;
} else {
that.setFocalPoint(position);
}
that.updateCamera();
}
that.moveFocalPoint = function(dPos) {
that.setFocalPoint(Vec3.sum(that.focalPoint, dPos));
}
that.setFocalPoint = function(pos) {
that.targetFocalPoint = pos
that.updateCamera();
}
that.addYaw = function(yaw) {
that.targetYaw += yaw;
that.updateCamera();
}
that.addPitch = function(pitch) {
that.targetPitch += pitch;
that.updateCamera();
}
that.addZoom = function(zoom) {
zoom *= that.targetZoomDistance * ZOOM_SCALING;
that.targetZoomDistance = Math.min(Math.max(that.targetZoomDistance + zoom, MIN_ZOOM_DISTANCE), MAX_ZOOM_DISTANCE);
that.updateCamera();
}
that.getZoomPercentage = function() {
return (that.zoomDistance - MIN_ZOOM_DISTANCE) / MAX_ZOOM_DISTANCE;
}
that.setZoomPercentage = function(pct) {
that.targetZoomDistance = pct * (MAX_ZOOM_DISTANCE - MIN_ZOOM_DISTANCE);
}
that.pan = function(offset) {
var up = Quat.getUp(Camera.getOrientation());
var right = Quat.getRight(Camera.getOrientation());
up = Vec3.multiply(up, offset.y * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
right = Vec3.multiply(right, offset.x * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
var dPosition = Vec3.sum(up, right);
that.moveFocalPoint(dPosition);
}
that.mouseMoveEvent = function(event) {
if (that.enabled && that.mode != MODE_INACTIVE) {
var x = Window.getCursorPositionX();
var y = Window.getCursorPositionY();
if (that.mode == MODE_ORBIT) {
var diffX = x - that.lastMousePosition.x;
var diffY = y - that.lastMousePosition.y;
that.targetYaw -= MOUSE_SENSITIVITY * (diffX / 5.0)
that.targetPitch += MOUSE_SENSITIVITY * (diffY / 10.0)
while (that.targetYaw > 180.0) that.targetYaw -= 360;
while (that.targetYaw < -180.0) that.targetYaw += 360;
if (that.targetPitch > 90) that.targetPitch = 90;
if (that.targetPitch < -90) that.targetPitch = -90;
that.updateCamera();
} else if (that.mode == MODE_PAN) {
var diffX = x - that.lastMousePosition.x;
var diffY = y - that.lastMousePosition.y;
var up = Quat.getUp(Camera.getOrientation());
var right = Quat.getRight(Camera.getOrientation());
up = Vec3.multiply(up, diffY * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
right = Vec3.multiply(right, -diffX * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
var dPosition = Vec3.sum(up, right);
that.moveFocalPoint(dPosition);
}
var newX = Window.x + Window.innerWidth / 2;
var newY = Window.y + Window.innerHeight / 2;
Window.setCursorPosition(newX, newY);
that.lastMousePosition.x = newX;
that.lastMousePosition.y = newY;
return true;
}
return false;
}
that.mousePressEvent = function(event) {
if (!that.enabled) return;
if (event.isRightButton || (event.isLeftButton && event.isControl && !event.isShifted)) {
that.mode = MODE_ORBIT;
} else if (event.isMiddleButton || (event.isLeftButton && event.isControl && event.isShifted)) {
that.mode = MODE_PAN;
}
if (that.mode != MODE_INACTIVE) {
var newX = Window.x + Window.innerWidth / 2;
var newY = Window.y + Window.innerHeight / 2;
Window.setCursorPosition(newX, newY);
that.lastMousePosition.x = newX;
that.lastMousePosition.y = newY;
Window.setCursorVisible(false);
return true;
}
return cameraTool.mousePressEvent(event);
}
that.mouseReleaseEvent = function(event) {
if (!that.enabled) return;
Window.setCursorVisible(true);
that.mode = MODE_INACTIVE;
}
that.wheelEvent = function(event) {
if (!that.enabled) return;
var dZoom = -event.delta * SCROLL_SENSITIVITY;
// Scale based on current zoom level
dZoom *= that.targetZoomDistance * ZOOM_SCALING;
that.targetZoomDistance = Math.min(Math.max(that.targetZoomDistance + dZoom, MIN_ZOOM_DISTANCE), MAX_ZOOM_DISTANCE);
that.updateCamera();
}
that.updateCamera = function() {
if (!that.enabled || Camera.mode != "independent") return;
var yRot = Quat.angleAxis(that.yaw, { x: 0, y: 1, z: 0 });
var xRot = Quat.angleAxis(that.pitch, { x: 1, y: 0, z: 0 });
var q = Quat.multiply(yRot, xRot);
var pos = Vec3.multiply(Quat.getFront(q), that.zoomDistance);
Camera.setPosition(Vec3.sum(that.focalPoint, pos));
yRot = Quat.angleAxis(that.yaw - 180, { x: 0, y: 1, z: 0 });
xRot = Quat.angleAxis(-that.pitch, { x: 1, y: 0, z: 0 });
q = Quat.multiply(yRot, xRot);
if (easing) {
var t = easeOutCubic(easingTime / EASE_TIME);
q = Quat.slerp(startOrientation, q, t);
}
Camera.setOrientation(q);
}
function normalizeDegrees(degrees) {
while (degrees > 180) degrees -= 360;
while (degrees < -180) degrees += 360;
return degrees;
}
// Ease the position and orbit of the camera
that.update = function(dt) {
if (Camera.mode != "independent") {
return;
}
if (easing) {
easingTime = Math.min(EASE_TIME, easingTime + dt);
}
var scale = Math.min(dt * EASING_MULTIPLIER, 1.0);
var dYaw = that.targetYaw - that.yaw;
if (dYaw > 180) dYaw -= 360;
if (dYaw < -180) dYaw += 360;
var dPitch = that.targetPitch - that.pitch;
that.yaw += scale * dYaw;
that.pitch += scale * dPitch;
// Normalize between [-180, 180]
that.yaw = normalizeDegrees(that.yaw);
that.pitch = normalizeDegrees(that.pitch);
var dFocal = Vec3.subtract(that.targetFocalPoint, that.focalPoint);
that.focalPoint = Vec3.sum(that.focalPoint, Vec3.multiply(scale, dFocal));
var dZoom = that.targetZoomDistance - that.zoomDistance;
that.zoomDistance += scale * dZoom;
that.updateCamera();
if (easingTime >= 1) {
easing = false;
}
}
// Last mode that was first or third person
var lastAvatarCameraMode = "first person";
Camera.modeUpdated.connect(function(newMode) {
if (newMode == "first person" || newMode == "third person") {
lastAvatarCameraMode = newMode;
that.disable(true);
} else {
that.enable();
}
});
Controller.keyReleaseEvent.connect(function (event) {
if (event.text == "ESC" && that.enabled) {
Camera.mode = lastAvatarCameraMode;
cameraManager.disable(true);
}
});
Script.update.connect(that.update);
Controller.wheelEvent.connect(that.wheelEvent);
var cameraTool = new CameraTool(that);
return that;
}
var ZoomTool = function(opts) {
var that = {};
var position = opts.position || { x: 0, y: 0 };
var height = opts.height || 200;
var color = opts.color || { red: 255, green: 0, blue: 0 };
var arrowButtonSize = opts.buttonSize || 20;
var arrowButtonBackground = opts.arrowBackground || { red: 255, green: 255, blue: 255 };
var zoomBackground = { red: 128, green: 0, blue: 0 };
var zoomHeight = height - (arrowButtonSize * 2);
var zoomBarY = position.y + arrowButtonSize,
var onIncreasePressed = opts.onIncreasePressed;
var onDecreasePressed = opts.onDecreasePressed;
var onPercentageSet = opts.onPercentageSet;
var increaseButton = Overlays.addOverlay("text", {
x: position.x,
y: position.y,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "+",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var decreaseButton = Overlays.addOverlay("text", {
x: position.x,
y: position.y + arrowButtonSize + zoomHeight,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "-",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var zoomBar = Overlays.addOverlay("text", {
x: position.x + 5,
y: zoomBarY,
width: 10,
height: zoomHeight,
color: { red: 0, green: 255, blue: 0 },
backgroundColor: zoomBackground,
topMargin: 4,
leftMargin: 4,
text: "",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var zoomHandle = Overlays.addOverlay("text", {
x: position.x,
y: position.y + arrowButtonSize,
width: arrowButtonSize,
height: 10,
backgroundColor: { red: 0, green: 255, blue: 0 },
topMargin: 4,
leftMargin: 4,
text: "",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var allOverlays = [
increaseButton,
decreaseButton,
zoomBar,
zoomHandle,
];
that.destroy = function() {
for (var i = 0; i < allOverlays.length; i++) {
Overlays.deleteOverlay(allOverlays[i]);
}
};
that.setVisible = function(visible) {
for (var i = 0; i < allOverlays.length; i++) {
Overlays.editOverlay(allOverlays[i], { visible: visible });
}
}
that.setZoomPercentage = function(pct) {
var yOffset = (zoomHeight - 10) * pct;
Overlays.editOverlay(zoomHandle, {
y: position.y + arrowButtonSize + yOffset,
});
}
that.mouseReleaseEvent = function(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
var clicked = false;
if (clickedOverlay == increaseButton) {
if (onIncreasePressed) onIncreasePressed();
clicked = true;
} else if (clickedOverlay == decreaseButton) {
if (onDecreasePressed) onDecreasePressed();
clicked = true;
} else if (clickedOverlay == zoomBar) {
if (onPercentageSet) onPercentageSet((event.y - zoomBarY) / zoomHeight);
clicked = true;
}
return clicked;
}
return that;
};
var ArrowTool = function(opts) {
var that = {};
var position = opts.position || { x: 0, y: 0 };
var arrowButtonSize = opts.buttonSize || 20;
var color = opts.color || { red: 255, green: 0, blue: 0 };
var arrowButtonBackground = opts.arrowBackground || { red: 255, green: 255, blue: 255 };
var centerButtonBackground = opts.centerBackground || { red: 255, green: 255, blue: 255 };
var onUpPressed = opts.onUpPressed;
var onDownPressed = opts.onDownPressed;
var onLeftPressed = opts.onLeftPressed;
var onRightPressed = opts.onRightPressed;
var onCenterPressed = opts.onCenterPressed;
var upButton = Overlays.addOverlay("text", {
x: position.x + arrowButtonSize,
y: position.y,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "^",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var leftButton = Overlays.addOverlay("text", {
x: position.x,
y: position.y + arrowButtonSize,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "<",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var rightButton = Overlays.addOverlay("text", {
x: position.x + (arrowButtonSize * 2),
y: position.y + arrowButtonSize,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: ">",
alpha: 1.0,
visible: true,
});
var downButton = Overlays.addOverlay("text", {
x: position.x + arrowButtonSize,
y: position.y + (arrowButtonSize * 2),
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: arrowButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "v",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var centerButton = Overlays.addOverlay("text", {
x: position.x + arrowButtonSize,
y: position.y + arrowButtonSize,
width: arrowButtonSize,
height: arrowButtonSize,
color: color,
backgroundColor: centerButtonBackground,
topMargin: 4,
leftMargin: 4,
text: "",
alpha: 1.0,
backgroundAlpha: 1.0,
visible: true,
});
var allOverlays = [
upButton,
downButton,
leftButton,
rightButton,
centerButton,
];
that.destroy = function() {
for (var i = 0; i < allOverlays.length; i++) {
Overlays.deleteOverlay(allOverlays[i]);
}
};
that.setVisible = function(visible) {
for (var i = 0; i < allOverlays.length; i++) {
Overlays.editOverlay(allOverlays[i], { visible: visible });
}
}
that.mouseReleaseEvent = function(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
var clicked = false;
if (clickedOverlay == leftButton) {
if (onLeftPressed) onLeftPressed();
clicked = true;
} else if (clickedOverlay == rightButton) {
if (onRightPressed) onRightPressed();
clicked = true;
} else if (clickedOverlay == upButton) {
if (onUpPressed) onUpPressed();
clicked = true;
} else if (clickedOverlay == downButton) {
if (onDownPressed) onDownPressed();
clicked = true;
} else if (clickedOverlay == centerButton) {
if (onCenterPressed) onCenterPressed();
clicked = true;
}
return clicked;
}
return that;
}
CameraTool = function(cameraManager) {
var that = {};
var toolsPosition = { x: 20, y: 280 };
var orbitToolPosition = toolsPosition;
var panToolPosition = { x: toolsPosition.x + 80, y: toolsPosition.y };
var zoomToolPosition = { x: toolsPosition.x + 20, y: toolsPosition.y + 80 };
var orbitIncrement = 15;
orbitTool = ArrowTool({
position: orbitToolPosition,
arrowBackground: { red: 192, green: 192, blue: 192 },
centerBackground: { red: 128, green: 128, blue: 255 },
color: { red: 0, green: 0, blue: 0 },
onUpPressed: function() { cameraManager.addPitch(orbitIncrement); },
onDownPressed: function() { cameraManager.addPitch(-orbitIncrement); },
onLeftPressed: function() { cameraManager.addYaw(-orbitIncrement); },
onRightPressed: function() { cameraManager.addYaw(orbitIncrement); },
onCenterPressed: function() { cameraManager.focus(); },
});
panTool = ArrowTool({
position: panToolPosition,
arrowBackground: { red: 192, green: 192, blue: 192 },
centerBackground: { red: 128, green: 128, blue: 255 },
color: { red: 0, green: 0, blue: 0 },
onUpPressed: function() { cameraManager.pan({ x: 0, y: 15 }); },
onDownPressed: function() { cameraManager.pan({ x: 0, y: -15 }); },
onLeftPressed: function() { cameraManager.pan({ x: -15, y: 0 }); },
onRightPressed: function() { cameraManager.pan({ x: 15, y: 0 }); },
});
zoomTool = ZoomTool({
position: zoomToolPosition,
arrowBackground: { red: 192, green: 192, blue: 192 },
color: { red: 0, green: 0, blue: 0 },
onIncreasePressed: function() { cameraManager.addZoom(-10); },
onDecreasePressed: function() { cameraManager.addZoom(10); },
onPercentageSet: function(pct) { cameraManager.setZoomPercentage(pct); }
});
Script.scriptEnding.connect(function() {
orbitTool.destroy();
panTool.destroy();
zoomTool.destroy();
});
that.mousePressEvent = function(event) {
return orbitTool.mouseReleaseEvent(event)
|| panTool.mouseReleaseEvent(event)
|| zoomTool.mouseReleaseEvent(event);
};
that.setVisible = function(visible) {
orbitTool.setVisible(visible);
panTool.setVisible(visible);
zoomTool.setVisible(visible);
};
Script.update.connect(function() {
cameraManager.getZoomPercentage();
zoomTool.setZoomPercentage(cameraManager.getZoomPercentage());
});
that.setVisible(false);
return that;
};