feat: create the sketch to character design mvp

This commit is contained in:
godlike0108
2023-12-11 19:47:25 +08:00
parent 3383db5655
commit 48d97d3da1
11 changed files with 1090 additions and 36 deletions

View File

@@ -10,14 +10,19 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"fabric": "^5.3.0",
"firebase": "^10.7.1",
"model": "../model",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.20.1",
"react-router-dom": "^6.20.1"
"react-router-dom": "^6.20.1",
"styled-components": "^6.1.1",
"styled-normalize": "^8.1.0"
},
"devDependencies": {
"@types/fabric": "^5.3.6",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",

927
app/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,9 @@ import { Suspense } from "react";
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
import routes from "~react-pages";
import ThemeProvider from "./styles/ThemeProvider";
import theme from "./styles/theme";
import GlobalStyles from "./styles/GlobalStyles";
const Content = () => {
return useRoutes(routes);
@@ -9,10 +12,15 @@ const Content = () => {
export default function App() {
return (
<Router>
<Suspense fallback={<p>Loading...</p>}>
<Content />
</Suspense>
</Router>
<>
<GlobalStyles />
<ThemeProvider theme={theme}>
<Router>
<Suspense fallback={<p>Loading...</p>}>
<Content />
</Suspense>
</Router>
</ThemeProvider>
</>
);
}

View File

@@ -0,0 +1,68 @@
import { useEffect, useRef } from "react";
import styled from "styled-components";
import { fabric } from "fabric";
const Container = styled.div`
width: 100%;
flex: 1;
border: 1px solid red;
`;
const RawCanvas = styled.canvas`
border: 1px solid black;
width: 100% !important;
height: auto% !important;
`;
const dataURLToBlob = (dataURL: string) => {
const byteString = atob(dataURL.split(",")[1]);
const mimeString = dataURL.split(",")[0].split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
};
type SketchCanvas = {
onUpdate: (val: Blob) => void;
};
const SketchCanvas: React.FC<SketchCanvas> = ({ onUpdate }) => {
const canvasRef = useRef(null);
const fabricCanvasRef = useRef<fabric.Canvas>();
useEffect(() => {
// Initialize the Fabric canvas
fabricCanvasRef.current = new fabric.Canvas(canvasRef.current, {
isDrawingMode: true,
});
// Configure drawing brush
fabricCanvasRef.current.freeDrawingBrush.color = "black";
fabricCanvasRef.current.freeDrawingBrush.width = 5;
fabricCanvasRef.current.on("mouse:up", () => {
fabricCanvasRef.current?.renderAll();
const dataURL =
fabricCanvasRef.current?.toDataURL({ format: "png" }) || "";
const blob = dataURLToBlob(dataURL);
onUpdate(blob);
});
return () => {
// Dispose of the canvas on unmount
fabricCanvasRef.current?.dispose();
};
}, []);
return (
<Container>
<RawCanvas width={600} height={600} ref={canvasRef} />
</Container>
);
};
export default SketchCanvas;

View File

@@ -0,0 +1,16 @@
import styled from "styled-components";
const Container = styled.div`
width: 100%;
flex: 1;
`;
const Preview: React.FC<{ data: Blob }> = ({ data }) => {
return (
<Container>
<img src={URL.createObjectURL(data)} />
</Container>
);
};
export default Preview;

View File

@@ -1,5 +1,60 @@
import { useState } from "react";
import axios from "axios";
import styled from "styled-components";
import Canvas from "../components/Canvas";
import Preview from "../components/Preview";
const Container = styled.div`
display: flex;
align-items: stretch;
`;
const HomePage = () => {
return <div>WIP</div>;
const [sketchBlob, setSketchBlob] = useState<Blob>(new Blob());
const [prompt, setPrompt] = useState("");
const [preview, setPreview] = useState<Blob>(new Blob());
const convert = () => {
const formData = new FormData();
formData.append("sketch_file", sketchBlob);
formData.append("prompt", prompt);
axios
.post(
"https://clipdrop-api.co/sketch-to-image/v1/sketch-to-image",
formData,
{
headers: {
"x-api-key": import.meta.env.VITE_CLIPDROP_SECRET,
},
responseType: "blob",
}
)
.then((response) => {
setPreview(response.data);
})
.catch((error) => {
console.error("Error:", error);
});
};
return (
<>
<Container>
<div>
<Canvas onUpdate={(val) => setSketchBlob(val)} />
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</div>
<Preview data={preview} />
</Container>
<div>
<button onClick={convert}>Convert</button>
</div>
</>
);
};
export default HomePage;

View File

@@ -0,0 +1,12 @@
import { createGlobalStyle, css } from "styled-components";
import { normalize } from "styled-normalize";
const styles = css`
${normalize}
`;
const GlobalStyles = createGlobalStyle`
${styles}
`;
export default GlobalStyles;

View File

@@ -0,0 +1,14 @@
import { PropsWithChildren } from "react";
import {
ThemeProvider as RawThemeProvider,
DefaultTheme,
} from "styled-components";
const ThemeProvider = ({
theme,
children,
}: PropsWithChildren<{ theme: DefaultTheme }>) => {
return <RawThemeProvider theme={theme}>{children}</RawThemeProvider>;
};
export default ThemeProvider;

3
app/src/styles/theme.ts Normal file
View File

@@ -0,0 +1,3 @@
const theme = {};
export default theme;

View File

@@ -7,7 +7,7 @@
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"moduleResolution": "Node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,

View File

@@ -3,7 +3,7 @@
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]