fix: refine canvas angle preview panel
This commit is contained in:
parent
b78a2701bb
commit
1f8e19598c
@ -665,6 +665,7 @@ function resolveCanvasToolbarItemWithFallback(
|
|||||||
? item.label
|
? item.label
|
||||||
: fallback?.label || item.label,
|
: fallback?.label || item.label,
|
||||||
iconUrl: item.iconUrl || fallback?.iconUrl || "",
|
iconUrl: item.iconUrl || fallback?.iconUrl || "",
|
||||||
|
enabled: item.enabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -946,13 +946,14 @@ export function RealtimeCanvasDialog({
|
|||||||
const resolveRealtimeModeItem = useCallback(
|
const resolveRealtimeModeItem = useCallback(
|
||||||
(
|
(
|
||||||
key: "realtimeModeRealtime" | "realtimeModeEnhance",
|
key: "realtimeModeRealtime" | "realtimeModeEnhance",
|
||||||
fallback: { label: string; iconUrl: string }
|
fallback: { label: string; iconUrl: string; enabled?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const item = getCanvasToolbarItem(canvasToolbarConfig, key);
|
const item = getCanvasToolbarItem(canvasToolbarConfig, key);
|
||||||
const defaultItem = getDefaultCanvasToolbarItem(key);
|
const defaultItem = getDefaultCanvasToolbarItem(key);
|
||||||
return {
|
return {
|
||||||
label: item.label === defaultItem.label ? fallback.label : item.label,
|
label: item.label === defaultItem.label ? fallback.label : item.label,
|
||||||
iconUrl: item.iconUrl || fallback.iconUrl,
|
iconUrl: item.iconUrl || fallback.iconUrl,
|
||||||
|
enabled: item.enabled ?? fallback.enabled ?? true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[canvasToolbarConfig]
|
[canvasToolbarConfig]
|
||||||
|
|||||||
@ -218,6 +218,14 @@ type CropInteractionState = {
|
|||||||
startRect: NormalizedCropRect;
|
startRect: NormalizedCropRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AnglePreviewDragState = {
|
||||||
|
pointerId: number;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
startHorizontalAngle: number;
|
||||||
|
startVerticalAngle: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface BoxCutoutPoint {
|
interface BoxCutoutPoint {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@ -227,6 +235,8 @@ const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08;
|
|||||||
const BOX_CUTOUT_MIN_EDGE_PX = 24;
|
const BOX_CUTOUT_MIN_EDGE_PX = 24;
|
||||||
const FLOATING_TOOLBAR_SCALE = 0.8;
|
const FLOATING_TOOLBAR_SCALE = 0.8;
|
||||||
const IMAGE_EDIT_PANEL_SCALE = 0.5;
|
const IMAGE_EDIT_PANEL_SCALE = 0.5;
|
||||||
|
const ANGLE_PREVIEW_HORIZONTAL_DRAG_MULTIPLIER = 0.72;
|
||||||
|
const ANGLE_PREVIEW_VERTICAL_DRAG_MULTIPLIER = 0.46;
|
||||||
const DEFAULT_CROP_PRESET_ID = "1:1";
|
const DEFAULT_CROP_PRESET_ID = "1:1";
|
||||||
const IMAGE_ADJUST_NOISE_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
const IMAGE_ADJUST_NOISE_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.18' numOctaves='2' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(#n)' opacity='1'/></svg>"
|
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.18' numOctaves='2' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(#n)' opacity='1'/></svg>"
|
||||||
@ -485,6 +495,51 @@ function AngleEditPanelSlider({
|
|||||||
const clampNumber = (value: number, min: number, max: number) =>
|
const clampNumber = (value: number, min: number, max: number) =>
|
||||||
Math.min(max, Math.max(min, value));
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
const fitSizeWithinBox = ({
|
||||||
|
maxHeight,
|
||||||
|
maxWidth,
|
||||||
|
minHeight = 72,
|
||||||
|
minWidth = 72,
|
||||||
|
sourceHeight,
|
||||||
|
sourceWidth,
|
||||||
|
}: {
|
||||||
|
maxHeight: number;
|
||||||
|
maxWidth: number;
|
||||||
|
minHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
sourceHeight: number;
|
||||||
|
sourceWidth: number;
|
||||||
|
}) => {
|
||||||
|
const safeSourceWidth = sourceWidth > 0 ? sourceWidth : maxWidth;
|
||||||
|
const safeSourceHeight = sourceHeight > 0 ? sourceHeight : maxHeight;
|
||||||
|
let scale = Math.min(maxWidth / safeSourceWidth, maxHeight / safeSourceHeight);
|
||||||
|
let fittedWidth = safeSourceWidth * scale;
|
||||||
|
let fittedHeight = safeSourceHeight * scale;
|
||||||
|
|
||||||
|
if (fittedWidth < minWidth) {
|
||||||
|
scale = minWidth / safeSourceWidth;
|
||||||
|
fittedWidth = safeSourceWidth * scale;
|
||||||
|
fittedHeight = safeSourceHeight * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fittedHeight < minHeight) {
|
||||||
|
scale = minHeight / safeSourceHeight;
|
||||||
|
fittedWidth = safeSourceWidth * scale;
|
||||||
|
fittedHeight = safeSourceHeight * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fittedWidth > maxWidth || fittedHeight > maxHeight) {
|
||||||
|
scale = Math.min(maxWidth / fittedWidth, maxHeight / fittedHeight);
|
||||||
|
fittedWidth *= scale;
|
||||||
|
fittedHeight *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.round(fittedWidth),
|
||||||
|
height: Math.round(fittedHeight),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createNormalizedRect = (
|
const createNormalizedRect = (
|
||||||
start: BoxCutoutPoint,
|
start: BoxCutoutPoint,
|
||||||
end: BoxCutoutPoint
|
end: BoxCutoutPoint
|
||||||
@ -1498,6 +1553,7 @@ export function ImageNode({
|
|||||||
const [angleVerticalAngle, setAngleVerticalAngle] = useState(-30);
|
const [angleVerticalAngle, setAngleVerticalAngle] = useState(-30);
|
||||||
const [angleZoom, setAngleZoom] = useState(50);
|
const [angleZoom, setAngleZoom] = useState(50);
|
||||||
const [isApplyingAngleEdit, setIsApplyingAngleEdit] = useState(false);
|
const [isApplyingAngleEdit, setIsApplyingAngleEdit] = useState(false);
|
||||||
|
const [isAnglePreviewDragging, setIsAnglePreviewDragging] = useState(false);
|
||||||
const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false);
|
const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false);
|
||||||
const [resizeAnnotatorOpen, setResizeAnnotatorOpen] = useState(false);
|
const [resizeAnnotatorOpen, setResizeAnnotatorOpen] = useState(false);
|
||||||
const [promptDialogOpen, setPromptDialogOpen] = useState(false);
|
const [promptDialogOpen, setPromptDialogOpen] = useState(false);
|
||||||
@ -1546,6 +1602,7 @@ export function ImageNode({
|
|||||||
useState<BoxCutoutPoint | null>(null);
|
useState<BoxCutoutPoint | null>(null);
|
||||||
const [boxCutoutDragEnd, setBoxCutoutDragEnd] =
|
const [boxCutoutDragEnd, setBoxCutoutDragEnd] =
|
||||||
useState<BoxCutoutPoint | null>(null);
|
useState<BoxCutoutPoint | null>(null);
|
||||||
|
const anglePreviewDragRef = useRef<AnglePreviewDragState | null>(null);
|
||||||
const activeImageAdjustments = imageAdjustPanelOpen
|
const activeImageAdjustments = imageAdjustPanelOpen
|
||||||
? imageAdjustmentsDraft
|
? imageAdjustmentsDraft
|
||||||
: storedImageAdjustments;
|
: storedImageAdjustments;
|
||||||
@ -1569,8 +1626,8 @@ export function ImageNode({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cropPanelOpen) return;
|
if (!cropPanelOpen) return;
|
||||||
if (cropRect) return;
|
if (cropRect) return;
|
||||||
const centerX = cropRect ? cropRect.x + cropRect.width / 2 : 0.5;
|
const centerX = 0.5;
|
||||||
const centerY = cropRect ? cropRect.y + cropRect.height / 2 : 0.5;
|
const centerY = 0.5;
|
||||||
const preset = getCropPresetById(cropPresetId);
|
const preset = getCropPresetById(cropPresetId);
|
||||||
if (preset) {
|
if (preset) {
|
||||||
setCropRect(createCenteredCropRectForAspect(preset.ratio, centerX, centerY));
|
setCropRect(createCenteredCropRectForAspect(preset.ratio, centerX, centerY));
|
||||||
@ -1920,6 +1977,64 @@ export function ImageNode({
|
|||||||
if (!source) return "";
|
if (!source) return "";
|
||||||
return `${source.width} x ${source.height}`;
|
return `${source.width} x ${source.height}`;
|
||||||
}, [loadedResolution, metadataResolution]);
|
}, [loadedResolution, metadataResolution]);
|
||||||
|
const angleSourceResolution = useMemo(() => {
|
||||||
|
const source = metadataResolution || loadedResolution;
|
||||||
|
if (source?.width && source?.height) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: Math.max(1, Math.round(node.size.width || 1)),
|
||||||
|
height: Math.max(1, Math.round(node.size.height || 1)),
|
||||||
|
};
|
||||||
|
}, [loadedResolution, metadataResolution, node.size.height, node.size.width]);
|
||||||
|
const anglePreviewFrameSize = useMemo(
|
||||||
|
() =>
|
||||||
|
fitSizeWithinBox({
|
||||||
|
sourceWidth: angleSourceResolution.width,
|
||||||
|
sourceHeight: angleSourceResolution.height,
|
||||||
|
maxWidth: anglePanelMode === "subject" ? 178 : 136,
|
||||||
|
maxHeight: anglePanelMode === "subject" ? 150 : 112,
|
||||||
|
minWidth: 74,
|
||||||
|
minHeight: 62,
|
||||||
|
}),
|
||||||
|
[anglePanelMode, angleSourceResolution.height, angleSourceResolution.width]
|
||||||
|
);
|
||||||
|
const anglePreviewDepth = useMemo(
|
||||||
|
() =>
|
||||||
|
Math.round(
|
||||||
|
clampNumber(
|
||||||
|
anglePanelMode === "subject"
|
||||||
|
? anglePreviewFrameSize.width * 0.18
|
||||||
|
: anglePreviewFrameSize.width * 0.22,
|
||||||
|
16,
|
||||||
|
36
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[anglePanelMode, anglePreviewFrameSize.width]
|
||||||
|
);
|
||||||
|
const anglePreviewScale = useMemo(
|
||||||
|
() =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
(anglePanelMode === "subject" ? 0.9 : 0.92) +
|
||||||
|
(angleZoom / 100) * (anglePanelMode === "subject" ? 0.46 : 0.34)
|
||||||
|
).toFixed(3)
|
||||||
|
),
|
||||||
|
[anglePanelMode, angleZoom]
|
||||||
|
);
|
||||||
|
const anglePreviewImageSrc = displayImageSrc || data.src;
|
||||||
|
const angleCameraOrbitPosition = useMemo(() => {
|
||||||
|
const horizontalRadians = (angleHorizontalAngle * Math.PI) / 180;
|
||||||
|
const normalizedVertical = (angleVerticalAngle + 30) / 90;
|
||||||
|
const x = Math.sin(horizontalRadians) * 92;
|
||||||
|
const y = -normalizedVertical * 60 + 24;
|
||||||
|
const z = Math.cos(horizontalRadians) * 28;
|
||||||
|
return {
|
||||||
|
x: Number(x.toFixed(2)),
|
||||||
|
y: Number(y.toFixed(2)),
|
||||||
|
z: Number(z.toFixed(2)),
|
||||||
|
};
|
||||||
|
}, [angleHorizontalAngle, angleVerticalAngle]);
|
||||||
const cropImageMetrics = useMemo(() => {
|
const cropImageMetrics = useMemo(() => {
|
||||||
if (boxCutoutMetrics) {
|
if (boxCutoutMetrics) {
|
||||||
return {
|
return {
|
||||||
@ -2165,6 +2280,12 @@ export function ImageNode({
|
|||||||
}
|
}
|
||||||
}, [isSelected, resetBoxCutoutState]);
|
}, [isSelected, resetBoxCutoutState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (anglePanelOpen) return;
|
||||||
|
anglePreviewDragRef.current = null;
|
||||||
|
setIsAnglePreviewDragging(false);
|
||||||
|
}, [anglePanelOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasCompareSource || isThisNodePicking) {
|
if (!hasCompareSource || isThisNodePicking) {
|
||||||
setIsCompareMode(false);
|
setIsCompareMode(false);
|
||||||
@ -3390,6 +3511,64 @@ export function ImageNode({
|
|||||||
setAngleZoom(50);
|
setAngleZoom(50);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleAnglePreviewPointerDown = useCallback(
|
||||||
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (isApplyingAngleEdit) return;
|
||||||
|
anglePreviewDragRef.current = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
startX: event.clientX,
|
||||||
|
startY: event.clientY,
|
||||||
|
startHorizontalAngle: angleHorizontalAngle,
|
||||||
|
startVerticalAngle: angleVerticalAngle,
|
||||||
|
};
|
||||||
|
setIsAnglePreviewDragging(true);
|
||||||
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
[angleHorizontalAngle, angleVerticalAngle, isApplyingAngleEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAnglePreviewPointerMove = useCallback(
|
||||||
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const dragState = anglePreviewDragRef.current;
|
||||||
|
if (!dragState || dragState.pointerId !== event.pointerId) return;
|
||||||
|
const deltaX = event.clientX - dragState.startX;
|
||||||
|
const deltaY = event.clientY - dragState.startY;
|
||||||
|
setAngleHorizontalAngle(
|
||||||
|
clampNumber(
|
||||||
|
dragState.startHorizontalAngle +
|
||||||
|
deltaX * ANGLE_PREVIEW_HORIZONTAL_DRAG_MULTIPLIER,
|
||||||
|
-180,
|
||||||
|
180
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setAngleVerticalAngle(
|
||||||
|
clampNumber(
|
||||||
|
dragState.startVerticalAngle -
|
||||||
|
deltaY * ANGLE_PREVIEW_VERTICAL_DRAG_MULTIPLIER,
|
||||||
|
-30,
|
||||||
|
60
|
||||||
|
)
|
||||||
|
);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAnglePreviewPointerEnd = useCallback(
|
||||||
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (anglePreviewDragRef.current?.pointerId !== event.pointerId) return;
|
||||||
|
anglePreviewDragRef.current = null;
|
||||||
|
setIsAnglePreviewDragging(false);
|
||||||
|
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleApplyFlipRotate = useCallback(
|
const handleApplyFlipRotate = useCallback(
|
||||||
async (action: FlipRotateEditAction) => {
|
async (action: FlipRotateEditAction) => {
|
||||||
if (!data.src) {
|
if (!data.src) {
|
||||||
@ -7917,86 +8096,272 @@ export function ImageNode({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mt-6 overflow-hidden rounded-[26px] bg-[#faf8f4] px-4 py-5">
|
<div className="relative mt-6 overflow-hidden rounded-[26px] bg-[#faf8f4] px-4 py-5">
|
||||||
<div className="relative flex h-[410px] items-center justify-center">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-[410px] items-center justify-center overflow-hidden rounded-[22px] [perspective:1200px]",
|
||||||
|
isApplyingAngleEdit
|
||||||
|
? "cursor-default"
|
||||||
|
: isAnglePreviewDragging
|
||||||
|
? "cursor-grabbing"
|
||||||
|
: "cursor-grab"
|
||||||
|
)}
|
||||||
|
onLostPointerCapture={handleAnglePreviewPointerEnd}
|
||||||
|
onPointerCancel={handleAnglePreviewPointerEnd}
|
||||||
|
onPointerDown={handleAnglePreviewPointerDown}
|
||||||
|
onPointerMove={handleAnglePreviewPointerMove}
|
||||||
|
onPointerUp={handleAnglePreviewPointerEnd}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
>
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
<div className="absolute inset-[54px] rounded-full border border-[#ece8e2]" />
|
<svg
|
||||||
<div
|
className="h-[300px] w-[300px]"
|
||||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
fill="none"
|
||||||
style={{ transform: "rotate(18deg) scaleX(1.06)" }}
|
viewBox="0 0 320 320"
|
||||||
/>
|
>
|
||||||
<div
|
<ellipse
|
||||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
cx="160"
|
||||||
style={{ transform: "rotate(42deg) scaleX(0.94)" }}
|
cy="160"
|
||||||
/>
|
rx="116"
|
||||||
<div
|
ry="116"
|
||||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
stroke="#ebe5dc"
|
||||||
style={{ transform: "rotate(68deg) scaleY(0.84)" }}
|
strokeWidth="1.4"
|
||||||
/>
|
/>
|
||||||
<div
|
<ellipse
|
||||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
cx="160"
|
||||||
style={{ transform: "rotate(96deg) scaleY(0.72)" }}
|
cy="160"
|
||||||
/>
|
rx="116"
|
||||||
<div
|
ry="116"
|
||||||
className="absolute inset-[54px] rounded-full border border-[#f2eee8]"
|
stroke="#efe8de"
|
||||||
style={{ transform: "rotate(132deg) scaleX(1.08) scaleY(0.76)" }}
|
strokeWidth="1.2"
|
||||||
/>
|
transform="rotate(28 160 160)"
|
||||||
<div className="absolute inset-[90px] rounded-full border border-dashed border-[#ebe6df]" />
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="160"
|
||||||
|
cy="160"
|
||||||
|
rx="116"
|
||||||
|
ry="86"
|
||||||
|
stroke="#efe8de"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
transform="rotate(56 160 160)"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="160"
|
||||||
|
cy="160"
|
||||||
|
rx="116"
|
||||||
|
ry="76"
|
||||||
|
stroke="#efe8de"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
transform="rotate(92 160 160)"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="160"
|
||||||
|
cy="160"
|
||||||
|
rx="86"
|
||||||
|
ry="116"
|
||||||
|
stroke="#f2ece3"
|
||||||
|
strokeDasharray="4 6"
|
||||||
|
strokeWidth="1.1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="absolute top-1 left-1/2 inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
className="absolute top-1 left-1/2 z-[3] inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
||||||
disabled={isApplyingAngleEdit}
|
disabled={isApplyingAngleEdit}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAngleVerticalAngle((prev) => clampNumber(prev + 5, -30, 60))
|
setAngleVerticalAngle((prev) => clampNumber(prev + 5, -30, 60))
|
||||||
}
|
}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="absolute bottom-1 left-1/2 inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
className="absolute bottom-1 left-1/2 z-[3] inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
||||||
disabled={isApplyingAngleEdit}
|
disabled={isApplyingAngleEdit}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAngleVerticalAngle((prev) => clampNumber(prev - 5, -30, 60))
|
setAngleVerticalAngle((prev) => clampNumber(prev - 5, -30, 60))
|
||||||
}
|
}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="absolute top-1/2 left-2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
className="absolute top-1/2 left-2 z-[3] inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
||||||
disabled={isApplyingAngleEdit}
|
disabled={isApplyingAngleEdit}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAngleHorizontalAngle((prev) =>
|
setAngleHorizontalAngle((prev) =>
|
||||||
clampNumber(prev - 10, -180, 180)
|
clampNumber(prev - 10, -180, 180)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="absolute top-1/2 right-2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
className="absolute top-1/2 right-2 z-[3] inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
|
||||||
disabled={isApplyingAngleEdit}
|
disabled={isApplyingAngleEdit}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAngleHorizontalAngle((prev) =>
|
setAngleHorizontalAngle((prev) =>
|
||||||
clampNumber(prev + 10, -180, 180)
|
clampNumber(prev + 10, -180, 180)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="relative z-[1] flex h-[92px] w-[92px] items-center justify-center rounded-[24px] border border-white/80 bg-white p-2 shadow-[0_16px_34px_rgba(15,23,42,0.12)]">
|
{anglePanelMode === "subject" ? (
|
||||||
<img
|
<div
|
||||||
alt={displayTitle}
|
className="relative z-[2]"
|
||||||
className="h-full w-full rounded-[18px] object-cover"
|
style={{
|
||||||
src={data.src}
|
width: `${anglePreviewFrameSize.width}px`,
|
||||||
/>
|
height: `${anglePreviewFrameSize.height}px`,
|
||||||
</div>
|
transform: `rotateX(${-angleVerticalAngle * 0.46}deg) rotateY(${angleHorizontalAngle * 0.62}deg) scale(${anglePreviewScale})`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
transition: isAnglePreviewDragging
|
||||||
|
? "none"
|
||||||
|
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-[10%] rounded-full bg-black/12 blur-[20px]"
|
||||||
|
style={{
|
||||||
|
transform: `translate3d(0, ${anglePreviewFrameSize.height * 0.34}px, -18px) scale(${0.9 + angleZoom / 260})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-[26px] border border-white/90 bg-white shadow-[0_16px_34px_rgba(15,23,42,0.14)]"
|
||||||
|
style={{
|
||||||
|
transform: `translateZ(${anglePreviewDepth / 2}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-[6px] overflow-hidden rounded-[20px] bg-[#fffdfa]">
|
||||||
|
<img
|
||||||
|
alt={displayTitle}
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
draggable={false}
|
||||||
|
src={anglePreviewImageSrc}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-[26px] border border-[#ebe2d8] bg-[linear-gradient(180deg,#f6efe5,#eadfce)]"
|
||||||
|
style={{
|
||||||
|
transform: `translateZ(-${anglePreviewDepth / 2}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-[10px] bottom-[10px] rounded-l-[18px] border border-[#e6dccc] bg-[linear-gradient(180deg,#f3ebdf,#dccfbc)]"
|
||||||
|
style={{
|
||||||
|
width: `${anglePreviewDepth}px`,
|
||||||
|
transform: `translateX(-${anglePreviewDepth / 2}px) rotateY(-90deg)`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute right-0 top-[10px] bottom-[10px] rounded-r-[18px] border border-[#e6dccc] bg-[linear-gradient(180deg,#f2e9dd,#dbceb9)]"
|
||||||
|
style={{
|
||||||
|
width: `${anglePreviewDepth}px`,
|
||||||
|
transform: `translateX(${anglePreviewDepth / 2}px) rotateY(90deg)`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute left-[10px] right-[10px] top-0 rounded-t-[16px] border border-white/90 bg-[linear-gradient(180deg,#fffefe,#ece1d0)]"
|
||||||
|
style={{
|
||||||
|
height: `${anglePreviewDepth}px`,
|
||||||
|
transform: `translateY(-${anglePreviewDepth / 2}px) rotateX(90deg)`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute left-[10px] right-[10px] bottom-0 rounded-b-[16px] border border-[#e9dfd1] bg-[linear-gradient(180deg,#e2d6c5,#d3c5b0)]"
|
||||||
|
style={{
|
||||||
|
height: `${anglePreviewDepth}px`,
|
||||||
|
transform: `translateY(${anglePreviewDepth / 2}px) rotateX(-90deg)`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="relative z-[2] overflow-hidden rounded-[24px] border border-white/90 bg-white p-[6px] shadow-[0_16px_34px_rgba(15,23,42,0.12)]"
|
||||||
|
style={{
|
||||||
|
width: `${anglePreviewFrameSize.width}px`,
|
||||||
|
height: `${anglePreviewFrameSize.height}px`,
|
||||||
|
transform: `translateZ(${10 + angleZoom * 0.14}px) rotateX(${-angleVerticalAngle * 0.08}deg) rotateY(${angleHorizontalAngle * 0.18}deg) scale(${anglePreviewScale})`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
transition: isAnglePreviewDragging
|
||||||
|
? "none"
|
||||||
|
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full overflow-hidden rounded-[18px] bg-[#fffdfa]">
|
||||||
|
<img
|
||||||
|
alt={displayTitle}
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
draggable={false}
|
||||||
|
src={anglePreviewImageSrc}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-[56px] left-[34px] flex h-[48px] w-[48px] items-center justify-center rounded-[16px] border border-[#ece7df] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.08)]">
|
<div
|
||||||
|
className="absolute z-[3]"
|
||||||
|
style={{
|
||||||
|
transform: `translate3d(${angleCameraOrbitPosition.x}px, ${angleCameraOrbitPosition.y}px, ${angleCameraOrbitPosition.z}px)`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
transition: isAnglePreviewDragging
|
||||||
|
? "none"
|
||||||
|
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{
|
||||||
|
width: "54px",
|
||||||
|
height: "54px",
|
||||||
|
transform: `rotateX(${12 - angleVerticalAngle * 0.12}deg) rotateY(${angleHorizontalAngle + 26}deg)`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 overflow-hidden rounded-[16px] border border-white/90 bg-white shadow-[0_10px_24px_rgba(15,23,42,0.12)]"
|
||||||
|
style={{ transform: "translateZ(10px)" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
draggable={false}
|
||||||
|
src={anglePreviewImageSrc}
|
||||||
|
/>
|
||||||
|
<span className="absolute right-[6px] bottom-[6px] h-[9px] w-[9px] rounded-full border border-white/90 bg-[#2f3136]/85" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-[4px] right-0 rounded-r-[12px] border border-[#e6dccf] bg-[linear-gradient(180deg,#f6f0e8,#e4d9ca)]"
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
transform: "translateX(10px) rotateY(90deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-x-[4px] top-0 rounded-t-[12px] border border-white/90 bg-[linear-gradient(180deg,#ffffff,#ece2d6)]"
|
||||||
|
style={{
|
||||||
|
height: "20px",
|
||||||
|
transform: "translateY(-10px) rotateX(90deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute bottom-[54px] left-[34px] z-[3] flex h-[48px] w-[48px] items-center justify-center rounded-[16px] border border-[#ece7df] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.08)]">
|
||||||
{anglePanelMode === "subject" ? (
|
{anglePanelMode === "subject" ? (
|
||||||
<Box className="h-5 w-5 text-[#949494]" />
|
<Box className="h-5 w-5 text-[#949494]" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export function FloatingSidebarRail({
|
|||||||
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
|
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
|
||||||
isHorizontal
|
isHorizontal
|
||||||
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
|
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
|
||||||
: "w-[60px] flex-col overflow-y-auto rounded-[28px] px-2 py-2.5",
|
: "w-[56px] flex-col overflow-y-auto rounded-[24px] px-2 py-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -153,7 +153,7 @@ export function FloatingSidebarAction({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-10 items-center justify-center rounded-full text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
|
"flex h-10 w-10 items-center justify-center rounded-xl text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
|
||||||
active &&
|
active &&
|
||||||
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
|
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -446,7 +446,7 @@ export function LovartHome({
|
|||||||
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
|
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
|
||||||
const lovartSidebarButtonClass = (active = false) =>
|
const lovartSidebarButtonClass = (active = false) =>
|
||||||
cn(
|
cn(
|
||||||
"group flex h-11 w-11 items-center justify-center rounded-[16px] text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
|
"group flex h-10 w-10 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
|
||||||
active &&
|
active &&
|
||||||
"bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
|
"bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
|
||||||
);
|
);
|
||||||
@ -1717,13 +1717,13 @@ export function LovartHome({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openNewCanvas}
|
onClick={openNewCanvas}
|
||||||
className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[22px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
|
className="flex h-[56px] w-[56px] shrink-0 items-center justify-center rounded-[20px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
|
||||||
title={getLovartSidebarItem("newProject").label}
|
title={getLovartSidebarItem("newProject").label}
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-[14px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
|
<div className="flex h-9 w-9 items-center justify-center rounded-[12px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
|
||||||
{renderLovartSidebarIcon("newProject", {
|
{renderLovartSidebarIcon("newProject", {
|
||||||
filled: true,
|
filled: true,
|
||||||
sizeClassName: "h-[18px] w-[18px]",
|
sizeClassName: "h-4 w-4",
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user