-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c941ced
commit 31b363d
Showing
8 changed files
with
330 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
import * as SPLAT from "gsplat"; | ||
|
||
class Controls { | ||
minAngle: number = -90; | ||
maxAngle: number = 90; | ||
minZoom: number = 0.1; | ||
maxZoom: number = 30; | ||
orbitSpeed: number = 2; | ||
panSpeed: number = 1.25; | ||
zoomSpeed: number = 2; | ||
dampening: number = 0.5; | ||
setCameraTarget: (newTarget: SPLAT.Vector3) => void = () => {}; | ||
update: () => void; | ||
dispose: () => void; | ||
|
||
constructor( | ||
scene: SPLAT.Scene, | ||
domElement: HTMLElement, | ||
alpha: number = 0.5, | ||
beta: number = 0.5, | ||
radius: number = 5, | ||
inputTarget: SPLAT.Vector3 = new SPLAT.Vector3(), | ||
) { | ||
const camera = scene.findObjectOfType(SPLAT.Camera) as SPLAT.Camera; | ||
if (!camera) { | ||
throw new Error("No camera found in scene"); | ||
} | ||
|
||
let target = inputTarget.clone(); | ||
|
||
let desiredTarget = target.clone(); | ||
let desiredAlpha = alpha; | ||
let desiredBeta = beta; | ||
let desiredRadius = radius; | ||
|
||
let dragging = false; | ||
let panning = false; | ||
let lastDist = 0; | ||
let lastX = 0; | ||
let lastY = 0; | ||
|
||
let isUpdatingCamera = false; | ||
|
||
const onCameraChange = () => { | ||
if (isUpdatingCamera) return; | ||
|
||
const eulerRotation = camera.rotation.toEuler(); | ||
desiredAlpha = -eulerRotation.y; | ||
desiredBeta = -eulerRotation.x; | ||
|
||
const x = camera.position.x - desiredRadius * Math.sin(desiredAlpha) * Math.cos(desiredBeta); | ||
const y = camera.position.y + desiredRadius * Math.sin(desiredBeta); | ||
const z = camera.position.z + desiredRadius * Math.cos(desiredAlpha) * Math.cos(desiredBeta); | ||
|
||
desiredTarget = new SPLAT.Vector3(x, y, z); | ||
}; | ||
|
||
camera.addEventListener("change", onCameraChange); | ||
|
||
this.setCameraTarget = (newTarget: SPLAT.Vector3) => { | ||
const dx = newTarget.x - camera.position.x; | ||
const dy = newTarget.y - camera.position.y; | ||
const dz = newTarget.z - camera.position.z; | ||
desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz); | ||
desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)); | ||
desiredAlpha = -Math.atan2(dx, dz); | ||
desiredTarget = new SPLAT.Vector3(newTarget.x, newTarget.y, newTarget.z); | ||
}; | ||
|
||
const computeZoomNorm = () => { | ||
return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom); | ||
}; | ||
|
||
const onMouseDown = (e: MouseEvent) => { | ||
preventDefault(e); | ||
|
||
if (e.button === 1) { | ||
dragging = true; | ||
panning = e.shiftKey; | ||
lastX = e.clientX; | ||
lastY = e.clientY; | ||
} | ||
}; | ||
|
||
const onMouseUp = (e: MouseEvent) => { | ||
preventDefault(e); | ||
|
||
if (e.button === 1) { | ||
dragging = false; | ||
panning = false; | ||
} | ||
}; | ||
|
||
const onMouseMove = (e: MouseEvent) => { | ||
preventDefault(e); | ||
|
||
if (!dragging || !camera) return; | ||
|
||
const dx = e.clientX - lastX; | ||
const dy = e.clientY - lastY; | ||
|
||
if (panning) { | ||
const zoomNorm = computeZoomNorm(); | ||
const panX = -dx * this.panSpeed * 0.01 * zoomNorm; | ||
const panY = -dy * this.panSpeed * 0.01 * zoomNorm; | ||
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer; | ||
const right = new SPLAT.Vector3(R[0], R[3], R[6]); | ||
const up = new SPLAT.Vector3(R[1], R[4], R[7]); | ||
desiredTarget = desiredTarget.add(right.multiply(panX)); | ||
desiredTarget = desiredTarget.add(up.multiply(panY)); | ||
} else { | ||
desiredAlpha -= dx * this.orbitSpeed * 0.003; | ||
desiredBeta += dy * this.orbitSpeed * 0.003; | ||
desiredBeta = Math.min( | ||
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180), | ||
(this.maxAngle * Math.PI) / 180, | ||
); | ||
} | ||
|
||
lastX = e.clientX; | ||
lastY = e.clientY; | ||
}; | ||
|
||
const onWheel = (e: WheelEvent) => { | ||
preventDefault(e); | ||
|
||
const zoomNorm = computeZoomNorm(); | ||
desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm; | ||
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom); | ||
}; | ||
|
||
const onTouchStart = (e: TouchEvent) => { | ||
preventDefault(e); | ||
|
||
if (e.touches.length === 1) { | ||
dragging = true; | ||
panning = false; | ||
lastX = e.touches[0].clientX; | ||
lastY = e.touches[0].clientY; | ||
lastDist = 0; | ||
} else if (e.touches.length === 2) { | ||
dragging = true; | ||
panning = true; | ||
lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2; | ||
lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2; | ||
const distX = e.touches[0].clientX - e.touches[1].clientX; | ||
const distY = e.touches[0].clientY - e.touches[1].clientY; | ||
lastDist = Math.sqrt(distX * distX + distY * distY); | ||
} | ||
}; | ||
|
||
const onTouchEnd = (e: TouchEvent) => { | ||
preventDefault(e); | ||
|
||
dragging = false; | ||
panning = false; | ||
}; | ||
|
||
const onTouchMove = (e: TouchEvent) => { | ||
preventDefault(e); | ||
|
||
if (!dragging || !camera) return; | ||
|
||
if (panning) { | ||
const zoomNorm = computeZoomNorm(); | ||
|
||
const distX = e.touches[0].clientX - e.touches[1].clientX; | ||
const distY = e.touches[0].clientY - e.touches[1].clientY; | ||
const dist = Math.sqrt(distX * distX + distY * distY); | ||
const delta = lastDist - dist; | ||
desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm; | ||
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom); | ||
lastDist = dist; | ||
|
||
const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2; | ||
const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2; | ||
const dx = touchX - lastX; | ||
const dy = touchY - lastY; | ||
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer; | ||
const right = new SPLAT.Vector3(R[0], R[3], R[6]); | ||
const up = new SPLAT.Vector3(R[1], R[4], R[7]); | ||
desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm)); | ||
desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm)); | ||
lastX = touchX; | ||
lastY = touchY; | ||
} else { | ||
const dx = e.touches[0].clientX - lastX; | ||
const dy = e.touches[0].clientY - lastY; | ||
|
||
desiredAlpha -= dx * this.orbitSpeed * 0.003; | ||
desiredBeta += dy * this.orbitSpeed * 0.003; | ||
desiredBeta = Math.min( | ||
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180), | ||
(this.maxAngle * Math.PI) / 180, | ||
); | ||
|
||
lastX = e.touches[0].clientX; | ||
lastY = e.touches[0].clientY; | ||
} | ||
}; | ||
|
||
const lerp = (a: number, b: number, t: number) => { | ||
return (1 - t) * a + t * b; | ||
}; | ||
|
||
this.update = () => { | ||
isUpdatingCamera = true; | ||
|
||
alpha = lerp(alpha, desiredAlpha, this.dampening); | ||
beta = lerp(beta, desiredBeta, this.dampening); | ||
radius = lerp(radius, desiredRadius, this.dampening); | ||
target = target.lerp(desiredTarget, this.dampening); | ||
|
||
const x = target.x + radius * Math.sin(alpha) * Math.cos(beta); | ||
const y = target.y - radius * Math.sin(beta); | ||
const z = target.z - radius * Math.cos(alpha) * Math.cos(beta); | ||
camera.position = new SPLAT.Vector3(x, y, z); | ||
|
||
const direction = target.subtract(camera.position).normalize(); | ||
const rx = Math.asin(-direction.y); | ||
const ry = Math.atan2(direction.x, direction.z); | ||
camera.rotation = SPLAT.Quaternion.FromEuler(new SPLAT.Vector3(rx, ry, 0)); | ||
|
||
isUpdatingCamera = false; | ||
}; | ||
|
||
const preventDefault = (e: Event) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}; | ||
|
||
this.dispose = () => { | ||
domElement.removeEventListener("dragenter", preventDefault); | ||
domElement.removeEventListener("dragover", preventDefault); | ||
domElement.removeEventListener("dragleave", preventDefault); | ||
domElement.removeEventListener("contextmenu", preventDefault); | ||
|
||
domElement.removeEventListener("mousedown", onMouseDown); | ||
domElement.removeEventListener("mousemove", onMouseMove); | ||
domElement.removeEventListener("wheel", onWheel); | ||
|
||
domElement.removeEventListener("touchstart", onTouchStart); | ||
domElement.removeEventListener("touchend", onTouchEnd); | ||
domElement.removeEventListener("touchmove", onTouchMove); | ||
}; | ||
|
||
domElement.addEventListener("dragenter", preventDefault); | ||
domElement.addEventListener("dragover", preventDefault); | ||
domElement.addEventListener("dragleave", preventDefault); | ||
domElement.addEventListener("contextmenu", preventDefault); | ||
|
||
domElement.addEventListener("mousedown", onMouseDown); | ||
domElement.addEventListener("mouseup", onMouseUp); | ||
domElement.addEventListener("mousemove", onMouseMove); | ||
domElement.addEventListener("wheel", onWheel); | ||
|
||
domElement.addEventListener("touchstart", onTouchStart); | ||
domElement.addEventListener("touchend", onTouchEnd); | ||
domElement.addEventListener("touchmove", onTouchMove); | ||
|
||
this.update(); | ||
} | ||
} | ||
|
||
export { Controls }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.