在实际场景中,实现全景看图功能通常会结合使用React和Three.js等3D库。以下是一个简化版的基于React + TypeScript创建一个基础全景查看器组件的例子:
1.基础版
tsx
// 首先确保安装了必要的依赖:
// npm install three @types/three react-three-fiber
import React, { Suspense } from 'react';
import { Canvas, useLoader } from 'react-three-fiber';
import { TextureLoader } from 'three';
interface PanoramaProps {
src: string; // 全景图片资源路径
}
const Panorama: React.FC<PanoramaProps> = ({ src }) => {
const texture = useLoader(TextureLoader, src);
return (
<Canvas>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<sphereGeometry args={[500, 60, 40]} />
<meshBasicMaterial map={texture} />
</mesh>
</Canvas>
);
};
export default function App() {
return (
<div style={{ height: '100vh', width: '100%' }}>
<Suspense fallback={<div>Loading...</div>}>
<Panorama src="/path/to/your/panorama.jpg" />
</Suspense>
</div>
);
}
上述代码创建了一个基本的全景查看器组件Panorama
,它使用Three.js中的纹理加载器加载全景图片,并将其映射到一个球体上以模拟360度全景视图效果。同时,添加了环境光和点光源以提供光照。
为了实现交互式的全景查看,你可能还需要处理鼠标或触摸事件来改变视角。这可以通过监听Canvas组件上的onPointerMove事件并调整相机方向来实现,但此处并未包含具体的交互逻辑部分。
注意:在实际应用中,为了更好的全景浏览体验,可能需要更复杂的全景图像投影方式(如equirectangular投影)以及平滑的视角变换动画等。
2.优化版
为了实现全景图像的平滑浏览和交互控制,我们可以在上述组件中添加更多的功能。以下是一个包含了基本视角拖拽控制的增强版全景查看器组件:
tsx
import React, { useState, useRef } from 'react';
import { Canvas, useLoader, useFrame } from 'react-three-fiber';
import { TextureLoader, PerspectiveCamera } from 'three';
interface PanoramaProps {
src: string;
}
const Panorama: React.FC<PanoramaProps> = ({ src }) => {
const texture = useLoader(TextureLoader, src);
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
// 获取到canvas元素上的鼠标事件
const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
if (isDragging) {
setMouseX(event.clientX - canvasRef.current!.offsetLeft);
setMouseY(event.clientY - canvasRef.current!.offsetTop);
}
};
const handleMouseDown = () => setIsDragging(true);
const handleMouseUp = () => setIsDragging(false);
// 在每一帧更新相机角度以响应鼠标移动
useFrame(() => {
if (isDragging) {
const camera = canvasRef.current!.getBoundingClientRect();
const x = ((mouseX / camera.width) * 2 - 1) * Math.PI;
const y = ((mouseY / camera.height) * 2 - 1) * Math.PI;
// 根据鼠标的相对位置调整相机方向
// 注意:这里假设已经有一个初始设置好的camera(如在Canvas组件内设置)
const cameraObject = canvasRef.current!.parentElement!.getObjectByName('camera') as PerspectiveCamera;
cameraObject.position.set(0, 0, 500);
cameraObject.lookAt(0, 0, 0);
cameraObject.rotation.x = x;
cameraObject.rotation.y = y;
}
});
return (
<Canvas
ref={canvasRef}
onPointerMove={handleMouseMove}
onPointerDown={handleMouseDown}
onPointerUp={handleMouseUp}
camera={{ position: [0, 0, 500], fov: 75 }}
>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<sphereGeometry args={[500, 60, 40]} />
<meshBasicMaterial map={texture} />
</mesh>
{/* 添加一个隐藏的物体作为相机的目标点 */}
<mesh name="cameraTarget" position={[0, 0, 0]} visible={false} />
</Canvas>
);
};
export default function App() {
return (
<div style={{ height: '100vh', width: '100%' }}>
<Panorama src="/path/to/your/panorama.jpg" />
</div>
);
}
在这个版本中,我们添加了对鼠标拖拽事件的监听,并在每一帧更新时根据鼠标的位置旋转相机,从而实现了全景视图的拖动交互。同时,在Canvas中指定了一个名为cameraTarget
的隐藏对象作为相机的注视目标。
请注意,实际应用中可能需要针对移动端触摸事件进行优化,并且在某些情况下可能还需要更复杂的摄像机控制逻辑来处理3D空间中的旋转和平移。此外,这里的示例使用了透视相机(PerspectiveCamera),它更适合全景效果,但需确保设置了合适的视野(fov)值。
3.完善版
为了进一步优化全景查看器,我们还可以加入鼠标滚轮缩放、双击重置视角等功能,并确保在移动设备上支持触摸操作。以下是一个更完整的示例:
tsx
import React, { useState, useRef } from 'react';
import { Canvas, useLoader, useFrame, useThree } from 'react-three-fiber';
import { TextureLoader, PerspectiveCamera, Vector2 } from 'three';
interface PanoramaProps {
src: string;
}
const Panorama: React.FC<PanoramaProps> = ({ src }) => {
const texture = useLoader(TextureLoader, src);
const [isDragging, setIsDragging] = useState(false);
const [startPosition, setStartPosition] = useState(new Vector2());
const canvasRef = useRef<HTMLCanvasElement>(null);
const { camera } = useThree();
// 鼠标/触摸事件处理
const handleMouseDown = (event: React.MouseEvent<HTMLCanvasElement>) => {
event.preventDefault();
setIsDragging(true);
setStartPosition(new Vector2(event.clientX, event.clientY));
};
const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
if (isDragging) {
const movementX = event.clientX - startPosition.x;
const movementY = event.clientY - startPosition.y;
camera.position.x += movementX * 0.01;
camera.position.y -= movementY * 0.01;
camera.lookAt(0, 0, 0);
setStartPosition(new Vector2(event.clientX, event.clientY));
}
};
const handleMouseUp = () => setIsDragging(false);
const handleMouseLeave = () => setIsDragging(false);
const handleWheel = (event: React.WheelEvent<HTMLCanvasElement>) => {
event.preventDefault();
const scaleDelta = event.deltaY > 0 ? 0.95 : 1.05;
camera.position.z *= scaleDelta;
};
const handleDoubleClick = () => {
camera.position.set(0, 0, 500);
camera.lookAt(0, 0, 0);
};
useEffect(() => {
// 添加移动端触摸事件支持
const touchStartHandler = (event: TouchEvent) => {
setIsDragging(true);
const touch = event.touches[0];
setStartPosition(new Vector2(touch.clientX, touch.clientY));
};
const touchMoveHandler = (event: TouchEvent) => {
if (isDragging) {
const touch = event.touches[0];
const movementX = touch.clientX - startPosition.x;
const movementY = touch.clientY - startPosition.y;
camera.position.x += movementX * 0.01;
camera.position.y -= movementY * 0.01;
camera.lookAt(0, 0, 0);
setStartPosition(new Vector2(touch.clientX, touch.clientY));
}
};
const touchEndHandler = () => setIsDragging(false);
canvasRef.current?.addEventListener('mousedown', handleMouseDown);
canvasRef.current?.addEventListener('mousemove', handleMouseMove);
canvasRef.current?.addEventListener('mouseup', handleMouseUp);
canvasRef.current?.addEventListener('mouseleave', handleMouseLeave);
canvasRef.current?.addEventListener('wheel', handleWheel);
canvasRef.current?.addEventListener('dblclick', handleDoubleClick);
canvasRef.current?.addEventListener('touchstart', touchStartHandler);
canvasRef.current?.addEventListener('touchmove', touchMoveHandler);
canvasRef.current?.addEventListener('touchend', touchEndHandler);
return () => {
canvasRef.current?.removeEventListener('mousedown', handleMouseDown);
canvasRef.current?.removeEventListener('mousemove', handleMouseMove);
canvasRef.current?.removeEventListener('mouseup', handleMouseUp);
canvasRef.current?.removeEventListener('mouseleave', handleMouseLeave);
canvasRef.current?.removeEventListener('wheel', handleWheel);
canvasRef.current?.removeEventListener('dblclick', handleDoubleClick);
canvasRef.current?.removeEventListener('touchstart', touchStartHandler);
canvasRef.current?.removeEventListener('touchmove', touchMoveHandler);
canvasRef.current?.removeEventListener('touchend', touchEndHandler);
};
}, [camera]);
return (
<Canvas
ref={canvasRef}
camera={{ position: [0, 0, 500], fov: 75 }}
>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<sphereGeometry args={[500, 60, 40]} />
<meshBasicMaterial map={texture} />
</mesh>
</Canvas>
);
};
export default function App() {
return (
<div style={{ height: '100vh', width: '100%' }}>
<Panorama src="/path/to/your/panorama.jpg" />
</div>
);
}
在这个版本中,我们添加了鼠标滚轮缩放功能(handleWheel)、双击重置视角功能(handleDoubleClick)以及对移动设备触屏的兼容(handleTouchStart、handleTouchMove 和 handleTouchEnd)。同时使用了useEffect
来正确地添加和移除事件监听器,以防止内存泄漏。