fix: refine angle panel proportions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-08 20:04:36 +08:00
parent b82bace466
commit e157480ec2

View File

@ -238,6 +238,17 @@ 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 ANGLE_PANEL_SOURCE_WIDTH = 520;
const ANGLE_PANEL_SOURCE_HEIGHT = 700;
const ANGLE_PANEL_PREVIEW_SIZE = 392;
const ANGLE_PANEL_CAMERA_SCENE_SIZE = 384;
const ANGLE_PANEL_WIREFRAME_RADIUS = 132;
const ANGLE_PANEL_WIREFRAME_CENTER = 160;
const ANGLE_PREVIEW_ORBIT_ROTATIONS = [
0, 14, 28, 42, 56, 70, 84, 98, 112, 126, 140, 154, 168,
];
const ANGLE_PREVIEW_LATITUDE_RATIOS = [1, 0.9, 0.78, 0.64, 0.5];
const ANGLE_PREVIEW_MERIDIAN_RATIOS = [1, 0.9, 0.78, 0.64];
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>"
@ -469,12 +480,12 @@ function AngleEditPanelSlider({
valueLabel: string;
}) {
return (
<div className="space-y-3">
<div className="space-y-2">
<div className="flex items-center justify-between gap-4">
<span className="text-[18px] font-semibold tracking-[0] text-[#26272b]">
<span className="text-[17px] font-semibold tracking-[0] text-[#26272b]">
{label}
</span>
<span className="text-[17px] font-medium tabular-nums text-[#7d7f86]">
<span className="text-[16px] font-medium tabular-nums text-[#7d7f86]">
{valueLabel}
</span>
</div>
@ -485,7 +496,7 @@ function AngleEditPanelSlider({
onValueChange={([nextValue]) => onChange(nextValue ?? value)}
rangeClassName="bg-[#2d3035]"
step={step}
thumbClassName="h-[22px] w-[22px] border-[5px] border-[#2d3035] bg-white shadow-[0_2px_10px_rgba(24,24,27,0.14)]"
thumbClassName="h-5 w-5 border-[4px] border-[#2d3035] bg-white shadow-[0_2px_10px_rgba(24,24,27,0.14)]"
trackClassName="h-[4px] bg-[#e7e8ec]"
value={[value]}
/>
@ -2069,10 +2080,10 @@ export function ImageNode({
fitSizeWithinBox({
sourceWidth: angleSourceResolution.width,
sourceHeight: angleSourceResolution.height,
maxWidth: anglePanelMode === "subject" ? 178 : 136,
maxHeight: anglePanelMode === "subject" ? 150 : 112,
minWidth: 74,
minHeight: 62,
maxWidth: anglePanelMode === "subject" ? 236 : 188,
maxHeight: anglePanelMode === "subject" ? 196 : 150,
minWidth: 96,
minHeight: 80,
}),
[anglePanelMode, angleSourceResolution.height, angleSourceResolution.width]
);
@ -2103,9 +2114,9 @@ export function ImageNode({
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;
const x = Math.sin(horizontalRadians) * 110;
const y = -normalizedVertical * 72 + 26;
const z = Math.cos(horizontalRadians) * 38;
return {
x: Number(x.toFixed(2)),
y: Number(y.toFixed(2)),
@ -2127,8 +2138,18 @@ export function ImageNode({
length: Number(length.toFixed(2)),
};
}, [angleCameraOrbitPosition]);
const angleOrbitWireframeStyle = useMemo<React.CSSProperties>(
() => ({
transform: `rotateX(${-angleVerticalAngle * 0.22}deg) rotateY(${angleHorizontalAngle * 0.28}deg) rotateZ(${angleHorizontalAngle * 0.04}deg)`,
transformStyle: "preserve-3d",
transition: isAnglePreviewDragging
? "none"
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
}),
[angleHorizontalAngle, angleVerticalAngle, isAnglePreviewDragging]
);
const angleCameraSceneScale = useMemo(
() => Number((0.9 + (angleZoom / 100) * 0.24).toFixed(3)),
() => Number((0.94 + (angleZoom / 100) * 0.22).toFixed(3)),
[angleZoom]
);
const cropImageMetrics = useMemo(() => {
@ -3586,8 +3607,8 @@ export function ImageNode({
}
const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 372 * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = 760 * IMAGE_EDIT_PANEL_SCALE;
const panelWidth = ANGLE_PANEL_SOURCE_WIDTH * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = ANGLE_PANEL_SOURCE_HEIGHT * IMAGE_EDIT_PANEL_SCALE;
const spacing = 16;
const maxTop = Math.max(12, window.innerHeight - panelHeight);
@ -8191,7 +8212,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[9999] w-[372px] rounded-[28px] border border-[#ececef] bg-white p-5 shadow-[0_24px_64px_rgba(15,23,42,0.12)]"
className="fixed z-[9999] w-[520px] rounded-[28px] border border-[#ececef] bg-white p-6 shadow-[0_24px_64px_rgba(15,23,42,0.12)]"
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@ -8228,7 +8249,7 @@ export function ImageNode({
return (
<button
className={cn(
"h-14 rounded-[16px] text-[16px] font-semibold tracking-[0] transition-all",
"h-[52px] rounded-[16px] text-[16px] font-semibold tracking-[0] transition-all",
active
? "bg-white text-[#1f2023] shadow-[0_1px_2px_rgba(16,24,40,0.06)]"
: "text-[#767982] hover:text-[#2d3036]"
@ -8245,10 +8266,10 @@ export function ImageNode({
</div>
</div>
<div className="relative mt-6 overflow-hidden rounded-[26px] border border-[#efeff2] bg-[#fcfcfd] px-4 py-5">
<div className="relative mt-4 overflow-hidden rounded-[26px] border border-[#efeff2] bg-[#fcfcfd] px-4 py-4">
<div
className={cn(
"relative flex h-[410px] items-center justify-center overflow-hidden rounded-[22px] [perspective:1200px]",
"relative flex h-[332px] items-center justify-center overflow-hidden rounded-[22px] [perspective:1200px]",
isApplyingAngleEdit
? "cursor-default"
: isAnglePreviewDragging
@ -8262,73 +8283,64 @@ export function ImageNode({
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"
style={angleOrbitWireframeStyle}
>
<svg
className="h-[312px] w-[312px]"
className="shrink-0"
fill="none"
height={ANGLE_PANEL_PREVIEW_SIZE}
viewBox="0 0 320 320"
width={ANGLE_PANEL_PREVIEW_SIZE}
>
<ellipse
cx="160"
cy="160"
rx="118"
ry="118"
stroke="#e6e8ee"
strokeWidth="1.2"
/>
<ellipse
cx="160"
cy="160"
rx="118"
ry="118"
stroke="#e8eaf0"
strokeWidth="1.1"
transform="rotate(22 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="118"
ry="118"
stroke="#e8eaf0"
strokeWidth="1.1"
transform="rotate(44 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="118"
ry="118"
stroke="#e8eaf0"
strokeWidth="1.1"
transform="rotate(66 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="118"
ry="94"
stroke="#eceef4"
strokeWidth="1.05"
/>
<ellipse
cx="160"
cy="160"
rx="118"
ry="72"
stroke="#eceef4"
strokeWidth="1.05"
/>
<ellipse
cx="160"
cy="160"
rx="94"
ry="118"
stroke="#eceef4"
strokeDasharray="4 6"
strokeWidth="1.05"
transform="rotate(90 160 160)"
/>
{ANGLE_PREVIEW_ORBIT_ROTATIONS.map((rotation, index) => (
<ellipse
key={`orbit-rotation-${rotation}`}
cx={ANGLE_PANEL_WIREFRAME_CENTER}
cy={ANGLE_PANEL_WIREFRAME_CENTER}
rx={ANGLE_PANEL_WIREFRAME_RADIUS}
ry={ANGLE_PANEL_WIREFRAME_RADIUS}
stroke={index === 0 ? "#e5e8ef" : "#e8ebf1"}
strokeOpacity={index === 0 ? 0.92 : 0.84}
strokeWidth={index % 3 === 0 ? "1.12" : "1.02"}
transform={
rotation === 0
? undefined
: `rotate(${rotation} ${ANGLE_PANEL_WIREFRAME_CENTER} ${ANGLE_PANEL_WIREFRAME_CENTER})`
}
/>
))}
{ANGLE_PREVIEW_LATITUDE_RATIOS.map((ratio, index) => (
<ellipse
key={`orbit-latitude-${ratio}`}
cx={ANGLE_PANEL_WIREFRAME_CENTER}
cy={ANGLE_PANEL_WIREFRAME_CENTER}
rx={ANGLE_PANEL_WIREFRAME_RADIUS}
ry={Number(
(ANGLE_PANEL_WIREFRAME_RADIUS * ratio).toFixed(2)
)}
stroke="#eceff4"
strokeOpacity={0.86 - index * 0.08}
strokeWidth={index === 0 ? "1.08" : "1.0"}
/>
))}
{ANGLE_PREVIEW_MERIDIAN_RATIOS.map((ratio, index) => (
<ellipse
key={`orbit-meridian-${ratio}`}
cx={ANGLE_PANEL_WIREFRAME_CENTER}
cy={ANGLE_PANEL_WIREFRAME_CENTER}
rx={Number(
(ANGLE_PANEL_WIREFRAME_RADIUS * ratio).toFixed(2)
)}
ry={ANGLE_PANEL_WIREFRAME_RADIUS}
stroke="#e9edf3"
strokeOpacity={0.78 - index * 0.08}
strokeDasharray={index === 0 ? undefined : "4 6"}
strokeWidth="1.0"
transform={`rotate(90 ${ANGLE_PANEL_WIREFRAME_CENTER} ${ANGLE_PANEL_WIREFRAME_CENTER})`}
/>
))}
</svg>
</div>
@ -8387,7 +8399,7 @@ export function ImageNode({
style={{
width: `${anglePreviewFrameSize.width}px`,
height: `${anglePreviewFrameSize.height}px`,
transform: `rotateX(${-angleVerticalAngle * 0.46}deg) rotateY(${angleHorizontalAngle * 0.62}deg) scale(${anglePreviewScale})`,
transform: `rotateX(${-angleVerticalAngle * 0.5}deg) rotateY(${angleHorizontalAngle * 0.68}deg) scale(${anglePreviewScale})`,
transformStyle: "preserve-3d",
transition: isAnglePreviewDragging
? "none"
@ -8397,7 +8409,7 @@ export function ImageNode({
<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})`,
transform: `translate3d(0, ${anglePreviewFrameSize.height * 0.31}px, -18px) scale(${0.96 + angleZoom / 240})`,
}}
/>
<div
@ -8456,22 +8468,24 @@ export function ImageNode({
</div>
) : (
<div
className="relative z-[2] h-[320px] w-[320px]"
className="relative z-[2]"
style={{
height: `${ANGLE_PANEL_CAMERA_SCENE_SIZE}px`,
transform: `scale(${angleCameraSceneScale})`,
transition: isAnglePreviewDragging
? "none"
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
width: `${ANGLE_PANEL_CAMERA_SCENE_SIZE}px`,
}}
>
<div
className="absolute left-1/2 top-1/2 z-[2] h-[90px] w-[90px] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-[18px] border border-[#eceff4] bg-white/96 shadow-[0_12px_28px_rgba(15,23,42,0.08)]"
className="absolute left-1/2 top-1/2 z-[2] h-[116px] w-[116px] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-[22px] border border-[#eceff4] bg-white/96 shadow-[0_12px_28px_rgba(15,23,42,0.08)]"
style={{
backdropFilter: "blur(8px)",
}}
>
<div
className="absolute inset-[8px] rounded-[14px] border border-[#eef1f5]"
className="absolute inset-[10px] rounded-[17px] border border-[#eef1f5]"
style={{
backgroundImage:
"linear-gradient(180deg,#fcfcfd,#f5f6f8), linear-gradient(90deg,rgba(227,230,236,0.55) 1px,transparent 1px), linear-gradient(rgba(227,230,236,0.55) 1px,transparent 1px)",
@ -8482,7 +8496,7 @@ export function ImageNode({
>
<img
alt={displayTitle}
className="absolute left-1/2 top-1/2 max-h-[58px] max-w-[58px] -translate-x-1/2 -translate-y-1/2 object-contain"
className="absolute left-1/2 top-1/2 max-h-[76px] max-w-[76px] -translate-x-1/2 -translate-y-1/2 object-contain"
draggable={false}
src={anglePreviewImageSrc}
/>
@ -8492,7 +8506,7 @@ export function ImageNode({
<div
className="absolute left-1/2 top-1/2 z-[1] h-px bg-[#dfe4ec]"
style={{
width: `${Math.max(32, angleCameraConnection.length - 18)}px`,
width: `${Math.max(40, angleCameraConnection.length - 12)}px`,
transform: `translateY(-0.5px) rotate(${angleCameraConnection.angle}deg)`,
transformOrigin: "0 50%",
}}
@ -8511,8 +8525,8 @@ export function ImageNode({
<div
className="relative"
style={{
width: "56px",
height: "56px",
width: "60px",
height: "60px",
transform:
"translate(-50%, -50%) rotateX(-18deg) rotateY(28deg)",
transformStyle: "preserve-3d",
@ -8548,14 +8562,14 @@ export function ImageNode({
)}
{anglePanelMode === "subject" ? (
<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-[#eceef3] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.07)]">
<div className="pointer-events-none absolute bottom-[36px] left-[34px] z-[3] flex h-[50px] w-[50px] items-center justify-center rounded-[16px] border border-[#eceef3] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.07)]">
<Box className="h-5 w-5 text-[#8a8f98]" />
</div>
) : null}
</div>
</div>
<div className="mt-7 space-y-6">
<div className="mt-5 space-y-4">
<AngleEditPanelSlider
label="旋转"
max={180}
@ -8582,9 +8596,9 @@ export function ImageNode({
/>
</div>
<div className="mt-7 grid grid-cols-2 gap-3">
<div className="mt-5 grid grid-cols-2 gap-3">
<button
className="inline-flex h-14 items-center justify-center rounded-[18px] border border-[#dfe2e8] bg-white text-[18px] font-medium text-[#27292f] transition-colors hover:bg-[#f7f8fa]"
className="inline-flex h-[50px] items-center justify-center rounded-[18px] border border-[#dfe2e8] bg-white text-[17px] font-medium text-[#27292f] transition-colors hover:bg-[#f7f8fa]"
disabled={isApplyingAngleEdit}
onClick={() => setAnglePanelOpen(false)}
type="button"
@ -8592,7 +8606,7 @@ export function ImageNode({
取消
</button>
<button
className="inline-flex h-14 items-center justify-center gap-2 rounded-[18px] bg-[#2d2f34] text-[18px] font-semibold text-white transition-colors hover:bg-[#23252a] disabled:cursor-not-allowed disabled:bg-[#cfd2d8] disabled:text-white/80"
className="inline-flex h-[50px] items-center justify-center gap-2 rounded-[18px] bg-[#2d2f34] text-[17px] font-semibold text-white transition-colors hover:bg-[#23252a] disabled:cursor-not-allowed disabled:bg-[#cfd2d8] disabled:text-white/80"
disabled={isApplyingAngleEdit}
onClick={() => void handleApplyAngleEdit()}
type="button"