Size dropped image nodes to intrinsic aspect ratio

Dropped images landed in fixed 420x280 nodes and rendered with
object-cover, cropping portraits and wide photos. Read the image's
natural size during ingestion and fit the node frame to the aspect
ratio (landscape caps height, portrait caps width) so the full
picture stays visible like the reference product.
This commit is contained in:
134 2026-09-18 18:34:23 +08:00
parent 885cab9fca
commit b4b6e1a738

View File

@ -5375,14 +5375,54 @@ export function LiblibVideoCanvasReplica({
}; };
}, []); }, []);
const readImageIntrinsicSize = useCallback(async (file: File) => {
if (!file.type.startsWith("image/")) return null;
try {
const bitmap = await createImageBitmap(file);
const size = { width: bitmap.width, height: bitmap.height };
bitmap.close();
return size;
} catch {
try {
return await new Promise<{ width: number; height: number }>((resolve, reject) => {
const url = URL.createObjectURL(file);
const image = new Image();
image.onload = () => {
URL.revokeObjectURL(url);
resolve({ width: image.naturalWidth, height: image.naturalHeight });
};
image.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("image decode failed"));
};
image.src = url;
});
} catch {
return null;
}
}
}, []);
const addUploadedFilesAtPoint = useCallback( const addUploadedFilesAtPoint = useCallback(
(files: File[], center: { x: number; y: number }, connectTargetId?: string) => { async (files: File[], center: { x: number; y: number }, connectTargetId?: string) => {
if (files.length === 0) return; if (files.length === 0) return;
const { files: batch, errors } = prepareCanvasMediaFiles(files); const { files: batch, errors } = prepareCanvasMediaFiles(files);
if (errors.length) toast.error(errors.slice(0, 3).join("\n")); if (errors.length) toast.error(errors.slice(0, 3).join("\n"));
if (!batch.length) return; if (!batch.length) return;
const sizes = await Promise.all(batch.map(file => readImageIntrinsicSize(file)));
const newNodes = batch.map((file, index) => { const newNodes = batch.map((file, index) => {
const created = createUploadedMediaNode(file, "", 0, 0, nodes.length + index + 1); const created = createUploadedMediaNode(file, "", 0, 0, nodes.length + index + 1);
const size = sizes[index];
if (size && size.width > 0 && size.height > 0) {
const aspect = size.width / size.height;
if (aspect >= 1) {
created.width = 420;
created.height = Math.min(560, Math.max(180, Math.round(420 / aspect)));
} else {
created.height = 560;
created.width = Math.min(420, Math.max(220, Math.round(560 * aspect)));
}
}
return { ...created, x: Math.round(center.x - created.width / 2 + index * 450), return { ...created, x: Math.round(center.x - created.width / 2 + index * 450),
y: Math.round(center.y - created.height / 2), y: Math.round(center.y - created.height / 2),
draftValues: { ...created.draftValues, uploadStatus: "uploading" } }; draftValues: { ...created.draftValues, uploadStatus: "uploading" } };
@ -5412,12 +5452,12 @@ export function LiblibVideoCanvasReplica({
} }
}); });
}, },
[addNodes, nodes.length, uploadCanvasMediaFile] [addNodes, nodes.length, readImageIntrinsicSize, uploadCanvasMediaFile]
); );
const addUploadedFiles = useCallback( const addUploadedFiles = useCallback(
(files: File[]) => { (files: File[]) => {
addUploadedFilesAtPoint(files, toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2)); void addUploadedFilesAtPoint(files, toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2));
}, },
[addUploadedFilesAtPoint, toCanvasPoint] [addUploadedFilesAtPoint, toCanvasPoint]
); );
@ -5763,7 +5803,7 @@ export function LiblibVideoCanvasReplica({
const addCanvasCommandUploadedFiles = useCallback( const addCanvasCommandUploadedFiles = useCallback(
(files: File[]) => { (files: File[]) => {
if (!canvasCommand) return; if (!canvasCommand) return;
addUploadedFilesAtPoint(files, { x: canvasCommand.canvasX, y: canvasCommand.canvasY }); void addUploadedFilesAtPoint(files, { x: canvasCommand.canvasX, y: canvasCommand.canvasY });
}, },
[addUploadedFilesAtPoint, canvasCommand] [addUploadedFilesAtPoint, canvasCommand]
); );
@ -10111,7 +10151,7 @@ export function LiblibVideoCanvasReplica({
if (isBusy) { toast.error("画布正在加载,请稍候再拖入素材"); return; } if (isBusy) { toast.error("画布正在加载,请稍候再拖入素材"); return; }
const files = Array.from(event.dataTransfer.files); const files = Array.from(event.dataTransfer.files);
if (!files.length) { toast.error("未能读取拖入的文件,请从桌面直接拖拽素材后松开"); return; } if (!files.length) { toast.error("未能读取拖入的文件,请从桌面直接拖拽素材后松开"); return; }
addUploadedFilesAtPoint(files, toCanvasPoint(event.clientX, event.clientY)); void addUploadedFilesAtPoint(files, toCanvasPoint(event.clientX, event.clientY));
}} }}
> >
<div className="pointer-events-none absolute inset-0" style={{backgroundImage:"radial-gradient(circle,rgba(255,255,255,0.14) 0.7px,transparent 0.7px)",backgroundPosition:"0.5px 0.5px",backgroundSize:"16px 16px"}} /> <div className="pointer-events-none absolute inset-0" style={{backgroundImage:"radial-gradient(circle,rgba(255,255,255,0.14) 0.7px,transparent 0.7px)",backgroundPosition:"0.5px 0.5px",backgroundSize:"16px 16px"}} />
@ -10547,7 +10587,7 @@ export function LiblibVideoCanvasReplica({
setNodes(current => current.map(node => node.id === selectedNode.id ? {...node,draftValues:{...node.draftValues,smartEditDraft:{...(node.draftValues?.smartEditDraft as Record<string,any> || {}),assets:((node.draftValues?.smartEditDraft as Record<string,any>)?.assets || []).filter((asset:{id:string}) => asset.id !== source)}}} : node)); setNodes(current => current.map(node => node.id === selectedNode.id ? {...node,draftValues:{...node.draftValues,smartEditDraft:{...(node.draftValues?.smartEditDraft as Record<string,any> || {}),assets:((node.draftValues?.smartEditDraft as Record<string,any>)?.assets || []).filter((asset:{id:string}) => asset.id !== source)}}} : node));
}} }}
onReference={() => setLineConnectState({sourceId:null,targetId:selectedNode.id,cursorCanvasX:selectedNode.x-80,cursorCanvasY:selectedNode.y+selectedNode.height/2})} onReference={() => setLineConnectState({sourceId:null,targetId:selectedNode.id,cursorCanvasX:selectedNode.x-80,cursorCanvasY:selectedNode.y+selectedNode.height/2})}
onUpload={files => addUploadedFilesAtPoint(files,{x:selectedNode.x-600,y:selectedNode.y},selectedNode.id)} onUpload={files => void addUploadedFilesAtPoint(files,{x:selectedNode.x-600,y:selectedNode.y},selectedNode.id)}
onSubmit={async () => { onSubmit={async () => {
assertConnectedInputsReady(selectedTargetSourceNodes); assertConnectedInputsReady(selectedTargetSourceNodes);
const draft = resolveSmartEditIntent((selectedNode.draftValues?.smartEditDraft || {}) as Record<string,any>); const draft = resolveSmartEditIntent((selectedNode.draftValues?.smartEditDraft || {}) as Record<string,any>);