gemini-3d-drawing / components /DisplayCanvas.js
Trudy's picture
Initial commit with proper LFS tracking
5f07a23
import { LoaderCircle, ImageIcon, Send, RotateCw, Maximize, Wand2, Sun, Plus, Palette, Image as ImageLucide, Brush, SendToBack, Download, ArrowLeftRight } from 'lucide-react';
import { useEffect, useState, useCallback } from 'react';
import ImageRefiner from './ImageRefiner';
import HeaderButtons from './HeaderButtons';
// Update REFINEMENT_SUGGESTIONS with cleaner labels and full prompts
const REFINEMENT_SUGGESTIONS = [
{ label: 'Rotate', prompt: 'Can you rotate this by ', icon: RotateCw },
{ label: 'Add light', prompt: 'Can you add a light from the ', icon: Sun },
{ label: 'Add object', prompt: 'Can you add a ', icon: Plus },
{ label: 'Background', prompt: 'Can you change the background to ', icon: ImageLucide },
{ label: 'Color', prompt: 'Can you make the color more ', icon: Palette },
{ label: 'Scale', prompt: 'Can you make this ', icon: Maximize },
{ label: 'Lighting', prompt: 'Can you make the lighting more ', icon: Sun },
{ label: 'Style', prompt: 'Can you make it look more ', icon: Wand2 },
{ label: 'Material', prompt: 'Can you change the material to ', icon: Wand2 },
// Add more suggestions as needed...
];
// Update SURPRISE_REFINEMENTS with more fun, bonkers prompts
const SURPRISE_REFINEMENTS = [
// Open-ended prompts (higher probability)
"Do something cool with this. I leave it up to you!",
"Surprise me! Take this image somewhere unexpected.",
"Transform this however you want. Be creative!",
"Do something wild with this image. No limits!",
"Make this image magical in your own way.",
"Take creative freedom with this image. Surprise me!",
"Show me what you can do with this. Go crazy!",
"Transform this in a way I wouldn't expect.",
"Have fun with this and do whatever inspires you.",
"Go wild with this image! Show me something amazing.",
"Put your own creative spin on this image.",
"Reimagine this image however you want. Be bold!",
"Do something unexpected with this. Totally up to you!",
"Surprise me with your creativity. Anything goes!",
"Make this extraordinary in whatever way you choose.",
"Show off your creative abilities with this image!",
"Take this in any direction that excites you!",
"Transform this however your imagination guides you.",
"Make this magical in your own unique way.",
"Do something fun and unexpected with this!",
"Surprise me! Show me your creativity.",
"Make this more beautiful <3",
"Put your artistic spin on this image!",
"Let your imagination run wild with this!",
"Take this image to a whole new level of awesome!",
"Make this image extraordinary in your own way.",
"Do something fantastic with this. Full creative freedom!",
"Surprise me with a totally unexpected transformation!",
"Go nuts with this! Show me something incredible!",
"Add your own wild twist to this image!",
"Make this image come alive however you want!",
"Transform this in the most creative way possible!",
"Go crazy with this. I want to be wowed :))",
"Do whatever magical things you want with this image!",
"Reinvent this image however inspires you!",
// Specific wild ideas (lower probability)
"Can you add this to outer space with aliens having a BBQ?",
"Can you add a giraffe wearing a tuxedo to this?",
"Can you make tiny vikings invade this image?",
"Can you turn this into an ice cream sundae being eaten by robots?",
"Can you make this float in a sea of rainbow soup?",
"Can you add dancing pickles to this image?",
"Can you make this the centerpiece of an alien museum?",
"Can you add this to a cereal bowl being eaten by a giant?",
"Can you make this the star of a bizarre music video?",
"Can you add tiny dinosaurs having a tea party?",
"Can you turn this into something from a fever dream?",
"Can you make this the main character in a surreal fairytale?",
"Can you put this in the middle of a candy landscape?",
"Can you add this to a world where physics works backwards?",
"Can you make this the prize in a cosmic game show?",
"Can you add tiny people worshipping this as a deity?",
"Can you put this in the paws of a giant cosmic cat?",
"Can you make this wearing sunglasses and surfing?",
"Can you add this to a world made entirely of cheese?",
"Can you make this the centerpiece of a goblin birthday party?",
"Can you transform this into a cloud creature floating in the sky?",
"Can you add this to a world where everything is made of pasta?",
"Can you turn this into a piñata at a monster celebration?",
"Can you add this to outer space?",
"Can you add this to a landscape made of breakfast foods?",
"Can you make this the conductor of an orchestra of unusual animals?",
"Can you turn this into a strange plant growing in an alien garden?",
"Can you add this to a world inside a snow globe?",
"Can you make this the secret ingredient in a witch's cauldron?",
"Can you turn this into a superhero with an unusual power?",
"Can you make this swimming in a sea of jelly beans?",
"Can you add this to a planet where everything is upside down?",
"Can you make this the treasure in a dragon's unusual collection?",
"Can you transform this into a character in a bizarre cartoon?",
"Can you add this to a world where shadows come to life?"
];
const DisplayCanvas = ({
displayCanvasRef,
isLoading,
handleSaveImage,
handleRegenerate,
hasGeneratedContent = false,
currentDimension,
onOpenHistory,
onRefineImage,
onSendToDoodle,
hasHistory,
openHistoryModal,
toggleLibrary
}) => {
const [showPlaceholder, setShowPlaceholder] = useState(true);
const [inputValue, setInputValue] = useState('');
const [showDoodleTooltip, setShowDoodleTooltip] = useState(false);
const [showSaveTooltip, setShowSaveTooltip] = useState(false);
const [isHoveringCanvas, setIsHoveringCanvas] = useState(false);
// Update placeholder visibility when loading or content prop changes
useEffect(() => {
if (hasGeneratedContent) {
setShowPlaceholder(false);
} else if (isLoading) {
setShowPlaceholder(true);
}
}, [isLoading, hasGeneratedContent]);
const handleSubmit = (e) => {
e.preventDefault();
if (!inputValue.trim()) return;
onRefineImage(inputValue);
setInputValue('');
};
const handleSuggestionClick = (suggestion) => {
setInputValue(suggestion.prompt);
document.querySelector('input[name="refiner"]').focus();
};
const handleSurpriseMe = () => {
const randomPrompt = SURPRISE_REFINEMENTS[Math.floor(Math.random() * SURPRISE_REFINEMENTS.length)];
setInputValue(randomPrompt);
document.querySelector('input[name="refiner"]').focus();
};
const handleSendToDoodle = useCallback(() => {
if (displayCanvasRef.current && onSendToDoodle) {
const imageDataUrl = displayCanvasRef.current.toDataURL('image/png');
onSendToDoodle(imageDataUrl);
}
}, [displayCanvasRef, onSendToDoodle]);
// Function to handle clicking the canvas for regeneration
const handleCanvasClickForRegenerate = () => {
if (hasGeneratedContent && !isLoading) {
handleRegenerate();
}
};
// Placeholder function for fullscreen action
const handleFullscreen = (e) => {
e.stopPropagation(); // Prevent triggering the refresh
alert('Fullscreen action triggered!');
// TODO: Implement actual fullscreen logic (e.g., using Fullscreen API or opening image in new tab)
};
return (
<div className="flex flex-col">
{/* Canvas container with fixed aspect ratio */}
<div
className="relative w-full"
style={{ aspectRatio: `${currentDimension.width} / ${currentDimension.height}` }}
>
<button
type="button"
className="w-full h-full absolute inset-0 z-10 cursor-pointer appearance-none bg-transparent border-0 group"
onMouseEnter={() => setIsHoveringCanvas(true)}
onMouseLeave={() => setIsHoveringCanvas(false)}
onClick={handleCanvasClickForRegenerate}
disabled={!hasGeneratedContent || isLoading}
aria-label="Regenerate image"
/>
<canvas
ref={displayCanvasRef}
width={currentDimension.width}
height={currentDimension.height}
className="absolute inset-0 w-full h-full border border-gray-300 bg-white rounded-xl shadow-soft"
aria-label="Generated image canvas"
/>
{/* Loading overlay */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/5 rounded-xl">
<div className="bg-white/90 rounded-full p-3 shadow-medium">
<LoaderCircle className="w-8 h-8 animate-spin text-gray-700" />
</div>
</div>
)}
{/* Placeholder overlay */}
{showPlaceholder && !isLoading && !hasGeneratedContent && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<ImageIcon className="w-7 h-7 text-gray-400 mb-2" />
<p className="text-gray-400 text-lg font-medium">Generation will appear here</p>
</div>
)}
{/* Hover-to-Refresh Overlay */}
{isHoveringCanvas && hasGeneratedContent && !isLoading && (
// Added transition classes for smoother appearance
// Changed bg-black/20 to bg-black/10 for a gentler overlay
<div className="absolute inset-0 flex items-center justify-center bg-black/0 rounded-xl pointer-events-none transition-opacity duration-300 ease-in-out">
{/* Container for icon - allows separate click handling */}
{/* Removed Maximize icon */}
<div className="flex items-center gap-4 bg-white/90 rounded-full p-3 shadow-medium pointer-events-auto">
{/* Refresh Icon (clickable via parent div onClick) */}
<RotateCw className="w-8 h-8 text-gray-700" title="Click canvas to regenerate"/>
</div>
</div>
)}
</div>
{/* Action bar and refiner section - Combined layout */}
<div className="mt-4 flex items-stretch justify-between gap-2 max-w-full">
{/* Left side wrapper for Refiner and action buttons */}
<div className="flex items-stretch gap-2 flex-1 min-w-0">
{/* Refiner input - only shown when there's generated content */}
{hasGeneratedContent ? (
<>
<form onSubmit={handleSubmit} className="flex-1 min-w-0">
<div className="group flex items-center bg-gray-50 focus-within:bg-white rounded-xl shadow-soft p-2 border border-gray-200 focus-within:border-gray-300 h-14 transition-colors">
<input
name="refiner"
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type to refine the image..."
disabled={isLoading}
className="flex-1 px-2 bg-transparent border-none text-sm text-gray-400 placeholder-gray-300 group-focus-within:text-gray-600 group-focus-within:placeholder-gray-400 focus:outline-none transition-colors"
/>
<button
type="submit"
disabled={isLoading || !inputValue.trim()}
className="p-2 rounded-lg text-gray-400 group-focus-within:text-gray-700 hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Send refinement"
>
<Send className="w-5 h-5" />
</button>
</div>
</form>
{/* Action buttons wrapper */}
<div className="flex gap-2">
{/* "Send to Doodle" button with tooltip */}
<div className="relative">
<button
type="button"
onClick={handleSendToDoodle}
disabled={isLoading}
onMouseEnter={() => setShowDoodleTooltip(true)}
onMouseLeave={() => setShowDoodleTooltip(false)}
className="group w-14 h-14 p-2 rounded-lg bg-gray-50 border border-gray-200 shadow-soft hover:bg-white hover:border-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
aria-label="Send image back to doodle canvas"
>
<ArrowLeftRight className="w-5 h-5 text-gray-400 group-hover:text-gray-700 transition-colors" />
</button>
{/* Custom Tooltip - Right aligned */}
{showDoodleTooltip && (
<div className="absolute bottom-full right-0 mb-2 px-3 py-1.5 bg-white border border-gray-200 text-gray-700 text-xs rounded-lg shadow-soft whitespace-nowrap pointer-events-none">
Send Back to Doodle Canvas
</div>
)}
</div>
</div>
</>
) : (
// Placeholder div to maintain layout when no content
<div className="flex-1 h-0" />
)}
</div>
</div>
{/* Refined suggestion chips */}
{hasGeneratedContent && (
<div className="mt-4 flex flex-wrap gap-2 text-xs mb-4">
{/* Surprise Me button with updated styling */}
<button
type="button"
onClick={handleSurpriseMe}
className="group flex items-center gap-2 px-3 py-1 bg-gray-50 hover:bg-white rounded-full border border-gray-200 text-gray-400 hover:border-gray-300 transition-all focus:outline-none focus:ring-2 focus:ring-gray-200"
>
<Wand2 className="w-4 h-4 group-hover:text-gray-600" />
<span className="group-hover:text-gray-600">Surprise Me</span>
</button>
{/* Regular suggestion buttons with updated styling */}
{REFINEMENT_SUGGESTIONS.map((suggestion) => (
<button
key={`suggestion-${suggestion.label}`}
type="button"
onClick={() => handleSuggestionClick(suggestion)}
className="group flex items-center gap-2 px-3 py-1 bg-gray-50 hover:bg-white rounded-full border border-gray-200 text-gray-400 hover:border-gray-300 transition-all focus:outline-none focus:ring-2 focus:ring-gray-200"
>
<suggestion.icon className="w-4 h-4 group-hover:text-gray-600" />
<span className="group-hover:text-gray-600">{suggestion.label}</span>
</button>
))}
</div>
)}
{/* Header buttons - Mobile only, appearing below refinement options */}
<div className="md:hidden flex flex-wrap justify-between items-center gap-2 mt-6 mb-2 w-full">
<div className="grid grid-cols-3 w-full gap-2">
<HeaderButtons
hasHistory={hasHistory}
openHistoryModal={openHistoryModal}
toggleLibrary={toggleLibrary}
handleSaveImage={handleSaveImage}
isLoading={isLoading}
hasGeneratedContent={hasGeneratedContent}
/>
</div>
</div>
</div>
);
};
export default DisplayCanvas;