diff --git a/samples/ui/index.html b/samples/ui/index.html index 82a9fd8..5dc8267 100644 --- a/samples/ui/index.html +++ b/samples/ui/index.html @@ -176,7 +176,7 @@ createPlayerPanel() { // Panel 2: Interactive Media Player (Top-Center) const panel = new xb.SpatialPanel({ - width: 1.5, + width: 1.3, height: 1.25, backgroundColor: '#00000000', }); @@ -189,11 +189,11 @@ this.add(panel); const grid = panel.addGrid(); // Space for orbiter - grid.addRow({weight: 0.25}); + grid.addRow({weight: 0.0}); // player row - const playerRow = grid.addRow({weight: 0.75}); + const playerRow = grid.addRow({weight: 1.0}); const playerPanel = playerRow.addPanel({ - width: 1.0, + width: 1.2, height: 1.2, backgroundColor: '#21252baa', }); @@ -221,7 +221,9 @@ .addIconButton({text: 'skip_next', fontSize: 0.4}); controlsRow.addCol({weight: 0.2}); } - const orbiter = playerGrid.addOrbiter(); + const orbiter = playerGrid.addOrbiter({ + orbiterScale: 0.1, + }); orbiter.addExitButton(); panel.updateLayouts(); } diff --git a/src/ui/layouts/Orbiter.ts b/src/ui/layouts/Orbiter.ts index 547aeeb..42b013f 100644 --- a/src/ui/layouts/Orbiter.ts +++ b/src/ui/layouts/Orbiter.ts @@ -5,13 +5,119 @@ import {Grid, GridOptions} from './Grid.js'; * as an exit button or settings icon. It typically "orbits" or remains * attached to a corner of its parent panel, outside the main content area. */ -export type OrbiterOptions = GridOptions; + +export type OrbiterPosition = + | 'top-right' + | 'top-left' + | 'bottom-right' + | 'bottom-left' + | 'top' + | 'bottom' + | 'left' + | 'right'; + +export type OrbiterOptions = GridOptions & { + orbiterPosition?: OrbiterPosition; + orbiterScale?: number; + offset?: number; + elevation?: number; +}; export class Orbiter extends Grid { + orbiterPosition: OrbiterPosition; + orbiterScale: number; + offset: number; + elevation: number; + + // These values are based on Material Design guidelines: https://developer.android.com/design/ui/xr/guides/spatial-ui + private static readonly BASE_OFFSET = 0.02; // put the orbiter outside of the parent panel's "draggable region" by default + private static readonly BASE_ELEVATION = 0.02; // put the orbiter at 15dp above the parent panel by default + private static readonly MAX_OUTWARD = 0.05; // avoid the orbiter being too far away from the parent panel + + constructor(options: OrbiterOptions = {}) { + const { + orbiterPosition = 'top-left', + orbiterScale = 0.2, + offset = 0.0, + elevation = 0.0, + ...gridOptions + } = options; + + super(gridOptions); + + this.orbiterPosition = orbiterPosition; + this.orbiterScale = orbiterScale; + this.offset = offset; + this.elevation = elevation; + } + init() { super.init(); + this.scale.set(this.orbiterScale, this.orbiterScale, 1.0); + this._place(); + } + + private _place() { + const hx = this.rangeX * 0.5; + const hy = this.rangeY * 0.5; + + const rightEdge = +hx; + const leftEdge = -hx; + const topEdge = +hy; + const bottomEdge = -hy; + + // Clamp edge spacing so the orbiter stays within the recommended range: + // edgeDelta == -orbiterScale / 2 corresponds to the 50% overlap boundary. + const edgeDelta = Math.max( + -this.orbiterScale / 2, + Math.min(Orbiter.MAX_OUTWARD, Orbiter.BASE_OFFSET + this.offset) + ); + + // Clamp elevation so the orbiter remains in front of the parent panel and doesn’t float excessively. + const zDelta = Math.max( + 0, + Math.min(Orbiter.MAX_OUTWARD, Orbiter.BASE_ELEVATION + this.elevation) + ); + + let x = 0.0; + let y = 0.0; + + switch (this.orbiterPosition) { + case 'top': + x = 0.0; + y = topEdge + this.orbiterScale / 2 + edgeDelta; + break; + case 'bottom': + x = 0.0; + y = bottomEdge - this.orbiterScale / 2 - edgeDelta; + break; + case 'right': + x = rightEdge + this.orbiterScale / 2 + edgeDelta; + y = 0.0; + break; + case 'left': + x = leftEdge - this.orbiterScale / 2 - edgeDelta; + y = 0.0; + break; + case 'top-right': + x = rightEdge - this.orbiterScale / 2; + y = topEdge + this.orbiterScale / 2 + edgeDelta; + break; + case 'top-left': + x = leftEdge + this.orbiterScale / 2; + y = topEdge + this.orbiterScale / 2 + edgeDelta; + break; + case 'bottom-right': + x = rightEdge - this.orbiterScale / 2; + y = bottomEdge - this.orbiterScale / 2 - edgeDelta; + break; + case 'bottom-left': + x = leftEdge + this.orbiterScale / 2; + y = bottomEdge - this.orbiterScale / 2 - edgeDelta; + break; + } - this.position.set(-0.45 * this.rangeX, 0.7 * this.rangeY, this.position.z); - this.scale.set(0.2, 0.2, 1.0); + const z = this.position.z + zDelta; + this.position.set(x, y, z); } }