mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 13:22:57 +00:00
Feature/ stylization 3 (#60)
* fix style picking css, preliminarily, ipadaptor ui * fix the sizing of ipadapter * IP Adapter workflow works * react-easy-crop has much better UI
This commit is contained in:
12 files changed
+469
-84
No files matched your search
Generated
+6
-16
@@ -19,7 +19,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-easy-crop": "^5.0.8",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"uuid": "^10.0.0",
|
||||
@@ -4102,11 +4102,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-wheel": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
|
||||
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
|
||||
},
|
||||
"node_modules/npm": {
|
||||
"version": "10.8.3",
|
||||
"resolved": "https://registry.npmjs.org/npm/-/npm-10.8.3.tgz",
|
||||
@@ -7737,17 +7732,12 @@
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-easy-crop": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.8.tgz",
|
||||
"integrity": "sha512-KjulxXhR5iM7+ATN2sGCum/IyDxGw7xT0dFoGcqUP+ysaPU5Ka7gnrDa2tUHFHUoMNyPrVZ05QA+uvMgC5ym/g==",
|
||||
"dependencies": {
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"node_modules/react-image-crop": {
|
||||
"version": "11.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.0.7.tgz",
|
||||
"integrity": "sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.4.0",
|
||||
"react-dom": ">=16.4.0"
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-easy-crop": "^5.0.8",
|
||||
"react-easy-crop": "^5.1.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"uuid": "^10.0.0",
|
||||
|
||||
+10
-10
@@ -9,20 +9,20 @@ import { PostHogProvider } from "posthog-js/react";
|
||||
import "./global.css";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
|
||||
function PostHogInit() {
|
||||
const apiKey = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb";
|
||||
posthog.init(apiKey, {
|
||||
api_host: "https://us.i.posthog.com/",
|
||||
ui_host: "https://us.i.posthog.com/",
|
||||
});
|
||||
}
|
||||
// function PostHogInit() {
|
||||
// const apiKey = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb";
|
||||
// posthog.init(apiKey, {
|
||||
// api_host: "https://us.i.posthog.com/",
|
||||
// ui_host: "https://us.i.posthog.com/",
|
||||
// });
|
||||
// }
|
||||
|
||||
const App = () => {
|
||||
const useStrictMode = false;
|
||||
useRenderCounter("App");
|
||||
useEffect(() => {
|
||||
PostHogInit();
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// PostHogInit();
|
||||
// }, []);
|
||||
if (useStrictMode) {
|
||||
return (
|
||||
<StrictMode>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
import {
|
||||
faWandSparkles,
|
||||
faChevronLeft,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faWandSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components/ui";
|
||||
import {
|
||||
@@ -115,35 +112,25 @@ export const DialogAiStylize = ({
|
||||
)}
|
||||
style={{ height: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<DialogTitle className="font-bold">Use AI to Stylize</DialogTitle>
|
||||
|
||||
{panelState === SubPanelNames.BASIC && (
|
||||
<>
|
||||
<DialogTitle className="font-bold">Use AI to Stylize</DialogTitle>
|
||||
<SubPanelBasic
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
positivePrompt={positivePrompt}
|
||||
negativePrompt={negativePrompt}
|
||||
onSelectedArtStyle={onSelectedArtStyle}
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
onChangePositivePrompt={onChangePositivePrompt}
|
||||
onChangePanel={onChangePanel}
|
||||
/>
|
||||
</>
|
||||
<SubPanelBasic
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
positivePrompt={positivePrompt}
|
||||
negativePrompt={negativePrompt}
|
||||
onSelectedArtStyle={onSelectedArtStyle}
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
onChangePositivePrompt={onChangePositivePrompt}
|
||||
onChangePanel={onChangePanel}
|
||||
/>
|
||||
)}
|
||||
{panelState === SubPanelNames.ADVANCED && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => onChangePanel(SubPanelNames.BASIC)}
|
||||
variant="tertiary"
|
||||
className="my-1 w-fit"
|
||||
icon={faChevronLeft}
|
||||
>
|
||||
Back to Basic Options
|
||||
</Button>
|
||||
<SubPanelAdvance
|
||||
aiStylizeProps={aiStylizeProps}
|
||||
onStylizeOptionsChanged={setStylizeOptions}
|
||||
/>
|
||||
</>
|
||||
<SubPanelAdvance
|
||||
aiStylizeProps={aiStylizeProps}
|
||||
onStylizeOptionsChanged={setStylizeOptions}
|
||||
onChangePanel={onChangePanel}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-center gap-4">
|
||||
<Button onClick={closeCallback} variant="secondary">
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import Cropper, { Point, Area } from "react-easy-crop";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFileImage, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components/ui";
|
||||
import getCroppedImg, {
|
||||
base64ToFile,
|
||||
uploadIPA,
|
||||
fetchIPAUrl,
|
||||
} from "./utilities";
|
||||
|
||||
type Crop = {
|
||||
location: Point;
|
||||
zoom: number;
|
||||
cropArea?: Area;
|
||||
croppedAreaPixels?: Area;
|
||||
};
|
||||
type State = {
|
||||
ipaUrl?: string;
|
||||
imageFileString?: string;
|
||||
crop: Crop;
|
||||
};
|
||||
|
||||
const initialCrop = {
|
||||
location: { x: 0, y: 0 },
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
export const IPAdapter = ({
|
||||
ipaToken,
|
||||
onUploadedIPA,
|
||||
}: {
|
||||
ipaToken?: string;
|
||||
onUploadedIPA: (newIpaToken: string) => void;
|
||||
}) => {
|
||||
const FILE_TYPES = ["JPG", "PNG", "JPEG"];
|
||||
const [state, setState] = useState<State>({
|
||||
crop: initialCrop,
|
||||
});
|
||||
const { imageFileString, ipaUrl, crop } = state;
|
||||
|
||||
const prevIpaToken = useRef<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const setIpaUrl = async () => {
|
||||
if (ipaToken && ipaToken !== prevIpaToken.current) {
|
||||
const ipaUrl = await fetchIPAUrl(ipaToken);
|
||||
setState((curr) => ({ ...curr, ipaUrl }));
|
||||
}
|
||||
};
|
||||
setIpaUrl();
|
||||
}, [ipaToken]);
|
||||
|
||||
// this loads the file from drag-and-drop file uploader
|
||||
// turn it into a string for the react-image-crop to use
|
||||
const onFileChange = useCallback((file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setState((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
imageFileString: reader.result as string,
|
||||
};
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}, []);
|
||||
|
||||
// handler for react-image-crop's completion
|
||||
const onCropComplete = (croppedArea: Area, croppedAreaPixels: Area) => {
|
||||
console.log("on CROP COMPLETE", croppedArea, croppedAreaPixels);
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
crop: {
|
||||
...curr.crop,
|
||||
cropArea: croppedArea,
|
||||
croppedAreaPixels: croppedAreaPixels,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const onSaveCrop = async () => {
|
||||
if (!crop.croppedAreaPixels || !imageFileString) {
|
||||
console.error("Error: do not have the assets for saving a cropped image");
|
||||
setState({ crop: initialCrop }); // complete reset
|
||||
return;
|
||||
}
|
||||
const croppedImageUrl = await getCroppedImg(
|
||||
imageFileString,
|
||||
crop.croppedAreaPixels,
|
||||
);
|
||||
if (!croppedImageUrl) {
|
||||
console.error("Error: failed to crop image");
|
||||
setState({ crop: initialCrop }); // complete reset
|
||||
return;
|
||||
}
|
||||
// converts base64 cropped image into a file for upload
|
||||
const croppedImageFile = base64ToFile(croppedImageUrl, "cropped-image.jpg");
|
||||
const ipaToken = await uploadIPA(croppedImageFile);
|
||||
if (ipaToken) {
|
||||
onUploadedIPA(ipaToken);
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteImage = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setState({ crop: initialCrop });
|
||||
};
|
||||
|
||||
const DragAndDropZone = () => {
|
||||
return (
|
||||
<div className="border-ui-controls-button/50 bg-brand-secondary hover:bg-ui-controls-button/40 flex cursor-pointer items-center gap-3.5 rounded-lg border-2 border-dashed p-3 transition-all duration-150">
|
||||
<FontAwesomeIcon icon={faFileImage} className="text-3xl" />
|
||||
<div className="flex flex-col gap-0 text-sm">
|
||||
<p className="font-medium">
|
||||
<u>Upload an image</u> or drop it here
|
||||
</p>
|
||||
<p className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
{FILE_TYPES.join(", ").toString()} supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<div>
|
||||
<h4>IP Adapter</h4>
|
||||
Additional Style Reference Image{" "}
|
||||
<span className="text-xs font-normal">(Optional)</span>
|
||||
</div>
|
||||
|
||||
{!imageFileString &&
|
||||
!ipaUrl && ( //case of no file
|
||||
<FileUploader
|
||||
handleChange={onFileChange}
|
||||
name="file"
|
||||
types={FILE_TYPES}
|
||||
>
|
||||
<DragAndDropZone />
|
||||
</FileUploader>
|
||||
)}
|
||||
|
||||
{imageFileString &&
|
||||
!ipaUrl && ( // file staged for cropping
|
||||
<div
|
||||
title="Crop Reference Image"
|
||||
className="flex grow flex-col gap-4"
|
||||
>
|
||||
<div className="relative w-full grow">
|
||||
<div className="absolute h-full w-full overflow-scroll rounded bg-black/25">
|
||||
<Cropper
|
||||
image={imageFileString}
|
||||
crop={crop.location}
|
||||
zoom={crop.zoom}
|
||||
aspect={1}
|
||||
onCropChange={(newCropLocation) => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
crop: {
|
||||
...curr.crop,
|
||||
location: newCropLocation,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
onCropComplete={onCropComplete}
|
||||
onZoomChange={(newZoom) => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
zoom: newZoom,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
crop: initialCrop,
|
||||
imageFileString: undefined,
|
||||
}));
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSaveCrop} variant="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ipaUrl && ( //case of having ipaFileUrl
|
||||
<div className="relative">
|
||||
<FileUploader
|
||||
handleChange={onFileChange}
|
||||
name="file"
|
||||
types={FILE_TYPES}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 border-white/10 bg-black/25">
|
||||
<img
|
||||
src={ipaUrl}
|
||||
alt="IPAdapter"
|
||||
className="object-contain"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</div>
|
||||
</FileUploader>
|
||||
<Button
|
||||
className="text-md absolute right-2 top-2 z-10 h-6 w-6 rounded-full font-medium hover:bg-primary"
|
||||
onClick={onDeleteImage}
|
||||
icon={faXmark}
|
||||
variant="action"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { StyleOptionSwitches } from "./StyleOptionSwitches";
|
||||
import { AIStylizeProps } from "../utilities";
|
||||
import { StyleStrengthSlider } from "./StyleStrengthSlider";
|
||||
import { IPAdapter } from "./IPAdapter";
|
||||
import { Button } from "~/components/ui";
|
||||
import { SubPanelNames } from "../enums";
|
||||
|
||||
export const SubPanelAdvance = ({
|
||||
aiStylizeProps,
|
||||
onStylizeOptionsChanged,
|
||||
onChangePanel,
|
||||
}: {
|
||||
aiStylizeProps: AIStylizeProps;
|
||||
onStylizeOptionsChanged: (newOptions: Partial<AIStylizeProps>) => void;
|
||||
onChangePanel: (newP: SubPanelNames) => void;
|
||||
}) => {
|
||||
const {
|
||||
cinematic,
|
||||
@@ -19,11 +25,18 @@ export const SubPanelAdvance = ({
|
||||
} = aiStylizeProps;
|
||||
|
||||
return (
|
||||
<div className="flex w-full grow gap-2">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<h6>IP Adapter</h6>
|
||||
<div className="flex w-full grow gap-4">
|
||||
<div className="w-2/3">
|
||||
<IPAdapter
|
||||
ipaToken={aiStylizeProps.globalIpaMediaToken}
|
||||
onUploadedIPA={(newToken) =>
|
||||
onStylizeOptionsChanged({
|
||||
globalIpaMediaToken: newToken,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-1/3 flex-col gap-4">
|
||||
<div className="flex w-1/3 flex-col justify-between gap-4">
|
||||
<h4>Advanced Options</h4>
|
||||
<StyleOptionSwitches
|
||||
faceDetail={faceDetail}
|
||||
@@ -38,6 +51,15 @@ export const SubPanelAdvance = ({
|
||||
styleStrength={styleStrength}
|
||||
onStylizeOptionsChanged={onStylizeOptionsChanged}
|
||||
/>
|
||||
<span className="grow" />
|
||||
<Button
|
||||
onClick={() => onChangePanel(SubPanelNames.BASIC)}
|
||||
variant="tertiary"
|
||||
className="w-fit self-end"
|
||||
icon={faChevronLeft}
|
||||
>
|
||||
Back to Basic Options
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { Area } from "react-easy-crop";
|
||||
|
||||
export const base64ToFile = (base64: string, filename: string): File => {
|
||||
const arr = base64.split(",");
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
|
||||
return new File([u8arr], filename, { type: mime });
|
||||
};
|
||||
|
||||
export const uploadIPA = async (file: File) => {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const response = await mediaUploadApi.UploadImage({
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
if (!response.success || !response.data) {
|
||||
console.error("Error IPA: Fail in uploading image");
|
||||
return;
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const fetchIPAUrl = async (ipaToken: string) => {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
try {
|
||||
const response = await mediaFilesApi.GetMediaFileByToken({
|
||||
mediaFileToken: ipaToken,
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
return response.data.media_links.cdn_url;
|
||||
} else {
|
||||
console.error("Failed to fetch media file:", response.errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching media file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createImage = (url: string) =>
|
||||
new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener("load", () => resolve(image));
|
||||
image.addEventListener("error", (error) => reject(error));
|
||||
image.setAttribute("crossOrigin", "anonymous");
|
||||
// needed to avoid cross-origin issues
|
||||
image.src = url;
|
||||
});
|
||||
|
||||
export function getRadianAngle(degreeValue: number) {
|
||||
return (degreeValue * Math.PI) / 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new bounding area of a rotated rectangle.
|
||||
*/
|
||||
export function rotateSize(width: number, height: number, rotation: number) {
|
||||
const rotRad = getRadianAngle(rotation);
|
||||
|
||||
return {
|
||||
width:
|
||||
Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
|
||||
height:
|
||||
Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
|
||||
*/
|
||||
export default async function getCroppedImg(
|
||||
imageSrc: string,
|
||||
pixelCrop: Area,
|
||||
rotation = 0,
|
||||
flip = { horizontal: false, vertical: false },
|
||||
) {
|
||||
const image = await createImage(imageSrc);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (!ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rotRad = getRadianAngle(rotation);
|
||||
|
||||
// calculate bounding box of the rotated image
|
||||
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
|
||||
image.width,
|
||||
image.height,
|
||||
rotation,
|
||||
);
|
||||
|
||||
// set canvas size to match the bounding box
|
||||
canvas.width = bBoxWidth;
|
||||
canvas.height = bBoxHeight;
|
||||
|
||||
// translate canvas context to a central location to allow rotating and flipping around the center
|
||||
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
|
||||
ctx.rotate(rotRad);
|
||||
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
|
||||
ctx.translate(-image.width / 2, -image.height / 2);
|
||||
|
||||
// draw rotated image
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
const croppedCanvas = document.createElement("canvas");
|
||||
|
||||
const croppedCtx = croppedCanvas.getContext("2d");
|
||||
|
||||
if (!croppedCtx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the size of the cropped canvas
|
||||
croppedCanvas.width = pixelCrop.width;
|
||||
croppedCanvas.height = pixelCrop.height;
|
||||
|
||||
// Draw the cropped image onto the new canvas
|
||||
croppedCtx.drawImage(
|
||||
canvas,
|
||||
pixelCrop.x,
|
||||
pixelCrop.y,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
0,
|
||||
0,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
);
|
||||
|
||||
// As Base64 string
|
||||
// return croppedCanvas.toDataURL('image/jpeg');
|
||||
|
||||
// As a blob
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
croppedCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
} else {
|
||||
reject(new Error("Canvas is empty"));
|
||||
}
|
||||
}, "image/jpeg");
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export const ArtStyleItem = ({
|
||||
return (
|
||||
<button
|
||||
className={twMerge(
|
||||
"relative aspect-video cursor-pointer overflow-hidden rounded-lg border-2 transition-colors",
|
||||
"relative aspect-video w-full cursor-pointer overflow-hidden rounded-lg border-2 transition-colors",
|
||||
transitionTimingStyles,
|
||||
selected
|
||||
? "border-primary"
|
||||
@@ -47,12 +47,7 @@ export const ArtStyleItem = ({
|
||||
)}
|
||||
onClick={handleSelected}
|
||||
>
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
src={imageSrc}
|
||||
{...imgProps}
|
||||
alt={label}
|
||||
/>
|
||||
<img className="object-cover" src={imageSrc} {...imgProps} alt={label} />
|
||||
<div className="absolute bottom-0 left-0 h-1/2 w-full bg-gradient-to-t from-ui-panel" />
|
||||
<h4 className="absolute bottom-0 left-1 truncate text-start text-sm drop-shadow-md">
|
||||
{label}
|
||||
|
||||
@@ -11,8 +11,8 @@ export const ArtStyleSelector = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-0 flex-auto overflow-hidden">
|
||||
<div className="overflow-y-auto">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="w-full overflow-y-auto">
|
||||
<div className="grid w-full grid-cols-3 gap-2">
|
||||
{styleList.map((style) => (
|
||||
<ArtStyleItem
|
||||
key={style.type}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ArtStyleSelector } from "./ArtStyleSelector";
|
||||
import { Prompts } from "./Prompts";
|
||||
|
||||
// import { Button } from "~/components/ui";
|
||||
import { Button } from "~/components/ui";
|
||||
import { ArtStyleNames, SubPanelNames } from "../enums";
|
||||
|
||||
export const SubPanelBasic = ({
|
||||
@@ -13,7 +13,7 @@ export const SubPanelBasic = ({
|
||||
onSelectedArtStyle,
|
||||
onChangePositivePrompt,
|
||||
onChangeNegativePrompt,
|
||||
// onChangePanel,
|
||||
onChangePanel,
|
||||
}: {
|
||||
selectedArtStyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
@@ -24,7 +24,7 @@ export const SubPanelBasic = ({
|
||||
onChangePanel: (newP: SubPanelNames) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full grow gap-2">
|
||||
<div className="flex w-full grow gap-4">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<h4>Pick a Style</h4>
|
||||
<ArtStyleSelector
|
||||
@@ -41,14 +41,14 @@ export const SubPanelBasic = ({
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
/>
|
||||
<span className="grow" />
|
||||
{/* <Button
|
||||
<Button
|
||||
onClick={() => onChangePanel(SubPanelNames.ADVANCED)}
|
||||
variant="tertiary"
|
||||
className="w-fit self-end"
|
||||
>
|
||||
Advanced Options
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "./data/randomTextList";
|
||||
|
||||
export type AIStylizeProps = {
|
||||
globalIpaMediaToken?: string;
|
||||
selectedArtStyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
|
||||
@@ -2221,7 +2221,7 @@ fs-minipass@^3.0.0, fs-minipass@^3.0.3:
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
@@ -3225,7 +3225,7 @@ normalize-range@^0.1.2:
|
||||
|
||||
normalize-wheel@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
|
||||
integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==
|
||||
|
||||
npm-audit-report@^5.0.0:
|
||||
@@ -3779,10 +3779,10 @@ react-drag-drop-files@^2.3.10:
|
||||
prop-types "^15.7.2"
|
||||
styled-components "^5.3.0"
|
||||
|
||||
react-easy-crop@^5.0.8:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.8.tgz"
|
||||
integrity sha512-KjulxXhR5iM7+ATN2sGCum/IyDxGw7xT0dFoGcqUP+ysaPU5Ka7gnrDa2tUHFHUoMNyPrVZ05QA+uvMgC5ym/g==
|
||||
react-easy-crop@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-5.1.0.tgz#79897cbd8fbc93f64817276a02de525582a6334b"
|
||||
integrity sha512-UsYeF/N7zoqtfOSD+2xSt1nRaoBYCI2YLkzmq+hi+aVepS4/bAMhbrLwJtDAP60jsVzWRiQCX7JG+ZtfWcHsiw==
|
||||
dependencies:
|
||||
normalize-wheel "^1.0.1"
|
||||
tslib "^2.0.1"
|
||||
@@ -4394,7 +4394,12 @@ tsconfck@^3.0.3:
|
||||
resolved "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz"
|
||||
integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.4.0:
|
||||
tslib@^2.0.1:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b"
|
||||
integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
|
||||
Reference in new issue
Block a user