Spaces:
Running
Running
made changes to the code
Browse files- app/api/process/route.ts +11 -14
- app/nodes.tsx +608 -178
app/api/process/route.ts
CHANGED
|
@@ -315,7 +315,8 @@ The result should look like all subjects were photographed together in the same
|
|
| 315 |
if (params.stylePreset) {
|
| 316 |
const strength = params.styleStrength || 50;
|
| 317 |
const styleMap: { [key: string]: string } = {
|
| 318 |
-
"90s-anime": "Convert the image to 90's anime art style with classic anime features
|
|
|
|
| 319 |
"mha": "Transform the image into My Hero Academia anime style with modern crisp lines, vibrant colors, dynamic character design, and heroic aesthetics typical of the series",
|
| 320 |
"dbz": "Apply Dragon Ball Z anime style with sharp angular features, spiky hair, intense expressions, bold outlines, high contrast shading, and dramatic action-oriented aesthetics",
|
| 321 |
"ukiyo-e": "Render in traditional Japanese Ukiyo-e woodblock print style with flat colors, bold outlines, stylized waves and clouds, traditional Japanese artistic elements",
|
|
@@ -325,10 +326,9 @@ The result should look like all subjects were photographed together in the same
|
|
| 325 |
"van-gogh": "Apply Post-Impressionist Van Gogh style with thick swirling brushstrokes, vibrant yellows and blues, expressive texture, starry night-like patterns",
|
| 326 |
"simpsons": "Convert to The Simpsons cartoon style with yellow skin tones, simple rounded features, bulging eyes, overbite, Matt Groening's distinctive character design",
|
| 327 |
"family-guy": "Transform into Family Guy animation style with rounded character design, simplified features, Seth MacFarlane's distinctive art style, bold outlines",
|
| 328 |
-
"arcane": "Apply Arcane (League of Legends) style with painterly brush-stroke textures, neon rim lighting, hand-painted feel, stylized realism, vibrant color grading",
|
| 329 |
"wildwest": "Render in Wild West style with dusty desert tones, sunset orange lighting, vintage film grain, cowboy aesthetic, sepia and brown color palette",
|
| 330 |
-
"
|
| 331 |
-
"
|
| 332 |
};
|
| 333 |
|
| 334 |
const styleDescription = styleMap[params.stylePreset];
|
|
@@ -347,7 +347,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 347 |
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition) {
|
| 348 |
const cameraSettings: string[] = [];
|
| 349 |
if (params.focalLength) {
|
| 350 |
-
if (params.focalLength === "8mm
|
| 351 |
cameraSettings.push("Apply 8mm fisheye lens effect with 180-degree circular distortion");
|
| 352 |
} else {
|
| 353 |
cameraSettings.push(`Focal Length: ${params.focalLength}`);
|
|
@@ -358,12 +358,10 @@ The result should look like all subjects were photographed together in the same
|
|
| 358 |
if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
|
| 359 |
if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
|
| 360 |
if (params.iso) cameraSettings.push(`${params.iso}`);
|
| 361 |
-
if (params.filmStyle) cameraSettings.push(`${params.filmStyle}`);
|
| 362 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 363 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 364 |
-
if (params.
|
| 365 |
-
|
| 366 |
-
if (cameraSettings.length > 0) {
|
| 367 |
prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
|
| 368 |
}
|
| 369 |
}
|
|
@@ -375,8 +373,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 375 |
|
| 376 |
// Lightning effects
|
| 377 |
if (params.lightingImage && params.selectedLighting) {
|
| 378 |
-
|
| 379 |
-
prompts.push(`Apply ${params.selectedLighting} lighting effect to the person in the image. Adjust the lighting, shadows, and highlights to match the reference lighting style shown in the second image. Maintain the person's appearance, pose, and background while enhancing the lighting at ${lightingStrength}% intensity.`);
|
| 380 |
|
| 381 |
try {
|
| 382 |
const lightingRef = await toInlineDataFromAny(params.lightingImage);
|
|
@@ -390,8 +387,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 390 |
|
| 391 |
// Pose modifications
|
| 392 |
if (params.poseImage && params.selectedPose) {
|
| 393 |
-
|
| 394 |
-
prompts.push(`Change the pose of the person in the first image to match the pose shown in the reference image. Keep the person's facial features, clothing, and overall appearance the same, only modify their body position and pose to match the reference at ${poseStrength}% strength.`);
|
| 395 |
|
| 396 |
try {
|
| 397 |
const poseRef = await toInlineDataFromAny(params.poseImage);
|
|
@@ -413,6 +409,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 413 |
if (face.changeHairstyle) modifications.push(`change hairstyle to ${face.changeHairstyle}`);
|
| 414 |
if (face.facialExpression) modifications.push(`change facial expression to ${face.facialExpression}`);
|
| 415 |
if (face.beardStyle) modifications.push(`add/change beard to ${face.beardStyle}`);
|
|
|
|
| 416 |
|
| 417 |
if (modifications.length > 0) {
|
| 418 |
prompts.push(`Face modifications: ${modifications.join(", ")}`);
|
|
@@ -421,7 +418,7 @@ The result should look like all subjects were photographed together in the same
|
|
| 421 |
|
| 422 |
// Combine all prompts
|
| 423 |
let prompt = prompts.length > 0
|
| 424 |
-
? prompts.join("\n\n") + "\
|
| 425 |
: "Process this image with high quality output.";
|
| 426 |
|
| 427 |
// Add the custom prompt if provided
|
|
|
|
| 315 |
if (params.stylePreset) {
|
| 316 |
const strength = params.styleStrength || 50;
|
| 317 |
const styleMap: { [key: string]: string } = {
|
| 318 |
+
"90s-anime": "Convert the image to 90's anime art style with classic anime features",
|
| 319 |
+
"Gibhli": "Apply Studio Ghibli style with vibrant colors, detailed character design, and fantasy elements typical of Studio Ghibli movies",
|
| 320 |
"mha": "Transform the image into My Hero Academia anime style with modern crisp lines, vibrant colors, dynamic character design, and heroic aesthetics typical of the series",
|
| 321 |
"dbz": "Apply Dragon Ball Z anime style with sharp angular features, spiky hair, intense expressions, bold outlines, high contrast shading, and dramatic action-oriented aesthetics",
|
| 322 |
"ukiyo-e": "Render in traditional Japanese Ukiyo-e woodblock print style with flat colors, bold outlines, stylized waves and clouds, traditional Japanese artistic elements",
|
|
|
|
| 326 |
"van-gogh": "Apply Post-Impressionist Van Gogh style with thick swirling brushstrokes, vibrant yellows and blues, expressive texture, starry night-like patterns",
|
| 327 |
"simpsons": "Convert to The Simpsons cartoon style with yellow skin tones, simple rounded features, bulging eyes, overbite, Matt Groening's distinctive character design",
|
| 328 |
"family-guy": "Transform into Family Guy animation style with rounded character design, simplified features, Seth MacFarlane's distinctive art style, bold outlines",
|
|
|
|
| 329 |
"wildwest": "Render in Wild West style with dusty desert tones, sunset orange lighting, vintage film grain, cowboy aesthetic, sepia and brown color palette",
|
| 330 |
+
"star-wars": "Apply Star Wars style with vibrant colors, detailed character design, and fantasy elements typical of Star Wars movies",
|
| 331 |
+
"star-trek": "Apply Star Trek style with vibrant colors, detailed character design, and fantasy elements typical of Star Trek movies",
|
| 332 |
};
|
| 333 |
|
| 334 |
const styleDescription = styleMap[params.stylePreset];
|
|
|
|
| 347 |
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition) {
|
| 348 |
const cameraSettings: string[] = [];
|
| 349 |
if (params.focalLength) {
|
| 350 |
+
if (params.focalLength === "8mm") {
|
| 351 |
cameraSettings.push("Apply 8mm fisheye lens effect with 180-degree circular distortion");
|
| 352 |
} else {
|
| 353 |
cameraSettings.push(`Focal Length: ${params.focalLength}`);
|
|
|
|
| 358 |
if (params.whiteBalance) cameraSettings.push(`White Balance: ${params.whiteBalance}`);
|
| 359 |
if (params.angle) cameraSettings.push(`Camera Angle: ${params.angle}`);
|
| 360 |
if (params.iso) cameraSettings.push(`${params.iso}`);
|
|
|
|
| 361 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 362 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 363 |
+
if (params.filmStyle == "RAW") cameraSettings.push(`Convert the image to RAW image`); else cameraSettings.push(`Film Style: ${params.filmStyle}`);
|
| 364 |
+
if (cameraSettings.length > 0) {
|
|
|
|
| 365 |
prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
|
| 366 |
}
|
| 367 |
}
|
|
|
|
| 373 |
|
| 374 |
// Lightning effects
|
| 375 |
if (params.lightingImage && params.selectedLighting) {
|
| 376 |
+
prompts.push(`Apply ${params.selectedLighting} lighting effect to the person in the image. Adjust the lighting, shadows, and highlights to match the reference lighting style shown in the second image. Maintain the person's appearance, pose, and background`);
|
|
|
|
| 377 |
|
| 378 |
try {
|
| 379 |
const lightingRef = await toInlineDataFromAny(params.lightingImage);
|
|
|
|
| 387 |
|
| 388 |
// Pose modifications
|
| 389 |
if (params.poseImage && params.selectedPose) {
|
| 390 |
+
prompts.push(`Change the pose of the person in the first image to match the pose shown in the reference image. Keep the person's facial features, clothing, and overall appearance the same, only modify their body position and pose to match the reference`);
|
|
|
|
| 391 |
|
| 392 |
try {
|
| 393 |
const poseRef = await toInlineDataFromAny(params.poseImage);
|
|
|
|
| 409 |
if (face.changeHairstyle) modifications.push(`change hairstyle to ${face.changeHairstyle}`);
|
| 410 |
if (face.facialExpression) modifications.push(`change facial expression to ${face.facialExpression}`);
|
| 411 |
if (face.beardStyle) modifications.push(`add/change beard to ${face.beardStyle}`);
|
| 412 |
+
if (face.selectedMakeup) modifications.push(`add a face makeup with red colors on cheeks and and some yellow blue colors around the eye area`);
|
| 413 |
|
| 414 |
if (modifications.length > 0) {
|
| 415 |
prompts.push(`Face modifications: ${modifications.join(", ")}`);
|
|
|
|
| 418 |
|
| 419 |
// Combine all prompts
|
| 420 |
let prompt = prompts.length > 0
|
| 421 |
+
? prompts.join("\n\n") + "\nApply all these modifications while maintaining the person's identity and keeping unspecified aspects unchanged."
|
| 422 |
: "Process this image with high quality output.";
|
| 423 |
|
| 424 |
// Add the custom prompt if provided
|
app/nodes.tsx
CHANGED
|
@@ -1,22 +1,52 @@
|
|
| 1 |
/**
|
| 2 |
-
* NODE COMPONENT VIEWS
|
| 3 |
*
|
| 4 |
-
* This file contains all the visual node components for the Nano Banana Editor
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
*
|
| 8 |
-
* - Connection port rendering
|
| 9 |
-
* - Processing status display
|
| 10 |
-
* - Image upload/preview
|
| 11 |
*
|
| 12 |
-
*
|
| 13 |
-
* -
|
| 14 |
-
* -
|
| 15 |
-
* -
|
| 16 |
-
* -
|
| 17 |
-
* -
|
| 18 |
-
*
|
| 19 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
*/
|
| 21 |
// Enable React Server Components client-side rendering for this file
|
| 22 |
"use client";
|
|
@@ -101,7 +131,32 @@ async function copyImageToClipboard(dataUrl: string) {
|
|
| 101 |
}
|
| 102 |
|
| 103 |
/**
|
| 104 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
*/
|
| 106 |
function NodeOutputSection({
|
| 107 |
nodeId, // Unique identifier for the node
|
|
@@ -281,13 +336,15 @@ function Port({
|
|
| 281 |
nodeId,
|
| 282 |
isOutput,
|
| 283 |
onStartConnection,
|
| 284 |
-
onEndConnection
|
|
|
|
| 285 |
}: {
|
| 286 |
className?: string;
|
| 287 |
nodeId?: string;
|
| 288 |
isOutput?: boolean;
|
| 289 |
onStartConnection?: (nodeId: string) => void;
|
| 290 |
onEndConnection?: (nodeId: string) => void;
|
|
|
|
| 291 |
}) {
|
| 292 |
/**
|
| 293 |
* Handle starting a connection (pointer down on output port)
|
|
@@ -309,16 +366,61 @@ function Port({
|
|
| 309 |
}
|
| 310 |
};
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
return (
|
| 313 |
<div
|
| 314 |
-
className={cx("nb-port", className)}
|
| 315 |
-
onPointerDown={handlePointerDown}
|
| 316 |
-
onPointerUp={handlePointerUp}
|
| 317 |
-
onPointerEnter={handlePointerUp}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
/>
|
| 319 |
);
|
| 320 |
}
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
export function BackgroundNodeView({
|
| 323 |
node,
|
| 324 |
onDelete,
|
|
@@ -327,40 +429,50 @@ export function BackgroundNodeView({
|
|
| 327 |
onEndConnection,
|
| 328 |
onProcess,
|
| 329 |
onUpdatePosition,
|
| 330 |
-
getNodeHistoryInfo,
|
| 331 |
-
navigateNodeHistory,
|
| 332 |
-
getCurrentNodeImage,
|
| 333 |
}: any) {
|
|
|
|
| 334 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 337 |
if (e.target.files?.length) {
|
| 338 |
-
const reader = new FileReader();
|
| 339 |
reader.onload = () => {
|
| 340 |
-
onUpdate(node.id, { customBackgroundImage: reader.result });
|
| 341 |
};
|
| 342 |
-
reader.readAsDataURL(e.target.files[0]);
|
| 343 |
}
|
| 344 |
};
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
const handleImagePaste = (e: React.ClipboardEvent) => {
|
| 347 |
-
const items = e.clipboardData.items;
|
|
|
|
|
|
|
| 348 |
for (let i = 0; i < items.length; i++) {
|
| 349 |
-
if (items[i].type.startsWith("image/")) {
|
| 350 |
-
const file = items[i].getAsFile();
|
| 351 |
if (file) {
|
| 352 |
-
const reader = new FileReader();
|
| 353 |
reader.onload = () => {
|
| 354 |
-
onUpdate(node.id, { customBackgroundImage: reader.result });
|
| 355 |
};
|
| 356 |
-
reader.readAsDataURL(file);
|
| 357 |
-
return;
|
| 358 |
}
|
| 359 |
}
|
| 360 |
}
|
| 361 |
-
|
|
|
|
|
|
|
| 362 |
if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
|
| 363 |
-
onUpdate(node.id, { customBackgroundImage: text });
|
| 364 |
}
|
| 365 |
};
|
| 366 |
|
|
@@ -390,7 +502,7 @@ export function BackgroundNodeView({
|
|
| 390 |
onPointerMove={onPointerMove}
|
| 391 |
onPointerUp={onPointerUp}
|
| 392 |
>
|
| 393 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 394 |
<div className="font-semibold text-sm flex-1 text-center">BACKGROUND</div>
|
| 395 |
<div className="flex items-center gap-1">
|
| 396 |
<Button
|
|
@@ -413,6 +525,7 @@ export function BackgroundNodeView({
|
|
| 413 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 414 |
</div>
|
| 415 |
</div>
|
|
|
|
| 416 |
<div className="p-3 space-y-3">
|
| 417 |
{node.input && (
|
| 418 |
<div className="flex justify-end mb-2">
|
|
@@ -422,7 +535,7 @@ export function BackgroundNodeView({
|
|
| 422 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 423 |
className="text-xs"
|
| 424 |
>
|
| 425 |
-
Clear Connection
|
| 426 |
</Button>
|
| 427 |
</div>
|
| 428 |
)}
|
|
@@ -551,9 +664,6 @@ export function BackgroundNodeView({
|
|
| 551 |
nodeId={node.id}
|
| 552 |
output={node.output}
|
| 553 |
downloadFileName={`background-${Date.now()}.png`}
|
| 554 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 555 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 556 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 557 |
/>
|
| 558 |
{node.error && (
|
| 559 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -563,14 +673,46 @@ export function BackgroundNodeView({
|
|
| 563 |
);
|
| 564 |
}
|
| 565 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 567 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 568 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
const presetClothes = [
|
| 570 |
-
{ name: "Sukajan", path: "/clothes/sukajan.png" },
|
| 571 |
-
{ name: "Blazer", path: "/clothes/blazzer.png" },
|
| 572 |
-
{ name: "Suit", path: "/clothes/suit.png" },
|
| 573 |
-
{ name: "Women's Outfit", path: "/clothes/womenoutfit.png" },
|
| 574 |
];
|
| 575 |
|
| 576 |
const onDrop = async (e: React.DragEvent) => {
|
|
@@ -620,7 +762,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 620 |
onPointerMove={onPointerMove}
|
| 621 |
onPointerUp={onPointerUp}
|
| 622 |
>
|
| 623 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 624 |
<div className="font-semibold text-sm flex-1 text-center">CLOTHES</div>
|
| 625 |
<div className="flex items-center gap-1">
|
| 626 |
<Button
|
|
@@ -643,6 +785,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 643 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 644 |
</div>
|
| 645 |
</div>
|
|
|
|
| 646 |
<div className="p-3 space-y-3">
|
| 647 |
{node.input && (
|
| 648 |
<div className="flex justify-end mb-2">
|
|
@@ -652,7 +795,7 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 652 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 653 |
className="text-xs"
|
| 654 |
>
|
| 655 |
-
Clear Connection
|
| 656 |
</Button>
|
| 657 |
</div>
|
| 658 |
)}
|
|
@@ -725,9 +868,6 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 725 |
nodeId={node.id}
|
| 726 |
output={node.output}
|
| 727 |
downloadFileName={`clothes-${Date.now()}.png`}
|
| 728 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 729 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 730 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 731 |
/>
|
| 732 |
{node.error && (
|
| 733 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -737,7 +877,38 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 737 |
);
|
| 738 |
}
|
| 739 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 741 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 742 |
|
| 743 |
return (
|
|
@@ -748,7 +919,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 748 |
onPointerMove={onPointerMove}
|
| 749 |
onPointerUp={onPointerUp}
|
| 750 |
>
|
| 751 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 752 |
<div className="font-semibold text-sm flex-1 text-center">AGE</div>
|
| 753 |
<div className="flex items-center gap-1">
|
| 754 |
<Button
|
|
@@ -771,6 +942,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 771 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 772 |
</div>
|
| 773 |
</div>
|
|
|
|
| 774 |
<div className="p-3 space-y-3">
|
| 775 |
{node.input && (
|
| 776 |
<div className="flex justify-end mb-2">
|
|
@@ -780,7 +952,7 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 780 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 781 |
className="text-xs"
|
| 782 |
>
|
| 783 |
-
Clear Connection
|
| 784 |
</Button>
|
| 785 |
</div>
|
| 786 |
)}
|
|
@@ -806,9 +978,6 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 806 |
nodeId={node.id}
|
| 807 |
output={node.output}
|
| 808 |
downloadFileName={`age-${Date.now()}.png`}
|
| 809 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 810 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 811 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 812 |
/>
|
| 813 |
{node.error && (
|
| 814 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -818,19 +987,74 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 818 |
);
|
| 819 |
}
|
| 820 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 822 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
const whiteBalances = ["None", "2800K candlelight", "3200K tungsten", "4000K fluorescent", "5600K daylight", "6500K cloudy", "7000K shade", "8000K blue sky"];
|
|
|
|
|
|
|
| 827 |
const angles = ["None", "eye level", "low angle", "high angle", "Dutch tilt", "bird's eye", "worm's eye", "over the shoulder", "POV"];
|
| 828 |
-
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
const lightingTypes = ["None", "Natural Light", "Golden Hour", "Blue Hour", "Studio Lighting", "Rembrandt", "Split Lighting", "Butterfly Lighting", "Loop Lighting", "Rim Lighting", "Silhouette", "High Key", "Low Key"];
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
const
|
|
|
|
|
|
|
|
|
|
| 834 |
|
| 835 |
return (
|
| 836 |
<div className="nb-node absolute text-white w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
@@ -840,7 +1064,7 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 840 |
onPointerMove={onPointerMove}
|
| 841 |
onPointerUp={onPointerUp}
|
| 842 |
>
|
| 843 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 844 |
<div className="font-semibold text-sm flex-1 text-center">CAMERA</div>
|
| 845 |
<div className="flex items-center gap-1">
|
| 846 |
<Button
|
|
@@ -872,49 +1096,72 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 872 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 873 |
className="text-xs"
|
| 874 |
>
|
| 875 |
-
Clear Connection
|
| 876 |
</Button>
|
| 877 |
</div>
|
| 878 |
)}
|
| 879 |
-
{/* Basic Camera Settings */}
|
| 880 |
<div className="text-xs text-white/50 font-semibold mb-1">Basic Settings</div>
|
| 881 |
-
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
<div>
|
| 883 |
<label className="text-xs text-white/70">Focal Length</label>
|
| 884 |
<Select
|
| 885 |
className="w-full"
|
| 886 |
-
value={node.focalLength || "None"}
|
| 887 |
onChange={(e) => onUpdate(node.id, { focalLength: (e.target as HTMLSelectElement).value })}
|
|
|
|
| 888 |
>
|
| 889 |
{focalLengths.map(f => <option key={f} value={f}>{f}</option>)}
|
| 890 |
</Select>
|
| 891 |
</div>
|
|
|
|
|
|
|
| 892 |
<div>
|
| 893 |
<label className="text-xs text-white/70">Aperture</label>
|
| 894 |
<Select
|
| 895 |
className="w-full"
|
| 896 |
-
value={node.aperture || "None"}
|
| 897 |
onChange={(e) => onUpdate(node.id, { aperture: (e.target as HTMLSelectElement).value })}
|
|
|
|
| 898 |
>
|
| 899 |
{apertures.map(a => <option key={a} value={a}>{a}</option>)}
|
| 900 |
</Select>
|
| 901 |
</div>
|
|
|
|
|
|
|
| 902 |
<div>
|
| 903 |
<label className="text-xs text-white/70">Shutter Speed</label>
|
| 904 |
<Select
|
| 905 |
className="w-full"
|
| 906 |
-
value={node.shutterSpeed || "None"}
|
| 907 |
onChange={(e) => onUpdate(node.id, { shutterSpeed: (e.target as HTMLSelectElement).value })}
|
|
|
|
| 908 |
>
|
| 909 |
{shutterSpeeds.map(s => <option key={s} value={s}>{s}</option>)}
|
| 910 |
</Select>
|
| 911 |
</div>
|
|
|
|
|
|
|
| 912 |
<div>
|
| 913 |
<label className="text-xs text-white/70">ISO</label>
|
| 914 |
<Select
|
| 915 |
className="w-full"
|
| 916 |
-
value={node.iso || "None"}
|
| 917 |
onChange={(e) => onUpdate(node.id, { iso: (e.target as HTMLSelectElement).value })}
|
|
|
|
| 918 |
>
|
| 919 |
{isoValues.map(i => <option key={i} value={i}>{i}</option>)}
|
| 920 |
</Select>
|
|
@@ -979,26 +1226,6 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 979 |
{angles.map(a => <option key={a} value={a}>{a}</option>)}
|
| 980 |
</Select>
|
| 981 |
</div>
|
| 982 |
-
<div>
|
| 983 |
-
<label className="text-xs text-white/70">Composition</label>
|
| 984 |
-
<Select
|
| 985 |
-
className="w-full"
|
| 986 |
-
value={node.composition || "None"}
|
| 987 |
-
onChange={(e) => onUpdate(node.id, { composition: (e.target as HTMLSelectElement).value })}
|
| 988 |
-
>
|
| 989 |
-
{compositions.map(c => <option key={c} value={c}>{c}</option>)}
|
| 990 |
-
</Select>
|
| 991 |
-
</div>
|
| 992 |
-
<div>
|
| 993 |
-
<label className="text-xs text-white/70">Aspect Ratio</label>
|
| 994 |
-
<Select
|
| 995 |
-
className="w-full"
|
| 996 |
-
value={node.aspectRatio || "None"}
|
| 997 |
-
onChange={(e) => onUpdate(node.id, { aspectRatio: (e.target as HTMLSelectElement).value })}
|
| 998 |
-
>
|
| 999 |
-
{aspectRatios.map(a => <option key={a} value={a}>{a}</option>)}
|
| 1000 |
-
</Select>
|
| 1001 |
-
</div>
|
| 1002 |
</div>
|
| 1003 |
<Button
|
| 1004 |
className="w-full"
|
|
@@ -1013,9 +1240,6 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 1013 |
nodeId={node.id}
|
| 1014 |
output={node.output}
|
| 1015 |
downloadFileName={`camera-${Date.now()}.png`}
|
| 1016 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1017 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1018 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1019 |
/>
|
| 1020 |
</div>
|
| 1021 |
{node.error && (
|
|
@@ -1026,10 +1250,50 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 1026 |
);
|
| 1027 |
}
|
| 1028 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 1030 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
|
|
|
|
|
|
| 1031 |
const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
|
|
|
|
|
|
|
| 1032 |
const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
|
|
|
|
|
|
|
| 1033 |
const beardStyles = ["None", "stubble", "goatee", "full beard", "mustache", "clean shaven"];
|
| 1034 |
|
| 1035 |
return (
|
|
@@ -1040,7 +1304,7 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1040 |
onPointerMove={onPointerMove}
|
| 1041 |
onPointerUp={onPointerUp}
|
| 1042 |
>
|
| 1043 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1044 |
<div className="font-semibold text-sm flex-1 text-center">FACE</div>
|
| 1045 |
<div className="flex items-center gap-1">
|
| 1046 |
<Button
|
|
@@ -1072,37 +1336,52 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1072 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1073 |
className="text-xs"
|
| 1074 |
>
|
| 1075 |
-
Clear Connection
|
| 1076 |
</Button>
|
| 1077 |
</div>
|
| 1078 |
)}
|
|
|
|
| 1079 |
<div className="space-y-2">
|
| 1080 |
-
|
|
|
|
| 1081 |
<Checkbox
|
| 1082 |
-
checked={node.faceOptions?.removePimples || false}
|
| 1083 |
onChange={(e) => onUpdate(node.id, {
|
| 1084 |
-
faceOptions: {
|
|
|
|
|
|
|
|
|
|
| 1085 |
})}
|
| 1086 |
/>
|
| 1087 |
-
Remove pimples
|
| 1088 |
</label>
|
| 1089 |
-
|
|
|
|
|
|
|
| 1090 |
<Checkbox
|
| 1091 |
-
checked={node.faceOptions?.addSunglasses || false}
|
| 1092 |
onChange={(e) => onUpdate(node.id, {
|
| 1093 |
-
faceOptions: {
|
|
|
|
|
|
|
|
|
|
| 1094 |
})}
|
| 1095 |
/>
|
| 1096 |
-
Add sunglasses
|
| 1097 |
</label>
|
| 1098 |
-
|
|
|
|
|
|
|
| 1099 |
<Checkbox
|
| 1100 |
-
checked={node.faceOptions?.addHat || false}
|
| 1101 |
onChange={(e) => onUpdate(node.id, {
|
| 1102 |
-
faceOptions: {
|
|
|
|
|
|
|
|
|
|
| 1103 |
})}
|
| 1104 |
/>
|
| 1105 |
-
Add hat
|
| 1106 |
</label>
|
| 1107 |
</div>
|
| 1108 |
|
|
@@ -1145,41 +1424,58 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1145 |
</Select>
|
| 1146 |
</div>
|
| 1147 |
|
|
|
|
| 1148 |
<div>
|
| 1149 |
<label className="text-xs text-white/70">Makeup</label>
|
| 1150 |
-
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
|
|
|
|
|
| 1151 |
<button
|
| 1152 |
-
className={`p-1 rounded border ${
|
| 1153 |
!node.faceOptions?.selectedMakeup || node.faceOptions?.selectedMakeup === "None"
|
| 1154 |
-
? "border-indigo-400 bg-indigo-500/20"
|
| 1155 |
-
: "border-white/20 hover:border-white/40"
|
| 1156 |
}`}
|
| 1157 |
onClick={() => onUpdate(node.id, {
|
| 1158 |
-
faceOptions: {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
})}
|
|
|
|
| 1160 |
>
|
|
|
|
| 1161 |
<div className="w-full h-24 flex items-center justify-center text-xs text-white/60 border border-dashed border-white/20 rounded mb-1">
|
| 1162 |
-
No Makeup
|
| 1163 |
</div>
|
| 1164 |
-
<div className="text-xs">None</div>
|
| 1165 |
</button>
|
|
|
|
|
|
|
| 1166 |
<button
|
| 1167 |
-
className={`p-1 rounded border ${
|
| 1168 |
node.faceOptions?.selectedMakeup === "Makeup"
|
| 1169 |
-
? "border-indigo-400 bg-indigo-500/20"
|
| 1170 |
-
: "border-white/20 hover:border-white/40"
|
| 1171 |
}`}
|
| 1172 |
onClick={() => onUpdate(node.id, {
|
| 1173 |
-
faceOptions: {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
})}
|
|
|
|
| 1175 |
>
|
|
|
|
| 1176 |
<img
|
| 1177 |
src="/makeup/makeup1.png"
|
| 1178 |
-
alt="Makeup"
|
| 1179 |
className="w-full h-24 object-contain rounded mb-1"
|
| 1180 |
-
title="
|
| 1181 |
/>
|
| 1182 |
-
<div className="text-xs">Makeup</div>
|
| 1183 |
</button>
|
| 1184 |
</div>
|
| 1185 |
</div>
|
|
@@ -1197,9 +1493,6 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1197 |
nodeId={node.id}
|
| 1198 |
output={node.output}
|
| 1199 |
downloadFileName={`face-${Date.now()}.png`}
|
| 1200 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1201 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1202 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1203 |
/>
|
| 1204 |
</div>
|
| 1205 |
{node.error && (
|
|
@@ -1210,24 +1503,65 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 1210 |
);
|
| 1211 |
}
|
| 1212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1213 |
export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 1214 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1216 |
const styleOptions = [
|
| 1217 |
-
{ value: "90s-anime", label: "90's Anime Style" },
|
| 1218 |
-
{ value: "
|
| 1219 |
-
{ value: "
|
| 1220 |
-
{ value: "
|
| 1221 |
-
{ value: "
|
| 1222 |
-
{ value: "
|
| 1223 |
-
{ value: "
|
| 1224 |
-
{ value: "
|
| 1225 |
-
{ value: "
|
| 1226 |
-
{ value: "
|
| 1227 |
-
{ value: "
|
| 1228 |
-
{ value: "wildwest", label: "Wild West Style" }
|
| 1229 |
-
{ value: "
|
| 1230 |
-
{ value: "
|
| 1231 |
];
|
| 1232 |
|
| 1233 |
return (
|
|
@@ -1241,7 +1575,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1241 |
onPointerMove={onPointerMove}
|
| 1242 |
onPointerUp={onPointerUp}
|
| 1243 |
>
|
| 1244 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1245 |
<div className="font-semibold text-sm flex-1 text-center">STYLE</div>
|
| 1246 |
<div className="flex items-center gap-1">
|
| 1247 |
<Button
|
|
@@ -1264,6 +1598,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1264 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1265 |
</div>
|
| 1266 |
</div>
|
|
|
|
| 1267 |
<div className="p-3 space-y-3">
|
| 1268 |
{node.input && (
|
| 1269 |
<div className="flex justify-end mb-2">
|
|
@@ -1273,7 +1608,7 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1273 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1274 |
className="text-xs"
|
| 1275 |
>
|
| 1276 |
-
Clear Connection
|
| 1277 |
</Button>
|
| 1278 |
</div>
|
| 1279 |
)}
|
|
@@ -1291,31 +1626,38 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1291 |
</option>
|
| 1292 |
))}
|
| 1293 |
</Select>
|
|
|
|
| 1294 |
<div>
|
| 1295 |
<Slider
|
| 1296 |
-
label="Style Strength"
|
| 1297 |
-
valueLabel={`${node.styleStrength || 50}%`}
|
| 1298 |
-
min={0}
|
| 1299 |
-
max={100}
|
| 1300 |
-
value={node.styleStrength || 50}
|
| 1301 |
-
onChange={(e) => onUpdate(node.id, {
|
|
|
|
|
|
|
|
|
|
| 1302 |
/>
|
| 1303 |
</div>
|
|
|
|
| 1304 |
<Button
|
| 1305 |
className="w-full"
|
| 1306 |
-
onClick={() => onProcess(node.id)}
|
| 1307 |
-
disabled={node.isRunning || !node.stylePreset}
|
| 1308 |
-
title={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1309 |
>
|
|
|
|
| 1310 |
{node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
|
| 1311 |
</Button>
|
| 1312 |
<NodeOutputSection
|
| 1313 |
nodeId={node.id}
|
| 1314 |
output={node.output}
|
| 1315 |
downloadFileName={`style-${Date.now()}.png`}
|
| 1316 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1317 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1318 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1319 |
/>
|
| 1320 |
{node.error && (
|
| 1321 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -1325,17 +1667,64 @@ export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1325 |
);
|
| 1326 |
}
|
| 1327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 1329 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1331 |
const presetLightings = [
|
| 1332 |
-
{ name: "Studio Light", path: "/lighting/light1.png" },
|
| 1333 |
-
{ name: "Natural Light", path: "/lighting/light2.png" },
|
| 1334 |
-
{ name: "Dramatic Light", path: "/lighting/light3.png" },
|
| 1335 |
];
|
| 1336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
const selectLighting = (lightingPath: string, lightingName: string) => {
|
| 1338 |
-
onUpdate(node.id, {
|
|
|
|
|
|
|
|
|
|
| 1339 |
};
|
| 1340 |
|
| 1341 |
return (
|
|
@@ -1346,7 +1735,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1346 |
onPointerMove={onPointerMove}
|
| 1347 |
onPointerUp={onPointerUp}
|
| 1348 |
>
|
| 1349 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1350 |
<div className="font-semibold text-sm flex-1 text-center">LIGHTNING</div>
|
| 1351 |
<div className="flex items-center gap-1">
|
| 1352 |
<Button
|
|
@@ -1369,6 +1758,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1369 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1370 |
</div>
|
| 1371 |
</div>
|
|
|
|
| 1372 |
<div className="p-3 space-y-3">
|
| 1373 |
{node.input && (
|
| 1374 |
<div className="flex justify-end mb-2">
|
|
@@ -1378,7 +1768,7 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1378 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1379 |
className="text-xs"
|
| 1380 |
>
|
| 1381 |
-
Clear Connection
|
| 1382 |
</Button>
|
| 1383 |
</div>
|
| 1384 |
)}
|
|
@@ -1419,9 +1809,6 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1419 |
nodeId={node.id}
|
| 1420 |
output={node.output}
|
| 1421 |
downloadFileName={`lightning-${Date.now()}.png`}
|
| 1422 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1423 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1424 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1425 |
/>
|
| 1426 |
{node.error && (
|
| 1427 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -1432,18 +1819,65 @@ export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection,
|
|
| 1432 |
}
|
| 1433 |
|
| 1434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1435 |
export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
|
|
|
| 1436 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1438 |
const presetPoses = [
|
| 1439 |
-
{ name: "Standing Pose 1", path: "/poses/stand1.png" },
|
| 1440 |
-
{ name: "Standing Pose 2", path: "/poses/stand2.png" },
|
| 1441 |
-
{ name: "Sitting Pose 1", path: "/poses/sit1.png" },
|
| 1442 |
-
{ name: "Sitting Pose 2", path: "/poses/sit2.png" },
|
| 1443 |
];
|
| 1444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1445 |
const selectPose = (posePath: string, poseName: string) => {
|
| 1446 |
-
onUpdate(node.id, {
|
|
|
|
|
|
|
|
|
|
| 1447 |
};
|
| 1448 |
|
| 1449 |
return (
|
|
@@ -1454,7 +1888,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1454 |
onPointerMove={onPointerMove}
|
| 1455 |
onPointerUp={onPointerUp}
|
| 1456 |
>
|
| 1457 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1458 |
<div className="font-semibold text-sm flex-1 text-center">POSES</div>
|
| 1459 |
<div className="flex items-center gap-1">
|
| 1460 |
<Button
|
|
@@ -1477,6 +1911,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1477 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1478 |
</div>
|
| 1479 |
</div>
|
|
|
|
| 1480 |
<div className="p-3 space-y-3">
|
| 1481 |
{node.input && (
|
| 1482 |
<div className="flex justify-end mb-2">
|
|
@@ -1486,7 +1921,7 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1486 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1487 |
className="text-xs"
|
| 1488 |
>
|
| 1489 |
-
Clear Connection
|
| 1490 |
</Button>
|
| 1491 |
</div>
|
| 1492 |
)}
|
|
@@ -1527,9 +1962,6 @@ export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 1527 |
nodeId={node.id}
|
| 1528 |
output={node.output}
|
| 1529 |
downloadFileName={`poses-${Date.now()}.png`}
|
| 1530 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1531 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1532 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1533 |
/>
|
| 1534 |
{node.error && (
|
| 1535 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
@@ -1649,7 +2081,7 @@ export function EditNodeView({
|
|
| 1649 |
onPointerUp={onPointerUp} // End dragging
|
| 1650 |
>
|
| 1651 |
{/* Input port (left side) - where connections come in */}
|
| 1652 |
-
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} />
|
| 1653 |
|
| 1654 |
{/* Node title */}
|
| 1655 |
<div className="font-semibold text-sm flex-1 text-center">EDIT</div>
|
|
@@ -1674,6 +2106,7 @@ export function EditNodeView({
|
|
| 1674 |
</div>
|
| 1675 |
|
| 1676 |
{/* Node Content - Contains all the controls and outputs */}
|
|
|
|
| 1677 |
<div className="p-3 space-y-3">
|
| 1678 |
{/* Show clear connection button if node has input */}
|
| 1679 |
{node.input && (
|
|
@@ -1685,7 +2118,7 @@ export function EditNodeView({
|
|
| 1685 |
className="text-xs"
|
| 1686 |
title="Remove input connection"
|
| 1687 |
>
|
| 1688 |
-
Clear Connection
|
| 1689 |
</Button>
|
| 1690 |
</div>
|
| 1691 |
)}
|
|
@@ -1733,9 +2166,6 @@ export function EditNodeView({
|
|
| 1733 |
nodeId={node.id}
|
| 1734 |
output={node.output}
|
| 1735 |
downloadFileName={`edit-${Date.now()}.png`}
|
| 1736 |
-
getNodeHistoryInfo={getNodeHistoryInfo}
|
| 1737 |
-
navigateNodeHistory={navigateNodeHistory}
|
| 1738 |
-
getCurrentNodeImage={getCurrentNodeImage}
|
| 1739 |
/>
|
| 1740 |
|
| 1741 |
{/* Error display */}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* NODE COMPONENT VIEWS FOR NANO BANANA EDITOR
|
| 3 |
*
|
| 4 |
+
* This file contains all the visual node components for the Nano Banana Editor,
|
| 5 |
+
* a visual node-based AI image processing application. Each node represents a
|
| 6 |
+
* specific image transformation or effect that can be chained together to create
|
| 7 |
+
* complex image processing workflows.
|
|
|
|
|
|
|
|
|
|
| 8 |
*
|
| 9 |
+
* ARCHITECTURE OVERVIEW:
|
| 10 |
+
* - Each node is a self-contained React component with its own state and UI
|
| 11 |
+
* - Nodes use a common dragging system (useNodeDrag hook) for positioning
|
| 12 |
+
* - All nodes follow a consistent structure: Header + Content + Output
|
| 13 |
+
* - Nodes communicate through a connection system using input/output ports
|
| 14 |
+
* - Processing is handled asynchronously with loading states and error handling
|
| 15 |
+
*
|
| 16 |
+
* NODE TYPES AVAILABLE:
|
| 17 |
+
* - BackgroundNodeView: Change/generate image backgrounds (color, preset, upload, AI-generated)
|
| 18 |
+
* - ClothesNodeView: Add/modify clothing on subjects (preset garments or custom uploads)
|
| 19 |
+
* - StyleNodeView: Apply artistic styles and filters (anime, fine art, cinematic styles)
|
| 20 |
+
* - EditNodeView: General text-based image editing (natural language instructions)
|
| 21 |
+
* - CameraNodeView: Apply camera effects and settings (focal length, aperture, film styles)
|
| 22 |
+
* - AgeNodeView: Transform subject age (AI-powered age progression/regression)
|
| 23 |
+
* - FaceNodeView: Modify facial features and accessories (hair, makeup, expressions)
|
| 24 |
+
* - LightningNodeView: Apply professional lighting effects
|
| 25 |
+
* - PosesNodeView: Modify body poses and positioning
|
| 26 |
+
*
|
| 27 |
+
* COMMON PATTERNS:
|
| 28 |
+
* - All nodes support drag-and-drop for repositioning in the editor
|
| 29 |
+
* - Input/output ports allow chaining nodes together in processing pipelines
|
| 30 |
+
* - File upload via drag-drop, file picker, or clipboard paste where applicable
|
| 31 |
+
* - Real-time preview of settings and processed results
|
| 32 |
+
* - History navigation for viewing different processing results
|
| 33 |
+
* - Error handling with user-friendly error messages
|
| 34 |
+
* - AI-powered prompt improvement using Gemini API where applicable
|
| 35 |
+
*
|
| 36 |
+
* USER WORKFLOW:
|
| 37 |
+
* 1. Add nodes to the editor canvas
|
| 38 |
+
* 2. Configure each node's settings (colors, styles, uploaded images, etc.)
|
| 39 |
+
* 3. Connect nodes using input/output ports to create processing chains
|
| 40 |
+
* 4. Process individual nodes or entire chains
|
| 41 |
+
* 5. Preview results, navigate history, and download final images
|
| 42 |
+
*
|
| 43 |
+
* TECHNICAL DETAILS:
|
| 44 |
+
* - Uses React hooks for state management (useState, useEffect, useRef)
|
| 45 |
+
* - Custom useNodeDrag hook handles node positioning and drag interactions
|
| 46 |
+
* - Port component manages connection logic between nodes
|
| 47 |
+
* - All image data is handled as base64 data URLs for browser compatibility
|
| 48 |
+
* - Processing results are cached with history navigation support
|
| 49 |
+
* - Responsive UI components from shadcn/ui component library
|
| 50 |
*/
|
| 51 |
// Enable React Server Components client-side rendering for this file
|
| 52 |
"use client";
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
/**
|
| 134 |
+
* REUSABLE OUTPUT SECTION COMPONENT
|
| 135 |
+
*
|
| 136 |
+
* This component provides a standardized output display for all node types.
|
| 137 |
+
* It handles the common functionality that every node needs for showing results:
|
| 138 |
+
*
|
| 139 |
+
* Key Features:
|
| 140 |
+
* - Displays processed output images with click-to-copy functionality
|
| 141 |
+
* - Provides download functionality with custom filenames
|
| 142 |
+
* - Visual feedback when images are copied to clipboard
|
| 143 |
+
* - Consistent styling across all node types
|
| 144 |
+
* - Hover effects and tooltips for better UX
|
| 145 |
+
*
|
| 146 |
+
* User Interactions:
|
| 147 |
+
* - Left-click or right-click image to copy to clipboard
|
| 148 |
+
* - Click download button to save image with timestamp
|
| 149 |
+
* - Visual feedback shows when image is successfully copied
|
| 150 |
+
*
|
| 151 |
+
* Technical Implementation:
|
| 152 |
+
* - Converts images to clipboard-compatible format (PNG)
|
| 153 |
+
* - Uses browser's native download API
|
| 154 |
+
* - Provides visual feedback through temporary styling changes
|
| 155 |
+
* - Handles both base64 data URLs and regular image URLs
|
| 156 |
+
*
|
| 157 |
+
* @param nodeId - Unique identifier for the node (for potential future features)
|
| 158 |
+
* @param output - Optional current output image (base64 data URL or image URL)
|
| 159 |
+
* @param downloadFileName - Filename to use when downloading (should include extension)
|
| 160 |
*/
|
| 161 |
function NodeOutputSection({
|
| 162 |
nodeId, // Unique identifier for the node
|
|
|
|
| 336 |
nodeId,
|
| 337 |
isOutput,
|
| 338 |
onStartConnection,
|
| 339 |
+
onEndConnection,
|
| 340 |
+
onDisconnect
|
| 341 |
}: {
|
| 342 |
className?: string;
|
| 343 |
nodeId?: string;
|
| 344 |
isOutput?: boolean;
|
| 345 |
onStartConnection?: (nodeId: string) => void;
|
| 346 |
onEndConnection?: (nodeId: string) => void;
|
| 347 |
+
onDisconnect?: (nodeId: string) => void;
|
| 348 |
}) {
|
| 349 |
/**
|
| 350 |
* Handle starting a connection (pointer down on output port)
|
|
|
|
| 366 |
}
|
| 367 |
};
|
| 368 |
|
| 369 |
+
/**
|
| 370 |
+
* Handle clicking on input port to disconnect
|
| 371 |
+
* Allows users to remove connections by clicking on input ports
|
| 372 |
+
*/
|
| 373 |
+
const handleClick = (e: React.MouseEvent) => {
|
| 374 |
+
e.stopPropagation(); // Prevent event from bubbling to parent elements
|
| 375 |
+
if (!isOutput && nodeId && onDisconnect) {
|
| 376 |
+
onDisconnect(nodeId); // Disconnect from this input port
|
| 377 |
+
}
|
| 378 |
+
};
|
| 379 |
+
|
| 380 |
return (
|
| 381 |
<div
|
| 382 |
+
className={cx("nb-port", className)} // Combine base port classes with custom ones
|
| 383 |
+
onPointerDown={handlePointerDown} // Start connection drag from output ports
|
| 384 |
+
onPointerUp={handlePointerUp} // End connection drag at input ports
|
| 385 |
+
onPointerEnter={handlePointerUp} // Also accept connections on hover (better UX)
|
| 386 |
+
onClick={handleClick} // Allow clicking input ports to disconnect
|
| 387 |
+
title={
|
| 388 |
+
isOutput
|
| 389 |
+
? "Drag from here to connect to another node's input"
|
| 390 |
+
: "Drop connections here or click to disconnect"
|
| 391 |
+
}
|
| 392 |
/>
|
| 393 |
);
|
| 394 |
}
|
| 395 |
|
| 396 |
+
/**
|
| 397 |
+
* BACKGROUND NODE VIEW COMPONENT
|
| 398 |
+
*
|
| 399 |
+
* Allows users to change or generate image backgrounds using various methods:
|
| 400 |
+
* - Solid colors with color picker
|
| 401 |
+
* - Preset background images (beach, office, studio, etc.)
|
| 402 |
+
* - Custom uploaded images via file upload or drag/drop
|
| 403 |
+
* - AI-generated backgrounds from text descriptions
|
| 404 |
+
*
|
| 405 |
+
* Key Features:
|
| 406 |
+
* - Multiple background source types (color/preset/upload/custom prompt)
|
| 407 |
+
* - Drag and drop image upload functionality
|
| 408 |
+
* - Paste image from clipboard support
|
| 409 |
+
* - AI-powered prompt improvement using Gemini
|
| 410 |
+
* - Real-time preview of uploaded images
|
| 411 |
+
* - Connection management for node-based workflow
|
| 412 |
+
*
|
| 413 |
+
* @param node - Background node data containing backgroundType, backgroundColor, etc.
|
| 414 |
+
* @param onDelete - Callback to delete this node from the editor
|
| 415 |
+
* @param onUpdate - Callback to update node properties (backgroundType, colors, images, etc.)
|
| 416 |
+
* @param onStartConnection - Callback when user starts dragging from output port
|
| 417 |
+
* @param onEndConnection - Callback when user drops connection on input port
|
| 418 |
+
* @param onProcess - Callback to process this node and apply background changes
|
| 419 |
+
* @param onUpdatePosition - Callback to update node position when dragged
|
| 420 |
+
* @param getNodeHistoryInfo - Function to get processing history for this node
|
| 421 |
+
* @param navigateNodeHistory - Function to navigate through different processing results
|
| 422 |
+
* @param getCurrentNodeImage - Function to get the current processed image
|
| 423 |
+
*/
|
| 424 |
export function BackgroundNodeView({
|
| 425 |
node,
|
| 426 |
onDelete,
|
|
|
|
| 429 |
onEndConnection,
|
| 430 |
onProcess,
|
| 431 |
onUpdatePosition,
|
|
|
|
|
|
|
|
|
|
| 432 |
}: any) {
|
| 433 |
+
// Use custom drag hook to handle node positioning in the editor
|
| 434 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 435 |
|
| 436 |
+
/**
|
| 437 |
+
* Handle image file upload from file input
|
| 438 |
+
* Converts uploaded file to base64 data URL for storage and preview
|
| 439 |
+
*/
|
| 440 |
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 441 |
if (e.target.files?.length) {
|
| 442 |
+
const reader = new FileReader(); // Create file reader
|
| 443 |
reader.onload = () => {
|
| 444 |
+
onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data URL
|
| 445 |
};
|
| 446 |
+
reader.readAsDataURL(e.target.files[0]); // Convert file to base64
|
| 447 |
}
|
| 448 |
};
|
| 449 |
|
| 450 |
+
/**
|
| 451 |
+
* Handle image paste from clipboard
|
| 452 |
+
* Supports both image files and image URLs pasted from clipboard
|
| 453 |
+
*/
|
| 454 |
const handleImagePaste = (e: React.ClipboardEvent) => {
|
| 455 |
+
const items = e.clipboardData.items; // Get clipboard items
|
| 456 |
+
|
| 457 |
+
// First, try to find image files in clipboard
|
| 458 |
for (let i = 0; i < items.length; i++) {
|
| 459 |
+
if (items[i].type.startsWith("image/")) { // Check if item is an image
|
| 460 |
+
const file = items[i].getAsFile(); // Get image file
|
| 461 |
if (file) {
|
| 462 |
+
const reader = new FileReader(); // Create file reader
|
| 463 |
reader.onload = () => {
|
| 464 |
+
onUpdate(node.id, { customBackgroundImage: reader.result }); // Store base64 data
|
| 465 |
};
|
| 466 |
+
reader.readAsDataURL(file); // Convert to base64
|
| 467 |
+
return; // Exit early if image found
|
| 468 |
}
|
| 469 |
}
|
| 470 |
}
|
| 471 |
+
|
| 472 |
+
// If no image files, check for text that might be image URLs
|
| 473 |
+
const text = e.clipboardData.getData("text"); // Get text from clipboard
|
| 474 |
if (text && (text.startsWith("http") || text.startsWith("data:image"))) {
|
| 475 |
+
onUpdate(node.id, { customBackgroundImage: text }); // Use URL directly
|
| 476 |
}
|
| 477 |
};
|
| 478 |
|
|
|
|
| 502 |
onPointerMove={onPointerMove}
|
| 503 |
onPointerUp={onPointerUp}
|
| 504 |
>
|
| 505 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 506 |
<div className="font-semibold text-sm flex-1 text-center">BACKGROUND</div>
|
| 507 |
<div className="flex items-center gap-1">
|
| 508 |
<Button
|
|
|
|
| 525 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 526 |
</div>
|
| 527 |
</div>
|
| 528 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 529 |
<div className="p-3 space-y-3">
|
| 530 |
{node.input && (
|
| 531 |
<div className="flex justify-end mb-2">
|
|
|
|
| 535 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 536 |
className="text-xs"
|
| 537 |
>
|
| 538 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 539 |
</Button>
|
| 540 |
</div>
|
| 541 |
)}
|
|
|
|
| 664 |
nodeId={node.id}
|
| 665 |
output={node.output}
|
| 666 |
downloadFileName={`background-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 667 |
/>
|
| 668 |
{node.error && (
|
| 669 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 673 |
);
|
| 674 |
}
|
| 675 |
|
| 676 |
+
/**
|
| 677 |
+
* CLOTHES NODE VIEW COMPONENT
|
| 678 |
+
*
|
| 679 |
+
* Allows users to add or modify clothing on subjects in images.
|
| 680 |
+
* Supports both preset clothing options and custom uploaded garments.
|
| 681 |
+
*
|
| 682 |
+
* Key Features:
|
| 683 |
+
* - Preset clothing gallery (Sukajan, Blazer, Suit, Women's Outfit)
|
| 684 |
+
* - Custom clothing upload via drag/drop, file picker, or clipboard paste
|
| 685 |
+
* - Visual selection interface with thumbnails
|
| 686 |
+
* - Real-time preview of selected clothing
|
| 687 |
+
* - Integration with AI processing pipeline
|
| 688 |
+
*
|
| 689 |
+
* The node processes input images and applies the selected clothing using
|
| 690 |
+
* AI models that understand garment fitting and realistic clothing application.
|
| 691 |
+
*
|
| 692 |
+
* @param node - Clothes node data containing clothesImage, selectedPreset, etc.
|
| 693 |
+
* @param onDelete - Callback to delete this node
|
| 694 |
+
* @param onUpdate - Callback to update node properties
|
| 695 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 696 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 697 |
+
* @param onProcess - Callback to process this node
|
| 698 |
+
* @param onUpdatePosition - Callback to update node position
|
| 699 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 700 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 701 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 702 |
+
*/
|
| 703 |
export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 704 |
+
// Handle node dragging functionality
|
| 705 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 706 |
|
| 707 |
+
/**
|
| 708 |
+
* Preset clothing options available for quick selection
|
| 709 |
+
* Each preset includes a display name and path to the reference image
|
| 710 |
+
*/
|
| 711 |
const presetClothes = [
|
| 712 |
+
{ name: "Sukajan", path: "/clothes/sukajan.png" }, // Japanese-style embroidered jacket
|
| 713 |
+
{ name: "Blazer", path: "/clothes/blazzer.png" }, // Business blazer/jacket
|
| 714 |
+
{ name: "Suit", path: "/clothes/suit.png" }, // Formal business suit
|
| 715 |
+
{ name: "Women's Outfit", path: "/clothes/womenoutfit.png" }, // Women's clothing ensemble
|
| 716 |
];
|
| 717 |
|
| 718 |
const onDrop = async (e: React.DragEvent) => {
|
|
|
|
| 762 |
onPointerMove={onPointerMove}
|
| 763 |
onPointerUp={onPointerUp}
|
| 764 |
>
|
| 765 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 766 |
<div className="font-semibold text-sm flex-1 text-center">CLOTHES</div>
|
| 767 |
<div className="flex items-center gap-1">
|
| 768 |
<Button
|
|
|
|
| 785 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 786 |
</div>
|
| 787 |
</div>
|
| 788 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 789 |
<div className="p-3 space-y-3">
|
| 790 |
{node.input && (
|
| 791 |
<div className="flex justify-end mb-2">
|
|
|
|
| 795 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 796 |
className="text-xs"
|
| 797 |
>
|
| 798 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 799 |
</Button>
|
| 800 |
</div>
|
| 801 |
)}
|
|
|
|
| 868 |
nodeId={node.id}
|
| 869 |
output={node.output}
|
| 870 |
downloadFileName={`clothes-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 871 |
/>
|
| 872 |
{node.error && (
|
| 873 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 877 |
);
|
| 878 |
}
|
| 879 |
|
| 880 |
+
/**
|
| 881 |
+
* AGE NODE VIEW COMPONENT
|
| 882 |
+
*
|
| 883 |
+
* Allows users to transform the apparent age of subjects in images.
|
| 884 |
+
* Uses AI age transformation models to make people appear older or younger
|
| 885 |
+
* while maintaining facial features and identity.
|
| 886 |
+
*
|
| 887 |
+
* Key Features:
|
| 888 |
+
* - Slider-based age selection (18-100 years)
|
| 889 |
+
* - Real-time age value display
|
| 890 |
+
* - Preserves facial identity during transformation
|
| 891 |
+
* - Smooth age progression/regression
|
| 892 |
+
*
|
| 893 |
+
* The AI models understand facial aging patterns and can:
|
| 894 |
+
* - Add/remove wrinkles and age lines
|
| 895 |
+
* - Adjust skin texture and tone
|
| 896 |
+
* - Modify facial structure subtly
|
| 897 |
+
* - Maintain eye color and basic facial features
|
| 898 |
+
*
|
| 899 |
+
* @param node - Age node data containing targetAge, input, output, etc.
|
| 900 |
+
* @param onDelete - Callback to delete this node
|
| 901 |
+
* @param onUpdate - Callback to update node properties (targetAge)
|
| 902 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 903 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 904 |
+
* @param onProcess - Callback to process age transformation
|
| 905 |
+
* @param onUpdatePosition - Callback to update node position
|
| 906 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 907 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 908 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 909 |
+
*/
|
| 910 |
export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 911 |
+
// Handle node dragging functionality
|
| 912 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 913 |
|
| 914 |
return (
|
|
|
|
| 919 |
onPointerMove={onPointerMove}
|
| 920 |
onPointerUp={onPointerUp}
|
| 921 |
>
|
| 922 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 923 |
<div className="font-semibold text-sm flex-1 text-center">AGE</div>
|
| 924 |
<div className="flex items-center gap-1">
|
| 925 |
<Button
|
|
|
|
| 942 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 943 |
</div>
|
| 944 |
</div>
|
| 945 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 946 |
<div className="p-3 space-y-3">
|
| 947 |
{node.input && (
|
| 948 |
<div className="flex justify-end mb-2">
|
|
|
|
| 952 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 953 |
className="text-xs"
|
| 954 |
>
|
| 955 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 956 |
</Button>
|
| 957 |
</div>
|
| 958 |
)}
|
|
|
|
| 978 |
nodeId={node.id}
|
| 979 |
output={node.output}
|
| 980 |
downloadFileName={`age-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 981 |
/>
|
| 982 |
{node.error && (
|
| 983 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 987 |
);
|
| 988 |
}
|
| 989 |
|
| 990 |
+
/**
|
| 991 |
+
* CAMERA NODE VIEW COMPONENT
|
| 992 |
+
*
|
| 993 |
+
* Applies professional camera settings and photographic effects to images.
|
| 994 |
+
* Simulates various camera equipment, settings, and photographic techniques
|
| 995 |
+
* to achieve specific visual styles and technical characteristics.
|
| 996 |
+
*
|
| 997 |
+
* Key Features:
|
| 998 |
+
* - Complete camera settings simulation (focal length, aperture, shutter speed, ISO)
|
| 999 |
+
* - Film stock emulation (Kodak, Fuji, Ilford, etc.)
|
| 1000 |
+
* - Professional lighting setups (studio, natural, dramatic)
|
| 1001 |
+
* - Composition guides (rule of thirds, golden ratio, etc.)
|
| 1002 |
+
* - Bokeh effects and depth of field control
|
| 1003 |
+
* - Color temperature and white balance adjustment
|
| 1004 |
+
* - Aspect ratio modifications
|
| 1005 |
+
*
|
| 1006 |
+
* Technical Settings Available:
|
| 1007 |
+
* - Focal lengths from fisheye (8mm) to telephoto (400mm)
|
| 1008 |
+
* - Aperture range from f/0.95 to f/22
|
| 1009 |
+
* - Shutter speeds from 1/8000s to 30s
|
| 1010 |
+
* - ISO values from 50 to 12800
|
| 1011 |
+
* - Professional lighting setups
|
| 1012 |
+
* - Film stock characteristics
|
| 1013 |
+
*
|
| 1014 |
+
* @param node - Camera node data containing all camera settings
|
| 1015 |
+
* @param onDelete - Callback to delete this node
|
| 1016 |
+
* @param onUpdate - Callback to update camera settings
|
| 1017 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 1018 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 1019 |
+
* @param onProcess - Callback to process camera effects
|
| 1020 |
+
* @param onUpdatePosition - Callback to update node position
|
| 1021 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 1022 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 1023 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 1024 |
+
*/
|
| 1025 |
export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1026 |
+
// Handle node dragging functionality
|
| 1027 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1028 |
+
|
| 1029 |
+
// Camera lens focal length options (affects field of view and perspective)
|
| 1030 |
+
const focalLengths = ["None", "8mm", "12mm", "24mm", "35mm", "50mm", "85mm"];
|
| 1031 |
+
|
| 1032 |
+
// Aperture settings (affects depth of field and exposure)
|
| 1033 |
+
const apertures = ["None", "f/0.95", "f/1.2", "f/1.4", "f/1.8", "f/2", "f/2.8", "f/4", "f/5.6","f/11"];
|
| 1034 |
+
|
| 1035 |
+
// Shutter speed options (affects motion blur and exposure)
|
| 1036 |
+
const shutterSpeeds = ["None", "1/1000s", "1/250s","1/30s","1/15", "5s", ];
|
| 1037 |
+
|
| 1038 |
+
// White balance presets for different lighting conditions
|
| 1039 |
const whiteBalances = ["None", "2800K candlelight", "3200K tungsten", "4000K fluorescent", "5600K daylight", "6500K cloudy", "7000K shade", "8000K blue sky"];
|
| 1040 |
+
|
| 1041 |
+
// Camera angle and perspective options
|
| 1042 |
const angles = ["None", "eye level", "low angle", "high angle", "Dutch tilt", "bird's eye", "worm's eye", "over the shoulder", "POV"];
|
| 1043 |
+
|
| 1044 |
+
// ISO sensitivity values (affects image noise and exposure)
|
| 1045 |
+
const isoValues = ["None", "ISO 100", "ISO 400", "ISO 1600", "ISO 6400"];
|
| 1046 |
+
|
| 1047 |
+
// Film stock emulation for different photographic styles
|
| 1048 |
+
const filmStyles = ["None","RAW","Kodak Portra", "Fuji Velvia", "Kodak Gold 200","Black & White", "Sepia", "Vintage", "Film Noir"];
|
| 1049 |
+
|
| 1050 |
+
// Professional lighting setups and natural lighting conditions
|
| 1051 |
const lightingTypes = ["None", "Natural Light", "Golden Hour", "Blue Hour", "Studio Lighting", "Rembrandt", "Split Lighting", "Butterfly Lighting", "Loop Lighting", "Rim Lighting", "Silhouette", "High Key", "Low Key"];
|
| 1052 |
+
|
| 1053 |
+
// Bokeh (background blur) styles for different lens characteristics
|
| 1054 |
+
const bokehStyles = ["None", "Smooth Bokeh", "Swirly Bokeh", "Hexagonal Bokeh", "Cat Eye Bokeh", "Bubble Bokeh"];
|
| 1055 |
+
|
| 1056 |
+
// Manual or automatic
|
| 1057 |
+
const manualOrAutomatic = ["None", "AF-S", "AF-C", "AF-A"];
|
| 1058 |
|
| 1059 |
return (
|
| 1060 |
<div className="nb-node absolute text-white w-[360px]" style={{ left: localPos.x, top: localPos.y }}>
|
|
|
|
| 1064 |
onPointerMove={onPointerMove}
|
| 1065 |
onPointerUp={onPointerUp}
|
| 1066 |
>
|
| 1067 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 1068 |
<div className="font-semibold text-sm flex-1 text-center">CAMERA</div>
|
| 1069 |
<div className="flex items-center gap-1">
|
| 1070 |
<Button
|
|
|
|
| 1096 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1097 |
className="text-xs"
|
| 1098 |
>
|
| 1099 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 1100 |
</Button>
|
| 1101 |
</div>
|
| 1102 |
)}
|
| 1103 |
+
{/* Basic Camera Settings Section */}
|
| 1104 |
<div className="text-xs text-white/50 font-semibold mb-1">Basic Settings</div>
|
| 1105 |
+
<div className="grid grid-cols-2 gap-2"> {/* 2-column grid for compact layout */}
|
| 1106 |
+
{/* Automaticormanual Control - affects field of view and perspective */}
|
| 1107 |
+
<div>
|
| 1108 |
+
<label className="text-xs text-white/70">Manual or Automatic</label>
|
| 1109 |
+
<Select
|
| 1110 |
+
className="w-full"
|
| 1111 |
+
value={node.manualOrAutomatic || "None"} // Default to "None" if not set
|
| 1112 |
+
onChange={(e) => onUpdate(node.id, { manualOrAutomatic: (e.target as HTMLSelectElement).value })}
|
| 1113 |
+
title="Select Focus Modes"
|
| 1114 |
+
>
|
| 1115 |
+
{manualOrAutomatic.map(f => <option key={f} value={f}>{f}</option>)}
|
| 1116 |
+
</Select>
|
| 1117 |
+
</div>
|
| 1118 |
+
{/* Focal Length Control - affects field of view and perspective */}
|
| 1119 |
<div>
|
| 1120 |
<label className="text-xs text-white/70">Focal Length</label>
|
| 1121 |
<Select
|
| 1122 |
className="w-full"
|
| 1123 |
+
value={node.focalLength || "None"} // Default to "None" if not set
|
| 1124 |
onChange={(e) => onUpdate(node.id, { focalLength: (e.target as HTMLSelectElement).value })}
|
| 1125 |
+
title="Select lens focal length - affects field of view and perspective distortion"
|
| 1126 |
>
|
| 1127 |
{focalLengths.map(f => <option key={f} value={f}>{f}</option>)}
|
| 1128 |
</Select>
|
| 1129 |
</div>
|
| 1130 |
+
|
| 1131 |
+
{/* Aperture Control - affects depth of field and exposure */}
|
| 1132 |
<div>
|
| 1133 |
<label className="text-xs text-white/70">Aperture</label>
|
| 1134 |
<Select
|
| 1135 |
className="w-full"
|
| 1136 |
+
value={node.aperture || "None"} // Default to "None" if not set
|
| 1137 |
onChange={(e) => onUpdate(node.id, { aperture: (e.target as HTMLSelectElement).value })}
|
| 1138 |
+
title="Select aperture value - lower f-numbers create shallower depth of field"
|
| 1139 |
>
|
| 1140 |
{apertures.map(a => <option key={a} value={a}>{a}</option>)}
|
| 1141 |
</Select>
|
| 1142 |
</div>
|
| 1143 |
+
|
| 1144 |
+
{/* Shutter Speed Control - affects motion blur and exposure */}
|
| 1145 |
<div>
|
| 1146 |
<label className="text-xs text-white/70">Shutter Speed</label>
|
| 1147 |
<Select
|
| 1148 |
className="w-full"
|
| 1149 |
+
value={node.shutterSpeed || "None"} // Default to "None" if not set
|
| 1150 |
onChange={(e) => onUpdate(node.id, { shutterSpeed: (e.target as HTMLSelectElement).value })}
|
| 1151 |
+
title="Select shutter speed - faster speeds freeze motion, slower speeds create blur"
|
| 1152 |
>
|
| 1153 |
{shutterSpeeds.map(s => <option key={s} value={s}>{s}</option>)}
|
| 1154 |
</Select>
|
| 1155 |
</div>
|
| 1156 |
+
|
| 1157 |
+
{/* ISO Control - affects sensor sensitivity and image noise */}
|
| 1158 |
<div>
|
| 1159 |
<label className="text-xs text-white/70">ISO</label>
|
| 1160 |
<Select
|
| 1161 |
className="w-full"
|
| 1162 |
+
value={node.iso || "None"} // Default to "None" if not set
|
| 1163 |
onChange={(e) => onUpdate(node.id, { iso: (e.target as HTMLSelectElement).value })}
|
| 1164 |
+
title="Select ISO value - higher values increase sensitivity but add noise"
|
| 1165 |
>
|
| 1166 |
{isoValues.map(i => <option key={i} value={i}>{i}</option>)}
|
| 1167 |
</Select>
|
|
|
|
| 1226 |
{angles.map(a => <option key={a} value={a}>{a}</option>)}
|
| 1227 |
</Select>
|
| 1228 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
</div>
|
| 1230 |
<Button
|
| 1231 |
className="w-full"
|
|
|
|
| 1240 |
nodeId={node.id}
|
| 1241 |
output={node.output}
|
| 1242 |
downloadFileName={`camera-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 1243 |
/>
|
| 1244 |
</div>
|
| 1245 |
{node.error && (
|
|
|
|
| 1250 |
);
|
| 1251 |
}
|
| 1252 |
|
| 1253 |
+
/**
|
| 1254 |
+
* FACE NODE VIEW COMPONENT
|
| 1255 |
+
*
|
| 1256 |
+
* Provides comprehensive facial feature modification capabilities.
|
| 1257 |
+
* Allows users to change various aspects of faces in images including
|
| 1258 |
+
* hairstyles, expressions, facial hair, accessories, and makeup.
|
| 1259 |
+
*
|
| 1260 |
+
* Key Features:
|
| 1261 |
+
* - Hairstyle modifications (short, long, curly, straight, etc.)
|
| 1262 |
+
* - Facial expression changes (happy, sad, surprised, etc.)
|
| 1263 |
+
* - Beard and mustache styling options
|
| 1264 |
+
* - Accessory addition (sunglasses, hats)
|
| 1265 |
+
* - Makeup application with preset styles
|
| 1266 |
+
* - Skin enhancement (pimple removal)
|
| 1267 |
+
*
|
| 1268 |
+
* The AI models can:
|
| 1269 |
+
* - Preserve facial identity while making changes
|
| 1270 |
+
* - Apply realistic hair textures and colors
|
| 1271 |
+
* - Generate natural-looking expressions
|
| 1272 |
+
* - Add accessories that fit properly
|
| 1273 |
+
* - Apply makeup that matches lighting and skin tone
|
| 1274 |
+
*
|
| 1275 |
+
* @param node - Face node data containing all face modification settings
|
| 1276 |
+
* @param onDelete - Callback to delete this node
|
| 1277 |
+
* @param onUpdate - Callback to update face settings
|
| 1278 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 1279 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 1280 |
+
* @param onProcess - Callback to process face modifications
|
| 1281 |
+
* @param onUpdatePosition - Callback to update node position
|
| 1282 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 1283 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 1284 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 1285 |
+
*/
|
| 1286 |
export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1287 |
+
// Handle node dragging functionality
|
| 1288 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1289 |
+
|
| 1290 |
+
// Available hairstyle options for hair modification
|
| 1291 |
const hairstyles = ["None", "short", "long", "curly", "straight", "bald", "mohawk", "ponytail"];
|
| 1292 |
+
|
| 1293 |
+
// Facial expression options for emotion changes
|
| 1294 |
const expressions = ["None", "happy", "serious", "smiling", "laughing", "sad", "surprised", "angry"];
|
| 1295 |
+
|
| 1296 |
+
// Beard and facial hair styling options
|
| 1297 |
const beardStyles = ["None", "stubble", "goatee", "full beard", "mustache", "clean shaven"];
|
| 1298 |
|
| 1299 |
return (
|
|
|
|
| 1304 |
onPointerMove={onPointerMove}
|
| 1305 |
onPointerUp={onPointerUp}
|
| 1306 |
>
|
| 1307 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 1308 |
<div className="font-semibold text-sm flex-1 text-center">FACE</div>
|
| 1309 |
<div className="flex items-center gap-1">
|
| 1310 |
<Button
|
|
|
|
| 1336 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1337 |
className="text-xs"
|
| 1338 |
>
|
| 1339 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 1340 |
</Button>
|
| 1341 |
</div>
|
| 1342 |
)}
|
| 1343 |
+
{/* Face Enhancement Checkboxes - toggleable options for face improvements and accessories */}
|
| 1344 |
<div className="space-y-2">
|
| 1345 |
+
{/* Pimple removal option for skin enhancement */}
|
| 1346 |
+
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
| 1347 |
<Checkbox
|
| 1348 |
+
checked={node.faceOptions?.removePimples || false} // Default to false if not set
|
| 1349 |
onChange={(e) => onUpdate(node.id, {
|
| 1350 |
+
faceOptions: {
|
| 1351 |
+
...node.faceOptions, // Preserve existing options
|
| 1352 |
+
removePimples: (e.target as HTMLInputElement).checked // Update pimple removal setting
|
| 1353 |
+
}
|
| 1354 |
})}
|
| 1355 |
/>
|
| 1356 |
+
Remove pimples {/* Clean up skin imperfections */}
|
| 1357 |
</label>
|
| 1358 |
+
|
| 1359 |
+
{/* Sunglasses addition option */}
|
| 1360 |
+
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
| 1361 |
<Checkbox
|
| 1362 |
+
checked={node.faceOptions?.addSunglasses || false} // Default to false if not set
|
| 1363 |
onChange={(e) => onUpdate(node.id, {
|
| 1364 |
+
faceOptions: {
|
| 1365 |
+
...node.faceOptions, // Preserve existing options
|
| 1366 |
+
addSunglasses: (e.target as HTMLInputElement).checked // Update sunglasses setting
|
| 1367 |
+
}
|
| 1368 |
})}
|
| 1369 |
/>
|
| 1370 |
+
Add sunglasses {/* Add stylish sunglasses accessory */}
|
| 1371 |
</label>
|
| 1372 |
+
|
| 1373 |
+
{/* Hat addition option */}
|
| 1374 |
+
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
| 1375 |
<Checkbox
|
| 1376 |
+
checked={node.faceOptions?.addHat || false} // Default to false if not set
|
| 1377 |
onChange={(e) => onUpdate(node.id, {
|
| 1378 |
+
faceOptions: {
|
| 1379 |
+
...node.faceOptions, // Preserve existing options
|
| 1380 |
+
addHat: (e.target as HTMLInputElement).checked // Update hat setting
|
| 1381 |
+
}
|
| 1382 |
})}
|
| 1383 |
/>
|
| 1384 |
+
Add hat {/* Add hat accessory */}
|
| 1385 |
</label>
|
| 1386 |
</div>
|
| 1387 |
|
|
|
|
| 1424 |
</Select>
|
| 1425 |
</div>
|
| 1426 |
|
| 1427 |
+
{/* Makeup Selection Section - allows users to choose makeup application */}
|
| 1428 |
<div>
|
| 1429 |
<label className="text-xs text-white/70">Makeup</label>
|
| 1430 |
+
<div className="grid grid-cols-2 gap-2 mt-2"> {/* 2-column grid for makeup options */}
|
| 1431 |
+
|
| 1432 |
+
{/* No Makeup Option - removes or prevents makeup application */}
|
| 1433 |
<button
|
| 1434 |
+
className={`p-1 rounded border transition-colors ${
|
| 1435 |
!node.faceOptions?.selectedMakeup || node.faceOptions?.selectedMakeup === "None"
|
| 1436 |
+
? "border-indigo-400 bg-indigo-500/20" // Highlighted when selected
|
| 1437 |
+
: "border-white/20 hover:border-white/40" // Default and hover states
|
| 1438 |
}`}
|
| 1439 |
onClick={() => onUpdate(node.id, {
|
| 1440 |
+
faceOptions: {
|
| 1441 |
+
...node.faceOptions, // Preserve other face options
|
| 1442 |
+
selectedMakeup: "None", // Set makeup to none
|
| 1443 |
+
makeupImage: null // Clear makeup image reference
|
| 1444 |
+
}
|
| 1445 |
})}
|
| 1446 |
+
title="No makeup application - natural look"
|
| 1447 |
>
|
| 1448 |
+
{/* Visual placeholder for no makeup option */}
|
| 1449 |
<div className="w-full h-24 flex items-center justify-center text-xs text-white/60 border border-dashed border-white/20 rounded mb-1">
|
| 1450 |
+
No Makeup {/* Text indicator for no makeup */}
|
| 1451 |
</div>
|
| 1452 |
+
<div className="text-xs">None</div> {/* Option label */}
|
| 1453 |
</button>
|
| 1454 |
+
|
| 1455 |
+
{/* Makeup Application Option - applies preset makeup style */}
|
| 1456 |
<button
|
| 1457 |
+
className={`p-1 rounded border transition-colors ${
|
| 1458 |
node.faceOptions?.selectedMakeup === "Makeup"
|
| 1459 |
+
? "border-indigo-400 bg-indigo-500/20" // Highlighted when selected
|
| 1460 |
+
: "border-white/20 hover:border-white/40" // Default and hover states
|
| 1461 |
}`}
|
| 1462 |
onClick={() => onUpdate(node.id, {
|
| 1463 |
+
faceOptions: {
|
| 1464 |
+
...node.faceOptions, // Preserve other face options
|
| 1465 |
+
selectedMakeup: "Makeup", // Set makeup type
|
| 1466 |
+
makeupImage: "/makeup/makeup1.png" // Reference image for makeup style
|
| 1467 |
+
}
|
| 1468 |
})}
|
| 1469 |
+
title="Apply makeup style - enhances facial features"
|
| 1470 |
>
|
| 1471 |
+
{/* Makeup preview image */}
|
| 1472 |
<img
|
| 1473 |
src="/makeup/makeup1.png"
|
| 1474 |
+
alt="Makeup Style Preview"
|
| 1475 |
className="w-full h-24 object-contain rounded mb-1"
|
| 1476 |
+
title="Preview of makeup style that will be applied"
|
| 1477 |
/>
|
| 1478 |
+
<div className="text-xs">Makeup</div> {/* Option label */}
|
| 1479 |
</button>
|
| 1480 |
</div>
|
| 1481 |
</div>
|
|
|
|
| 1493 |
nodeId={node.id}
|
| 1494 |
output={node.output}
|
| 1495 |
downloadFileName={`face-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 1496 |
/>
|
| 1497 |
</div>
|
| 1498 |
{node.error && (
|
|
|
|
| 1503 |
);
|
| 1504 |
}
|
| 1505 |
|
| 1506 |
+
/**
|
| 1507 |
+
* STYLE NODE VIEW COMPONENT
|
| 1508 |
+
*
|
| 1509 |
+
* Applies artistic style transfer to images, transforming them to match
|
| 1510 |
+
* various artistic movements, pop culture aesthetics, and visual styles.
|
| 1511 |
+
*
|
| 1512 |
+
* Key Features:
|
| 1513 |
+
* - Wide variety of artistic styles (anime, fine art, pop culture)
|
| 1514 |
+
* - Adjustable style strength for subtle or dramatic transformations
|
| 1515 |
+
* - Preserves original image content while applying style characteristics
|
| 1516 |
+
* - Real-time style preview and processing
|
| 1517 |
+
*
|
| 1518 |
+
* Style Categories Available:
|
| 1519 |
+
* - Anime styles (90s anime, My Hero Academia, Dragon Ball Z)
|
| 1520 |
+
* - Fine art movements (Ukiyo-e, Cubism, Post-Impressionism)
|
| 1521 |
+
* - Modern aesthetics (Cyberpunk, Steampunk)
|
| 1522 |
+
* - Pop culture (Simpsons, Family Guy, Arcane)
|
| 1523 |
+
* - Cinematic styles (Breaking Bad, Stranger Things)
|
| 1524 |
+
*
|
| 1525 |
+
* The AI style transfer models can:
|
| 1526 |
+
* - Apply artistic brushstrokes and textures
|
| 1527 |
+
* - Adapt color palettes to match target styles
|
| 1528 |
+
* - Maintain subject recognition while stylizing
|
| 1529 |
+
* - Handle various image compositions and subjects
|
| 1530 |
+
*
|
| 1531 |
+
* @param node - Style node data containing stylePreset, styleStrength, etc.
|
| 1532 |
+
* @param onDelete - Callback to delete this node
|
| 1533 |
+
* @param onUpdate - Callback to update style settings
|
| 1534 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 1535 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 1536 |
+
* @param onProcess - Callback to process style transfer
|
| 1537 |
+
* @param onUpdatePosition - Callback to update node position
|
| 1538 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 1539 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 1540 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 1541 |
+
*/
|
| 1542 |
export function StyleNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1543 |
+
// Handle node dragging functionality
|
| 1544 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1545 |
|
| 1546 |
+
/**
|
| 1547 |
+
* Available artistic style options with descriptive labels
|
| 1548 |
+
* Each style represents a different artistic movement or pop culture aesthetic
|
| 1549 |
+
*/
|
| 1550 |
const styleOptions = [
|
| 1551 |
+
{ value: "90s-anime", label: "90's Anime Style" }, // Classic 90s anime art style
|
| 1552 |
+
{ value: "gibhli", label: "Gibhli Style" },
|
| 1553 |
+
{ value: "mha", label: "My Hero Academia Style" }, // Modern anime style
|
| 1554 |
+
{ value: "dbz", label: "Dragon Ball Z Style" }, // Iconic manga/anime style
|
| 1555 |
+
{ value: "ukiyo-e", label: "Ukiyo-e Style" }, // Traditional Japanese woodblock prints
|
| 1556 |
+
{ value: "cyberpunk", label: "Cyberpunk Style" }, // Futuristic neon aesthetic
|
| 1557 |
+
{ value: "steampunk", label: "Steampunk Style" }, // Victorian-era industrial aesthetic
|
| 1558 |
+
{ value: "cubism", label: "Cubism Style" }, // Picasso-style geometric art
|
| 1559 |
+
{ value: "van-gogh", label: "Post-Impressionist (Van Gogh) Style" }, // Van Gogh's distinctive brushwork
|
| 1560 |
+
{ value: "simpsons", label: "Simpsons Style" }, // Cartoon animation style
|
| 1561 |
+
{ value: "family-guy", label: "Family Guy Style" }, // Modern cartoon animation // Netflix series visual style
|
| 1562 |
+
{ value: "wildwest", label: "Wild West Style" },// Cinematic color grading
|
| 1563 |
+
{ value: "star-wars", label: "Star Wars Style" },
|
| 1564 |
+
{ value: "star-trek", label: "Star Trek Style" },
|
| 1565 |
];
|
| 1566 |
|
| 1567 |
return (
|
|
|
|
| 1575 |
onPointerMove={onPointerMove}
|
| 1576 |
onPointerUp={onPointerUp}
|
| 1577 |
>
|
| 1578 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 1579 |
<div className="font-semibold text-sm flex-1 text-center">STYLE</div>
|
| 1580 |
<div className="flex items-center gap-1">
|
| 1581 |
<Button
|
|
|
|
| 1598 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1599 |
</div>
|
| 1600 |
</div>
|
| 1601 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 1602 |
<div className="p-3 space-y-3">
|
| 1603 |
{node.input && (
|
| 1604 |
<div className="flex justify-end mb-2">
|
|
|
|
| 1608 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1609 |
className="text-xs"
|
| 1610 |
>
|
| 1611 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 1612 |
</Button>
|
| 1613 |
</div>
|
| 1614 |
)}
|
|
|
|
| 1626 |
</option>
|
| 1627 |
))}
|
| 1628 |
</Select>
|
| 1629 |
+
{/* Style Strength Slider - controls how strongly the style is applied */}
|
| 1630 |
<div>
|
| 1631 |
<Slider
|
| 1632 |
+
label="Style Strength" // Slider label
|
| 1633 |
+
valueLabel={`${node.styleStrength || 50}%`} // Display current percentage value
|
| 1634 |
+
min={0} // Minimum strength (subtle effect)
|
| 1635 |
+
max={100} // Maximum strength (full style transfer)
|
| 1636 |
+
value={node.styleStrength || 50} // Current value (default 50%)
|
| 1637 |
+
onChange={(e) => onUpdate(node.id, {
|
| 1638 |
+
styleStrength: parseInt((e.target as HTMLInputElement).value) // Update strength value
|
| 1639 |
+
})}
|
| 1640 |
+
title="Adjust how strongly the artistic style is applied - lower values are more subtle"
|
| 1641 |
/>
|
| 1642 |
</div>
|
| 1643 |
+
{/* Style Processing Button - triggers the style transfer operation */}
|
| 1644 |
<Button
|
| 1645 |
className="w-full"
|
| 1646 |
+
onClick={() => onProcess(node.id)} // Start style transfer processing
|
| 1647 |
+
disabled={node.isRunning || !node.stylePreset} // Disable if processing or no style selected
|
| 1648 |
+
title={
|
| 1649 |
+
!node.input ? "Connect an input first" : // No input connection
|
| 1650 |
+
!node.stylePreset ? "Select a style first" : // No style selected
|
| 1651 |
+
"Apply the selected artistic style to your input image" // Ready to process
|
| 1652 |
+
}
|
| 1653 |
>
|
| 1654 |
+
{/* Dynamic button text based on processing state */}
|
| 1655 |
{node.isRunning ? "Applying Style..." : "Apply Style Transfer"}
|
| 1656 |
</Button>
|
| 1657 |
<NodeOutputSection
|
| 1658 |
nodeId={node.id}
|
| 1659 |
output={node.output}
|
| 1660 |
downloadFileName={`style-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 1661 |
/>
|
| 1662 |
{node.error && (
|
| 1663 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 1667 |
);
|
| 1668 |
}
|
| 1669 |
|
| 1670 |
+
/**
|
| 1671 |
+
* LIGHTNING NODE VIEW COMPONENT
|
| 1672 |
+
*
|
| 1673 |
+
* Applies professional lighting effects to images to enhance mood,
|
| 1674 |
+
* atmosphere, and visual impact. Simulates various lighting setups
|
| 1675 |
+
* commonly used in photography and cinematography.
|
| 1676 |
+
*
|
| 1677 |
+
* Key Features:
|
| 1678 |
+
* - Professional lighting presets (studio, natural, dramatic)
|
| 1679 |
+
* - Visual preset selection with thumbnails
|
| 1680 |
+
* - Realistic lighting simulation
|
| 1681 |
+
* - Shadow and highlight adjustment
|
| 1682 |
+
*
|
| 1683 |
+
* Lighting Types Available:
|
| 1684 |
+
* - Studio Light: Controlled, even lighting for professional portraits
|
| 1685 |
+
* - Natural Light: Soft, organic lighting that mimics daylight
|
| 1686 |
+
* - Dramatic Light: High-contrast lighting for artistic effect
|
| 1687 |
+
*
|
| 1688 |
+
* The lighting effects can:
|
| 1689 |
+
* - Add realistic shadows and highlights
|
| 1690 |
+
* - Enhance subject dimensionality
|
| 1691 |
+
* - Create mood and atmosphere
|
| 1692 |
+
* - Simulate professional lighting equipment
|
| 1693 |
+
*
|
| 1694 |
+
* @param node - Lightning node data containing selectedLighting, lightingImage
|
| 1695 |
+
* @param onDelete - Callback to delete this node
|
| 1696 |
+
* @param onUpdate - Callback to update lighting settings
|
| 1697 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 1698 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 1699 |
+
* @param onProcess - Callback to process lighting effects
|
| 1700 |
+
* @param onUpdatePosition - Callback to update node position
|
| 1701 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 1702 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 1703 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 1704 |
+
*/
|
| 1705 |
export function LightningNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1706 |
+
// Handle node dragging functionality
|
| 1707 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1708 |
|
| 1709 |
+
/**
|
| 1710 |
+
* Available lighting preset options with reference images
|
| 1711 |
+
* Each preset demonstrates a different lighting setup and mood
|
| 1712 |
+
*/
|
| 1713 |
const presetLightings = [
|
| 1714 |
+
{ name: "Studio Light", path: "/lighting/light1.png" }, // Professional studio lighting
|
| 1715 |
+
{ name: "Natural Light", path: "/lighting/light2.png" }, // Soft natural daylight
|
| 1716 |
+
{ name: "Dramatic Light", path: "/lighting/light3.png" }, // High-contrast dramatic lighting
|
| 1717 |
];
|
| 1718 |
|
| 1719 |
+
/**
|
| 1720 |
+
* Handle selection of a lighting preset
|
| 1721 |
+
* Updates both the lighting image path and the selected preset name
|
| 1722 |
+
*/
|
| 1723 |
const selectLighting = (lightingPath: string, lightingName: string) => {
|
| 1724 |
+
onUpdate(node.id, {
|
| 1725 |
+
lightingImage: lightingPath, // Path to lighting reference image
|
| 1726 |
+
selectedLighting: lightingName // Name of selected lighting preset
|
| 1727 |
+
});
|
| 1728 |
};
|
| 1729 |
|
| 1730 |
return (
|
|
|
|
| 1735 |
onPointerMove={onPointerMove}
|
| 1736 |
onPointerUp={onPointerUp}
|
| 1737 |
>
|
| 1738 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 1739 |
<div className="font-semibold text-sm flex-1 text-center">LIGHTNING</div>
|
| 1740 |
<div className="flex items-center gap-1">
|
| 1741 |
<Button
|
|
|
|
| 1758 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1759 |
</div>
|
| 1760 |
</div>
|
| 1761 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 1762 |
<div className="p-3 space-y-3">
|
| 1763 |
{node.input && (
|
| 1764 |
<div className="flex justify-end mb-2">
|
|
|
|
| 1768 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1769 |
className="text-xs"
|
| 1770 |
>
|
| 1771 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 1772 |
</Button>
|
| 1773 |
</div>
|
| 1774 |
)}
|
|
|
|
| 1809 |
nodeId={node.id}
|
| 1810 |
output={node.output}
|
| 1811 |
downloadFileName={`lightning-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 1812 |
/>
|
| 1813 |
{node.error && (
|
| 1814 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 1819 |
}
|
| 1820 |
|
| 1821 |
|
| 1822 |
+
/**
|
| 1823 |
+
* POSES NODE VIEW COMPONENT
|
| 1824 |
+
*
|
| 1825 |
+
* Modifies the pose and body positioning of subjects in images.
|
| 1826 |
+
* Uses AI-powered pose estimation and transfer to change how people
|
| 1827 |
+
* are positioned while maintaining natural proportions and anatomy.
|
| 1828 |
+
*
|
| 1829 |
+
* Key Features:
|
| 1830 |
+
* - Multiple preset poses (standing, sitting variations)
|
| 1831 |
+
* - Visual pose selection with reference thumbnails
|
| 1832 |
+
* - Natural pose transfer that preserves identity
|
| 1833 |
+
* - Anatomically correct pose adjustments
|
| 1834 |
+
*
|
| 1835 |
+
* Pose Categories Available:
|
| 1836 |
+
* - Standing poses: Various upright positions and postures
|
| 1837 |
+
* - Sitting poses: Different seated positions and arrangements
|
| 1838 |
+
*
|
| 1839 |
+
* The AI pose models can:
|
| 1840 |
+
* - Detect and map human body keypoints
|
| 1841 |
+
* - Transfer poses while maintaining proportions
|
| 1842 |
+
* - Adjust clothing to fit new poses naturally
|
| 1843 |
+
* - Preserve facial features and identity
|
| 1844 |
+
* - Handle complex body positioning
|
| 1845 |
+
*
|
| 1846 |
+
* @param node - Poses node data containing selectedPose, poseImage
|
| 1847 |
+
* @param onDelete - Callback to delete this node
|
| 1848 |
+
* @param onUpdate - Callback to update pose settings
|
| 1849 |
+
* @param onStartConnection - Callback when starting connection from output
|
| 1850 |
+
* @param onEndConnection - Callback when ending connection at input
|
| 1851 |
+
* @param onProcess - Callback to process pose modifications
|
| 1852 |
+
* @param onUpdatePosition - Callback to update node position
|
| 1853 |
+
* @param getNodeHistoryInfo - Function to get processing history
|
| 1854 |
+
* @param navigateNodeHistory - Function to navigate history
|
| 1855 |
+
* @param getCurrentNodeImage - Function to get current image
|
| 1856 |
+
*/
|
| 1857 |
export function PosesNodeView({ node, onDelete, onUpdate, onStartConnection, onEndConnection, onProcess, onUpdatePosition, getNodeHistoryInfo, navigateNodeHistory, getCurrentNodeImage }: any) {
|
| 1858 |
+
// Handle node dragging functionality
|
| 1859 |
const { localPos, onPointerDown, onPointerMove, onPointerUp } = useNodeDrag(node, onUpdatePosition);
|
| 1860 |
|
| 1861 |
+
/**
|
| 1862 |
+
* Available pose preset options with reference images
|
| 1863 |
+
* Each preset shows a different body position or posture
|
| 1864 |
+
*/
|
| 1865 |
const presetPoses = [
|
| 1866 |
+
{ name: "Standing Pose 1", path: "/poses/stand1.png" }, // First standing position variant
|
| 1867 |
+
{ name: "Standing Pose 2", path: "/poses/stand2.png" }, // Second standing position variant
|
| 1868 |
+
{ name: "Sitting Pose 1", path: "/poses/sit1.png" }, // First sitting position variant
|
| 1869 |
+
{ name: "Sitting Pose 2", path: "/poses/sit2.png" }, // Second sitting position variant
|
| 1870 |
];
|
| 1871 |
|
| 1872 |
+
/**
|
| 1873 |
+
* Handle selection of a pose preset
|
| 1874 |
+
* Updates both the pose reference image and the selected pose name
|
| 1875 |
+
*/
|
| 1876 |
const selectPose = (posePath: string, poseName: string) => {
|
| 1877 |
+
onUpdate(node.id, {
|
| 1878 |
+
poseImage: posePath, // Path to pose reference image
|
| 1879 |
+
selectedPose: poseName // Name of selected pose preset
|
| 1880 |
+
});
|
| 1881 |
};
|
| 1882 |
|
| 1883 |
return (
|
|
|
|
| 1888 |
onPointerMove={onPointerMove}
|
| 1889 |
onPointerUp={onPointerUp}
|
| 1890 |
>
|
| 1891 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 1892 |
<div className="font-semibold text-sm flex-1 text-center">POSES</div>
|
| 1893 |
<div className="flex items-center gap-1">
|
| 1894 |
<Button
|
|
|
|
| 1911 |
<Port className="out" nodeId={node.id} isOutput={true} onStartConnection={onStartConnection} />
|
| 1912 |
</div>
|
| 1913 |
</div>
|
| 1914 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 1915 |
<div className="p-3 space-y-3">
|
| 1916 |
{node.input && (
|
| 1917 |
<div className="flex justify-end mb-2">
|
|
|
|
| 1921 |
onClick={() => onUpdate(node.id, { input: undefined })}
|
| 1922 |
className="text-xs"
|
| 1923 |
>
|
| 1924 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 1925 |
</Button>
|
| 1926 |
</div>
|
| 1927 |
)}
|
|
|
|
| 1962 |
nodeId={node.id}
|
| 1963 |
output={node.output}
|
| 1964 |
downloadFileName={`poses-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 1965 |
/>
|
| 1966 |
{node.error && (
|
| 1967 |
<div className="text-xs text-red-400 mt-2">{node.error}</div>
|
|
|
|
| 2081 |
onPointerUp={onPointerUp} // End dragging
|
| 2082 |
>
|
| 2083 |
{/* Input port (left side) - where connections come in */}
|
| 2084 |
+
<Port className="in" nodeId={node.id} isOutput={false} onEndConnection={onEndConnection} onDisconnect={(nodeId) => onUpdate(nodeId, { input: undefined })} />
|
| 2085 |
|
| 2086 |
{/* Node title */}
|
| 2087 |
<div className="font-semibold text-sm flex-1 text-center">EDIT</div>
|
|
|
|
| 2106 |
</div>
|
| 2107 |
|
| 2108 |
{/* Node Content - Contains all the controls and outputs */}
|
| 2109 |
+
{/* Node Content Area - Contains all controls, inputs, and outputs */}
|
| 2110 |
<div className="p-3 space-y-3">
|
| 2111 |
{/* Show clear connection button if node has input */}
|
| 2112 |
{node.input && (
|
|
|
|
| 2118 |
className="text-xs"
|
| 2119 |
title="Remove input connection"
|
| 2120 |
>
|
| 2121 |
+
Clear Connection {/* Remove input connection to this node */}
|
| 2122 |
</Button>
|
| 2123 |
</div>
|
| 2124 |
)}
|
|
|
|
| 2166 |
nodeId={node.id}
|
| 2167 |
output={node.output}
|
| 2168 |
downloadFileName={`edit-${Date.now()}.png`}
|
|
|
|
|
|
|
|
|
|
| 2169 |
/>
|
| 2170 |
|
| 2171 |
{/* Error display */}
|