1
0

added image support

This commit is contained in:
Aiden
2026-06-10 12:48:36 +10:00
parent 24a166046e
commit 030a8b724b
13 changed files with 477 additions and 171 deletions

View File

@@ -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, () => {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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, ' ') || '';
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;