forked from EXT/VR180-Web-Player
added image support
This commit is contained in:
@@ -41,7 +41,7 @@ export function bootstrapPlayer(playerBase: string, onReady: (context: Bootstrap
|
||||
|
||||
const mediaAdapter = createMediaAdapter(playerContainer);
|
||||
if (!mediaAdapter) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a supported media element (video).`);
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain exactly one supported media element: video or img.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,15 @@ export function bootstrapPlayer(playerBase: string, onReady: (context: Bootstrap
|
||||
playerContainer.appendChild(playButton);
|
||||
playerContainer.appendChild(create2DControlPanel());
|
||||
playButton.disabled = true;
|
||||
mediaAdapter.bindLoadState({
|
||||
onError: (event) => {
|
||||
console.error(`VR_WEB_PLAYER_MEDIA: Failed to load ${mediaAdapter.kind} media.`, event);
|
||||
playButton.disabled = true;
|
||||
},
|
||||
onReady: () => {
|
||||
playButton.disabled = false;
|
||||
}
|
||||
});
|
||||
mediaAdapter.load();
|
||||
|
||||
completeXrSupportCheck(playButton, () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createPlayButton(): HTMLButtonElement {
|
||||
const playButton = document.createElement('button');
|
||||
playButton.type = 'button';
|
||||
playButton.className = 'vrwp-play-button';
|
||||
playButton.setAttribute('aria-label', 'Play video');
|
||||
playButton.setAttribute('aria-label', 'Open media');
|
||||
playButton.appendChild(createLucideIcon('circle-play'));
|
||||
|
||||
return playButton;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { setLucideIcon } from './icons.js';
|
||||
import { formatTime } from '../utils/time.js';
|
||||
import type { MediaCapabilities } from '../media/media-adapter.js';
|
||||
|
||||
type TwoDControlPanelCallbacks = {
|
||||
onForward: () => void;
|
||||
@@ -13,6 +14,7 @@ type TwoDControlPanelOptions = {
|
||||
callbacks: TwoDControlPanelCallbacks;
|
||||
fullscreenTarget: HTMLElement;
|
||||
getIsActive: () => boolean;
|
||||
mediaCapabilities: MediaCapabilities;
|
||||
playerContainer: HTMLElement;
|
||||
title: string;
|
||||
};
|
||||
@@ -32,8 +34,10 @@ export class TwoDControlPanel {
|
||||
private totalTimeDisplay: HTMLElement | null;
|
||||
private playButton: HTMLButtonElement | null;
|
||||
private muteButton: HTMLButtonElement | null;
|
||||
private navControls: HTMLElement | null;
|
||||
private progressControls: HTMLElement | null;
|
||||
|
||||
constructor({ callbacks, fullscreenTarget, getIsActive, playerContainer, title }: TwoDControlPanelOptions) {
|
||||
constructor({ callbacks, fullscreenTarget, getIsActive, mediaCapabilities, playerContainer, title }: TwoDControlPanelOptions) {
|
||||
this.callbacks = callbacks;
|
||||
this.fullscreenTarget = fullscreenTarget;
|
||||
this.getIsActive = getIsActive;
|
||||
@@ -43,10 +47,12 @@ export class TwoDControlPanel {
|
||||
const videoTitle = playerContainer.querySelector<HTMLElement>('.vrwp-video-title');
|
||||
this.currentTimeDisplay = playerContainer.querySelector('.vrwp-current-time');
|
||||
this.totalTimeDisplay = playerContainer.querySelector('.vrwp-total-time');
|
||||
this.progressControls = playerContainer.querySelector('.vrwp-progress');
|
||||
this.progressBar = playerContainer.querySelector('.vrwp-bar');
|
||||
this.playedBar = playerContainer.querySelector('.vrwp-played');
|
||||
this.playButton = playerContainer.querySelector('.vrwp-play-toggle');
|
||||
this.muteButton = playerContainer.querySelector('.vrwp-mute');
|
||||
this.navControls = playerContainer.querySelector('.vrwp-nav');
|
||||
|
||||
if (!this.controlPanel) {
|
||||
console.error('VR_WEB_PLAYER_DOM: 2D control panel was not found.');
|
||||
@@ -57,7 +63,8 @@ export class TwoDControlPanel {
|
||||
videoTitle.textContent = title;
|
||||
}
|
||||
|
||||
this.bindControls(playerContainer);
|
||||
this.applyCapabilities(mediaCapabilities);
|
||||
this.bindControls(playerContainer, mediaCapabilities);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
@@ -144,38 +151,58 @@ export class TwoDControlPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private bindControls(playerContainer: HTMLElement): void {
|
||||
private applyCapabilities(mediaCapabilities: MediaCapabilities): void {
|
||||
if (!mediaCapabilities.timeline && this.progressControls) {
|
||||
this.progressControls.hidden = true;
|
||||
}
|
||||
|
||||
if (!mediaCapabilities.playback && this.navControls) {
|
||||
this.navControls.hidden = true;
|
||||
}
|
||||
|
||||
if (!mediaCapabilities.audio && this.muteButton) {
|
||||
this.muteButton.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bindControls(playerContainer: HTMLElement, mediaCapabilities: MediaCapabilities): void {
|
||||
playerContainer.querySelector('.vrwp-fullscreen')?.addEventListener('click', () => {
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
|
||||
playerContainer.querySelector('.vrwp-back')?.addEventListener('click', () => {
|
||||
this.callbacks.onRewind();
|
||||
this.show();
|
||||
});
|
||||
if (mediaCapabilities.playback) {
|
||||
playerContainer.querySelector('.vrwp-back')?.addEventListener('click', () => {
|
||||
this.callbacks.onRewind();
|
||||
this.show();
|
||||
});
|
||||
|
||||
this.playButton?.addEventListener('click', () => {
|
||||
this.callbacks.onPlayPause();
|
||||
this.show();
|
||||
});
|
||||
this.playButton?.addEventListener('click', () => {
|
||||
this.callbacks.onPlayPause();
|
||||
this.show();
|
||||
});
|
||||
|
||||
playerContainer.querySelector('.vrwp-forward')?.addEventListener('click', () => {
|
||||
this.callbacks.onForward();
|
||||
this.show();
|
||||
});
|
||||
playerContainer.querySelector('.vrwp-forward')?.addEventListener('click', () => {
|
||||
this.callbacks.onForward();
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
this.muteButton?.addEventListener('click', () => {
|
||||
this.callbacks.onMute();
|
||||
this.show();
|
||||
});
|
||||
if (mediaCapabilities.audio) {
|
||||
this.muteButton?.addEventListener('click', () => {
|
||||
this.callbacks.onMute();
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
this.progressBar?.addEventListener('click', (event) => {
|
||||
const rect = this.progressBar?.getBoundingClientRect();
|
||||
if (rect && rect.width > 0) {
|
||||
this.callbacks.onSeek((event.clientX - rect.left) / rect.width);
|
||||
}
|
||||
this.show();
|
||||
});
|
||||
if (mediaCapabilities.timeline) {
|
||||
this.progressBar?.addEventListener('click', (event) => {
|
||||
const rect = this.progressBar?.getBoundingClientRect();
|
||||
if (rect && rect.width > 0) {
|
||||
this.callbacks.onSeek((event.clientX - rect.left) / rect.width);
|
||||
}
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private clearHideTimeout(): void {
|
||||
|
||||
@@ -5,11 +5,19 @@ export type MediaCapabilities = {
|
||||
timeline: boolean;
|
||||
};
|
||||
|
||||
type MediaLoadCallbacks = {
|
||||
onError: (event: Event) => void;
|
||||
onReady: () => void;
|
||||
};
|
||||
|
||||
export type MediaKind = 'image' | 'video';
|
||||
|
||||
export interface MediaAdapter<TElement extends HTMLElement = HTMLElement, TTextureSource = TElement> {
|
||||
readonly capabilities: MediaCapabilities;
|
||||
readonly element: TElement;
|
||||
readonly kind: string;
|
||||
readonly kind: MediaKind;
|
||||
readonly textureSource: TTextureSource;
|
||||
bindLoadState(callbacks: MediaLoadCallbacks): void;
|
||||
getTitle(): string;
|
||||
hideElement(): void;
|
||||
load(): void;
|
||||
@@ -17,7 +25,7 @@ export interface MediaAdapter<TElement extends HTMLElement = HTMLElement, TTextu
|
||||
showElement(): void;
|
||||
}
|
||||
|
||||
export type SupportedMediaAdapter = VideoMediaAdapter;
|
||||
export type SupportedMediaAdapter = ImageMediaAdapter | VideoMediaAdapter;
|
||||
|
||||
const VIDEO_CAPABILITIES: MediaCapabilities = {
|
||||
audio: true,
|
||||
@@ -26,9 +34,16 @@ const VIDEO_CAPABILITIES: MediaCapabilities = {
|
||||
timeline: true
|
||||
};
|
||||
|
||||
const IMAGE_CAPABILITIES: MediaCapabilities = {
|
||||
audio: false,
|
||||
dynamicTexture: false,
|
||||
playback: false,
|
||||
timeline: false
|
||||
};
|
||||
|
||||
export class VideoMediaAdapter implements MediaAdapter<HTMLVideoElement, HTMLVideoElement> {
|
||||
readonly capabilities = VIDEO_CAPABILITIES;
|
||||
readonly kind = 'video';
|
||||
readonly kind = 'video' as const;
|
||||
|
||||
constructor(readonly element: HTMLVideoElement) {}
|
||||
|
||||
@@ -42,6 +57,16 @@ export class VideoMediaAdapter implements MediaAdapter<HTMLVideoElement, HTMLVid
|
||||
'Video Title';
|
||||
}
|
||||
|
||||
bindLoadState({ onError, onReady }: MediaLoadCallbacks): void {
|
||||
if (this.element.readyState >= this.element.HAVE_METADATA) {
|
||||
queueMicrotask(onReady);
|
||||
}
|
||||
|
||||
this.element.addEventListener('loadedmetadata', onReady);
|
||||
this.element.addEventListener('canplaythrough', onReady);
|
||||
this.element.addEventListener('error', onError);
|
||||
}
|
||||
|
||||
hideElement(): void {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
@@ -59,12 +84,75 @@ export class VideoMediaAdapter implements MediaAdapter<HTMLVideoElement, HTMLVid
|
||||
}
|
||||
}
|
||||
|
||||
export class ImageMediaAdapter implements MediaAdapter<HTMLImageElement, HTMLImageElement> {
|
||||
readonly capabilities = IMAGE_CAPABILITIES;
|
||||
readonly kind = 'image' as const;
|
||||
|
||||
constructor(readonly element: HTMLImageElement) {}
|
||||
|
||||
get textureSource(): HTMLImageElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return this.element.getAttribute('title') ||
|
||||
this.element.getAttribute('alt') ||
|
||||
getFilenameTitle(this.element.currentSrc || this.element.src) ||
|
||||
'Image Title';
|
||||
}
|
||||
|
||||
bindLoadState({ onError, onReady }: MediaLoadCallbacks): void {
|
||||
if (this.element.complete && this.element.naturalWidth > 0) {
|
||||
queueMicrotask(onReady);
|
||||
}
|
||||
|
||||
this.element.addEventListener('load', onReady);
|
||||
this.element.addEventListener('error', onError);
|
||||
}
|
||||
|
||||
hideElement(): void {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
load(): void {
|
||||
// Images begin loading from markup. Kept for parity with video media.
|
||||
}
|
||||
|
||||
shouldUpdateTexture(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
showElement(): void {
|
||||
this.element.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
export function createMediaAdapter(playerContainer: HTMLElement): SupportedMediaAdapter | null {
|
||||
const videoElement = playerContainer.querySelector<HTMLVideoElement>('video');
|
||||
if (!videoElement) {
|
||||
const mediaElements = Array.from(
|
||||
playerContainer.querySelectorAll<HTMLVideoElement | HTMLImageElement>('video,img')
|
||||
);
|
||||
|
||||
if (mediaElements.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
videoElement.classList.add('vrwp-video');
|
||||
return new VideoMediaAdapter(videoElement);
|
||||
const mediaElement = mediaElements[0];
|
||||
const tagName = mediaElement.tagName.toLowerCase();
|
||||
mediaElement.classList.add('vrwp-media');
|
||||
|
||||
if (tagName === 'video') {
|
||||
mediaElement.classList.add('vrwp-video');
|
||||
return new VideoMediaAdapter(mediaElement as HTMLVideoElement);
|
||||
}
|
||||
|
||||
if (tagName === 'img') {
|
||||
mediaElement.classList.add('vrwp-image');
|
||||
return new ImageMediaAdapter(mediaElement as HTMLImageElement);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFilenameTitle(source: string): string {
|
||||
return source.split('/').pop()?.split('.')[0].replace(/-/g, ' ') || '';
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
showFallbackCanvas
|
||||
} from '../rendering/renderer-lifecycle.js';
|
||||
import { TwoDControlPanel } from '../dom/two-d-control-panel.js';
|
||||
import type { MediaCapabilities } from '../media/media-adapter.js';
|
||||
|
||||
type TwoDModeCallbacks = {
|
||||
createMediaTexture: () => any;
|
||||
@@ -21,6 +22,7 @@ type TwoDModeCallbacks = {
|
||||
type TwoDModeOptions = {
|
||||
callbacks: TwoDModeCallbacks;
|
||||
fullscreenTarget: HTMLElement;
|
||||
mediaCapabilities: MediaCapabilities;
|
||||
getActiveContentMesh: () => any;
|
||||
getCamera: () => any;
|
||||
getCameraControls: () => FallbackCameraControls | undefined;
|
||||
@@ -40,6 +42,7 @@ export class TwoDMode {
|
||||
private readonly callbacks: TwoDModeCallbacks;
|
||||
private readonly controls: TwoDControlPanel;
|
||||
private readonly fullscreenTarget: HTMLElement;
|
||||
private readonly mediaCapabilities: MediaCapabilities;
|
||||
private readonly getActiveContentMesh: () => any;
|
||||
private readonly getCamera: () => any;
|
||||
private readonly getCameraControls: () => FallbackCameraControls | undefined;
|
||||
@@ -55,6 +58,7 @@ export class TwoDMode {
|
||||
constructor(options: TwoDModeOptions) {
|
||||
this.callbacks = options.callbacks;
|
||||
this.fullscreenTarget = options.fullscreenTarget;
|
||||
this.mediaCapabilities = options.mediaCapabilities;
|
||||
this.getActiveContentMesh = options.getActiveContentMesh;
|
||||
this.getCamera = options.getCamera;
|
||||
this.getCameraControls = options.getCameraControls;
|
||||
@@ -82,6 +86,7 @@ export class TwoDMode {
|
||||
this.callbacks.seekToProgress(progress);
|
||||
}
|
||||
},
|
||||
mediaCapabilities: this.mediaCapabilities,
|
||||
fullscreenTarget: this.fullscreenTarget,
|
||||
getIsActive: () => this.active,
|
||||
playerContainer: this.playerContainer,
|
||||
@@ -120,7 +125,9 @@ export class TwoDMode {
|
||||
this.callbacks.showActiveContentMesh();
|
||||
}
|
||||
|
||||
this.callbacks.togglePlayPause();
|
||||
if (this.mediaCapabilities.playback) {
|
||||
this.callbacks.togglePlayPause();
|
||||
}
|
||||
this.addEventListeners(canvas);
|
||||
this.controls.show();
|
||||
this.positionControls();
|
||||
@@ -172,6 +179,7 @@ export class TwoDMode {
|
||||
|
||||
updateTimeline(): void {
|
||||
if (!this.active) return;
|
||||
if (!this.mediaCapabilities.timeline) return;
|
||||
|
||||
const video = this.getVideo();
|
||||
if (video) {
|
||||
@@ -181,6 +189,7 @@ export class TwoDMode {
|
||||
|
||||
updatePlaybackButton(): void {
|
||||
if (!this.active) return;
|
||||
if (!this.mediaCapabilities.playback) return;
|
||||
|
||||
const video = this.getVideo();
|
||||
if (video) {
|
||||
@@ -190,6 +199,7 @@ export class TwoDMode {
|
||||
|
||||
updateMuteButton(): void {
|
||||
if (!this.active) return;
|
||||
if (!this.mediaCapabilities.audio) return;
|
||||
|
||||
const video = this.getVideo();
|
||||
if (video) {
|
||||
@@ -199,6 +209,7 @@ export class TwoDMode {
|
||||
|
||||
handleVideoEnd(): void {
|
||||
if (!this.active) return;
|
||||
if (!this.mediaCapabilities.playback) return;
|
||||
|
||||
this.controls.showPersistent();
|
||||
this.updatePlaybackButton();
|
||||
|
||||
@@ -98,3 +98,20 @@ export function createVideoTexture(video: HTMLVideoElement) {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
return texture;
|
||||
}
|
||||
|
||||
export function createImageTexture(image: HTMLImageElement) {
|
||||
const texture = new THREE.Texture(image);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
export function createMediaTexture(source: HTMLImageElement | HTMLVideoElement) {
|
||||
if (source.tagName.toLowerCase() === 'img') {
|
||||
return createImageTexture(source as HTMLImageElement);
|
||||
}
|
||||
|
||||
return createVideoTexture(source as HTMLVideoElement);
|
||||
}
|
||||
|
||||
1
src/vr180player/types.d.ts
vendored
1
src/vr180player/types.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
declare module 'https://unpkg.com/three/build/three.module.js' {
|
||||
export const Matrix4: any;
|
||||
export const CanvasTexture: any;
|
||||
export const Texture: any;
|
||||
export const VideoTexture: any;
|
||||
export const LinearFilter: any;
|
||||
export const SRGBColorSpace: any;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
positionPlaneForPresentation as positionPlaneForPresentationCore,
|
||||
showActiveContentMesh as showActiveContentMeshCore
|
||||
} from './rendering/projection.js';
|
||||
import { createVideoTexture as createVideoTextureCore } from './rendering/three-utils.js';
|
||||
import { createMediaTexture as createMediaTextureCore } from './rendering/three-utils.js';
|
||||
import { FallbackCameraControls } from './modes/fallback-camera-controls.js';
|
||||
import { MediaController } from './media/media-controller.js';
|
||||
import {
|
||||
@@ -49,7 +49,7 @@ let frameCounter = 0;
|
||||
let isXrLoopActive = false;
|
||||
let vrControlPanel;
|
||||
let mediaController: MediaController | undefined;
|
||||
let textureManager: MediaTextureManager<HTMLVideoElement> | undefined;
|
||||
let textureManager: MediaTextureManager<HTMLImageElement | HTMLVideoElement> | undefined;
|
||||
let vrPanel: VrControlPanel | undefined;
|
||||
let twoDMode: TwoDMode | undefined;
|
||||
const vrPanelVisibility = new VrPanelVisibility();
|
||||
@@ -62,7 +62,7 @@ bootstrapPlayer(_playerBase, (context) => {
|
||||
playerContainer = context.playerContainer;
|
||||
projectionMode = context.projectionMode;
|
||||
mediaAdapter = context.mediaAdapter;
|
||||
video = mediaAdapter.element;
|
||||
video = mediaAdapter.kind === 'video' ? mediaAdapter.element : undefined;
|
||||
playBtn = context.playButton;
|
||||
init();
|
||||
});
|
||||
@@ -84,9 +84,9 @@ function positionPlaneForPresentation(isFallback2D = false) {
|
||||
positionPlaneForPresentationCore(planeMesh, camera2D, isFallback2D, PLANE_DISTANCE, PLANE_2D_DISTANCE);
|
||||
}
|
||||
|
||||
function createVideoTexture() {
|
||||
function createMediaTexture() {
|
||||
if (!textureManager) {
|
||||
throw new Error('Video texture manager is not initialized.');
|
||||
throw new Error('Media texture manager is not initialized.');
|
||||
}
|
||||
return textureManager.create();
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function restoreVideoTextureAfterContextRestored() {
|
||||
}
|
||||
|
||||
function getMediaTitle() {
|
||||
return mediaAdapter?.getTitle() || 'Video Title';
|
||||
return mediaAdapter?.getTitle() || 'Media Title';
|
||||
}
|
||||
|
||||
|
||||
@@ -136,18 +136,20 @@ function init() {
|
||||
throw new Error('Media adapter is not initialized.');
|
||||
}
|
||||
|
||||
video = mediaAdapter.element;
|
||||
video = mediaAdapter.kind === 'video' ? mediaAdapter.element : undefined;
|
||||
textureManager = new MediaTextureManager(
|
||||
mediaAdapter.textureSource,
|
||||
createVideoTextureCore,
|
||||
createMediaTextureCore,
|
||||
() => mediaAdapter?.shouldUpdateTexture() ?? false
|
||||
);
|
||||
mediaController = new MediaController({
|
||||
is2DModeActive,
|
||||
on2DPlaybackResume: show2DControlPanel,
|
||||
playButton: playBtn,
|
||||
video
|
||||
});
|
||||
mediaController = video
|
||||
? new MediaController({
|
||||
is2DModeActive,
|
||||
on2DPlaybackResume: show2DControlPanel,
|
||||
playButton: playBtn,
|
||||
video
|
||||
})
|
||||
: undefined;
|
||||
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
||||
applySbsTextureWindow(renderer, activeCamera, material);
|
||||
});
|
||||
@@ -165,7 +167,7 @@ function init() {
|
||||
});
|
||||
twoDMode = new TwoDMode({
|
||||
callbacks: {
|
||||
createMediaTexture: createVideoTexture,
|
||||
createMediaTexture,
|
||||
forward: () => mediaController?.forward(),
|
||||
positionPlaneForPresentation,
|
||||
rewind: () => mediaController?.rewind(),
|
||||
@@ -175,6 +177,7 @@ function init() {
|
||||
togglePlayPause: () => mediaController?.togglePlayPause()
|
||||
},
|
||||
fullscreenTarget: playerContainer,
|
||||
mediaCapabilities: mediaAdapter.capabilities,
|
||||
getActiveContentMesh: () => activeContentMesh,
|
||||
getCamera: () => camera2D,
|
||||
getCameraControls: () => fallbackCameraControls,
|
||||
@@ -194,7 +197,7 @@ function init() {
|
||||
}
|
||||
|
||||
try { // Phase 2: VR Control Panel UI
|
||||
vrPanel = createVrControlPanel(scene, getMediaTitle());
|
||||
vrPanel = createVrControlPanel(scene, getMediaTitle(), mediaAdapter?.capabilities);
|
||||
vrControlPanel = vrPanel.group;
|
||||
vrPanelVisibility.setPanel(vrPanel);
|
||||
uiElements.push(...vrPanel.interactables);
|
||||
@@ -386,8 +389,7 @@ async function handleEnterVRButtonClick() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the play button after click
|
||||
mediaController?.hidePlayButton();
|
||||
hideEnterButton();
|
||||
|
||||
// Check if VR is supported
|
||||
if (playBtn.dataset.xrSupported === "true") {
|
||||
@@ -448,7 +450,7 @@ async function actualSessionToggle() {
|
||||
throw new Error("VR mesh components not ready for texture.");
|
||||
}
|
||||
if (!textureManager) {
|
||||
throw new Error("Video texture manager is not initialized.");
|
||||
throw new Error("Media texture manager is not initialized.");
|
||||
}
|
||||
textureManager.assignToMaterial(sphereMaterial);
|
||||
showActiveContentMesh();
|
||||
@@ -491,6 +493,15 @@ async function actualSessionToggle() {
|
||||
}
|
||||
}
|
||||
|
||||
function hideEnterButton() {
|
||||
if (mediaController) {
|
||||
mediaController.hidePlayButton();
|
||||
return;
|
||||
}
|
||||
|
||||
playBtn?.classList.add('hidden');
|
||||
}
|
||||
|
||||
function onVRSessionEnd(event) {
|
||||
const endedSession = event.session;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||
import { drawLucideIcon } from '../dom/icons.js';
|
||||
import { createLucideButtonTexture, drawRoundedRect } from '../rendering/three-utils.js';
|
||||
import type { MediaCapabilities } from '../media/media-adapter.js';
|
||||
|
||||
type ButtonLayout = {
|
||||
centerX: number;
|
||||
@@ -12,21 +13,21 @@ type ButtonLayout = {
|
||||
|
||||
export type VrControlPanel = {
|
||||
exitButtonMesh: any;
|
||||
forwardButtonMesh: any;
|
||||
forwardButtonMesh?: any;
|
||||
group: any;
|
||||
interactables: any[];
|
||||
playPauseButtonCanvas: HTMLCanvasElement;
|
||||
playPauseButtonContext: CanvasRenderingContext2D | null;
|
||||
playPauseButtonMesh: any;
|
||||
playPauseButtonTexture: any;
|
||||
rewindButtonMesh: any;
|
||||
seekBarHitAreaMesh: any;
|
||||
seekBarProgressMesh: any;
|
||||
seekBarTrackMesh: any;
|
||||
volumeButtonCanvas: HTMLCanvasElement;
|
||||
volumeButtonContext: CanvasRenderingContext2D | null;
|
||||
volumeButtonMesh: any;
|
||||
volumeButtonTexture: any;
|
||||
playPauseButtonCanvas?: HTMLCanvasElement;
|
||||
playPauseButtonContext?: CanvasRenderingContext2D | null;
|
||||
playPauseButtonMesh?: any;
|
||||
playPauseButtonTexture?: any;
|
||||
rewindButtonMesh?: any;
|
||||
seekBarHitAreaMesh?: any;
|
||||
seekBarProgressMesh?: any;
|
||||
seekBarTrackMesh?: any;
|
||||
volumeButtonCanvas?: HTMLCanvasElement;
|
||||
volumeButtonContext?: CanvasRenderingContext2D | null;
|
||||
volumeButtonMesh?: any;
|
||||
volumeButtonTexture?: any;
|
||||
};
|
||||
|
||||
const FIGMA_PANEL_WIDTH_PX = 450;
|
||||
@@ -72,7 +73,18 @@ const PANEL_TEXTURE_HEIGHT = Math.round(PANEL_TEXTURE_WIDTH * (FIGMA_PANEL_HEIGH
|
||||
const VR_BUTTON_TEXTURE_SIZE = 128;
|
||||
const VR_BUTTON_ICON_SIZE = 82;
|
||||
|
||||
export function createVrControlPanel(scene: any, title: string): VrControlPanel {
|
||||
const DEFAULT_MEDIA_CAPABILITIES: MediaCapabilities = {
|
||||
audio: true,
|
||||
dynamicTexture: true,
|
||||
playback: true,
|
||||
timeline: true
|
||||
};
|
||||
|
||||
export function createVrControlPanel(
|
||||
scene: any,
|
||||
title: string,
|
||||
mediaCapabilities: MediaCapabilities = DEFAULT_MEDIA_CAPABILITIES
|
||||
): VrControlPanel {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(0, 0.5, -1.8);
|
||||
group.rotation.x = 0;
|
||||
@@ -83,74 +95,87 @@ export function createVrControlPanel(scene: any, title: string): VrControlPanel
|
||||
group.add(panelMesh);
|
||||
interactables.push(panelMesh);
|
||||
|
||||
const seekBarTrackMaterial = new THREE.MeshBasicMaterial({ color: 0x767676, transparent: true, opacity: 0 });
|
||||
const seekBarTrackGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT);
|
||||
const seekBarTrackMesh = new THREE.Mesh(seekBarTrackGeometry, seekBarTrackMaterial);
|
||||
seekBarTrackMesh.name = 'seekBarTrackVisual';
|
||||
seekBarTrackMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
|
||||
seekBarTrackMesh.position.z = 0.01;
|
||||
seekBarTrackMesh.renderOrder = 1;
|
||||
group.add(seekBarTrackMesh);
|
||||
let seekBarTrackMesh;
|
||||
let seekBarProgressMesh;
|
||||
let seekBarHitAreaMesh;
|
||||
if (mediaCapabilities.timeline) {
|
||||
const seekBarTrackMaterial = new THREE.MeshBasicMaterial({ color: 0x767676, transparent: true, opacity: 0 });
|
||||
const seekBarTrackGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_TRACK_HEIGHT);
|
||||
seekBarTrackMesh = new THREE.Mesh(seekBarTrackGeometry, seekBarTrackMaterial);
|
||||
seekBarTrackMesh.name = 'seekBarTrackVisual';
|
||||
seekBarTrackMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
|
||||
seekBarTrackMesh.position.z = 0.01;
|
||||
seekBarTrackMesh.renderOrder = 1;
|
||||
group.add(seekBarTrackMesh);
|
||||
|
||||
const seekBarProgressMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 });
|
||||
const seekBarProgressGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_PROGRESS_HEIGHT);
|
||||
const seekBarProgressMesh = new THREE.Mesh(seekBarProgressGeometry, seekBarProgressMaterial);
|
||||
seekBarProgressMesh.name = 'seekBarProgressVisual';
|
||||
seekBarProgressMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET + SCALE_FACTOR;
|
||||
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
|
||||
seekBarProgressMesh.position.z = 0.015;
|
||||
seekBarProgressMesh.scale.x = 0.001;
|
||||
seekBarProgressMesh.renderOrder = 2;
|
||||
group.add(seekBarProgressMesh);
|
||||
const seekBarProgressMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 });
|
||||
const seekBarProgressGeometry = new THREE.PlaneGeometry(WORLD_SEEK_BAR_WIDTH, WORLD_SEEK_BAR_PROGRESS_HEIGHT);
|
||||
seekBarProgressMesh = new THREE.Mesh(seekBarProgressGeometry, seekBarProgressMaterial);
|
||||
seekBarProgressMesh.name = 'seekBarProgressVisual';
|
||||
seekBarProgressMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET + SCALE_FACTOR;
|
||||
seekBarProgressMesh.position.x = -WORLD_SEEK_BAR_WIDTH / 2;
|
||||
seekBarProgressMesh.position.z = 0.015;
|
||||
seekBarProgressMesh.scale.x = 0.001;
|
||||
seekBarProgressMesh.renderOrder = 2;
|
||||
group.add(seekBarProgressMesh);
|
||||
|
||||
const seekBarHitAreaGeometry = new THREE.PlaneGeometry(
|
||||
WORLD_SEEK_BAR_WIDTH,
|
||||
WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER
|
||||
);
|
||||
const seekBarHitAreaMaterial = new THREE.MeshBasicMaterial({ visible: false, transparent: true, opacity: 0 });
|
||||
const seekBarHitAreaMesh = new THREE.Mesh(seekBarHitAreaGeometry, seekBarHitAreaMaterial);
|
||||
seekBarHitAreaMesh.name = 'seekBarHitArea';
|
||||
seekBarHitAreaMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
|
||||
seekBarHitAreaMesh.position.z = 0.012;
|
||||
seekBarHitAreaMesh.renderOrder = 2;
|
||||
group.add(seekBarHitAreaMesh);
|
||||
interactables.push(seekBarHitAreaMesh);
|
||||
const seekBarHitAreaGeometry = new THREE.PlaneGeometry(
|
||||
WORLD_SEEK_BAR_WIDTH,
|
||||
WORLD_SEEK_BAR_TRACK_HEIGHT * WORLD_SEEK_BAR_HIT_AREA_HEIGHT_MULTIPLIER
|
||||
);
|
||||
const seekBarHitAreaMaterial = new THREE.MeshBasicMaterial({ visible: false, transparent: true, opacity: 0 });
|
||||
seekBarHitAreaMesh = new THREE.Mesh(seekBarHitAreaGeometry, seekBarHitAreaMaterial);
|
||||
seekBarHitAreaMesh.name = 'seekBarHitArea';
|
||||
seekBarHitAreaMesh.position.y = WORLD_SEEK_BAR_Y_OFFSET;
|
||||
seekBarHitAreaMesh.position.z = 0.012;
|
||||
seekBarHitAreaMesh.renderOrder = 2;
|
||||
group.add(seekBarHitAreaMesh);
|
||||
interactables.push(seekBarHitAreaMesh);
|
||||
}
|
||||
|
||||
const playPauseButtonCanvas = document.createElement('canvas');
|
||||
playPauseButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
|
||||
playPauseButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
|
||||
const playPauseButtonContext = playPauseButtonCanvas.getContext('2d');
|
||||
const playPauseButtonTexture = new THREE.CanvasTexture(playPauseButtonCanvas);
|
||||
playPauseButtonTexture.minFilter = THREE.LinearFilter;
|
||||
const playPauseButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_PLAYPAUSE_BUTTON_X_PX,
|
||||
centerY: FIGMA_PLAYPAUSE_BUTTON_Y_PX,
|
||||
name: 'vrPlayPauseButton',
|
||||
size: FIGMA_PLAYPAUSE_BUTTON_SIZE_PX,
|
||||
texture: playPauseButtonTexture
|
||||
});
|
||||
group.add(playPauseButtonMesh);
|
||||
interactables.push(playPauseButtonMesh);
|
||||
let playPauseButtonCanvas;
|
||||
let playPauseButtonContext;
|
||||
let playPauseButtonTexture;
|
||||
let playPauseButtonMesh;
|
||||
let rewindButtonMesh;
|
||||
let forwardButtonMesh;
|
||||
if (mediaCapabilities.playback) {
|
||||
playPauseButtonCanvas = document.createElement('canvas');
|
||||
playPauseButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
|
||||
playPauseButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
|
||||
playPauseButtonContext = playPauseButtonCanvas.getContext('2d');
|
||||
playPauseButtonTexture = new THREE.CanvasTexture(playPauseButtonCanvas);
|
||||
playPauseButtonTexture.minFilter = THREE.LinearFilter;
|
||||
playPauseButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_PLAYPAUSE_BUTTON_X_PX,
|
||||
centerY: FIGMA_PLAYPAUSE_BUTTON_Y_PX,
|
||||
name: 'vrPlayPauseButton',
|
||||
size: FIGMA_PLAYPAUSE_BUTTON_SIZE_PX,
|
||||
texture: playPauseButtonTexture
|
||||
});
|
||||
group.add(playPauseButtonMesh);
|
||||
interactables.push(playPauseButtonMesh);
|
||||
|
||||
const rewindButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_REWIND_BUTTON_X_PX,
|
||||
centerY: FIGMA_REWIND_BUTTON_Y_PX,
|
||||
name: 'vrRewindButton',
|
||||
size: FIGMA_REWIND_BUTTON_SIZE_PX,
|
||||
texture: createLucideButtonTexture('rotate-ccw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
|
||||
});
|
||||
group.add(rewindButtonMesh);
|
||||
interactables.push(rewindButtonMesh);
|
||||
rewindButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_REWIND_BUTTON_X_PX,
|
||||
centerY: FIGMA_REWIND_BUTTON_Y_PX,
|
||||
name: 'vrRewindButton',
|
||||
size: FIGMA_REWIND_BUTTON_SIZE_PX,
|
||||
texture: createLucideButtonTexture('rotate-ccw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
|
||||
});
|
||||
group.add(rewindButtonMesh);
|
||||
interactables.push(rewindButtonMesh);
|
||||
|
||||
const forwardButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_FORWARD_BUTTON_X_PX,
|
||||
centerY: FIGMA_FORWARD_BUTTON_Y_PX,
|
||||
name: 'vrForwardButton',
|
||||
size: FIGMA_FORWARD_BUTTON_SIZE_PX,
|
||||
texture: createLucideButtonTexture('rotate-cw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
|
||||
});
|
||||
group.add(forwardButtonMesh);
|
||||
interactables.push(forwardButtonMesh);
|
||||
forwardButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_FORWARD_BUTTON_X_PX,
|
||||
centerY: FIGMA_FORWARD_BUTTON_Y_PX,
|
||||
name: 'vrForwardButton',
|
||||
size: FIGMA_FORWARD_BUTTON_SIZE_PX,
|
||||
texture: createLucideButtonTexture('rotate-cw', '#ffffff', VR_BUTTON_TEXTURE_SIZE, VR_BUTTON_ICON_SIZE, '15')
|
||||
});
|
||||
group.add(forwardButtonMesh);
|
||||
interactables.push(forwardButtonMesh);
|
||||
}
|
||||
|
||||
const exitButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_EXIT_BUTTON_X_PX,
|
||||
@@ -162,21 +187,27 @@ export function createVrControlPanel(scene: any, title: string): VrControlPanel
|
||||
group.add(exitButtonMesh);
|
||||
interactables.push(exitButtonMesh);
|
||||
|
||||
const volumeButtonCanvas = document.createElement('canvas');
|
||||
volumeButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
|
||||
volumeButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
|
||||
const volumeButtonContext = volumeButtonCanvas.getContext('2d');
|
||||
const volumeButtonTexture = new THREE.CanvasTexture(volumeButtonCanvas);
|
||||
volumeButtonTexture.minFilter = THREE.LinearFilter;
|
||||
const volumeButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_VOLUME_BUTTON_X_PX,
|
||||
centerY: FIGMA_VOLUME_BUTTON_Y_PX,
|
||||
name: 'vrVolumeButton',
|
||||
size: FIGMA_VOLUME_BUTTON_SIZE_PX,
|
||||
texture: volumeButtonTexture
|
||||
});
|
||||
group.add(volumeButtonMesh);
|
||||
interactables.push(volumeButtonMesh);
|
||||
let volumeButtonCanvas;
|
||||
let volumeButtonContext;
|
||||
let volumeButtonTexture;
|
||||
let volumeButtonMesh;
|
||||
if (mediaCapabilities.audio) {
|
||||
volumeButtonCanvas = document.createElement('canvas');
|
||||
volumeButtonCanvas.width = VR_BUTTON_TEXTURE_SIZE;
|
||||
volumeButtonCanvas.height = VR_BUTTON_TEXTURE_SIZE;
|
||||
volumeButtonContext = volumeButtonCanvas.getContext('2d');
|
||||
volumeButtonTexture = new THREE.CanvasTexture(volumeButtonCanvas);
|
||||
volumeButtonTexture.minFilter = THREE.LinearFilter;
|
||||
volumeButtonMesh = createButtonMesh({
|
||||
centerX: FIGMA_VOLUME_BUTTON_X_PX,
|
||||
centerY: FIGMA_VOLUME_BUTTON_Y_PX,
|
||||
name: 'vrVolumeButton',
|
||||
size: FIGMA_VOLUME_BUTTON_SIZE_PX,
|
||||
texture: volumeButtonTexture
|
||||
});
|
||||
group.add(volumeButtonMesh);
|
||||
interactables.push(volumeButtonMesh);
|
||||
}
|
||||
|
||||
group.visible = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user