Files
blitzkit/packages/website/src/components/CanvasControl.tsx
T

273 lines
8.4 KiB
TypeScript
Raw Normal View History

2025-09-01 22:56:39 -05:00
import { I_HAT, J_HAT } from "@blitzkit/core";
import { OrbitControls } from "@react-three/drei";
import { invalidate, useFrame, useThree } from "@react-three/fiber";
2025-09-30 16:34:08 -05:00
import type { QuicklimeEvent } from "quicklime";
2025-09-01 22:56:39 -05:00
import { useEffect, useRef } from "react";
import { PerspectiveCamera, Vector3 } from "three";
import { OrbitControls as OrbitControlsClass } from "three-stdlib";
2026-09-23 17:25:51 -05:00
import { api } from "../api/dynamic";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { Pose, poseEvent } from "../core/blitzkit/pose";
import { controlsEnabledEvent } from "../core/controlsEnabled";
2026-09-30 17:48:48 -05:00
import { DuelModule, getDuel, useDuel } from "../hooks/useDuel";
import { hasEquipment } from "../hooks/useEquipment";
2026-09-23 17:25:51 -05:00
import { Tankopedia } from "../stores/tankopedia";
2024-01-03 12:21:30 -06:00
2024-01-20 16:59:18 -06:00
const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15,
[Pose.FaceHug]: 5,
[Pose.Default]: -1,
};
2024-01-19 22:46:10 -06:00
2026-06-26 14:01:39 -05:00
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
2026-08-30 14:26:08 -05:00
const modelDefinitions = await api.models();
2024-10-05 16:44:07 -05:00
2024-12-14 22:32:28 -06:00
interface ControlsProps {
2025-05-04 13:08:28 -05:00
autoRotate?: boolean;
zoomable?: boolean;
enableRotate?: boolean;
2024-12-14 22:32:28 -06:00
}
2026-09-23 17:25:51 -05:00
export function CanvasControl({
2025-05-04 13:08:28 -05:00
autoRotate = true,
zoomable = true,
enableRotate = true,
}: ControlsProps) {
2024-12-12 19:06:06 -06:00
const camera = useThree((state) => state.camera as PerspectiveCamera);
2024-05-03 15:30:17 -05:00
const canvas = useThree((state) => state.gl.domElement);
2024-01-03 12:21:30 -06:00
const orbitControls = useRef<OrbitControlsClass>(null);
2026-09-30 17:48:48 -05:00
const protagonistTurret = useDuel("protagonist", DuelModule.Turret);
const antagonistTurret = useDuel("antagonist", DuelModule.Turret);
const protagonistTrack = useDuel("protagonist", DuelModule.Track);
const protagonistTank = useDuel("protagonist", DuelModule.Tank);
const antagonistTank = useDuel("antagonist", DuelModule.Tank);
const protagonistGun = useDuel("protagonist", DuelModule.Gun);
2024-01-20 16:59:18 -06:00
const protagonistModelDefinition =
modelDefinitions.models[protagonistTank.id];
2024-02-08 11:08:22 -06:00
const protagonistTrackModelDefinition =
modelDefinitions.models[protagonistTank.id].tracks[protagonistTrack.id];
const antagonistModelDefinition = modelDefinitions.models[antagonistTank.id];
2024-01-20 16:59:18 -06:00
const protagonistTurretModelDefinition =
protagonistModelDefinition.turrets[protagonistTurret.id];
2024-01-20 16:59:18 -06:00
const antagonistTurretModelDefinition =
antagonistModelDefinition.turrets[antagonistTurret.id];
2024-01-20 16:59:18 -06:00
const protagonistGunModelDefinition =
2025-08-18 12:06:14 -05:00
protagonistTurretModelDefinition.guns[protagonistGun.id];
2024-01-22 16:54:07 -06:00
const protagonistHullOrigin = new Vector3(
2026-03-31 14:11:45 -05:00
protagonistTrackModelDefinition.origin!.x,
protagonistTrackModelDefinition.origin!.y,
-protagonistTrackModelDefinition.origin!.z,
2024-01-22 16:54:07 -06:00
);
2024-01-20 16:59:18 -06:00
const protagonistTurretOrigin = new Vector3(
2026-03-31 14:11:45 -05:00
protagonistModelDefinition.turret_origin!.x,
protagonistModelDefinition.turret_origin!.y,
-protagonistModelDefinition.turret_origin!.z,
2024-01-19 22:46:10 -06:00
);
2024-01-20 16:59:18 -06:00
const protagonistGunOrigin = new Vector3(
2026-03-31 14:11:45 -05:00
protagonistTurretModelDefinition.gun_origin!.x,
protagonistTurretModelDefinition.gun_origin!.y,
-protagonistTurretModelDefinition.gun_origin!.z,
2024-01-19 22:46:10 -06:00
);
2024-01-20 16:59:18 -06:00
const antagonistGunHeight =
2026-03-31 14:11:45 -05:00
protagonistTrackModelDefinition.origin!.y +
antagonistModelDefinition.turret_origin!.y +
antagonistTurretModelDefinition.gun_origin!.y;
2025-09-01 22:56:39 -05:00
const disturbed = Tankopedia.use((state) => state.disturbed);
2025-05-04 22:11:41 -05:00
const doAutoRotate = autoRotate && !disturbed;
2024-12-01 21:47:09 -06:00
const gunHeight =
protagonistHullOrigin.y +
2024-12-01 21:47:09 -06:00
protagonistTurretOrigin.y +
protagonistGunOrigin.y;
2025-05-04 22:11:41 -05:00
2024-02-22 18:43:10 -06:00
useEffect(() => {
2025-09-30 16:34:08 -05:00
function handleControlsEnabled(event: QuicklimeEvent<boolean>) {
if (!orbitControls.current) return;
orbitControls.current.enabled = event.data;
}
controlsEnabledEvent.on(handleControlsEnabled);
2024-01-13 16:48:20 -06:00
2025-08-29 13:42:44 -05:00
function handlePoseEvent(event: Pose) {
2026-09-30 17:48:48 -05:00
const protagonist = getDuel("protagonist", DuelModule.Tank);
2025-08-29 13:42:44 -05:00
const hasImprovedVerticalStabilizer = hasEquipment(
2024-04-04 13:16:12 -05:00
122,
2026-09-30 17:48:48 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
2024-04-04 13:16:12 -05:00
);
2025-08-29 13:42:44 -05:00
const hasDownImprovedVerticalStabilizer = hasEquipment(
2025-03-11 10:13:02 -05:00
124,
2026-09-30 17:48:48 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
2025-03-11 10:13:02 -05:00
);
2024-01-19 22:46:10 -06:00
switch (event) {
case Pose.HullDown: {
2024-01-20 16:59:18 -06:00
const [pitch] = applyPitchYawLimits(
-Infinity,
0,
2026-03-31 14:11:45 -05:00
protagonistGunModelDefinition.pitch!,
2024-01-20 16:59:18 -06:00
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
2026-02-18 18:55:38 -06:00
hasDownImprovedVerticalStabilizer,
2024-01-20 16:59:18 -06:00
);
2024-01-19 22:46:10 -06:00
camera.position
.set(0, 0, 0)
2024-01-22 16:54:07 -06:00
.add(protagonistHullOrigin)
2024-01-20 16:59:18 -06:00
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
2024-01-19 22:46:10 -06:00
.add(
new Vector3(
0,
2024-01-20 16:59:18 -06:00
poseDistances[event] * Math.sin(pitch),
2026-02-18 18:55:38 -06:00
poseDistances[event] * -Math.cos(pitch),
),
2024-01-19 22:46:10 -06:00
);
2024-01-20 16:59:18 -06:00
camera.lookAt(
2024-01-22 16:54:07 -06:00
protagonistHullOrigin
.clone()
.add(protagonistTurretOrigin)
2026-02-18 18:55:38 -06:00
.add(protagonistGunOrigin),
2024-01-20 16:59:18 -06:00
);
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
break;
}
case Pose.FaceHug: {
const [pitch] = applyPitchYawLimits(
0,
0,
2026-03-31 14:11:45 -05:00
protagonistGunModelDefinition.pitch!,
2024-01-20 16:59:18 -06:00
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
2026-02-18 18:55:38 -06:00
hasDownImprovedVerticalStabilizer,
2024-01-20 16:59:18 -06:00
);
camera.position
.set(0, 0, 0)
2024-01-22 16:54:07 -06:00
.add(protagonistHullOrigin)
2024-01-20 16:59:18 -06:00
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(
new Vector3(
0,
poseDistances[event] * Math.sin(pitch),
2026-02-18 18:55:38 -06:00
poseDistances[event] * -Math.cos(pitch),
),
2024-01-20 16:59:18 -06:00
);
orbitControls.current?.target
.set(0, 0, 0)
2024-01-22 16:54:07 -06:00
.add(protagonistHullOrigin)
2024-01-20 16:59:18 -06:00
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(
new Vector3(
0,
0.5 * poseDistances[event] * Math.sin(pitch),
2026-02-18 18:55:38 -06:00
0.5 * poseDistances[event] * -Math.cos(pitch),
),
2024-01-20 16:59:18 -06:00
);
break;
}
case Pose.Default: {
2025-05-04 21:02:50 -05:00
camera.position.copy(inspectModeInitialPosition);
2024-02-22 18:36:21 -06:00
orbitControls.current?.target.set(0, 1.25, 0);
2024-01-20 16:59:18 -06:00
break;
2024-01-19 22:46:10 -06:00
}
}
2024-11-11 12:55:39 -06:00
invalidate();
2024-01-19 22:46:10 -06:00
}
poseEvent.on(handlePoseEvent);
return () => {
2025-09-30 16:34:08 -05:00
controlsEnabledEvent.off(handleControlsEnabled);
2024-01-19 22:46:10 -06:00
poseEvent.off(handlePoseEvent);
};
}, [camera, protagonistTank.id, antagonistTank.id]);
2024-01-03 12:21:30 -06:00
2024-05-03 15:30:17 -05:00
useEffect(() => {
2024-12-12 16:03:34 -06:00
function handleWheel(event: WheelEvent) {
event.preventDefault();
}
function handleScroll(event: Event) {
event.preventDefault();
}
2025-04-14 19:26:41 -05:00
function disturb() {
2025-09-01 22:56:39 -05:00
Tankopedia.mutate((draft) => {
2025-04-14 19:26:41 -05:00
draft.disturbed = true;
});
}
poseEvent.on(disturb);
2025-09-01 22:56:39 -05:00
canvas.addEventListener("pointerdown", disturb);
canvas.addEventListener("wheel", handleWheel);
document.body.addEventListener("scroll", handleScroll);
2024-12-12 16:03:34 -06:00
2024-12-12 19:06:06 -06:00
function updateCamera() {
if (!orbitControls.current) return;
orbitControls.current.rotateSpeed = 0.25;
2025-08-06 12:23:57 -05:00
camera.position.copy(inspectModeInitialPosition);
orbitControls.current.target.set(0, gunHeight * 0.6, 0);
2025-08-06 12:23:57 -05:00
orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true;
camera.fov = 25;
2024-12-12 19:06:06 -06:00
camera.updateProjectionMatrix();
}
updateCamera();
2024-05-03 15:30:17 -05:00
return () => {
2025-09-01 22:56:39 -05:00
canvas.removeEventListener("pointerdown", disturb);
2025-04-14 19:26:41 -05:00
poseEvent.off(disturb);
2025-09-01 22:56:39 -05:00
canvas.removeEventListener("wheel", handleWheel);
document.body.removeEventListener("scroll", handleScroll);
2024-05-03 15:30:17 -05:00
};
2025-08-06 12:23:57 -05:00
}, []);
2024-05-03 15:30:17 -05:00
2024-01-06 18:17:04 -06:00
return (
2026-06-26 14:01:39 -05:00
<>
{doAutoRotate && <Animator />}
<OrbitControls
enableRotate={enableRotate}
maxDistance={40}
minDistance={5}
enableZoom={zoomable}
zoomSpeed={zoomable ? undefined : 0}
ref={orbitControls}
enabled={controlsEnabledEvent.last!}
enableDamping={false}
/>
</>
2024-01-06 18:17:04 -06:00
);
2024-01-03 12:21:30 -06:00
}
2026-06-26 14:01:39 -05:00
function Animator() {
const clock = useThree((state) => state.clock);
const t0 = useRef(clock.elapsedTime);
useFrame(({ camera }) => {
const t = clock.elapsedTime - t0.current;
camera.position
.copy(inspectModeInitialPosition)
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
invalidate();
});
return null;
}