mirror of
https://github.com/phit/video2webxr.git
synced 2026-10-18 07:01:10 +02:00
feat: only show the passthrough button where it works
Without passthrough the projection switcher takes its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
859de21258
commit
ca98a5ed16
2 files changed
+45
-31
No files matched your search
@@ -59,7 +59,7 @@ A controls panel floats below the video: play/pause, the seek bar, skip 10 secon
|
||||
| Resize the flat screen | Drag its bottom right corner, or push the thumbstick left or right while holding it |
|
||||
| Play or pause | Click the flat screen |
|
||||
| Skip 10 seconds back or forward | Double click the left or right third of the flat screen, like on YouTube's mobile app; each further click skips another 10 seconds |
|
||||
| See the room around the screen | Press the passthrough button (the headset icon). It works where the browser supports AR with passthrough, e.g. Chrome Canary on the Steam Frame ([install guide](https://github.com/phit/chromium-webxr-steam-frame/tree/google#install-on-the-frame)), and is greyed out elsewhere |
|
||||
| See the room around the screen | Press the passthrough button (the headset icon). It works where the browser supports AR with passthrough, e.g. Chrome Canary on the Steam Frame ([install guide](https://github.com/phit/chromium-webxr-steam-frame/tree/google#install-on-the-frame)), and only shows there |
|
||||
| Put everything back in front of you | Recenter on the panel. On 360° and 180° videos it also turns the front of the video to where you look |
|
||||
|
||||
### Videos to try
|
||||
|
||||
+44
-30
@@ -34,7 +34,7 @@ const AXIS_STICK_Y = 3;
|
||||
const ROW_TOP = 24;
|
||||
const ROW_BOTTOM = 140;
|
||||
const ROW_HEIGHT = 92;
|
||||
const AREAS = {
|
||||
const AREAS_WITH_PASSTHROUGH = {
|
||||
seek: { x: 150, y: ROW_TOP + 28, w: 980, h: 36 },
|
||||
back: { x: 16, y: ROW_BOTTOM, w: 88, h: ROW_HEIGHT },
|
||||
play: { x: 112, y: ROW_BOTTOM, w: 88, h: ROW_HEIGHT },
|
||||
@@ -47,6 +47,12 @@ const AREAS = {
|
||||
recenter: { x: 1080, y: ROW_BOTTOM, w: 88, h: ROW_HEIGHT },
|
||||
exit: { x: 1176, y: ROW_BOTTOM, w: 88, h: ROW_HEIGHT },
|
||||
};
|
||||
// Without passthrough (no AR session) the projection switcher takes the button's place
|
||||
const { passthrough: _, ...withoutPassthrough } = AREAS_WITH_PASSTHROUGH;
|
||||
const AREAS_WITHOUT_PASSTHROUGH = {
|
||||
...withoutPassthrough,
|
||||
nextProjection: { x: 1004, y: ROW_BOTTOM, w: 56, h: ROW_HEIGHT },
|
||||
};
|
||||
const SLIDERS = new Set(['seek', 'volume']);
|
||||
|
||||
// Steam Deck / Steam Frame colours
|
||||
@@ -60,7 +66,6 @@ const THEME = {
|
||||
separator: '#3a3f4a',
|
||||
text: '#dcdedf',
|
||||
label: '#b8bcbf',
|
||||
disabled: '#5c616b',
|
||||
font: '"Motiva Sans", "Segoe UI", system-ui, sans-serif',
|
||||
};
|
||||
|
||||
@@ -333,9 +338,14 @@ export class VRControls {
|
||||
return { x: hit.uv.x * CANVAS_W, y: (1 - hit.uv.y) * CANVAS_H };
|
||||
}
|
||||
|
||||
// The panel's layout; the passthrough button only shows in sessions that can do it
|
||||
get areas() {
|
||||
return this.player.passthroughAvailable ? AREAS_WITH_PASSTHROUGH : AREAS_WITHOUT_PASSTHROUGH;
|
||||
}
|
||||
|
||||
areaAt(point) {
|
||||
if (!point) return null;
|
||||
for (const [name, area] of Object.entries(AREAS)) {
|
||||
for (const [name, area] of Object.entries(this.areas)) {
|
||||
// sliders get a taller hit area than they're drawn
|
||||
const pad = SLIDERS.has(name) ? 24 : 0;
|
||||
if (
|
||||
@@ -434,7 +444,7 @@ export class VRControls {
|
||||
}
|
||||
|
||||
slide(area, point) {
|
||||
const { x, w } = AREAS[area];
|
||||
const { x, w } = this.areas[area];
|
||||
const fraction = Math.min(1, Math.max(0, (point.x - x) / w));
|
||||
if (area === 'seek') {
|
||||
if (Number.isFinite(this.video.duration)) this.video.currentTime = fraction * this.video.duration;
|
||||
@@ -553,6 +563,7 @@ export class VRControls {
|
||||
this.drawnState = state;
|
||||
|
||||
const c = this.context;
|
||||
const areas = this.areas;
|
||||
const roundRect = (x, y, w, h, r, color) => {
|
||||
c.fillStyle = color;
|
||||
c.beginPath();
|
||||
@@ -579,14 +590,14 @@ export class VRControls {
|
||||
const line = w * 0.5;
|
||||
roundRect(x + (w - line) / 2, y + h - 10, line, 5, 2.5, THEME.accent);
|
||||
};
|
||||
// filled: a grey button; active: a switched-on toggle, drawn blue; disabled: dimmed
|
||||
const iconButton = (name, drawIcon, { filled = false, active = false, disabled = false } = {}) => {
|
||||
const { x, y, w, h } = AREAS[name];
|
||||
if (hovered.has(name) && !disabled) focus(x, y, w, h);
|
||||
// filled: a grey button; active: a switched-on toggle, drawn blue
|
||||
const iconButton = (name, drawIcon, { filled = false, active = false } = {}) => {
|
||||
const { x, y, w, h } = areas[name];
|
||||
if (hovered.has(name)) focus(x, y, w, h);
|
||||
else if (filled) roundRect(x, y, w, h, 4, THEME.button);
|
||||
c.save();
|
||||
c.translate(x + w / 2, y + h / 2);
|
||||
const color = disabled ? THEME.disabled : active ? THEME.accent : THEME.text;
|
||||
const color = active ? THEME.accent : THEME.text;
|
||||
c.fillStyle = color;
|
||||
c.strokeStyle = color;
|
||||
c.lineWidth = 4;
|
||||
@@ -596,7 +607,7 @@ export class VRControls {
|
||||
c.restore();
|
||||
};
|
||||
const slider = (name, fraction, buffered = 0) => {
|
||||
const { x, y, w, h } = AREAS[name];
|
||||
const { x, y, w, h } = areas[name];
|
||||
const mid = y + h / 2;
|
||||
roundRect(x, mid - 4, w, 8, 4, THEME.track);
|
||||
if (buffered) roundRect(x, mid - 4, w * buffered, 8, 4, THEME.buffered);
|
||||
@@ -655,7 +666,7 @@ export class VRControls {
|
||||
// Quick Settings slider row
|
||||
const duration = video.duration;
|
||||
const seekable = Number.isFinite(duration) && duration > 0;
|
||||
const seekMid = AREAS.seek.y + AREAS.seek.h / 2;
|
||||
const seekMid = areas.seek.y + areas.seek.h / 2;
|
||||
if (hovered.has('seek')) roundRect(16, ROW_TOP - 8, CANVAS_W - 32, 96, 4, THEME.focus);
|
||||
text(formatTime(video.currentTime), 80, seekMid, { color: THEME.label });
|
||||
text(seekable ? formatTime(duration) : 'LIVE', 1200, seekMid, {
|
||||
@@ -686,7 +697,7 @@ export class VRControls {
|
||||
iconButton('forward', () => skipIcon(1));
|
||||
iconButton('mute', () => speaker(video.muted || video.volume === 0));
|
||||
if (hovered.has('volume')) {
|
||||
const { x, w } = AREAS.volume;
|
||||
const { x, w } = areas.volume;
|
||||
roundRect(x - 20, ROW_BOTTOM, w + 40, ROW_HEIGHT, 4, THEME.focus);
|
||||
}
|
||||
slider('volume', video.muted ? 0 : video.volume);
|
||||
@@ -694,27 +705,30 @@ export class VRControls {
|
||||
iconButton('previousProjection', () => text('‹', 0, -3, { size: 52, weight: 400 }));
|
||||
iconButton('nextProjection', () => text('›', 0, -3, { size: 52, weight: 400 }));
|
||||
const label = PROJECTION_LABELS.find(([id]) => id === this.player.projection)?.[1] ?? '';
|
||||
const labelX = (AREAS.previousProjection.x + AREAS.previousProjection.w + AREAS.nextProjection.x) / 2;
|
||||
const labelX = (areas.previousProjection.x + areas.previousProjection.w + areas.nextProjection.x) / 2;
|
||||
text('PROJECTION', labelX, ROW_BOTTOM + 26, { size: 18, color: THEME.label, weight: 600, spacing: 2 });
|
||||
text(label, labelX, ROW_BOTTOM + 60, { size: 24 });
|
||||
|
||||
// passthrough: a headset with the room's horizon showing through its visor
|
||||
iconButton(
|
||||
'passthrough',
|
||||
() => {
|
||||
c.beginPath();
|
||||
c.roundRect(-26, -15, 52, 30, 10);
|
||||
c.stroke();
|
||||
c.beginPath();
|
||||
c.moveTo(-14, 5);
|
||||
c.lineTo(-5, -4);
|
||||
c.lineTo(3, 3);
|
||||
c.lineTo(8, -2);
|
||||
c.lineTo(14, 5);
|
||||
c.stroke();
|
||||
},
|
||||
{ filled: true, active: this.player.passthrough, disabled: !this.player.passthroughAvailable },
|
||||
);
|
||||
// passthrough, only in sessions that can show the room: a headset with the room's
|
||||
// horizon showing through its visor
|
||||
if (areas.passthrough) {
|
||||
iconButton(
|
||||
'passthrough',
|
||||
() => {
|
||||
c.beginPath();
|
||||
c.roundRect(-26, -15, 52, 30, 10);
|
||||
c.stroke();
|
||||
c.beginPath();
|
||||
c.moveTo(-14, 5);
|
||||
c.lineTo(-5, -4);
|
||||
c.lineTo(3, 3);
|
||||
c.lineTo(8, -2);
|
||||
c.lineTo(14, 5);
|
||||
c.stroke();
|
||||
},
|
||||
{ filled: true, active: this.player.passthrough },
|
||||
);
|
||||
}
|
||||
// recenter: a target
|
||||
iconButton(
|
||||
'recenter',
|
||||
|
||||
Reference in new issue
Block a user