mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
camera modal ui
This commit is contained in:
1 parent
d3d5e29fae
commit
be5d60d98f
3 files changed
+262
-70
No files matched your search
@@ -0,0 +1,184 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import React, { useState, useRef, useEffect, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "../Button";
|
||||
import { faMinus, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface SliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
step: number;
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
suffix?: string;
|
||||
innerLabel?: string;
|
||||
showDecrement?: boolean;
|
||||
showIncrement?: boolean;
|
||||
}
|
||||
|
||||
export const SliderV2 = ({
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
onChange,
|
||||
step,
|
||||
className,
|
||||
showTooltip,
|
||||
tooltipContent,
|
||||
suffix,
|
||||
innerLabel,
|
||||
showDecrement,
|
||||
showIncrement,
|
||||
}: SliderProps) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [showHandleTooltip, setShowHandleTooltip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const setClampedValue = (value: number) => {
|
||||
const roundedValue = Math.round(value / step) * step;
|
||||
const clampedValue = Math.min(Math.max(roundedValue, min), max);
|
||||
setLocalValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
const updatePosition = (x: number) => {
|
||||
if (!sliderRef.current) return;
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const fraction = (x - rect.left) / rect.width;
|
||||
const newValue = fraction * (max - min) + min;
|
||||
setClampedValue(newValue);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
setShowHandleTooltip(true);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
setShowHandleTooltip(false);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
setClampedValue(localValue - step);
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
setClampedValue(localValue + step);
|
||||
};
|
||||
|
||||
const percentage = ((localValue - min) / (max - min)) * 100;
|
||||
const displayValue =
|
||||
localValue.toFixed(step.toString().split(".")[1]?.length || 0) +
|
||||
(suffix || "");
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
{showDecrement && (
|
||||
<Button
|
||||
icon={faMinus}
|
||||
className="focus-visible:outline-primary my-auto mr-1 size-6 rounded-full bg-transparent text-white/80 hover:bg-white/10 active:bg-brand-primary/30"
|
||||
onClick={handleDecrement}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"glass border-ui-border group relative h-7 w-full cursor-pointer overflow-hidden rounded-lg border",
|
||||
isDragging && "!bg-ui-controls/90",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute h-full bg-brand-primary/30 transition-colors duration-300 group-hover:bg-brand-primary/50",
|
||||
isDragging && "!bg-brand-primary/50",
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
>
|
||||
{innerLabel && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute top-1/2 ml-2.5 -translate-y-1/2 text-nowrap text-sm font-medium text-white/60 transition-colors duration-300 group-hover:text-white",
|
||||
isDragging && "!text-white",
|
||||
)}
|
||||
>
|
||||
{innerLabel}
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute right-0 top-1/2 mr-1 h-4 w-0.5 -translate-y-1/2 rounded-full",
|
||||
isDragging ? "bg-white" : "bg-white/50",
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showIncrement && (
|
||||
<Button
|
||||
icon={faPlus}
|
||||
className="focus-visible:outline-primary my-auto ml-1 size-6 rounded-full bg-transparent text-white/80 hover:bg-white/10 active:bg-brand-primary/30"
|
||||
onClick={handleIncrement}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTooltip && (
|
||||
<Transition
|
||||
as={"div"}
|
||||
show={showHandleTooltip}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute -top-8 z-10 rounded-md bg-ui-panel px-2 py-1 text-xs text-black shadow-lg"
|
||||
style={{ left: `${percentage}%`, transform: "translateX(-50%)" }}
|
||||
>
|
||||
{tooltipContent ? tooltipContent : displayValue}
|
||||
</div>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SliderV2";
|
||||
@@ -2,7 +2,8 @@ import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
|
||||
import { faPlus, faTrashAlt, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
import { Input, Label, Tooltip } from "~/components";
|
||||
import { Button, Input, Label, Tooltip } from "~/components";
|
||||
import { SliderV2 } from "~/components/reusable/SliderV2/SliderV2";
|
||||
|
||||
interface ExtendedPopoverItem extends PopoverItem {
|
||||
id: string;
|
||||
@@ -73,77 +74,83 @@ export const CameraSettingsModal = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 p-3 ps-0 pt-2">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
||||
<h2 className="text-[18px] font-semibold">
|
||||
{selectedCamera?.label || "Camera"}
|
||||
</h2>
|
||||
<Tooltip
|
||||
content={
|
||||
selectedCamera?.id === "main"
|
||||
? "Cannot delete the main camera"
|
||||
: "Delete camera"
|
||||
}
|
||||
position="top"
|
||||
delay={200}
|
||||
>
|
||||
<button
|
||||
className={`h-6 w-6 rounded-lg transition-colors ${
|
||||
selectedCamera?.id === "main"
|
||||
? "cursor-not-allowed text-white/30"
|
||||
: "text-white/60 hover:text-white/100"
|
||||
}`}
|
||||
onClick={() =>
|
||||
selectedCamera &&
|
||||
selectedCamera.id !== "main" &&
|
||||
onDeleteCamera(selectedCamera.id)
|
||||
}
|
||||
disabled={selectedCamera?.id === "main"}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="text-lg" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="camera-name" className="opacity-70">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="camera-name"
|
||||
type="text"
|
||||
value={selectedCamera?.label || ""}
|
||||
onChange={(e) =>
|
||||
selectedCamera &&
|
||||
onCameraNameChange(selectedCamera.id, e.target.value)
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="focal-length" className="opacity-70">
|
||||
Focal Length
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center gap-4">
|
||||
<input
|
||||
id="focal-length"
|
||||
type="range"
|
||||
min="10"
|
||||
max="200"
|
||||
value={selectedCamera?.focalLength || 35}
|
||||
onChange={(e) =>
|
||||
selectedCamera &&
|
||||
onCameraFocalLengthChange(
|
||||
selectedCamera.id,
|
||||
Number(e.target.value),
|
||||
)
|
||||
<div className="flex h-full flex-col">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
||||
<h2 className="text-[18px] font-semibold">
|
||||
{selectedCamera?.label || "Camera"}
|
||||
</h2>
|
||||
<Tooltip
|
||||
content={
|
||||
selectedCamera?.id === "main"
|
||||
? "Cannot delete the main camera"
|
||||
: "Delete camera"
|
||||
}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-white/20"
|
||||
/>
|
||||
<span className="min-w-[60px] text-sm">
|
||||
{selectedCamera?.focalLength || 35}mm
|
||||
</span>
|
||||
position="top"
|
||||
delay={200}
|
||||
>
|
||||
<button
|
||||
className={`h-6 w-6 rounded-lg transition-colors ${
|
||||
selectedCamera?.id === "main"
|
||||
? "cursor-not-allowed text-white/30"
|
||||
: "text-white/60 hover:text-white/100"
|
||||
}`}
|
||||
onClick={() =>
|
||||
selectedCamera &&
|
||||
selectedCamera.id !== "main" &&
|
||||
onDeleteCamera(selectedCamera.id)
|
||||
}
|
||||
disabled={selectedCamera?.id === "main"}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="text-lg" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="camera-name" className="text-sm opacity-70">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="camera-name"
|
||||
type="text"
|
||||
value={selectedCamera?.label || ""}
|
||||
onChange={(e) =>
|
||||
selectedCamera &&
|
||||
onCameraNameChange(selectedCamera.id, e.target.value)
|
||||
}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="focal-length" className="text-sm opacity-70">
|
||||
Focal Length
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center gap-4">
|
||||
<SliderV2
|
||||
min={10}
|
||||
max={200}
|
||||
value={selectedCamera?.focalLength || 35}
|
||||
onChange={(value) =>
|
||||
selectedCamera &&
|
||||
onCameraFocalLengthChange(selectedCamera.id, value)
|
||||
}
|
||||
step={1}
|
||||
suffix="mm"
|
||||
showDecrement={true}
|
||||
showIncrement={true}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="min-w-[60px] text-sm">
|
||||
{selectedCamera?.focalLength || 35}mm
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-end pt-4">
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user