ai-chatbot/lib/agents/psd-image-placement.ts
Your Name 8c0ce434dc
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: correct psd masked image placement
2026-04-02 10:14:45 +08:00

153 lines
3.8 KiB
TypeScript

import type {
AgentPsdTemplateImageContentBounds,
AgentPsdTemplateReplaceMode,
} from "@/lib/agents/psd-template-config";
export interface AgentPsdImagePlacement {
scale: number;
offsetX: number;
offsetY: number;
}
export interface AgentPsdImageSize {
width: number;
height: number;
}
export function getDefaultAgentPsdImagePlacement(): AgentPsdImagePlacement {
return {
scale: 1,
offsetX: 0,
offsetY: 0,
};
}
export function normalizeAgentPsdImagePlacement(
value: Partial<AgentPsdImagePlacement> | null | undefined
): AgentPsdImagePlacement {
const scale = Number(value?.scale);
const offsetX = Number(value?.offsetX);
const offsetY = Number(value?.offsetY);
return {
scale: Number.isFinite(scale) && scale > 0 ? Math.max(0.1, Math.min(8, scale)) : 1,
offsetX: Number.isFinite(offsetX) ? offsetX : 0,
offsetY: Number.isFinite(offsetY) ? offsetY : 0,
};
}
function resolveBaseFittedSize(params: {
containerWidth: number;
containerHeight: number;
imageWidth: number;
imageHeight: number;
replaceMode: AgentPsdTemplateReplaceMode;
}) {
const { containerWidth, containerHeight, imageWidth, imageHeight, replaceMode } = params;
const safeImageWidth = Math.max(1, imageWidth);
const safeImageHeight = Math.max(1, imageHeight);
if (replaceMode === "stretch") {
return {
width: containerWidth,
height: containerHeight,
left: 0,
top: 0,
};
}
const fitRatio =
replaceMode === "contain"
? Math.min(containerWidth / safeImageWidth, containerHeight / safeImageHeight)
: Math.max(containerWidth / safeImageWidth, containerHeight / safeImageHeight);
const width = safeImageWidth * fitRatio;
const height = safeImageHeight * fitRatio;
return {
width,
height,
left: (containerWidth - width) / 2,
top: (containerHeight - height) / 2,
};
}
function normalizeContainerBounds(params: {
containerWidth: number;
containerHeight: number;
contentBounds?: AgentPsdTemplateImageContentBounds;
}) {
const { containerWidth, containerHeight, contentBounds } = params;
if (!contentBounds) {
return {
left: 0,
top: 0,
width: containerWidth,
height: containerHeight,
};
}
const left = Math.max(0, Math.min(containerWidth - 1, Math.round(contentBounds.left)));
const top = Math.max(0, Math.min(containerHeight - 1, Math.round(contentBounds.top)));
const width = Math.max(
1,
Math.min(Math.round(contentBounds.width), Math.max(1, containerWidth - left))
);
const height = Math.max(
1,
Math.min(Math.round(contentBounds.height), Math.max(1, containerHeight - top))
);
return {
left,
top,
width,
height,
};
}
export function resolveAgentPsdImagePlacementFrame(params: {
containerWidth: number;
containerHeight: number;
imageWidth: number;
imageHeight: number;
replaceMode: AgentPsdTemplateReplaceMode;
contentBounds?: AgentPsdTemplateImageContentBounds;
placement?: Partial<AgentPsdImagePlacement> | null;
}) {
const { containerWidth, containerHeight, imageWidth, imageHeight, replaceMode } = params;
const placement = normalizeAgentPsdImagePlacement(params.placement);
const contentBounds = normalizeContainerBounds({
containerWidth,
containerHeight,
contentBounds: params.contentBounds,
});
const baseFrame = resolveBaseFittedSize({
containerWidth: contentBounds.width,
containerHeight: contentBounds.height,
imageWidth,
imageHeight,
replaceMode,
});
const width = baseFrame.width * placement.scale;
const height = baseFrame.height * placement.scale;
const left =
contentBounds.left +
baseFrame.left -
(width - baseFrame.width) / 2 +
placement.offsetX;
const top =
contentBounds.top +
baseFrame.top -
(height - baseFrame.height) / 2 +
placement.offsetY;
return {
width,
height,
left,
top,
};
}