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 */