Implement cropped mask export

This commit is contained in:
Tanish Manku committed 2025-07-16 14:27:54 -07:00
1 parent 23cdf65fd7
commit 56faca60e4
8 files changed
+96 -4

No files matched your search

@@ -0,0 +1,14 @@
export const normalizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement => {
const newCanvas = document.createElement("canvas");
newCanvas.width = width;
newCanvas.height = height;
const ctx = newCanvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get canvas context");
}
ctx.imageSmoothingEnabled = true;
ctx.drawImage(canvas, 0, 0, width, height);
return newCanvas;
}
@@ -0,0 +1,20 @@
import { faImage } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export type BaseImageSelectorProps = {
onImageSelect: (imageUrl: string) => void;
}
const BaseImageSelector = ({
onImageSelect
}: BaseImageSelectorProps) => {
return (
<div className="flex items-center justify-center bg-ui-panel border-ui-panel-border border-4">
<FontAwesomeIcon icon={faImage} className="text-6xl text-ui-text-secondary" />
<span className="text-ui-text-secondary text-lg ml-2">Click to upload or drag and drop an image here to edit</span>
<div className="mt-4">
</div>
</div>
)
}
@@ -32,6 +32,8 @@ export type EditPaintSurfaceProps = {
onSelectionChange?: (isSelecting: boolean) => void;
stageRef: React.RefObject<Konva.Stage>;
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>;
leftPanelRef: React.RefObject<Konva.Layer>;
rectRef: React.RefObject<Konva.Rect>;
};
export const EditPaintSurface = ({
@@ -45,6 +47,8 @@ export const EditPaintSurface = ({
onSelectionChange,
stageRef,
transformerRefs,
leftPanelRef,
rectRef
}: EditPaintSurfaceProps) => {
// switch off to be preview panel mode.
const singlePaneMode = true
@@ -54,7 +58,6 @@ export const EditPaintSurface = ({
const [snapshotImage, setSnapshotImage] = useState<HTMLImageElement | null>(
null,
);
const leftPanelRef = React.useRef<Konva.Layer>(null);
const rightContainerRef = React.useRef<HTMLDivElement>(null);
const cursorLayerRef = React.useRef<Konva.Layer>(null);
const cursorShapeRef = React.useRef<Konva.Circle>(null);
@@ -977,7 +980,6 @@ export const EditPaintSurface = ({
}
}, [selectedNodeIds]);
return (
<SplitPane
singlePaneMode={singlePaneMode}
@@ -1011,6 +1013,7 @@ export const EditPaintSurface = ({
}}
>
<Rect
ref={rectRef}
x={0}
y={0}
width={store.getAspectRatioDimensions().width}
@@ -20,6 +20,7 @@ import PromptEditor from "./PromptEditor/PromptEditor";
import { AspectRatioType } from "../PageDraw/stores/SceneState";
import { ActiveEditTool, useEditStore } from "./stores/EditState";
import { EditPaintSurface } from "./EditPaintSurface";
import { normalizeCanvas } from "~/Helpers/CanvasHelpers";
const PageEdit = () => {
//useStateSceneLoader();
@@ -32,6 +33,8 @@ const PageEdit = () => {
const [selectedModel, setSelectedModel] = useState<string>("GPT-4o");
// Create refs for stage and image
const stageRef = useRef<Konva.Stage>({} as Konva.Stage);
const leftPanelRef = useRef<Konva.Layer>({} as Konva.Layer);
const rectRef = useRef<Konva.Rect>({} as Konva.Rect);
const transformerRefs = useRef<{ [key: string]: Konva.Transformer }>({});
// Use the Zustand store
@@ -179,9 +182,47 @@ const PageEdit = () => {
x: (containerWidth - canvasW) / 2,
y: (containerHeight - canvasH) / 2
});
}
// Create a function to use the left layer ref and download the bitmap from it
const downloadLeftPanelBitmap = () => {
if (!stageRef.current || !leftPanelRef.current || !rectRef.current) {
console.error("Stage or left panel ref is not available");
return;
}
const layer = leftPanelRef.current;
// Get the canvas area that's covered by the image/rectangle
const rect = rectRef.current;
const layerCrop = layer.toCanvas({
x: stageRef.current.x(),
y: stageRef.current.y(),
width: rect.width() * stageRef.current.scaleX(),
height: rect.height() * stageRef.current.scaleY(),
pixelRatio: 1 / stageRef.current.scaleX(),
});
// Using the pixelRatio scaling may result in off-by-one rounding errors,
// So we re-fit the image to a canvas of precise size.
const fittedCanvas = normalizeCanvas(layerCrop, rect.width(), rect.height());
const downloadCallback = (blob: Blob | null) => {
if (!blob) {
console.error("Failed to create blob from canvas");
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "artcraft_snapshot.png";
link.click();
}
fittedCanvas.toBlob(downloadCallback, "image/png", 1.0);
};
return (
<>
<div
@@ -205,6 +246,7 @@ const PageEdit = () => {
onModeChange={(mode: string) => { store.setActiveTool(mode as ActiveEditTool) }}
selectedMode={store.activeTool}
onEnqueuePressed={onEnqueuedPressed}
onGenerateClick={downloadLeftPanelBitmap}
/>
</div>
<SideToolbar
@@ -330,6 +372,8 @@ const PageEdit = () => {
onSelectionChange={setIsSelecting}
stageRef={stageRef}
transformerRefs={transformerRefs}
leftPanelRef={leftPanelRef}
rectRef={rectRef}
/>
</ContextMenuContainer>
</div>
@@ -11,6 +11,7 @@ const PromptEditor: React.FC<PromptEditorProps> = ({
onEnqueuePressed,
onModeChange,
selectedMode,
onGenerateClick,
}) => {
return (
<div className="mx-auto flex w-full max-w-3xl flex-col space-y-2">
@@ -26,6 +27,7 @@ const PromptEditor: React.FC<PromptEditorProps> = ({
onEnqueuePressed={onEnqueuePressed}
onModeSelectionChange={onModeChange}
selectedMode={selectedMode}
onGenerateClick={onGenerateClick}
/>
</JobProvider>
</div>
@@ -8,6 +8,7 @@ export interface PromptEditorProps {
onEnqueuePressed?: () => void;
onModeChange?: (mode: string) => void;
selectedMode?: string;
onGenerateClick: () => void;
}
export type AspectRatio = '1:1' | '3:2' | '2:3';
@@ -26,6 +26,9 @@ interface EditState {
cursorPosition: { x: number; y: number } | null;
cursorVisible: boolean;
// Base image state
baseImage: ImageBitmap | null;
// Actions
addNode: (node: Node) => void;
removeNode: (id: string, shouldSaveState?: boolean) => void;
@@ -131,6 +134,9 @@ export const useEditStore = create<EditState>((set, get) => ({
cursorPosition: null,
cursorVisible: false,
// Base image initial state
baseImage: null,
// Add initial aspect ratio state
aspectRatioType: AspectRatioType.NONE,
@@ -69,6 +69,7 @@ interface PromptBoxEditProps {
onEnqueuePressed?: () => void | Promise<void>;
onModeSelectionChange?: (mode: string) => void;
selectedMode?: string;
onGenerateClick: () => void;
}
export const PromptBoxEdit = ({
@@ -79,6 +80,7 @@ export const PromptBoxEdit = ({
onEnqueuePressed,
onModeSelectionChange,
selectedMode,
onGenerateClick
}: PromptBoxEditProps) => {
useSignals();
@@ -553,7 +555,7 @@ export const PromptBoxEdit = ({
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
onClick={handleEnqueue}
onClick={onGenerateClick}
disabled={isEnqueueing || !prompt.trim()}
>
{isEnqueueing ? (