Float Image
Float Image
import React, { useState, useCallback, useEffect, useRef } from 'react'; import { UploadCloud, CheckCircle, Scaling, Palette, Image as ImageIcon } from 'lucide-react'; // --- API CONFIGURATION (DO NOT CHANGE) --- // Note: API Key is handled by the execution environment. const GEMINI_VISION_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key="; const IMAGEN_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key="; const API_KEY = ""; // Kept empty; provided by runtime // --- TARGET SIZES FOR SOCIETY6 / DENY DESIGNS (IN PIXELS) --- const TARGET_SIZES = [ { name: "Horizontal Print", w: 7400, h: 5000, type: "Horizontal" }, { name: "Vertical Print", w: 5525, h: 6500, type: "Vertical" }, { name: "Square Standard", w: 6000, h: 6000, type: "Square" }, { name: "Square Small", w: 4000, h: 4000, type: "Square" }, ]; // Utility function to convert a file to a base64 string const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = error => reject(error); }); }; // Utility function to draw and return the high-resolution canvas element const drawTiledCanvas = (img, targetWidth, targetHeight, multiplier) => { const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext('2d'); // Calculate the scaled size of the pattern tile const tileWidth = img.width * multiplier; const tileHeight = img.height * multiplier; // Set up the pattern using the image const pattern = ctx.createPattern(img, 'repeat'); if (pattern) { // To scale the pattern repeat, we must use a temporary canvas to apply scaling // This is a known browser requirement for scaling patterns correctly. const tempCanvas = document.createElement('canvas'); tempCanvas.width = tileWidth; tempCanvas.height = tileHeight; const tempCtx = tempCanvas.getContext('2d'); // Draw the original image scaled to the desired tile size tempCtx.drawImage(img, 0, 0, tileWidth, tileHeight); // Create the pattern from the scaled content const finalPattern = ctx.createPattern(tempCanvas, 'repeat'); ctx.fillStyle = finalPattern; ctx.fillRect(0, 0, targetWidth, targetHeight); } else { // Fallback ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, targetWidth, targetHeight); } return canvas; }; // --- COMPONENTS --- // Canvas component for live preview of tiling const LivePreviewCanvas = React.memo(({ imgSrc, width, height, scale }) => { const canvasRef = useRef(null); const imgRef = useRef(null); // Dynamic size for preview element (max 250px on the longest side) const previewSize = 250; const aspectRatio = width / height; let previewWidth = previewSize; let previewHeight = previewSize; if (aspectRatio > 1) { // Horizontal previewHeight = previewSize / aspectRatio; } else { // Vertical or Square previewWidth = previewSize * aspectRatio; } useEffect(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); const img = imgRef.current; // Set the actual canvas pixel size to match the constrained preview size canvas.width = previewWidth; canvas.height = previewHeight; ctx.clearRect(0, 0, canvas.width, canvas.height); if (img && img.complete) { ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, canvas.width, canvas.height); const imgAspect = img.width / img.height; // Determine the size of the image when scaled by the 'scale' factor relative to the preview size const baseTileWidth = previewWidth * scale; // Adjust the draw size based on the image's inherent aspect ratio, NOT the target size aspect ratio const actualTileWidth = baseTileWidth; const actualTileHeight = actualTileWidth / imgAspect; // Draw tiles (handle repetition for visualization) for (let x = -actualTileWidth; x < previewWidth + actualTileWidth; x += actualTileWidth) { for (let y = -actualTileHeight; y < previewHeight + actualTileHeight; y += actualTileHeight) { ctx.drawImage(img, x, y, actualTileWidth, actualTileHeight); } } } }, [imgSrc, scale, previewWidth, previewHeight, width, height]); return (
{/* The hidden image loads the source data */} Source Pattern { // Trigger re-render once image is loaded to populate the canvas if (canvasRef.current) { canvasRef.current.dispatchEvent(new Event('load')); } }} />
); }); // Step 1: Upload const UploadStep = ({ setSourceImage, setStep }) => { const handleFileChange = async (event) => { const file = event.target.files[0]; if (file) { const base64 = await fileToBase64(file); setSourceImage(base64); setStep(2); } }; return (

1. Upload Your Flat Design

Please upload the pre-flattened pattern (JPG or PNG) you wish to scale.

); }; // Step 2: Tiling & Resizing const TilingResizingStep = ({ sourceImage, setFinalDesignUrl, setStep }) => { const [sizeSettings, setSizeSettings] = useState(() => TARGET_SIZES.map(s => ({ ...s, multiplier: 1, outputUrl: null, isProcessing: false })) ); const imgRef = useRef(null); const handleMultiplierChange = (index, newMultiplier) => { setSizeSettings(prevSettings => prevSettings.map((s, i) => i === index ? { ...s, multiplier: newMultiplier, outputUrl: null } : s ) ); }; const processDesignImage = useCallback(async (index) => { if (!imgRef.current) return; setSizeSettings(prevSettings => prevSettings.map((s, i) => i === index ? { ...s, isProcessing: true } : s ) ); const { w, h, multiplier, name } = sizeSettings[index]; const img = imgRef.current; try { // 1. Generate the high-resolution canvas const canvas = drawTiledCanvas(img, w, h, multiplier); // 2. Convert high-res canvas to a Blob (safe for large files) await new Promise(resolve => canvas.toBlob((blob) => { if (!blob) { throw new Error("Failed to create blob from canvas."); } // 3. Create a temporary URL for download const outputUrl = URL.createObjectURL(blob); // 4. Update state with the final URL setSizeSettings(prevSettings => prevSettings.map((s, i) => i === index ? { ...s, outputUrl: outputUrl, isProcessing: false } : s ) ); // Set the first generated output as the main final design for subsequent steps (Step 3/4) if (index === 0) { // Use Data URL format for AI call in step 3/4 setFinalDesignUrl(canvas.toDataURL('image/jpeg', 0.95)); // REMOVED: setStep(3); // NO LONGER AUTO ADVANCING } resolve(); }, 'image/jpeg', 0.95)); // Specify JPEG format and quality } catch (error) { console.error(`Error generating ${name} file:`, error); setSizeSettings(prevSettings => prevSettings.map((s, i) => i === index ? { ...s, isProcessing: false } : s ) ); } }, [sizeSettings, setFinalDesignUrl]); // Removed setStep from dependencies // Function to manually advance the step const handleAdvanceStep = () => { // Find the Data URL needed for the AI steps (Step 3/4). We use the first generated size for analysis. const firstGeneratedSize = sizeSettings.find(s => s.outputUrl); if (firstGeneratedSize) { // Need to convert the Blob URL back to a Data URL for the AI API calls fetch(firstGeneratedSize.outputUrl) .then(res => res.blob()) .then(blob => { const reader = new FileReader(); reader.onloadend = () => { setFinalDesignUrl(reader.result); setStep(3); }; reader.readAsDataURL(blob); }) .catch(error => { console.error("Error converting Blob back to Data URL:", error); alert("Could not prepare the final design for AI analysis. Please try generating a file again."); }); } else { alert("Please generate at least one high-resolution file before advancing to Step 3."); } }; return (

2. High-Resolution Tiling & Resizing

Adjust the **Tile Scale** (how many times the pattern repeats) for each required size. The canvas preview updates live. Click **Generate** to create the final high-resolution, production-ready JPG file for download.

{/* Hidden image element to load the source data once */} Source
{sizeSettings.map((size, index) => (

{size.name}

Target: **{size.w} x {size.h}** px

{/* Live Preview and Controls */}
handleMultiplierChange(index, parseFloat(e.target.value))} className="w-full h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer range-lg" />

1.0 = Original Tile Size on Preview

{/* Generation Button */} {/* Download Link */} {size.outputUrl && ( URL.revokeObjectURL(size.outputUrl)} // Clean up memory after download > Download Processed File )}
))}
{/* Manual Step Advance Button */}

Finished generating files? Advance to AI Content Generation.

); }; // Step 3: AI Content Generator const ContentGeneratorStep = ({ finalDesignUrl, setContent, content }) => { const [isLoading, setIsLoading] = useState(false); const generateContent = async () => { if (!finalDesignUrl) { console.error("No final design image available for content generation."); return; } setIsLoading(true); const systemPrompt = `You are a professional brand copywriter for high-end home decor products (like Society6 or Deny Designs). Analyze the uploaded design image. Your response MUST be a single JSON object. Mandatory rules: 1. Identify the primary style (e.g., 'Mid-Century Modern', 'Boho', 'Geometric', 'Floral'). 2. Keep the 'productDescription' under 300 characters. 3. The 'designTitle' must include the primary style/feature of the design. JSON Structure Required: { "collectionName": "A creative, marketable collection name (max 4 words)", "designTitle": "A descriptive title including the main style/feature (e.g., 'Vibrant Geometric Honeycomb')", "productDescription": "A brief, compelling description (under 300 characters) explaining the inspiration and how it enhances home decor.", "pinterestPinTitle": "A catchy title for a Pinterest pin (max 10 words)", "pinterestDescription": "A detailed, keyword-rich description for a Pinterest pin (max 500 characters)." } `; const payload = { contents: [ { role: "user", parts: [ { text: "Analyze the following pattern design and generate the required marketing copy." }, { inlineData: { mimeType: "image/jpeg", data: finalDesignUrl.split(',')[1] } } ] } ], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { collectionName: { type: "STRING" }, designTitle: { type: "STRING" }, productDescription: { type: "STRING" }, pinterestPinTitle: { type: "STRING" }, pinterestDescription: { type: "STRING" }, }, } } }; try { const response = await fetch(GEMINI_VISION_API_URL + API_KEY, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); const jsonText = result?.candidates?.[0]?.content?.parts?.[0]?.text; if (jsonText) { const parsedContent = JSON.parse(jsonText); setContent(parsedContent); } else { console.error("AI Response Missing or Invalid:", result); // Use custom message box instead of alert alert("Failed to generate content. Please check the design image and try again."); } } catch (e) { console.error("API Call or Parsing Error:", e); // Use custom message box instead of alert alert("An error occurred during content generation."); } finally { setIsLoading(false); } }; return (

3. AI Content Generator

The AI will analyze your scaled design to automatically generate marketable titles, descriptions, and social media copy.

{content && (

Generated Copy:

{Object.entries(content).map(([key, value]) => (

{value}

))}
)}
); }; // Step 4: AI Mockup Creator const MockupCreatorStep = ({ finalDesignUrl, content }) => { const [productType, setProductType] = useState('throw pillow'); const [mockupUrl, setMockupUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); const generateMockup = async () => { if (!finalDesignUrl) return; setIsLoading(true); setMockupUrl(null); const designDescription = content?.designTitle || "A stunning geometric pattern."; // We use the design description and the product type to generate a detailed prompt const prompt = `Generate a realistic, high-quality photograph of a single ${productType} featuring the following pattern seamlessly tiled across its surface. The pattern style is '${designDescription}'. Ensure the lighting is soft and the background complements the modern home decor style. The pattern should look like a printed product, not a digital overlay.`; const payload = { instances: [ { prompt: prompt, image: { bytesBase64Encoded: finalDesignUrl.split(',')[1] // Pass the generated pattern as input } } ], parameters: { sampleCount: 1, aspectRatio: "1:1", outputMimeType: "image/jpeg" } }; try { const response = await fetch(IMAGEN_API_URL + API_KEY, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); const base64Data = result?.predictions?.[0]?.bytesBase64Encoded; if (base64Data) { setMockupUrl(`data:image/png;base64,${base64Data}`); } else { console.error("Imagen API Error:", result); // Use custom message box instead of alert alert("Mockup generation failed. Check the console for details."); } } catch (e) { console.error("API Call Error:", e); } finally { setIsLoading(false); } }; return (

4. AI Mockup Creator

Generate high-quality product photos for social media using your final design.

{mockupUrl && (

Generated Mockup:

{`${productType}
Download Mockup Image
)}
); }; // --- MAIN APP COMPONENT --- const App = () => { const [step, setStep] = useState(1); const [sourceImage, setSourceImage] = useState(null); // Base64 of the initial upload const [finalDesignUrl, setFinalDesignUrl] = useState(null); // Data URL of the final scaled/tiled design const [content, setContent] = useState(null); // Generated copy from Step 3 const steps = [ { id: 1, name: 'Upload Source', component: UploadStep, ready: true }, { id: 2, name: 'Tiling & Resizing', component: TilingResizingStep, ready: !!sourceImage }, { id: 3, name: 'AI Content Generator', component: ContentGeneratorStep, ready: !!finalDesignUrl }, { id: 4, name: 'AI Mockup Creator', component: MockupCreatorStep, ready: !!finalDesignUrl && !!content }, ]; const CurrentComponent = steps.find(s => s.id === step)?.component || UploadStep; // Use optional chaining and default return (

Society6 & Deny Designs Studio

High-Resolution Preparation & AI Copy Generation

{/* Step Navigation */}
{/* Content Area */}
{/* Design Preview */} {(sourceImage || finalDesignUrl) && (

Current Design Status

{sourceImage && (

1. Uploaded Source (Flat)

Source
)} {finalDesignUrl && (

2. Final Processed Design (Scaled/Tiled)

Final
)}
)}
); }; export default App;