import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; import { Segmented, Slider, Tooltip } from "antd"; import { motion, useReducedMotion } from "motion/react"; import { Camera, RotateCcw, Send, X } from "lucide-react"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; export type CanvasImageAngleParams = { horizontalAngle: number; pitchAngle: number; cameraDistance: number; wideAngle: boolean; }; const defaultParams: CanvasImageAngleParams = { horizontalAngle: 0, pitchAngle: 0, cameraDistance: 4.8, wideAngle: false }; const presets = [ { label: "正面", horizontalAngle: 0, pitchAngle: 0 }, { label: "左侧", horizontalAngle: -90, pitchAngle: 0 }, { label: "右侧", horizontalAngle: 90, pitchAngle: 0 }, { label: "背面", horizontalAngle: 180, pitchAngle: 0 }, { label: "俯拍", horizontalAngle: 0, pitchAngle: 55 }, { label: "仰拍", horizontalAngle: 0, pitchAngle: -45 }, ]; export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl: string; onClose: () => void; onConfirm: (params: CanvasImageAngleParams) => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const reducedMotion = useReducedMotion(); const [params, setParams] = useState(defaultParams); const dragRef = useRef<{ x: number; y: number; horizontal: number; pitch: number } | null>(null); const update = (key: Key, value: CanvasImageAngleParams[Key]) => setParams((current) => ({ ...current, [key]: value })); const startCameraDrag = (event: ReactPointerEvent) => { event.preventDefault(); event.stopPropagation(); event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { x: event.clientX, y: event.clientY, horizontal: params.horizontalAngle, pitch: params.pitchAngle }; }; const moveCamera = (event: ReactPointerEvent) => { const drag = dragRef.current; if (!drag) return; update("horizontalAngle", clamp(Math.round(drag.horizontal + (event.clientX - drag.x) * 0.8), -180, 180)); update("pitchAngle", clamp(Math.round(drag.pitch - (event.clientY - drag.y) * 0.55), -75, 75)); }; const marker = cameraMarker(params.horizontalAngle, params.pitchAngle); const activePreset = presets.find((preset) => preset.horizontalAngle === params.horizontalAngle && preset.pitchAngle === params.pitchAngle); return ( event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} >
多角度编辑器
{presets.map((preset) => setParams((current) => ({ ...current, horizontalAngle: preset.horizontalAngle, pitchAngle: preset.pitchAngle }))} />)}
{ dragRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }} onPointerCancel={() => { dragRef.current = null; }}> 角度参考
拖动调整摄影机
update("horizontalAngle", value)} /> update("pitchAngle", value)} /> update("cameraDistance", value)} />
镜头 update("wideAngle", value === "wide")} />
当前视角 {activePreset?.label || "自定义"} {params.horizontalAngle}° / {params.pitchAngle}° · {params.cameraDistance.toFixed(1)} {distanceLabel(params.cameraDistance)} onConfirm(params)}>生成新角度
); } function AngleSlider({ label, value, min, max, step = 1, suffix, onChange }: { label: string; value: number; min: number; max: number; step?: number; suffix: string; onChange: (value: number) => void }) { return
{label}{Number.isInteger(value) ? value : value.toFixed(1)}{suffix.startsWith("°") ? suffix : ""}{!suffix.startsWith("°") ? ` ${suffix}` : ""}
; } function AnglePresetButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onClick?: () => void }) { return ; } function GlobeGrid({ color }: { color: string }) { return ; } function cameraMarker(horizontal: number, pitch: number) { const horizontalRad = horizontal * Math.PI / 180; const pitchRad = pitch * Math.PI / 180; return { x: 50 + Math.sin(horizontalRad) * Math.cos(pitchRad) * 40, y: 50 - Math.sin(pitchRad) * 40 }; } function previewTransform(params: CanvasImageAngleParams) { const scale = clamp(1.08 - params.cameraDistance * 0.035 - (params.wideAngle ? 0.08 : 0), 0.72, 1.08); return `perspective(520px) rotateY(${params.horizontalAngle * -0.18}deg) rotateX(${params.pitchAngle * 0.16}deg) scale(${scale})`; } function distanceLabel(value: number) { if (value <= 3) return "近景"; if (value >= 7) return "全景"; return "中景"; } function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); }