undistrubed animation

This commit is contained in:
tresabhi committed 2026-06-14 13:09:32 -05:00
1 parent 4248e19f68
commit d424679257
4 files changed
+91 -28

No files matched your search

@@ -1,12 +1,17 @@
import { I_HAT, J_HAT } from "@blitzkit/core";
import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from "react";
import { invalidate, useFrame, useThree } from "@react-three/fiber";
import { useCallback, useEffect, useRef, type RefObject } from "react";
import { Quaternion, Vector2, Vector3 } from "three";
import {
cameraSwingHorizontalAmplitude,
cameraSwingHorizontalFrequency,
cameraSwingVerticalAmplitude,
cameraSwingVerticalFrequency,
initialCameraPhi,
initialCameraR,
initialCameraTheta,
} from "../config/camera";
import { Tankopedia } from "../stores/tankopedia";
const clientPosition = new Vector2();
const workingVector3 = new Vector3();
@@ -20,10 +25,38 @@ export function TankopediaControls() {
const canvas = useThree((state) => state.gl.domElement);
const camera = useThree((state) => state.camera);
const r = useRef(initialCameraR);
const phi = useRef(initialCameraPhi);
const theta = useRef(initialCameraTheta);
const disturbed = Tankopedia.use((state) => state.disturbed);
const updateCameraPosition = useCallback(() => {
workingVector3
.set(
Math.sin(phi.current) * Math.sin(theta.current),
Math.cos(phi.current),
Math.sin(phi.current) * Math.cos(theta.current),
)
.multiplyScalar(r.current);
camera.position.copy(cameraCenter).add(workingVector3);
workingQuaternion.setFromAxisAngle(I_HAT, phi.current - Math.PI / 2);
camera.quaternion
.setFromAxisAngle(J_HAT, theta.current)
.multiply(workingQuaternion);
invalidate();
}, []);
useEffect(() => {
setCameraPosition(initialCameraR, initialCameraPhi, initialCameraTheta);
updateCameraPosition();
function handlePointerDown(event: PointerEvent) {
Tankopedia.mutate((draft) => {
draft.disturbed = true;
});
clientPosition.set(event.clientX, event.clientY);
window.addEventListener("pointermove", handlePointerMove);
@@ -35,21 +68,22 @@ export function TankopediaControls() {
workingVector3.copy(camera.position).sub(cameraCenter);
const r = workingVector3.length();
r.current = workingVector3.length();
const dx = event.movementX / window.innerWidth;
const dy = event.movementY / window.innerHeight;
const dTheta = -dx * Math.PI;
const dPhi = -dy * Math.PI;
const theta = Math.atan2(workingVector3.x, workingVector3.z) + dTheta;
const phi0 = Math.acos(workingVector3.y / r);
let phi = phi0 + dPhi;
const maxPhi = Math.PI / 2 + Math.asin(cameraCenter.y / r) - PHI_EPSILON;
theta.current = Math.atan2(workingVector3.x, workingVector3.z) + dTheta;
const phi0 = Math.acos(workingVector3.y / r.current);
phi.current = phi0 + dPhi;
const maxPhi =
Math.PI / 2 + Math.asin(cameraCenter.y / r.current) - PHI_EPSILON;
if (phi < 0 || phi > maxPhi) phi = phi0;
if (phi.current < 0 || phi.current > maxPhi) phi.current = phi0;
setCameraPosition(r, phi, theta);
updateCameraPosition();
}
function handlePointerUp() {
@@ -57,24 +91,6 @@ export function TankopediaControls() {
window.removeEventListener("pointerup", handlePointerUp);
}
function setCameraPosition(r: number, phi: number, theta: number) {
workingVector3
.set(
Math.sin(phi) * Math.sin(theta),
Math.cos(phi),
Math.sin(phi) * Math.cos(theta),
)
.multiplyScalar(r);
camera.position.copy(cameraCenter).add(workingVector3);
workingQuaternion.setFromAxisAngle(I_HAT, phi - Math.PI / 2);
camera.quaternion
.setFromAxisAngle(J_HAT, theta)
.multiply(workingQuaternion);
invalidate();
}
canvas.addEventListener("pointerdown", handlePointerDown);
return () => {
@@ -82,5 +98,39 @@ export function TankopediaControls() {
};
}, []);
return (
!disturbed && (
<Swing
r={r}
phi={phi}
theta={theta}
updateCameraPosition={updateCameraPosition}
/>
)
);
}
interface SwingProps {
r: RefObject<number>;
phi: RefObject<number>;
theta: RefObject<number>;
updateCameraPosition: () => void;
}
export function Swing({ r, phi, theta, updateCameraPosition }: SwingProps) {
useFrame(({ clock }) => {
const t = 2 * Math.PI * clock.getElapsedTime();
phi.current =
initialCameraPhi +
cameraSwingVerticalAmplitude * Math.cos(cameraSwingVerticalFrequency * t);
theta.current =
initialCameraTheta +
cameraSwingHorizontalAmplitude *
Math.sin(cameraSwingHorizontalFrequency * t);
updateCameraPosition();
});
return null;
}
+5
View File
@@ -5,3 +5,8 @@ export const fov = 20;
export const initialCameraR = 15;
export const initialCameraPhi = degToRad(90 - 10);
export const initialCameraTheta = degToRad(90 + 15);
export const cameraSwingVerticalAmplitude = degToRad(5);
export const cameraSwingVerticalFrequency = Math.E ** -6;
export const cameraSwingHorizontalAmplitude = degToRad(30);
export const cameraSwingHorizontalFrequency = Math.E ** -5;
@@ -5,6 +5,8 @@ import { type TankState } from "../tankopedia/tankState";
import { characteristicsOrder } from "../tankopedia/characteristicsOrder";
interface Tankopedia {
disturbed: boolean;
compare: TankopediaCompare;
groups: Record<string, boolean>;
@@ -24,6 +26,8 @@ for (const { name } of characteristicsOrder) {
}
export const Tankopedia = new Varuna<Tankopedia, Tank>((tank) => ({
disturbed: false,
compare: TankopediaCompare.TierAndClass,
groups,
@@ -43,6 +43,10 @@ export function createTankState({ tank, id }: Tank) {
speed: 0,
terrainHardness: TerrainHardness.Hard,
isHullRotating: false,
isTurretRotating: false,
isGunRotating: false,
status: status as Record<VehicleStatusKey, boolean>,
} satisfies TankState;
}