Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⛏️ Generate an image from a Prompt using SVG.io #151

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ label {
user-select: none;
}

input[type=number] {
input[type=number], input[type=text], textarea {
width: 100%;
padding: 0.3rem;
margin-bottom: 0.5rem;
Expand Down Expand Up @@ -236,6 +236,10 @@ select:not(:disabled):hover,
select:focus,
input[type=number]:hover,
input[type=number]:focus,
input[type=text]:hover,
input[type=text]:focus,
textarea:hover,
textarea:focus,
input[type=checkbox]:focus {
box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.25);
}
Expand Down
98 changes: 87 additions & 11 deletions src/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const defaultVisualizationOptions = {
colorPathsByStrokeOrder: false,
}

const defaultSvgIoOptions = {
apiKey: '',
prompt: '',
status: '',
vecType: 'FLAT_VECTOR'
}

const initialState = {
connected: true,

Expand All @@ -34,6 +41,7 @@ const initialState = {
// UI state
planOptions: defaultPlanOptions,
visualizationOptions: defaultVisualizationOptions,
svgIoOptions: defaultSvgIoOptions,

// Options used to produce the current value of |plan|.
plannedOptions: null as PlanOptions | null,
Expand Down Expand Up @@ -64,7 +72,9 @@ function reducer(state: State, action: any): State {
case "SET_PLAN_OPTION":
return {...state, planOptions: {...state.planOptions, ...action.value}};
case "SET_VISUALIZATION_OPTION":
return {...state, visualizationOptions: {...state.visualizationOptions, ...action.value}};
return {...state, visualizationOptions: {...state.visualizationOptions, ...action.value }};
case "SET_SVGIO_OPTION":
return {...state, svgIoOptions: {...state.svgIoOptions, ...action.value }}
case "SET_DEVICE_INFO":
return {...state, deviceInfo: action.value};
case "SET_PAUSED":
Expand Down Expand Up @@ -182,7 +192,7 @@ class WebSerialDriver implements Driver {
if (!penIsUp) {
// Move to the pen up position, or 50% if no position was found
const penMotion = plan.motions.find((motion): motion is PenMotion => motion instanceof PenMotion)
const penUpPosition = penMotion ? Math.max(penMotion.initialPos, penMotion.finalPos) : device.penPctToPos(50)
const penUpPosition = penMotion ? Math.max(penMotion.initialPos, penMotion.finalPos) : device.penPctToPos(50)
await this.ebb.setPenHeight(penUpPosition, 1000);
}
if (this.oncancelled) this.oncancelled()
Expand Down Expand Up @@ -348,7 +358,7 @@ class SaxiDriver implements Driver {
}

public setPenHeight(height: number, rate: number) {
this.send({ c: "setPenHeight", p: {height, rate} });
this.send({c: "setPenHeight", p: {height, rate}});
}

public limp() { this.send({ c: "limp" }); }
Expand Down Expand Up @@ -518,6 +528,66 @@ function VisualizationOptions({ state }: { state: State }) {
</>;
}

/**
* Options to get an AI-Generated SVG image.
* Use svg.io API: https://api.svg.io/v1/docs
*/
function SvgIoOptions({ state }: { state: State }) {
const { apiKey, prompt, vecType, status } = state.svgIoOptions;
const dispatch = useContext(DispatchContext);
// Call the API and load the generated image
const generateImage = async () => {
dispatch({ type: "SET_SVGIO_OPTION", value: { status: 'Generating ...' } })
try {
const apiResp = await fetch('https://api.svg.io/v1/generate-image', {
method: 'post',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt, style: vecType, negativePrompt: '' })
});
dispatch({ type: "SET_SVGIO_OPTION", value: { status: 'Loading ...' } })
const imageUrl = (await apiResp.json()).data[0].svgUrl;
const imgResp = await fetch(imageUrl);
const imgData = await imgResp.text();
dispatch(setPaths(readSvg(imgData)));
} catch (error) {
console.error(error);
} finally {
dispatch({ type: "SET_SVGIO_OPTION", value: { status: '' } });
}
}
return <>
<div>
<label title="Need an API Key">svg.io API Key
<input type="text" value={apiKey}
onChange={(e) => dispatch({ type: "SET_SVGIO_OPTION", value: { apiKey: e.target.value } })} />
</label>
<label>Type
<select value={vecType}
onChange={(e) => dispatch({ type: "SET_SVGIO_OPTION", value: { vecType: e.target.value } })}>
<option value={"FLAT_VECTOR"}>Flat</option>
<option value={"FLAT_VECTOR_OUTLINE"}>Outline</option>
<option value={"FLAT_VECTOR_SILHOUETTE"}>Silhouette</option>
<option value={"FLAT_VECTOR_ONE_LINE_ART"}>One Line Art</option>
<option value={"FLAT_VECTOR_LINE_ART"}>Line Art</option>
</select>
</label>
<label title="prompt">Prompt
<textarea value={prompt} size={"3"}
onChange={(e) => dispatch({ type: "SET_SVGIO_OPTION", value: { prompt: e.target.value } })}>
</textarea>
</label>
</div>
{apiKey !== '' && prompt !== ''
? <div>
{status ? <span>{status}</span> : <button onClick={generateImage}>Generate!</button>}
</div>
: ''}
</>;
}

function SwapPaperSizesButton({ onClick }: { onClick: () => void }) {
return <svg
className="paper-sizes__swap"
Expand Down Expand Up @@ -589,9 +659,9 @@ function PaperConfig({state}: {state: State}) {
</div>
<div>
<label>
rotate drawing (degrees)
rotate drawing (degrees)
<div className="horizontal-labels">
<img src={rotateDrawingIcon} alt="rotate drawing (degrees)"/>
<img src={rotateDrawingIcon} alt="rotate drawing (degrees)" />
<input type="number" min="-90" step="90" max="360" placeholder="0" value={state.planOptions.rotateDrawing}
onInput={(e) => {
const value = (e.target as HTMLInputElement).value;
Expand Down Expand Up @@ -630,7 +700,7 @@ function PlanStatistics({plan}: {plan: Plan}) {

function TimeLeft({plan, progress, currentMotionStartedTime, paused}: {
plan: Plan;
progress: number | null;
progress: number | null;
currentMotionStartedTime: Date | null;
paused: boolean;
}) {
Expand Down Expand Up @@ -931,7 +1001,7 @@ function PlanOptions({state}: {state: State}) {
<div className="horizontal-labels">

<label title="point-joining radius (mm)" >
<img src={pointJoinRadiusIcon} alt="point-joining radius (mm)"/>
<img src={pointJoinRadiusIcon} alt="point-joining radius (mm)" />
<input
type="number"
value={state.planOptions.pointJoinRadius}
Expand Down Expand Up @@ -1211,14 +1281,20 @@ function Root() {
<VisualizationOptions state={state} />
</div>
</details>
<details>
<summary className="section-header">AI</summary>
<div className="section-body">
<SvgIoOptions state={state} />
</div>
</details>
<div className="spacer" />
<div className="control-panel-bottom">
<div className="section-header">plot</div>
<div className="section-body section-body__plot">
<PlanStatistics plan={plan} />
<TimeLeft
plan={plan}
progress={state.progress}
<TimeLeft
plan={plan}
progress={state.progress}
currentMotionStartedTime={currentMotionStartedTime}
paused={state.paused}
/>
Expand All @@ -1233,7 +1309,7 @@ function Root() {
plan={plan}
/>
<PlanLoader isPlanning={isPlanning} isLoadingFile={isLoadingFile} />
{showDragTarget ? <DragTarget/> : null}
{showDragTarget ? <DragTarget /> : null}
</div>
</div>
</DispatchContext.Provider>;
Expand Down