diff --git a/apps/simple-video-layers/multiple-layers.js b/apps/simple-video-layers/multiple-layers.js
index 24a13d5..397cbfe 100644
--- a/apps/simple-video-layers/multiple-layers.js
+++ b/apps/simple-video-layers/multiple-layers.js
@@ -318,10 +318,26 @@ class App {
handleSelectEnd(controller) {
this.mediaLayers.forEach((layerObj, layerKey) => {
- if (layerObj.glassLayer) {
+ if (controller.userData.engagedMove && layerObj.glassLayer) {
layerObj.glassLayer.move();
controller.remove(layerObj.glass);
}
+ if (
+ layerObj.glassLayer &&
+ controller.userData.engagedResize &&
+ layerObj
+ ) {
+ layerObj.toolbar.disengageResize(controller);
+ controller.userData.engagedResize = false;
+ }
+ if (controller.userData.engagedResize && layerObj) {
+ layerObj.toolbar.disengageResize(controller);
+ controller.userData.engagedResize = false;
+ }
+ if (controller.userData.engagedResize && layerObj) {
+ layerObj.toolbar.disengageResize(controller);
+ controller.userData.engagedResize = false;
+ }
});
}
@@ -337,18 +353,13 @@ class App {
this.scene.add(layerObj.toolbarGroup);
if (layerObj.glassLayer) {
- this.scene.add(layerObj.glass);
+ this.scene.add(layerObj.glassLayer.object);
}
} else {
this.handleToolbarIntersections(controller, {
layerKey,
layerObj,
});
-
- // Handle moving of video layer
- if (layerObj.glassLayer) {
- controller.attach(layerObj.glass);
- }
}
});
}
@@ -361,6 +372,19 @@ class App {
if (intersections.length > 0) {
layerObj.update(intersections);
+
+ // Handle moving of video layer
+ if (intersections[0].object === layerObj.glassLayer.object) {
+ console.log("move engaged");
+ controller.userData.engagedMove = true;
+ controller.attach(layerObj.glass);
+ } else if (
+ intersections[0].object === layerObj.toolbar.resizeHandle
+ ) {
+ console.log("resize engaged");
+ controller.userData.engagedResize = true;
+ layerObj.toolbar.engageResize(controller);
+ }
} else {
this.scene.userData.isToolbarVisible[layerKey] = false;
this.scene.remove(layerObj.toolbarGroup);
@@ -432,8 +456,8 @@ class App {
if (!this.scene.userData.isToolbarVisible) {
this.scene.userData.isToolbarVisible = {};
}
-
- for(const layerKey of this.mediaLayers.keys()) {
+
+ for (const layerKey of this.mediaLayers.keys()) {
this.scene.userData.isToolbarVisible[layerKey] = false;
}
}
diff --git a/util/models/fbx/OculusHand_L.fbx b/util/models/fbx/OculusHand_L.fbx
new file mode 100644
index 0000000..87337ee
Binary files /dev/null and b/util/models/fbx/OculusHand_L.fbx differ
diff --git a/util/models/fbx/OculusHand_L_low.fbx b/util/models/fbx/OculusHand_L_low.fbx
new file mode 100644
index 0000000..64c4886
Binary files /dev/null and b/util/models/fbx/OculusHand_L_low.fbx differ
diff --git a/util/models/fbx/OculusHand_R.fbx b/util/models/fbx/OculusHand_R.fbx
new file mode 100644
index 0000000..7ee53a0
Binary files /dev/null and b/util/models/fbx/OculusHand_R.fbx differ
diff --git a/util/models/fbx/OculusHand_R_low.fbx b/util/models/fbx/OculusHand_R_low.fbx
new file mode 100644
index 0000000..1dc8ca3
Binary files /dev/null and b/util/models/fbx/OculusHand_R_low.fbx differ
diff --git a/util/webxr/MediaLayerManager/GlassLayer.js b/util/webxr/MediaLayerManager/GlassLayer.js
index 7ba1cf0..a39e12e 100644
--- a/util/webxr/MediaLayerManager/GlassLayer.js
+++ b/util/webxr/MediaLayerManager/GlassLayer.js
@@ -1,9 +1,9 @@
import * as THREE from "three";
-
export default class GlassLayer {
constructor(layer, renderer) {
this.layer = layer;
this.renderer = renderer;
+
this.glassObject = this.createGlassObject(this.layer);
}
@@ -30,18 +30,29 @@ export default class GlassLayer {
return glass;
}
- move({ x, y, z }) {
+ updateDimensions({ width, height }) {
+ this.glassObject.scale.set(2 * width, 2 * height, 1);
+ }
+
+ /**
+ * Updates position and quaternion of glass layer objects when quad video layer is moved
+ */
+ updateOrientation(position, quaternion) {
+ const { x, y, z } = position;
+ // update position x, y, z
this.glassObject.position.set(x, y, z);
- this.glassObject.position.needsUpdate = true;
+ // update quaternion (3d heading and orientation)
+ this.glassObject.quaternion.copy(quaternion);
}
- updatePosition() {
+ /**
+ * Updates position and quaternion of media layer when glass layer is moved
+ */
+ updateLayerOrientation() {
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
-
this.glassObject.getWorldPosition(position);
this.glassObject.getWorldQuaternion(quaternion);
-
const { x, y, z } = position;
this.layer.transform = new XRRigidTransform(
{
@@ -54,23 +65,9 @@ export default class GlassLayer {
);
}
- updateDimensions({ width, height }) {
- this.glassObject.scale.set(2 * width, 2 * height, 1);
- }
-
- /**
- * Updates position and quaternion of glass layer when quad video layer is moved
- */
- updateOrientation(position, quaternion) {
- // update position x, y, z
- this.glassObject.position.set(position.x, position.y, position.z);
- // update quaternion (3d heading and orientation)
- this.glassObject.quaternion.copy(quaternion);
- }
-
updateOnRender() {
this.updateDimensions(this.layer);
- this.updatePosition();
+ this.updateLayerOrientation();
}
move() {
diff --git a/util/webxr/Toolbar.js b/util/webxr/Toolbar.js
index d0cc852..85a70cc 100644
--- a/util/webxr/Toolbar.js
+++ b/util/webxr/Toolbar.js
@@ -1,6 +1,9 @@
import * as THREE from "three";
import { CanvasUI } from "../CanvasUI";
+const RESIZE_HANDLE_THICKNESS = 0.05;
+const MIN_LAYER_WIDTH = 0.5;
+const MAX_LAYER_WIDTH = 10;
class Toolbar {
constructor(layer, renderer, video, options) {
@@ -19,12 +22,30 @@ class Toolbar {
// Progress Bar
this.progressBar = this.createProgressBar();
+ // Resize Handle
+ this.resizeHandle = this.createResizeHandle();
+ this.resizeHandleClone = null;
+
// Toolbar Group
this.toolbarGroup = this.createToolbarGroup(toolbarGroupConfig);
}
get objects() {
- return [this.ui.mesh, ...this.progressBar.children];
+ return [this.ui.mesh, ...this.progressBar.children, this.resizeHandle];
+ }
+
+ createResizeHandle() {
+ const handleGeometry = new THREE.PlaneGeometry(1, 1); // to scale
+ const handleMaterial = new THREE.MeshBasicMaterial({ color: "white" });
+
+ // bottom handle
+ const handle = new THREE.Mesh(handleGeometry, handleMaterial);
+ handle.scale.set(this.layer.width, RESIZE_HANDLE_THICKNESS, 1);
+ const { x, y, z } = this.ui.mesh.position;
+ handle.position.set(x, y - this.uiHeight, z);
+
+ handle.name = "resizeHandle";
+ return handle;
}
createProgressBar() {
@@ -53,6 +74,7 @@ class Toolbar {
const toolbarGroup = new THREE.Group();
toolbarGroup.add(this.ui.mesh);
toolbarGroup.add(this.progressBar);
+ toolbarGroup.add(this.resizeHandle);
const { x, y, z } = toolbarGroupConfig.position;
toolbarGroup.position.set(x, y, z);
@@ -86,6 +108,16 @@ class Toolbar {
this.ui.updateElement("pause", label);
};
+ const onExpand = () => {
+ this.layer.width *= 1.25;
+ this.layer.height *= 1.25;
+ };
+
+ const onCompress = () => {
+ this.layer.width /= 1.25;
+ this.layer.height /= 1.25;
+ };
+
const colors = {
blue: {
light: "#1bf",
@@ -97,6 +129,7 @@ class Toolbar {
bright: "#ff0",
dark: "#bb0",
},
+ black: "#000",
};
const config = {
@@ -117,7 +150,7 @@ class Toolbar {
pause: {
type: "button",
position: { top: 35, left: 64 },
- width: 128,
+ width: 96,
height: 52,
fontColor: colors.white,
backgroundColor: colors.red,
@@ -126,18 +159,38 @@ class Toolbar {
},
next: {
type: "button",
- position: { top: 32, left: 192 },
+ position: { top: 32, left: 160 },
width: 64,
fontColor: colors.yellow.dark,
hover: colors.yellow.bright,
onSelect: () => onSkip(5),
},
+ expand: {
+ type: "button",
+ position: { top: 35, right: 200 },
+ width: 32,
+ height: 52,
+ fontColor: colors.black,
+ backgroundColor: colors.blue.light,
+ hover: colors.blue.lighter,
+ onSelect: onExpand,
+ },
+ compress: {
+ type: "button",
+ position: { top: 35, right: 240 },
+ width: 32,
+ height: 52,
+ fontColor: colors.black,
+ backgroundColor: colors.blue.light,
+ hover: colors.blue.lighter,
+ onSelect: onCompress,
+ },
restart: {
type: "button",
position: { top: 35, right: 10 },
- width: 200,
+ width: 150,
height: 52,
- fontColor: colors.white,
+ fontColor: colors.black,
backgroundColor: colors.blue.light,
hover: colors.blue.lighter,
onSelect: onRestart,
@@ -149,6 +202,8 @@ class Toolbar {
prev: "M 10 32 L 54 10 L 54 54 Z",
pause: "||",
next: "M 54 32 L 10 10 L 10 54 Z",
+ expand: "E",
+ compress: "C",
restart: "Restart",
};
@@ -200,12 +255,16 @@ class Toolbar {
this.updateProgressBar();
}
+ this.updateResizeHandle();
+
if (hasGlassLayer) {
this.updateOrientation(
this.layer.transform.position,
this.layer.transform.orientation
);
}
+
+ this.fluidResize();
}
/**
@@ -225,6 +284,91 @@ class Toolbar {
this.toolbarGroup.quaternion.needsUpdate = true;
}
+ updateResizeHandle() {
+ this.resizeHandle.scale.set(
+ this.layer.width,
+ RESIZE_HANDLE_THICKNESS,
+ 1
+ );
+ }
+
+ /**
+ * Store the resizeHandle's position at the moment of engaging fluid resizing.
+ * Currently uses the resizeHandle itself, but intend to use a transparent clone
+ * of the resizeHandle to handle the engage/disengage, while keeping the actual
+ * visible resizeHandle "fixed" at the same position as the layer is resized.
+ */
+ engageResize(controller) {
+ const { x, y, z } = this.resizeHandle.position;
+ const pointGeometry = new THREE.PlaneGeometry(0.1, 0.1);
+
+ this.handleLeftPoint = new THREE.Points(pointGeometry);
+ this.handleLeftPoint.position.set(x - this.layer.width / 2, y, z);
+ this.handleRightPoint = new THREE.Points(pointGeometry);
+ this.handleRightPoint.position.set(x + this.layer.width / 2, y, z);
+
+ this.resizeHandleClone = this.resizeHandle.clone();
+ this.resizeHandleClone.name = "resizeHandleClone";
+
+ const engagePosition = new THREE.Vector3();
+ // world position necessary
+ this.resizeHandleClone.getWorldPosition(engagePosition);
+ controller.attach(this.resizeHandleClone);
+ this.resizeHandleClone.userData.engageResizePosition = engagePosition;
+ }
+
+ fluidResize() {
+ if (
+ !this.resizeHandleClone ||
+ !this.resizeHandleClone.userData.engageResizePosition
+ ) {
+ return;
+ }
+
+ const engagePosition = this.resizeHandleClone.userData
+ .engageResizePosition;
+ const currPosition = new THREE.Vector3();
+ this.resizeHandleClone.getWorldPosition(currPosition);
+ // absolute euclidean distance
+ const distanceEtoCurr = engagePosition.distanceTo(currPosition);
+
+ const handleLeftPosition = new THREE.Vector3();
+ const handleRightPosition = new THREE.Vector3();
+ this.handleLeftPoint.getWorldPosition(handleLeftPosition);
+ this.handleRightPoint.getWorldPosition(handleRightPosition);
+
+ // console.log("handle left", handleLeftPosition);
+ // console.log("handle right", handleRightPosition);
+ // console.log("curr position", currPosition);
+
+ const distanceLtoCurr = handleLeftPosition.distanceTo(currPosition);
+ const distanceRtoCurr = handleRightPosition.distanceTo(currPosition);
+ // console.log("dist to left", distanceLtoCurr);
+ // console.log("dist to right", distanceRtoCurr);
+ // console.log(distanceRtoCurr < distanceLtoCurr ? "expand" : "compress");
+ const sign = distanceRtoCurr < distanceLtoCurr ? 1 : -1;
+
+ const resizeFactor =
+ 1 + (sign * distanceEtoCurr * 0.01) / this.layer.width;
+
+ // hack
+ if (this.layer.width <= MIN_LAYER_WIDTH) {
+ this.layer.width *= 1.001;
+ this.layer.height *= 1.001;
+ } else if (this.layer.width >= MAX_LAYER_WIDTH) {
+ this.layer.width *= 0.999;
+ this.layer.height *= 0.999;
+ } else {
+ this.layer.width *= resizeFactor;
+ this.layer.height *= resizeFactor;
+ }
+ }
+
+ disengageResize(controller) {
+ this.resizeHandleClone.userData.engageResizePosition = null;
+ controller.remove(this.resizeHandleClone);
+ }
+
/**
* Updates progress bar as video plays
*/