fix: refine canvas angle preview panel
This commit is contained in:
parent
b78a2701bb
commit
1f8e19598c
@ -665,6 +665,7 @@ function resolveCanvasToolbarItemWithFallback(
|
||||
? item.label
|
||||
: fallback?.label || item.label,
|
||||
iconUrl: item.iconUrl || fallback?.iconUrl || "",
|
||||
enabled: item.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -946,13 +946,14 @@ export function RealtimeCanvasDialog({
|
||||
const resolveRealtimeModeItem = useCallback(
|
||||
(
|
||||
key: "realtimeModeRealtime" | "realtimeModeEnhance",
|
||||
fallback: { label: string; iconUrl: string }
|
||||
fallback: { label: string; iconUrl: string; enabled?: boolean }
|
||||
) => {
|
||||
const item = getCanvasToolbarItem(canvasToolbarConfig, key);
|
||||
const defaultItem = getDefaultCanvasToolbarItem(key);
|
||||
return {
|
||||
label: item.label === defaultItem.label ? fallback.label : item.label,
|
||||
iconUrl: item.iconUrl || fallback.iconUrl,
|
||||
enabled: item.enabled ?? fallback.enabled ?? true,
|
||||
};
|
||||
},
|
||||
[canvasToolbarConfig]
|
||||
|
||||
@ -218,6 +218,14 @@ type CropInteractionState = {
|
||||
startRect: NormalizedCropRect;
|
||||
};
|
||||
|
||||
type AnglePreviewDragState = {
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
startHorizontalAngle: number;
|
||||
startVerticalAngle: number;
|
||||
};
|
||||
|
||||
interface BoxCutoutPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -227,6 +235,8 @@ const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08;
|
||||
const BOX_CUTOUT_MIN_EDGE_PX = 24;
|
||||
const FLOATING_TOOLBAR_SCALE = 0.8;
|
||||
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 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>"
|
||||
@ -485,6 +495,51 @@ function AngleEditPanelSlider({
|
||||
const clampNumber = (value: number, min: number, max: number) =>
|
||||
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 = (
|
||||
start: BoxCutoutPoint,
|
||||
end: BoxCutoutPoint
|
||||
@ -1498,6 +1553,7 @@ export function ImageNode({
|
||||
const [angleVerticalAngle, setAngleVerticalAngle] = useState(-30);
|
||||
const [angleZoom, setAngleZoom] = useState(50);
|
||||
const [isApplyingAngleEdit, setIsApplyingAngleEdit] = useState(false);
|
||||
const [isAnglePreviewDragging, setIsAnglePreviewDragging] = useState(false);
|
||||
const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false);
|
||||
const [resizeAnnotatorOpen, setResizeAnnotatorOpen] = useState(false);
|
||||
const [promptDialogOpen, setPromptDialogOpen] = useState(false);
|
||||
@ -1546,6 +1602,7 @@ export function ImageNode({
|
||||
useState<BoxCutoutPoint | null>(null);
|
||||
const [boxCutoutDragEnd, setBoxCutoutDragEnd] =
|
||||
useState<BoxCutoutPoint | null>(null);
|
||||
const anglePreviewDragRef = useRef<AnglePreviewDragState | null>(null);
|
||||
const activeImageAdjustments = imageAdjustPanelOpen
|
||||
? imageAdjustmentsDraft
|
||||
: storedImageAdjustments;
|
||||
@ -1569,8 +1626,8 @@ export function ImageNode({
|
||||
useEffect(() => {
|
||||
if (!cropPanelOpen) return;
|
||||
if (cropRect) return;
|
||||
const centerX = cropRect ? cropRect.x + cropRect.width / 2 : 0.5;
|
||||
const centerY = cropRect ? cropRect.y + cropRect.height / 2 : 0.5;
|
||||
const centerX = 0.5;
|
||||
const centerY = 0.5;
|
||||
const preset = getCropPresetById(cropPresetId);
|
||||
if (preset) {
|
||||
setCropRect(createCenteredCropRectForAspect(preset.ratio, centerX, centerY));
|
||||
@ -1920,6 +1977,64 @@ export function ImageNode({
|
||||
if (!source) return "";
|
||||
return `${source.width} x ${source.height}`;
|
||||
}, [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(() => {
|
||||
if (boxCutoutMetrics) {
|
||||
return {
|
||||
@ -2165,6 +2280,12 @@ export function ImageNode({
|
||||
}
|
||||
}, [isSelected, resetBoxCutoutState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anglePanelOpen) return;
|
||||
anglePreviewDragRef.current = null;
|
||||
setIsAnglePreviewDragging(false);
|
||||
}, [anglePanelOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCompareSource || isThisNodePicking) {
|
||||
setIsCompareMode(false);
|
||||
@ -3390,6 +3511,64 @@ export function ImageNode({
|
||||
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(
|
||||
async (action: FlipRotateEditAction) => {
|
||||
if (!data.src) {
|
||||
@ -7917,86 +8096,272 @@ export function ImageNode({
|
||||
</div>
|
||||
|
||||
<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="absolute inset-[54px] rounded-full border border-[#ece8e2]" />
|
||||
<div
|
||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
||||
style={{ transform: "rotate(18deg) scaleX(1.06)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
||||
style={{ transform: "rotate(42deg) scaleX(0.94)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
||||
style={{ transform: "rotate(68deg) scaleY(0.84)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
|
||||
style={{ transform: "rotate(96deg) scaleY(0.72)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-[54px] rounded-full border border-[#f2eee8]"
|
||||
style={{ transform: "rotate(132deg) scaleX(1.08) scaleY(0.76)" }}
|
||||
/>
|
||||
<div className="absolute inset-[90px] rounded-full border border-dashed border-[#ebe6df]" />
|
||||
<svg
|
||||
className="h-[300px] w-[300px]"
|
||||
fill="none"
|
||||
viewBox="0 0 320 320"
|
||||
>
|
||||
<ellipse
|
||||
cx="160"
|
||||
cy="160"
|
||||
rx="116"
|
||||
ry="116"
|
||||
stroke="#ebe5dc"
|
||||
strokeWidth="1.4"
|
||||
/>
|
||||
<ellipse
|
||||
cx="160"
|
||||
cy="160"
|
||||
rx="116"
|
||||
ry="116"
|
||||
stroke="#efe8de"
|
||||
strokeWidth="1.2"
|
||||
transform="rotate(28 160 160)"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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}
|
||||
onClick={() =>
|
||||
setAngleVerticalAngle((prev) => clampNumber(prev + 5, -30, 60))
|
||||
}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</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}
|
||||
onClick={() =>
|
||||
setAngleVerticalAngle((prev) => clampNumber(prev - 5, -30, 60))
|
||||
}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</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}
|
||||
onClick={() =>
|
||||
setAngleHorizontalAngle((prev) =>
|
||||
clampNumber(prev - 10, -180, 180)
|
||||
)
|
||||
}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</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}
|
||||
onClick={() =>
|
||||
setAngleHorizontalAngle((prev) =>
|
||||
clampNumber(prev + 10, -180, 180)
|
||||
)
|
||||
}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</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)]">
|
||||
<img
|
||||
alt={displayTitle}
|
||||
className="h-full w-full rounded-[18px] object-cover"
|
||||
src={data.src}
|
||||
/>
|
||||
</div>
|
||||
{anglePanelMode === "subject" ? (
|
||||
<div
|
||||
className="relative z-[2]"
|
||||
style={{
|
||||
width: `${anglePreviewFrameSize.width}px`,
|
||||
height: `${anglePreviewFrameSize.height}px`,
|
||||
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" ? (
|
||||
<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)]",
|
||||
isHorizontal
|
||||
? "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
|
||||
)}
|
||||
>
|
||||
@ -153,7 +153,7 @@ export function FloatingSidebarAction({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
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 &&
|
||||
"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";
|
||||
const lovartSidebarButtonClass = (active = false) =>
|
||||
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 &&
|
||||
"bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
|
||||
);
|
||||
@ -1717,13 +1717,13 @@ export function LovartHome({
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
>
|
||||
<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", {
|
||||
filled: true,
|
||||
sizeClassName: "h-[18px] w-[18px]",
|
||||
sizeClassName: "h-4 w-4",
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user