polish realtime canvas editing interactions
This commit is contained in:
parent
215abdfafe
commit
5ca33daa75
@ -35,7 +35,6 @@ import {
|
||||
Upload,
|
||||
Sparkles,
|
||||
Save,
|
||||
Trash2,
|
||||
Undo2,
|
||||
Redo2,
|
||||
Send,
|
||||
@ -686,6 +685,7 @@ export function RealtimeCanvasDialog({
|
||||
const imageLayersRef = useRef<ImageLayer[]>([]);
|
||||
const colorInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const layerObjectUrlsRef = useRef<Set<string>>(new Set());
|
||||
const sidebarDraggingLayerIdRef = useRef<string | null>(null);
|
||||
const drawingHasContentRef = useRef(false);
|
||||
const dragDepthRef = useRef(0);
|
||||
|
||||
@ -702,7 +702,7 @@ export function RealtimeCanvasDialog({
|
||||
const [, setStatusMessage] = useState("等待连接");
|
||||
const [prompt, setPrompt] = useState(FALLBACK_PROMPT);
|
||||
const [previewUrl, setPreviewUrl] = useState("");
|
||||
const [brushMode, setBrushMode] = useState<RealtimeBrushMode>("brush");
|
||||
const [brushMode, setBrushMode] = useState<RealtimeBrushMode>("select");
|
||||
const [brushColor, setBrushColor] = useState<string>(DEFAULT_BRUSH_COLOR);
|
||||
const [canvasBackground, setCanvasBackground] = useState<string>(canvasBackgroundRef.current);
|
||||
const [imageLayers, setImageLayers] = useState<ImageLayer[]>([]);
|
||||
@ -850,28 +850,35 @@ export function RealtimeCanvasDialog({
|
||||
if (!ctx) return;
|
||||
const renderSeq = layerRenderSeqRef.current + 1;
|
||||
layerRenderSeqRef.current = renderSeq;
|
||||
await drawImageLayersToContext(ctx, layerCanvas.width, layerCanvas.height, { renderSeq });
|
||||
const cssWidth = Math.max(1, parseFloat(layerCanvas.style.width) || layerCanvas.width);
|
||||
const cssHeight = Math.max(1, parseFloat(layerCanvas.style.height) || layerCanvas.height);
|
||||
await drawImageLayersToContext(ctx, cssWidth, cssHeight, { renderSeq });
|
||||
}, [drawImageLayersToContext]);
|
||||
|
||||
const buildCompositeCanvas = useCallback(async () => {
|
||||
const drawingCanvas = canvasRef.current;
|
||||
if (!drawingCanvas) return null;
|
||||
const cssWidth = Math.max(1, parseFloat(drawingCanvas.style.width) || drawingCanvas.width);
|
||||
const cssHeight = Math.max(1, parseFloat(drawingCanvas.style.height) || drawingCanvas.height);
|
||||
const dpr = Math.max(1, drawingCanvas.width / cssWidth);
|
||||
|
||||
const compositeCanvas = document.createElement("canvas");
|
||||
compositeCanvas.width = drawingCanvas.width;
|
||||
compositeCanvas.height = drawingCanvas.height;
|
||||
const compositeCtx = compositeCanvas.getContext("2d");
|
||||
if (!compositeCtx) return null;
|
||||
compositeCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
compositeCtx.scale(dpr, dpr);
|
||||
|
||||
const didRenderLayers = await drawImageLayersToContext(
|
||||
compositeCtx,
|
||||
compositeCanvas.width,
|
||||
compositeCanvas.height
|
||||
cssWidth,
|
||||
cssHeight
|
||||
);
|
||||
if (!didRenderLayers) {
|
||||
return null;
|
||||
}
|
||||
compositeCtx.drawImage(drawingCanvas, 0, 0);
|
||||
compositeCtx.drawImage(drawingCanvas, 0, 0, cssWidth, cssHeight);
|
||||
return compositeCanvas;
|
||||
}, [drawImageLayersToContext]);
|
||||
|
||||
@ -1374,6 +1381,30 @@ export function RealtimeCanvasDialog({
|
||||
[adaptiveThrottleMs, markCanvasDirty, pumpFlush]
|
||||
);
|
||||
|
||||
const reorderLayersFromSidebar = useCallback(
|
||||
(draggedLayerId: string, targetLayerId: string) => {
|
||||
if (!draggedLayerId || !targetLayerId || draggedLayerId === targetLayerId) return;
|
||||
|
||||
const displayLayers = [...imageLayersRef.current].reverse();
|
||||
const sourceIndex = displayLayers.findIndex((layer) => layer.id === draggedLayerId);
|
||||
const targetIndex = displayLayers.findIndex((layer) => layer.id === targetLayerId);
|
||||
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) return;
|
||||
|
||||
const nextDisplayLayers = [...displayLayers];
|
||||
const [movedLayer] = nextDisplayLayers.splice(sourceIndex, 1);
|
||||
nextDisplayLayers.splice(targetIndex, 0, movedLayer);
|
||||
const nextLayers = nextDisplayLayers.reverse();
|
||||
|
||||
imageLayersRef.current = nextLayers;
|
||||
setImageLayers(nextLayers);
|
||||
setSelectedLayerId(draggedLayerId);
|
||||
saveHistory(nextLayers);
|
||||
markCanvasDirty(null);
|
||||
scheduleFlush(true, false, "refine");
|
||||
},
|
||||
[markCanvasDirty, saveHistory, scheduleFlush]
|
||||
);
|
||||
|
||||
const importFilesAsLayers = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (files.length === 0) return 0;
|
||||
@ -1780,7 +1811,7 @@ export function RealtimeCanvasDialog({
|
||||
if (!point) return;
|
||||
pointerDownRef.current = true;
|
||||
lastPointRef.current = point;
|
||||
if (selectedImageLayer) {
|
||||
if (brushMode === "select" && selectedImageLayer) {
|
||||
const resizeHandle = getResizeHandleAtPoint(point, selectedImageLayer);
|
||||
if (resizeHandle) {
|
||||
resizingLayerIdRef.current = selectedImageLayer.id;
|
||||
@ -1814,7 +1845,7 @@ export function RealtimeCanvasDialog({
|
||||
return;
|
||||
}
|
||||
}
|
||||
const hitLayer = findLayerAtPoint(point);
|
||||
const hitLayer = brushMode === "select" ? findLayerAtPoint(point) : null;
|
||||
if (hitLayer) {
|
||||
setSelectedLayerId(hitLayer.id);
|
||||
draggingLayerIdRef.current = hitLayer.id;
|
||||
@ -1883,9 +1914,9 @@ export function RealtimeCanvasDialog({
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
if (cursorRef.current) {
|
||||
cursorRef.current.style.opacity =
|
||||
!selectedImageLayer && (brushMode === "brush" || brushMode === "eraser") ? "1" : "0";
|
||||
brushMode === "brush" || brushMode === "eraser" ? "1" : "0";
|
||||
}
|
||||
}, [brushMode, selectedImageLayer]);
|
||||
}, [brushMode]);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event: ReactPointerEvent<HTMLCanvasElement>) => {
|
||||
@ -1893,18 +1924,18 @@ export function RealtimeCanvasDialog({
|
||||
// Update cursor
|
||||
const point = getCanvasPoint(event);
|
||||
if (point && cursorRef.current) {
|
||||
const resizeHandle = getResizeHandleAtPoint(point, selectedImageLayer);
|
||||
const resizeHandle =
|
||||
brushMode === "select" ? getResizeHandleAtPoint(point, selectedImageLayer) : null;
|
||||
event.currentTarget.style.cursor = resizeHandle
|
||||
? getResizeCursor(resizeHandle)
|
||||
: selectedImageLayer
|
||||
: brushMode === "select" && selectedImageLayer
|
||||
? "move"
|
||||
: brushMode === "brush" || brushMode === "eraser"
|
||||
? "none"
|
||||
: brushMode === "select"
|
||||
? "default"
|
||||
: "crosshair";
|
||||
const showCursor =
|
||||
!selectedImageLayer && (brushMode === "brush" || brushMode === "eraser");
|
||||
const showCursor = brushMode === "brush" || brushMode === "eraser";
|
||||
if (showCursor) {
|
||||
cursorRef.current.style.opacity = "1";
|
||||
cursorRef.current.style.transform = `translate(${point.x - brushSize / 2}px, ${point.y - brushSize / 2}px)`;
|
||||
@ -2737,7 +2768,7 @@ export function RealtimeCanvasDialog({
|
||||
ref={canvasRef}
|
||||
className={cn(
|
||||
"absolute inset-0 touch-none shadow-sm block",
|
||||
selectedImageLayer
|
||||
brushMode === "select" && selectedImageLayer
|
||||
? "cursor-move"
|
||||
: brushMode === "brush" || brushMode === "eraser"
|
||||
? "cursor-none"
|
||||
@ -2756,7 +2787,7 @@ export function RealtimeCanvasDialog({
|
||||
ref={previewCanvasRef}
|
||||
className="pointer-events-none absolute inset-0 touch-none"
|
||||
/>
|
||||
{selectedImageLayer ? (
|
||||
{selectedImageLayer && brushMode === "select" ? (
|
||||
<div
|
||||
className="pointer-events-none absolute border border-dashed border-sky-500 shadow-[0_0_0_1px_rgba(14,165,233,0.25)]"
|
||||
style={{
|
||||
@ -2767,7 +2798,7 @@ export function RealtimeCanvasDialog({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{selectedImageLayer
|
||||
{selectedImageLayer && brushMode === "select"
|
||||
? ([
|
||||
{ key: "nw", left: selectedImageLayer.x, top: selectedImageLayer.y },
|
||||
{
|
||||
@ -2796,26 +2827,6 @@ export function RealtimeCanvasDialog({
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{selectedImageLayer ? (
|
||||
<div
|
||||
className="absolute z-20"
|
||||
style={{
|
||||
left: Math.max(8, selectedImageLayer.x + selectedImageLayer.width - 84),
|
||||
top: Math.max(8, selectedImageLayer.y - 42),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8 rounded-full bg-black px-3 text-xs font-medium text-white shadow-lg hover:bg-black/90"
|
||||
onClick={deleteSelectedImageLayer}
|
||||
title="删除图层"
|
||||
>
|
||||
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Custom Brush Cursor */}
|
||||
<div
|
||||
ref={cursorRef}
|
||||
@ -2845,19 +2856,38 @@ export function RealtimeCanvasDialog({
|
||||
<button
|
||||
key={layer.id}
|
||||
type="button"
|
||||
draggable
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center rounded-[18px] border bg-white p-1.5 shadow-sm transition hover:border-black/20",
|
||||
"flex w-full items-center justify-center rounded-[16px] p-1 transition hover:bg-zinc-50",
|
||||
selectedLayerId === layer.id
|
||||
? "border-black/20 ring-2 ring-black/5"
|
||||
: "border-black/10"
|
||||
? "bg-zinc-100 ring-2 ring-black/6"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => setSelectedLayerId(layer.id)}
|
||||
onDragStart={() => {
|
||||
sidebarDraggingLayerIdRef.current = layer.id;
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
sidebarDraggingLayerIdRef.current = null;
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
const draggingId = sidebarDraggingLayerIdRef.current;
|
||||
if (draggingId) {
|
||||
reorderLayersFromSidebar(draggingId, layer.id);
|
||||
}
|
||||
sidebarDraggingLayerIdRef.current = null;
|
||||
}}
|
||||
title={layer.name}
|
||||
>
|
||||
<img
|
||||
src={/^https?:\/\//i.test(layer.src) ? buildImageProxyUrl(layer.src) : layer.src}
|
||||
alt={layer.name}
|
||||
className="h-11 w-11 rounded-[14px] border border-black/10 object-cover"
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</button>
|
||||
))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user