mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: mixer buttons
This commit is contained in:
1 parent
d107813c37
commit
a646cc1e57
6 files changed
+213
-158
No files matched your search
@@ -867,7 +867,9 @@
|
|||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
"description": "Mix tank modules",
|
"description": "Mix tank modules",
|
||||||
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
},
|
},
|
||||||
|
|
||||||
"guess": {
|
"guess": {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { OrbitControls } from "@react-three/drei";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Suspense } from "react";
|
import { forwardRef, Suspense } from "react";
|
||||||
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
import { SceneProps } from "../Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
|
import { SceneProps } from "../Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
|
||||||
import { MixerFallback } from "./components/MixerFallback";
|
import { MixerFallback } from "./components/MixerFallback";
|
||||||
import { Model } from "./components/Model";
|
import { Model } from "./components/Model";
|
||||||
|
|
||||||
export function MixerScene() {
|
export const MixerScene = forwardRef<HTMLCanvasElement>((_, ref) => {
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
|
ref={ref}
|
||||||
shadows="soft"
|
shadows="soft"
|
||||||
style={{ background: "black" }}
|
style={{ background: "black" }}
|
||||||
camera={{
|
camera={{
|
||||||
@@ -31,4 +32,4 @@ export function MixerScene() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { CameraIcon, CopyIcon, DownloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
type ButtonProps,
|
||||||
|
} from "@radix-ui/themes";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
import { useLocale } from "../hooks/useLocale";
|
||||||
|
import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
|
||||||
|
|
||||||
|
interface Props extends ButtonProps {
|
||||||
|
canvas: RefObject<HTMLCanvasElement>;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenshotButton({ canvas, name, children, ...props }: Props) {
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
{children ? (
|
||||||
|
<Button {...props}>
|
||||||
|
<CameraIcon />
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<IconButton {...props}>
|
||||||
|
<CameraIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Popover.Close>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
screenshotReadyEvent.dispatch(true);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!canvas.current) return;
|
||||||
|
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
|
||||||
|
anchor.setAttribute("download", `${name}.png`);
|
||||||
|
anchor.setAttribute(
|
||||||
|
"href",
|
||||||
|
canvas.current.toDataURL("image/png")
|
||||||
|
);
|
||||||
|
anchor.click();
|
||||||
|
|
||||||
|
screenshotReadyEvent.dispatch(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
{strings.website.tools.tankopedia.sandbox.screenshot.download}
|
||||||
|
</Button>
|
||||||
|
</Popover.Close>
|
||||||
|
<Popover.Close>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
screenshotReadyEvent.dispatch(true);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!canvas.current) return;
|
||||||
|
|
||||||
|
canvas.current.toBlob((blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
|
||||||
|
navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ "image/png": blob }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
screenshotReadyEvent.dispatch(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
{strings.website.tools.tankopedia.sandbox.screenshot.copy}
|
||||||
|
</Button>
|
||||||
|
</Popover.Close>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
+7
-75
@@ -6,9 +6,6 @@ import {
|
|||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import {
|
import {
|
||||||
CameraIcon,
|
|
||||||
CopyIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
EnterFullScreenIcon,
|
EnterFullScreenIcon,
|
||||||
ExitFullScreenIcon,
|
ExitFullScreenIcon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
@@ -39,9 +36,9 @@ import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/co
|
|||||||
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||||
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
|
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
|
||||||
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
|
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
|
||||||
|
import { ScreenshotButton } from "../../../../ScreenshotButton";
|
||||||
import { SmallTankIcon } from "../../../../SmallTankIcon";
|
import { SmallTankIcon } from "../../../../SmallTankIcon";
|
||||||
import { TankSearch } from "../../../../TankSearch";
|
import { TankSearch } from "../../../../TankSearch";
|
||||||
import { screenshotReadyEvent } from "../TankSandbox/components/SceneProps";
|
|
||||||
import { CustomShellButton } from "./components/CustomShellButton";
|
import { CustomShellButton } from "./components/CustomShellButton";
|
||||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||||
import { QuickInputs } from "./components/QuickInputs";
|
import { QuickInputs } from "./components/QuickInputs";
|
||||||
@@ -440,77 +437,12 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<Popover.Root>
|
<ScreenshotButton
|
||||||
<Popover.Trigger>
|
color="gray"
|
||||||
<IconButton color="gray" variant="ghost">
|
variant="ghost"
|
||||||
<CameraIcon />
|
canvas={canvas}
|
||||||
</IconButton>
|
name={protagonistTank.slug}
|
||||||
</Popover.Trigger>
|
/>
|
||||||
|
|
||||||
<Popover.Content>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
<Popover.Close>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
screenshotReadyEvent.dispatch(true);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!canvas.current) return;
|
|
||||||
|
|
||||||
const anchor = document.createElement("a");
|
|
||||||
|
|
||||||
anchor.setAttribute(
|
|
||||||
"download",
|
|
||||||
`${protagonistTank.name}.png`
|
|
||||||
);
|
|
||||||
anchor.setAttribute(
|
|
||||||
"href",
|
|
||||||
canvas.current.toDataURL("image/png")
|
|
||||||
);
|
|
||||||
anchor.click();
|
|
||||||
|
|
||||||
screenshotReadyEvent.dispatch(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.screenshot
|
|
||||||
.download
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</Popover.Close>
|
|
||||||
<Popover.Close>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
screenshotReadyEvent.dispatch(true);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!canvas.current) return;
|
|
||||||
|
|
||||||
canvas.current.toBlob((blob) => {
|
|
||||||
if (!blob) return;
|
|
||||||
|
|
||||||
navigator.clipboard.write([
|
|
||||||
new ClipboardItem({ "image/png": blob }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
screenshotReadyEvent.dispatch(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CopyIcon />
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.screenshot
|
|
||||||
.copy
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</Popover.Close>
|
|
||||||
</Flex>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
|
|
||||||
{fullScreenAvailable && (
|
{fullScreenAvailable && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { awaitableModelDefinitions } from "../awaitables/modelDefinitions";
|
||||||
|
import { awaitableTankDefinitions } from "../awaitables/tankDefinitions";
|
||||||
|
|
||||||
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turret = tank.turrets.at(-1)!;
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|
||||||
|
return tank.tier >= 10 && !turretModel.yaw;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function curateMixer() {
|
||||||
|
const hull = tanks[Math.floor(Math.random() * tanks.length)];
|
||||||
|
const turretTank = tanks[Math.floor(Math.random() * tanks.length)];
|
||||||
|
const turret =
|
||||||
|
turretTank.turrets[Math.floor(Math.random() * turretTank.turrets.length)];
|
||||||
|
const gunTank = tanks[Math.floor(Math.random() * tanks.length)];
|
||||||
|
const gunTurret =
|
||||||
|
gunTank.turrets[Math.floor(Math.random() * gunTank.turrets.length)];
|
||||||
|
const gun = gunTurret.guns[Math.floor(Math.random() * gunTurret.guns.length)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
hull: hull,
|
||||||
|
turret: { tank: turretTank, turret: turret },
|
||||||
|
gun: {
|
||||||
|
tank: gunTank,
|
||||||
|
turret: gunTurret,
|
||||||
|
gun: gun,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { asset } from "@blitzkit/core";
|
import { asset } from "@blitzkit/core";
|
||||||
import { CaretRightIcon } from "@radix-ui/react-icons";
|
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { MixerScene } from "../../../components/MixerScene";
|
import { MixerScene } from "../../../components/MixerScene";
|
||||||
import { PageWrapper } from "../../../components/PageWrapper";
|
import { PageWrapper } from "../../../components/PageWrapper";
|
||||||
|
import { ScreenshotButton } from "../../../components/ScreenshotButton";
|
||||||
import {
|
import {
|
||||||
TankSearch,
|
TankSearch,
|
||||||
type TankSearchProps,
|
type TankSearchProps,
|
||||||
} from "../../../components/TankSearch";
|
} from "../../../components/TankSearch";
|
||||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
import { curateMixer } from "../../../core/blitzkit/curateMixer";
|
||||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
useLocale,
|
useLocale,
|
||||||
@@ -26,48 +26,13 @@ import {
|
|||||||
import { Mixer } from "../../../stores/mixer";
|
import { Mixer } from "../../../stores/mixer";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||||
|
|
||||||
const tankDefinitions = await awaitableTankDefinitions;
|
|
||||||
const modelDefinitions = await awaitableModelDefinitions;
|
|
||||||
const tanks = Object.values(tankDefinitions.tanks).filter((tank) => {
|
|
||||||
const tankModel = modelDefinitions.models[tank.id];
|
|
||||||
const turret = tank.turrets.at(-1)!;
|
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
|
||||||
|
|
||||||
return tank.tier >= 10 && !turretModel.yaw;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Page({
|
export function Page({
|
||||||
locale,
|
locale,
|
||||||
skeleton,
|
skeleton,
|
||||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||||
const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
const initial = useRef(curateMixer());
|
||||||
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
|
||||||
const turret = useRef(
|
|
||||||
turretTank.current.turrets[
|
|
||||||
Math.floor(Math.random() * turretTank.current.turrets.length)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
|
||||||
const gunTurret = useRef(
|
|
||||||
gunTank.current.turrets[
|
|
||||||
Math.floor(Math.random() * gunTank.current.turrets.length)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
const gun = useRef(
|
|
||||||
gunTurret.current.guns[
|
|
||||||
Math.floor(Math.random() * gunTurret.current.guns.length)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
Mixer.useInitialization({
|
Mixer.useInitialization(initial.current);
|
||||||
hull: hull.current,
|
|
||||||
turret: { tank: turretTank.current, turret: turret.current },
|
|
||||||
gun: {
|
|
||||||
tank: gunTank.current,
|
|
||||||
turret: gunTurret.current,
|
|
||||||
gun: gun.current,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="gray" p="0" maxWidth="unset">
|
<PageWrapper color="gray" p="0" maxWidth="unset">
|
||||||
@@ -141,60 +106,90 @@ function ModuleSeparator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const hullId = Mixer.use((state) => state.hull.id);
|
const hull = Mixer.use((state) => state.hull);
|
||||||
const turretId = Mixer.use((state) => state.turret.tank.id);
|
const turretId = Mixer.use((state) => state.turret.tank.id);
|
||||||
const gunId = Mixer.use((state) => state.gun.tank.id);
|
const gunId = Mixer.use((state) => state.gun.tank.id);
|
||||||
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexGrow="1" position="relative">
|
<Box flexGrow="1" position="relative">
|
||||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||||
{!skeleton && <MixerScene />}
|
{!skeleton && <MixerScene ref={canvas} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="2"
|
|
||||||
align="center"
|
align="center"
|
||||||
|
direction="column"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="4"
|
bottom="4"
|
||||||
left="50%"
|
left="50%"
|
||||||
style={{ transform: "translateX(-50%)" }}
|
style={{ transform: "translateX(-50%)" }}
|
||||||
|
gap="4"
|
||||||
>
|
>
|
||||||
<ModuleButton
|
<Flex gap="2">
|
||||||
skeleton={skeleton}
|
<ScreenshotButton
|
||||||
tank={hullId}
|
name={hull.slug}
|
||||||
onSelect={(tank) => {
|
canvas={canvas}
|
||||||
Mixer.mutate((draft) => {
|
highContrast
|
||||||
draft.hull = tank;
|
variant="surface"
|
||||||
});
|
>
|
||||||
}}
|
{strings.website.tools.mixer.screenshot}
|
||||||
/>
|
</ScreenshotButton>
|
||||||
<ModuleSeparator />
|
<Button
|
||||||
<ModuleButton
|
highContrast
|
||||||
skeleton={skeleton}
|
variant="surface"
|
||||||
tank={turretId}
|
onClick={() => {
|
||||||
onSelect={(tank) => {
|
const mixed = curateMixer();
|
||||||
Mixer.mutate((draft) => {
|
|
||||||
draft.turret = {
|
Mixer.mutate((draft) => {
|
||||||
tank,
|
Object.assign(draft, mixed);
|
||||||
turret: tank.turrets.at(-1)!,
|
});
|
||||||
};
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<UpdateIcon />
|
||||||
/>
|
{strings.website.tools.mixer.randomize}
|
||||||
<ModuleSeparator />
|
</Button>
|
||||||
<ModuleButton
|
</Flex>
|
||||||
skeleton={skeleton}
|
|
||||||
tank={gunId}
|
<Flex gap="2" align="center">
|
||||||
onSelect={(tank) => {
|
<ModuleButton
|
||||||
Mixer.mutate((draft) => {
|
skeleton={skeleton}
|
||||||
draft.gun = {
|
tank={hull.id}
|
||||||
tank,
|
onSelect={(tank) => {
|
||||||
turret: tank.turrets.at(-1)!,
|
Mixer.mutate((draft) => {
|
||||||
gun: tank.turrets.at(-1)!.guns.at(-1)!,
|
draft.hull = tank;
|
||||||
};
|
});
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<ModuleSeparator />
|
||||||
|
<ModuleButton
|
||||||
|
skeleton={skeleton}
|
||||||
|
tank={turretId}
|
||||||
|
onSelect={(tank) => {
|
||||||
|
Mixer.mutate((draft) => {
|
||||||
|
draft.turret = {
|
||||||
|
tank,
|
||||||
|
turret: tank.turrets.at(-1)!,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModuleSeparator />
|
||||||
|
<ModuleButton
|
||||||
|
skeleton={skeleton}
|
||||||
|
tank={gunId}
|
||||||
|
onSelect={(tank) => {
|
||||||
|
Mixer.mutate((draft) => {
|
||||||
|
draft.gun = {
|
||||||
|
tank,
|
||||||
|
turret: tank.turrets.at(-1)!,
|
||||||
|
gun: tank.turrets.at(-1)!.guns.at(-1)!,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user