mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
Implement cropped mask export
This commit is contained in:
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 ? (
|
||||
|
||||
Reference in new issue
Block a user