forked from EXT/VR180-Web-Player
Typescript conversion
This commit is contained in:
11
src/vr180player/config.ts
Normal file
11
src/vr180player/config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const PLAYER_SELECTOR = '[data-vr-web-player]';
|
||||
|
||||
export type ProjectionMode = 'vr180' | 'plane';
|
||||
|
||||
export const DEFAULT_PROJECTION: ProjectionMode = 'vr180';
|
||||
export const VALID_PROJECTIONS = new Set<ProjectionMode>(['vr180', 'plane']);
|
||||
|
||||
export const PLANE_WIDTH = 3.2;
|
||||
export const PLANE_HEIGHT = PLANE_WIDTH * (9 / 16);
|
||||
export const PLANE_DISTANCE = 3;
|
||||
export const PLANE_2D_DISTANCE = 1.2;
|
||||
112
src/vr180player/dom.ts
Normal file
112
src/vr180player/dom.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export function injectPlayerStyles(playerBase: string): void {
|
||||
if (document.querySelector('link[data-vr-web-player-stylesheet]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = playerBase + 'vr180-player.css';
|
||||
link.dataset.vrWebPlayerStylesheet = 'true';
|
||||
|
||||
if (document.head) {
|
||||
document.head.appendChild(link);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => document.head.appendChild(link), { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlayButton(playerBase: string): HTMLButtonElement {
|
||||
const playButton = document.createElement('button');
|
||||
playButton.type = 'button';
|
||||
playButton.className = 'vrwp-play-button';
|
||||
playButton.setAttribute('aria-label', 'Play video');
|
||||
|
||||
const playImg = document.createElement('img');
|
||||
playImg.src = playerBase + 'images/play.png';
|
||||
playImg.alt = 'Play';
|
||||
|
||||
playButton.appendChild(playImg);
|
||||
|
||||
return playButton;
|
||||
}
|
||||
|
||||
export function create2DControlPanel(): HTMLDivElement {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'vrwp-panel';
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.className = 'vrwp-status';
|
||||
|
||||
const videoTitle = document.createElement('p');
|
||||
videoTitle.className = 'vrwp-video-title';
|
||||
videoTitle.textContent = 'Title';
|
||||
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'vrwp-progress';
|
||||
|
||||
const currentTime = document.createElement('p');
|
||||
currentTime.className = 'vrwp-current-time';
|
||||
currentTime.textContent = '00:00:00';
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'vrwp-bar';
|
||||
|
||||
const played = document.createElement('div');
|
||||
played.className = 'vrwp-played';
|
||||
bar.appendChild(played);
|
||||
|
||||
const totalTime = document.createElement('p');
|
||||
totalTime.className = 'vrwp-total-time';
|
||||
totalTime.textContent = '00:00:00';
|
||||
|
||||
progress.appendChild(currentTime);
|
||||
progress.appendChild(bar);
|
||||
progress.appendChild(totalTime);
|
||||
|
||||
status.appendChild(videoTitle);
|
||||
status.appendChild(progress);
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'vrwp-controls';
|
||||
|
||||
const fullscreenBtn = document.createElement('button');
|
||||
fullscreenBtn.type = 'button';
|
||||
fullscreenBtn.className = 'vrwp-fullscreen';
|
||||
fullscreenBtn.setAttribute('aria-label', 'Toggle fullscreen');
|
||||
|
||||
const nav = document.createElement('div');
|
||||
nav.className = 'vrwp-nav';
|
||||
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.type = 'button';
|
||||
backBtn.className = 'vrwp-back';
|
||||
backBtn.setAttribute('aria-label', 'Back 15 seconds');
|
||||
|
||||
const play2Btn = document.createElement('button');
|
||||
play2Btn.type = 'button';
|
||||
play2Btn.className = 'vrwp-play-toggle';
|
||||
play2Btn.setAttribute('aria-label', 'Play or pause');
|
||||
|
||||
const forwardBtn = document.createElement('button');
|
||||
forwardBtn.type = 'button';
|
||||
forwardBtn.className = 'vrwp-forward';
|
||||
forwardBtn.setAttribute('aria-label', 'Forward 15 seconds');
|
||||
|
||||
nav.appendChild(backBtn);
|
||||
nav.appendChild(play2Btn);
|
||||
nav.appendChild(forwardBtn);
|
||||
|
||||
const muteBtn = document.createElement('button');
|
||||
muteBtn.type = 'button';
|
||||
muteBtn.className = 'vrwp-mute';
|
||||
muteBtn.setAttribute('aria-label', 'Toggle mute');
|
||||
|
||||
controls.appendChild(fullscreenBtn);
|
||||
controls.appendChild(nav);
|
||||
controls.appendChild(muteBtn);
|
||||
|
||||
panel.appendChild(status);
|
||||
panel.appendChild(controls);
|
||||
|
||||
return panel;
|
||||
}
|
||||
80
src/vr180player/projection.ts
Normal file
80
src/vr180player/projection.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
export function isLeftEyeCamera(renderingRenderer: any, activeCamera: any): boolean {
|
||||
const xrCamera = renderingRenderer.xr.getCamera();
|
||||
|
||||
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
|
||||
if (activeCamera === xrCamera.cameras[0]) {
|
||||
return true;
|
||||
}
|
||||
if (activeCamera === xrCamera.cameras[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const viewMatrixX = activeCamera.matrixWorldInverse.elements[12];
|
||||
const leftCamX = xrCamera.cameras[0].matrixWorldInverse.elements[12];
|
||||
const rightCamX = xrCamera.cameras[1].matrixWorldInverse.elements[12];
|
||||
const diffToLeft = Math.abs(viewMatrixX - leftCamX);
|
||||
const diffToRight = Math.abs(viewMatrixX - rightCamX);
|
||||
|
||||
if (diffToLeft < 0.001 || diffToLeft < diffToRight) {
|
||||
return true;
|
||||
}
|
||||
if (diffToRight < 0.001) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return activeCamera.projectionMatrix.elements[8] <= 0;
|
||||
}
|
||||
|
||||
export function applySbsTextureWindow(
|
||||
renderingRenderer: any,
|
||||
activeCamera: any,
|
||||
material: any,
|
||||
is2DMode: boolean
|
||||
): void {
|
||||
if (!material.map) return;
|
||||
const isPresentingXR = renderingRenderer.xr.isPresenting;
|
||||
|
||||
if (is2DMode && !isPresentingXR) {
|
||||
material.map.offset.x = 0;
|
||||
material.map.repeat.x = 0.5;
|
||||
material.map.offset.y = 0;
|
||||
material.map.repeat.y = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
material.map.offset.x = 0;
|
||||
material.map.repeat.x = 1;
|
||||
material.map.offset.y = 0;
|
||||
material.map.repeat.y = 1;
|
||||
if (!isPresentingXR) {
|
||||
return;
|
||||
}
|
||||
|
||||
material.map.offset.x = isLeftEyeCamera(renderingRenderer, activeCamera) ? 0 : 0.5;
|
||||
material.map.repeat.x = 0.5;
|
||||
}
|
||||
|
||||
export function hideContentMeshes(vr180Mesh: any, planeMesh: any): void {
|
||||
if (vr180Mesh) vr180Mesh.visible = false;
|
||||
if (planeMesh) planeMesh.visible = false;
|
||||
}
|
||||
|
||||
export function showActiveContentMesh(vr180Mesh: any, planeMesh: any, activeContentMesh: any): void {
|
||||
hideContentMeshes(vr180Mesh, planeMesh);
|
||||
if (activeContentMesh) {
|
||||
activeContentMesh.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function positionPlaneForPresentation(
|
||||
planeMesh: any,
|
||||
camera2D: any,
|
||||
isFallback2D: boolean,
|
||||
planeDistance: number,
|
||||
plane2DDistance: number
|
||||
): void {
|
||||
if (!planeMesh) return;
|
||||
const zPosition = isFallback2D && camera2D ? camera2D.position.z - plane2DDistance : -planeDistance;
|
||||
planeMesh.position.set(0, 1.6, zPosition);
|
||||
}
|
||||
116
src/vr180player/three-utils.ts
Normal file
116
src/vr180player/three-utils.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||
|
||||
type Radius = number | {
|
||||
tl?: number;
|
||||
tr?: number;
|
||||
br?: number;
|
||||
bl?: number;
|
||||
};
|
||||
|
||||
export function drawRoundedRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: Radius = 5,
|
||||
fill: boolean | string,
|
||||
stroke: boolean | string = true
|
||||
): void {
|
||||
let corners;
|
||||
if (typeof radius === 'number') {
|
||||
corners = { tl: radius, tr: radius, br: radius, bl: radius };
|
||||
} else {
|
||||
corners = { tl: 0, tr: 0, br: 0, bl: 0, ...radius };
|
||||
}
|
||||
|
||||
if (width < 2 * corners.tl) corners.tl = width / 2;
|
||||
if (width < 2 * corners.tr) corners.tr = width / 2;
|
||||
if (width < 2 * corners.bl) corners.bl = width / 2;
|
||||
if (width < 2 * corners.br) corners.br = width / 2;
|
||||
|
||||
if (height < 2 * corners.tl) corners.tl = height / 2;
|
||||
if (height < 2 * corners.tr) corners.tr = height / 2;
|
||||
if (height < 2 * corners.bl) corners.bl = height / 2;
|
||||
if (height < 2 * corners.br) corners.br = height / 2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + corners.tl, y);
|
||||
ctx.lineTo(x + width - corners.tr, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + corners.tr);
|
||||
ctx.lineTo(x + width, y + height - corners.br);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - corners.br, y + height);
|
||||
ctx.lineTo(x + corners.bl, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - corners.bl);
|
||||
ctx.lineTo(x, y + corners.tl);
|
||||
ctx.quadraticCurveTo(x, y, x + corners.tl, y);
|
||||
ctx.closePath();
|
||||
|
||||
if (fill) {
|
||||
if (typeof fill === 'string') ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (stroke) {
|
||||
if (typeof stroke === 'string') ctx.strokeStyle = stroke;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function createButtonTexture(
|
||||
textOrPathData: string,
|
||||
textColor = 'white',
|
||||
backgroundColor = 'rgba(0,0,0,0)',
|
||||
textureSize = 128,
|
||||
fontSize = 48,
|
||||
isSvgPath = false,
|
||||
svgViewBoxSize = 44
|
||||
) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = textureSize;
|
||||
canvas.height = textureSize;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Unable to create 2D canvas context for button texture.');
|
||||
}
|
||||
|
||||
if (backgroundColor !== 'rgba(0,0,0,0)') {
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(0, 0, textureSize, textureSize);
|
||||
}
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
|
||||
if (isSvgPath) {
|
||||
const path = new Path2D(textOrPathData);
|
||||
const iconTargetSize = textureSize;
|
||||
const scale = iconTargetSize / svgViewBoxSize;
|
||||
const offsetX = (textureSize - (svgViewBoxSize * scale)) / 2;
|
||||
const offsetY = (textureSize - (svgViewBoxSize * scale)) / 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(offsetX, offsetY);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.fill(path);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.font = `bold ${fontSize}px Helvetica, Arial, sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(textOrPathData, textureSize / 2, textureSize / 2);
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
export function createVideoTexture(video: HTMLVideoElement) {
|
||||
const texture = new THREE.VideoTexture(video);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
return texture;
|
||||
}
|
||||
27
src/vr180player/types.d.ts
vendored
Normal file
27
src/vr180player/types.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
declare module 'https://unpkg.com/three/build/three.module.js' {
|
||||
export const Matrix4: any;
|
||||
export const CanvasTexture: any;
|
||||
export const VideoTexture: any;
|
||||
export const LinearFilter: any;
|
||||
export const SRGBColorSpace: any;
|
||||
export const Scene: any;
|
||||
export const PerspectiveCamera: any;
|
||||
export const WebGLRenderer: any;
|
||||
export const SphereGeometry: any;
|
||||
export const MeshBasicMaterial: any;
|
||||
export const Mesh: any;
|
||||
export const PlaneGeometry: any;
|
||||
export const Group: any;
|
||||
export const Raycaster: any;
|
||||
export const LineBasicMaterial: any;
|
||||
export const BufferGeometry: any;
|
||||
export const Vector3: any;
|
||||
export const Line: any;
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
xr?: {
|
||||
isSessionSupported(mode: string): Promise<boolean>;
|
||||
requestSession(mode: string, options?: unknown): Promise<any>;
|
||||
};
|
||||
}
|
||||
1639
src/vr180player/vr180-player.ts
Normal file
1639
src/vr180player/vr180-player.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user