(null);
15 |
16 | useEffect(() => {
17 | // set up three js scene
18 | const scene = new THREE.Scene();
19 | const camera = new THREE.PerspectiveCamera(
20 | 75,
21 | window.innerWidth / window.innerHeight,
22 | 0.1,
23 | 1000
24 | );
25 | cameraRef.current = camera;
26 |
27 | const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.current! });
28 | renderer.setSize(window.innerWidth, window.innerHeight);
29 |
30 | {
31 | const visibleHeight = 2 * Math.tan((camera.fov * Math.PI) / 360) * 5;
32 | const zoomPixel = visibleHeight / window.innerHeight;
33 |
34 | const canvas = document.createElement("canvas");
35 | canvas.width = 2048;
36 | canvas.height = 2048;
37 | const ctx = canvas.getContext("2d")!;
38 | ctx.fillStyle = "white";
39 | ctx.fillRect(0, 0, canvas.width, canvas.height);
40 |
41 | const canvasTexture = new THREE.CanvasTexture(canvas);
42 |
43 | const img = new Image();
44 | img.onload = () => {
45 | ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
46 | canvasTexture.needsUpdate = true;
47 | };
48 | img.src = "/tekken.jpg";
49 |
50 | const geometry = new THREE.PlaneGeometry(
51 | (canvas.width * zoomPixel) / 4,
52 | (canvas.height * zoomPixel) / 4
53 | );
54 | const material = new THREE.MeshBasicMaterial({ map: canvasTexture });
55 | const mesh = new THREE.Mesh(geometry, material);
56 | scene.add(mesh);
57 | }
58 |
59 | camera.position.z = 5;
60 |
61 | // render
62 | function animate() {
63 | requestAnimationFrame(animate);
64 | renderer.render(scene, camera);
65 | }
66 | animate();
67 | }, []);
68 |
69 | UsePointerRay(canvasRef, cameraRef);
70 | UseWheelZoom(canvasRef, cameraRef);
71 | UseKeyboardPan(cameraRef);
72 |
73 | return (
74 |
75 |
76 |
Create Next App
77 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default Home;
87 |
--------------------------------------------------------------------------------
/components/PointerUtils.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef } from "react";
2 | import * as THREE from "three";
3 |
4 | export const UseWheelZoom = (rendererRef: any, cameraRef: any) => {
5 | const cameraDown = useRef(new THREE.Vector3());
6 |
7 | useEffect(() => {
8 | const renderer = rendererRef.current;
9 | const camera = cameraRef.current;
10 |
11 | const handleMousewheel = (e: WheelEvent) => {
12 | e.preventDefault();
13 |
14 | if (camera) {
15 | cameraDown.current.copy(camera.position);
16 |
17 | const percent =
18 | (window.innerHeight - e.deltaY * 2) / window.innerHeight;
19 | const nextZoom = Math.min(32, Math.max(1, camera.position.z / percent));
20 |
21 | const visibleHeight =
22 | 2 * Math.tan((camera.fov * Math.PI) / 360) * cameraDown.current.z;
23 | const zoomPixel = visibleHeight / window.innerHeight;
24 | const relx = e.clientX - window.innerWidth / 2;
25 | const rely = -(e.clientY - window.innerHeight / 2);
26 | const worldRelX = relx * zoomPixel;
27 | const worldRelY = rely * zoomPixel;
28 |
29 | const newVisibleHeight =
30 | 2 * Math.tan((camera.fov * Math.PI) / 360) * nextZoom;
31 | const newZoomPixel = newVisibleHeight / window.innerHeight;
32 |
33 | const newWorldX = relx * newZoomPixel;
34 | const newWorldY = rely * newZoomPixel;
35 |
36 | const diffX = newWorldX - worldRelX;
37 | const diffY = newWorldY - worldRelY;
38 |
39 | camera.position.x = cameraDown.current.x - diffX;
40 | camera.position.y = cameraDown.current.y - diffY;
41 | camera.position.z = nextZoom;
42 | }
43 | };
44 |
45 | if (renderer) {
46 | renderer.addEventListener("wheel", handleMousewheel, {
47 | passive: false,
48 | });
49 | return () => {
50 | renderer.removeEventListener("wheel", handleMousewheel);
51 | };
52 | }
53 | }, [rendererRef, cameraRef]);
54 | };
55 |
56 | export const UsePointerRay = (canvasRef: any, cameraRef: any) => {
57 | const initVectors = useMemo((): [THREE.Raycaster, THREE.Vector2] => {
58 | const raycaster = new THREE.Raycaster();
59 | const mouse = new THREE.Vector2();
60 | return [raycaster, mouse];
61 | }, []);
62 |
63 | const [raycaster, mouse] = initVectors;
64 | useEffect(() => {
65 | const canvas = canvasRef.current;
66 | const camera = cameraRef.current;
67 |
68 | const handlePointerMove = (e: MouseEvent) => {
69 | e.preventDefault();
70 |
71 | mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
72 | mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
73 |
74 | raycaster.setFromCamera(mouse, camera);
75 | };
76 |
77 | if (canvas) {
78 | canvas.addEventListener("pointermove", handlePointerMove);
79 | return () => {
80 | canvas.removeEventListener("pointermove", handlePointerMove);
81 | };
82 | }
83 | }, [raycaster, mouse, canvasRef, cameraRef, initVectors]);
84 |
85 | console.log(raycaster);
86 | return raycaster;
87 | };
88 |
89 | export const UsePointerPan = (rendererRef: any, cameraRef: any) => {
90 | const cameraDown = useRef(new THREE.Vector3());
91 | const diff = useRef(new THREE.Vector2());
92 | const pointersRef = useRef([]);
93 |
94 | useEffect(() => {
95 | const camera = cameraRef.current;
96 | const renderer = rendererRef.current;
97 | const pointers = pointersRef.current;
98 |
99 | if (!camera) return;
100 |
101 | const handlePointerDown = (e: PointerEvent) => {
102 | e.preventDefault();
103 |
104 | pointers.push({
105 | id: e.pointerId,
106 | x: e.clientX,
107 | y: e.clientY,
108 | pointerDown: [e.clientX, e.clientY],
109 | primary: e.isPrimary,
110 | });
111 | for (const pointer of pointers) {
112 | pointer.pointerDown = [pointer.x, pointer.y];
113 | }
114 | cameraDown.current.copy(camera.position);
115 |
116 | renderer.setPointerCapture(e.pointerId);
117 | };
118 |
119 | const handlePointerMove = (e: PointerEvent) => {
120 | e.preventDefault();
121 |
122 | if (pointers.length === 1) {
123 | const pointer = pointers[0];
124 | pointer.x = e.clientX;
125 | pointer.y = e.clientY;
126 | const visibleHeight =
127 | 2 * Math.tan((camera.fov * Math.PI) / 360) * cameraDown.current.z;
128 | const zoomPixel = visibleHeight / window.innerHeight;
129 | diff.current.x = (e.clientX - pointer.pointerDown[0]) * zoomPixel;
130 | diff.current.y = (e.clientY - pointer.pointerDown[1]) * zoomPixel;
131 | camera.position.x = cameraDown.current.x - diff.current.x;
132 | camera.position.y = cameraDown.current.y + diff.current.y;
133 | } else if (pointers.length === 2) {
134 | const pointer = pointers.filter((p) => p.id === e.pointerId)[0];
135 | pointer.x = e.clientX;
136 | pointer.y = e.clientY;
137 |
138 | const a = pointers[0];
139 | const b = pointers[1];
140 | const minDown = [
141 | Math.min(a.pointerDown[0], b.pointerDown[0]),
142 | Math.min(a.pointerDown[1], b.pointerDown[1]),
143 | ];
144 | const maxDown = [
145 | Math.max(a.pointerDown[0], b.pointerDown[0]),
146 | Math.max(a.pointerDown[1], b.pointerDown[1]),
147 | ];
148 | const min = [Math.min(a.x, b.x), Math.min(a.y, b.y)];
149 | const max = [Math.max(a.x, b.x), Math.max(a.y, b.y)];
150 | const combined = {
151 | down: [
152 | minDown[0] + (maxDown[0] - minDown[0]) / 2,
153 | minDown[1] + (maxDown[1] - minDown[1]) / 2,
154 | ],
155 | current: [
156 | min[0] + (max[0] - min[0]) / 2,
157 | min[1] + (max[1] - min[1]) / 2,
158 | ],
159 | };
160 |
161 | const visibleHeight =
162 | 2 * Math.tan((camera.fov * Math.PI) / 360) * cameraDown.current.z;
163 | const zoomPixel = visibleHeight / window.innerHeight;
164 |
165 | const dragged = [
166 | (combined.current[0] - combined.down[0]) * zoomPixel,
167 | (combined.current[1] - combined.down[1]) * zoomPixel,
168 | ];
169 |
170 | const adjustedDown = new THREE.Vector3();
171 | adjustedDown.x = cameraDown.current.x - dragged[0];
172 | adjustedDown.y = cameraDown.current.y + dragged[1];
173 |
174 | const downDiff = Math.sqrt(
175 | Math.pow(a.pointerDown[0] - b.pointerDown[0], 2) +
176 | Math.pow(a.pointerDown[1] - b.pointerDown[1], 2)
177 | );
178 | const currDiff = Math.sqrt(
179 | Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
180 | );
181 | const percent = (currDiff - downDiff) / downDiff + 1;
182 |
183 | const relx = combined.current[0] - window.innerWidth / 2;
184 | const rely = -(combined.current[1] - window.innerHeight / 2);
185 | const worldRelX = relx * zoomPixel;
186 | const worldRelY = rely * zoomPixel;
187 |
188 | const nextZoom = Math.min(
189 | 32,
190 | Math.max(1, cameraDown.current.z / percent)
191 | );
192 |
193 | const newVisibleHeight =
194 | 2 * Math.tan((camera.fov * Math.PI) / 360) * nextZoom;
195 | const newZoomPixel = newVisibleHeight / window.innerHeight;
196 |
197 | const newWorldX = relx * newZoomPixel;
198 | const newWorldY = rely * newZoomPixel;
199 |
200 | const diffX = newWorldX - worldRelX;
201 | const diffY = newWorldY - worldRelY;
202 |
203 | camera.position.x = adjustedDown.x - diffX;
204 | camera.position.y = adjustedDown.y - diffY;
205 | camera.position.z = nextZoom;
206 | }
207 | };
208 |
209 | const handlePointerUp = (e: PointerEvent) => {
210 | e.preventDefault();
211 |
212 | pointers.splice(
213 | pointers.findIndex((p) => p.id === e.pointerId),
214 | 1
215 | );
216 | for (const pointer of pointers) {
217 | pointer.pointerDown = [pointer.x, pointer.y];
218 | }
219 |
220 | cameraDown.current.copy(camera.position);
221 |
222 | renderer.releasePointerCapture(e.pointerId);
223 | };
224 |
225 | if (renderer) {
226 | renderer.addEventListener("pointerdown", handlePointerDown);
227 | renderer.addEventListener("pointermove", handlePointerMove);
228 | renderer.addEventListener("pointerup", handlePointerUp);
229 | return () => {
230 | renderer.removeEventListener("pointerdown", handlePointerDown);
231 | renderer.removeEventListener("pointermove", handlePointerMove);
232 | renderer.removeEventListener("pointerup", handlePointerUp);
233 | };
234 | }
235 | }, [rendererRef, cameraRef]);
236 | };
237 |
--------------------------------------------------------------------------------