← back to My Childhood Pics
app/page.tsx
588 lines
"use client"
import { useState, useCallback, useRef, useEffect } from "react"
import { Upload, X, Download, ArrowRight, Loader2 } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { removeBackground } from '@imgly/background-removal'
type UploadSlot = "person1" | "person2" | "environment"
export default function Home() {
const [person1, setPerson1] = useState<string | null>(null)
const [person1Processed, setPerson1Processed] = useState<string | null>(null)
const [person2, setPerson2] = useState<string | null>(null)
const [person2Processed, setPerson2Processed] = useState<string | null>(null)
const [environment, setEnvironment] = useState<string | null>(null)
const [selectedEnv, setSelectedEnv] = useState<string>("")
const [isGenerating, setIsGenerating] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [result, setResult] = useState<string | null>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
// Auto-request location permission on mount
useEffect(() => {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("Location access granted:", position.coords.latitude, position.coords.longitude)
},
(error) => {
console.log("Location access denied or error:", error.message)
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
)
}
}, [])
const environments = [
{ id: "beach", name: "BEACH", img: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200" },
{ id: "mountain", name: "MOUNTAIN", img: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200" },
{ id: "city", name: "CITY", img: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1200" },
{ id: "forest", name: "FOREST", img: "https://images.unsplash.com/photo-1448375240586-882707db888b?w=1200" },
{ id: "space", name: "SPACE", img: "https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?w=1200" },
{ id: "desert", name: "DESERT", img: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?w=1200" },
]
const handleFileUpload = useCallback(async (files: FileList | null, slot: UploadSlot) => {
if (files && files[0]) {
const reader = new FileReader()
reader.onload = async (e) => {
const result = e.target?.result as string
if (slot === "person1") {
setPerson1(result)
setIsProcessing(true)
try {
// Remove background using AI
const processedImage = await removeBackground(result)
const blob = processedImage
const url = URL.createObjectURL(blob)
setPerson1Processed(url)
} catch (error) {
console.error('Background removal failed:', error)
setPerson1Processed(result) // Fallback to original
}
setIsProcessing(false)
} else if (slot === "person2") {
setPerson2(result)
setIsProcessing(true)
try {
// Remove background using AI
const processedImage = await removeBackground(result)
const blob = processedImage
const url = URL.createObjectURL(blob)
setPerson2Processed(url)
} catch (error) {
console.error('Background removal failed:', error)
setPerson2Processed(result) // Fallback to original
}
setIsProcessing(false)
} else {
setEnvironment(result)
setSelectedEnv("")
}
}
reader.readAsDataURL(files[0])
}
}, [])
const extractPerson = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, x: number, y: number, width: number, height: number) => {
// For now, just composite the person image with blend mode
// Proper extraction would require ML-based segmentation
ctx.save()
// Add shadow for depth
ctx.shadowColor = 'rgba(0,0,0,0.5)'
ctx.shadowBlur = 20
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 10
// Draw the person image
// Using globalCompositeOperation for better blending
ctx.globalCompositeOperation = 'source-over'
ctx.drawImage(img, x, y, width, height)
ctx.restore()
}
const mergeImages = async () => {
return new Promise<string>((resolve) => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
canvas.width = 1200
canvas.height = 800
// Get the environment image to use
let envImage = environment
if (!envImage && selectedEnv) {
const selectedEnvData = environments.find(e => e.id === selectedEnv)
envImage = selectedEnvData?.img || null
}
const imagesToLoad: Promise<{type: string, img: HTMLImageElement}>[] = []
// Load environment first (as background)
if (envImage) {
imagesToLoad.push(
new Promise((resolve) => {
const img = new Image()
img.crossOrigin = "anonymous"
img.onload = () => resolve({type: 'env', img})
img.src = envImage
})
)
}
// Load person 1 (use processed version if available)
if (person1Processed || person1) {
imagesToLoad.push(
new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({type: 'person1', img})
img.src = person1Processed || person1
})
)
}
// Load person 2 (use processed version if available)
if (person2Processed || person2) {
imagesToLoad.push(
new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({type: 'person2', img})
img.src = person2Processed || person2
})
)
}
Promise.all(imagesToLoad).then((loadedImages) => {
// Clear canvas
ctx.fillStyle = '#000000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Find each image type
const envImg = loadedImages.find(i => i.type === 'env')?.img
const person1Img = loadedImages.find(i => i.type === 'person1')?.img
const person2Img = loadedImages.find(i => i.type === 'person2')?.img
// Draw environment as full background
if (envImg) {
ctx.drawImage(envImg, 0, 0, canvas.width, canvas.height)
// Add subtle vignette effect
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) / 2
)
gradient.addColorStop(0.5, 'rgba(0,0,0,0)')
gradient.addColorStop(1, 'rgba(0,0,0,0.3)')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
// Calculate positions and scales for people with better proportions
const groundLevel = canvas.height * 0.9 // Where feet should be
const targetPersonHeight = canvas.height * 0.45 // Target height for people in environment
// Calculate number of people to position
const peopleCount = (person1Img ? 1 : 0) + (person2Img ? 1 : 0)
if (peopleCount === 1) {
// Single person - center them
const personImg = person1Img || person2Img
if (personImg) {
const aspectRatio = personImg.width / personImg.height
const personHeight = targetPersonHeight
const personWidth = personHeight * aspectRatio
const personX = (canvas.width - personWidth) / 2
const personY = groundLevel - personHeight
extractPerson(ctx, personImg, personX, personY, personWidth, personHeight)
}
} else if (peopleCount === 2) {
// Two people - position them side by side
if (person1Img) {
const aspectRatio = person1Img.width / person1Img.height
const p1Height = targetPersonHeight
const p1Width = p1Height * aspectRatio
const p1X = canvas.width * 0.33 - p1Width / 2
const p1Y = groundLevel - p1Height
extractPerson(ctx, person1Img, p1X, p1Y, p1Width, p1Height)
}
if (person2Img) {
const aspectRatio = person2Img.width / person2Img.height
const p2Height = targetPersonHeight * 0.95 // Slightly smaller for depth
const p2Width = p2Height * aspectRatio
const p2X = canvas.width * 0.67 - p2Width / 2
const p2Y = groundLevel - p2Height
extractPerson(ctx, person2Img, p2X, p2Y, p2Width, p2Height)
}
}
// Add subtle overlay text at bottom
const gradient = ctx.createLinearGradient(0, canvas.height - 100, 0, canvas.height)
gradient.addColorStop(0, 'rgba(0,0,0,0)')
gradient.addColorStop(1, 'rgba(0,0,0,0.7)')
ctx.fillStyle = gradient
ctx.fillRect(0, canvas.height - 100, canvas.width, 100)
ctx.font = 'bold 20px sans-serif'
ctx.fillStyle = 'rgba(255,255,255,0.9)'
ctx.textAlign = 'center'
ctx.fillText('RECREATED MEMORY', canvas.width / 2, canvas.height - 20)
// Convert canvas to image
const mergedImage = canvas.toDataURL('image/png')
resolve(mergedImage)
})
})
}
const handleGenerate = async () => {
if (!person1) return
if (!selectedEnv && !environment) return
setIsGenerating(true)
// Small delay to show loading state
await new Promise(resolve => setTimeout(resolve, 500))
// Merge all images
const mergedImage = await mergeImages()
// Additional delay for effect
await new Promise(resolve => setTimeout(resolve, 1000))
setResult(mergedImage)
setIsGenerating(false)
}
const reset = () => {
setPerson1(null)
setPerson1Processed(null)
setPerson2(null)
setPerson2Processed(null)
setEnvironment(null)
setSelectedEnv("")
setResult(null)
}
const downloadImage = () => {
if (!result) return
const link = document.createElement('a')
link.download = 'recreated-memory.png'
link.href = result
link.click()
}
const canGenerate = person1 && (selectedEnv || environment)
return (
<div className="min-h-screen bg-black text-white p-8">
<canvas ref={canvasRef} style={{ display: 'none' }} />
<div className="max-w-6xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-16"
>
<h1 className="text-6xl md:text-8xl font-bold mb-4">RECREATE</h1>
<p className="text-xl text-white/60">Place your childhood photos in new environments</p>
<p className="text-sm text-white/40 mt-2">AI automatically removes backgrounds from your photos</p>
</motion.div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Left: Inputs */}
<div className="space-y-8">
{/* Upload Slots */}
<div className="space-y-4">
<h2 className="text-sm font-bold tracking-wider text-white/60">PEOPLE TO EXTRACT</h2>
{/* Person 1 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
>
<div className="border border-white/20 p-4 flex items-center justify-between hover:border-white/40 transition-colors relative">
{isProcessing && person1 && !person1Processed && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-10">
<Loader2 className="w-6 h-6 animate-spin text-white" />
</div>
)}
{person1 ? (
<>
<div className="flex items-center gap-4">
<img src={person1} alt="Person 1" className="w-16 h-16 object-cover" />
<span className="text-sm font-medium">PERSON 1 ✓</span>
</div>
<button
onClick={() => {
setPerson1(null)
setPerson1Processed(null)
}}
className="p-2 hover:bg-white/10 rounded transition-colors"
>
<X className="w-4 h-4" />
</button>
</>
) : (
<label className="flex items-center gap-4 cursor-pointer w-full">
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload(e.target.files, "person1")}
className="hidden"
/>
<Upload className="w-4 h-4 text-white/40" />
<span className="text-sm text-white/60">PERSON 1 (REQUIRED)</span>
</label>
)}
</div>
</motion.div>
{/* Person 2 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<div className="border border-white/20 p-4 flex items-center justify-between hover:border-white/40 transition-colors relative">
{isProcessing && person2 && !person2Processed && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-10">
<Loader2 className="w-6 h-6 animate-spin text-white" />
</div>
)}
{person2 ? (
<>
<div className="flex items-center gap-4">
<img src={person2} alt="Person 2" className="w-16 h-16 object-cover" />
<span className="text-sm font-medium">PERSON 2 ✓</span>
</div>
<button
onClick={() => {
setPerson2(null)
setPerson2Processed(null)
}}
className="p-2 hover:bg-white/10 rounded transition-colors"
>
<X className="w-4 h-4" />
</button>
</>
) : (
<label className="flex items-center gap-4 cursor-pointer w-full">
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload(e.target.files, "person2")}
className="hidden"
/>
<Upload className="w-4 h-4 text-white/40" />
<span className="text-sm text-white/60">PERSON 2 (OPTIONAL)</span>
</label>
)}
</div>
</motion.div>
{/* Custom Environment */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
>
<div className="border border-white/20 p-4 flex items-center justify-between hover:border-white/40 transition-colors">
{environment ? (
<>
<div className="flex items-center gap-4">
<img src={environment} alt="Environment" className="w-16 h-16 object-cover" />
<span className="text-sm font-medium">CUSTOM ENVIRONMENT ✓</span>
</div>
<button
onClick={() => {
setEnvironment(null)
setSelectedEnv("")
}}
className="p-2 hover:bg-white/10 rounded transition-colors"
>
<X className="w-4 h-4" />
</button>
</>
) : (
<label className="flex items-center gap-4 cursor-pointer w-full">
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload(e.target.files, "environment")}
className="hidden"
/>
<Upload className="w-4 h-4 text-white/40" />
<span className="text-sm text-white/60">CUSTOM ENVIRONMENT</span>
</label>
)}
</div>
</motion.div>
</div>
{/* Environment Presets */}
<div className="space-y-4">
<h2 className="text-sm font-bold tracking-wider text-white/60">SELECT DESTINATION</h2>
<div className="grid grid-cols-3 gap-2">
{environments.map((env, i) => (
<motion.button
key={env.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 + i * 0.05 }}
onClick={() => {
setSelectedEnv(env.id)
setEnvironment(null)
}}
className={`relative aspect-video overflow-hidden border ${
selectedEnv === env.id
? 'border-white ring-2 ring-white ring-offset-2 ring-offset-black'
: 'border-white/20 hover:border-white/40'
} transition-all`}
>
<img src={env.img} alt={env.name} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" />
<span className="absolute bottom-2 left-2 text-xs font-bold">{env.name}</span>
{selectedEnv === env.id && (
<div className="absolute top-2 right-2 w-4 h-4 bg-white rounded-full flex items-center justify-center">
<span className="text-black text-xs">✓</span>
</div>
)}
</motion.button>
))}
</div>
</div>
{/* Generate Button */}
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
onClick={handleGenerate}
disabled={!canGenerate || isGenerating}
className={`w-full p-4 border flex items-center justify-center gap-3 transition-all ${
canGenerate && !isGenerating
? 'border-white bg-white text-black hover:bg-white/90 cursor-pointer'
: 'border-white/20 text-white/40 cursor-not-allowed'
}`}
>
{isGenerating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span className="font-bold tracking-wider">PROCESSING...</span>
</>
) : (
<>
<span className="font-bold tracking-wider">GENERATE</span>
<ArrowRight className="w-4 h-4" />
</>
)}
</motion.button>
</div>
{/* Right: Result */}
<div className="space-y-8">
<h2 className="text-sm font-bold tracking-wider text-white/60">RECREATED MEMORY</h2>
<div className="aspect-[3/2] border border-white/20 flex items-center justify-center bg-black/50">
<AnimatePresence mode="wait">
{isGenerating ? (
<motion.div
key="loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<Loader2 className="w-12 h-12 animate-spin text-white/40 mx-auto mb-4" />
<p className="text-white/40 text-sm">REMOVING BACKGROUNDS...</p>
<p className="text-white/20 text-xs mt-2">AI PROCESSING</p>
</motion.div>
) : result ? (
<motion.div
key="result"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
className="relative w-full h-full"
>
<img src={result} alt="Recreated Memory" className="w-full h-full object-contain" />
</motion.div>
) : (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<p className="text-white/40 text-sm">YOUR RECREATION</p>
<p className="text-white/20 text-xs mt-2">WILL APPEAR HERE</p>
</motion.div>
)}
</AnimatePresence>
</div>
{result && !isGenerating && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-3"
>
<div className="flex gap-3">
<button
onClick={handleGenerate}
className="flex-1 p-3 border border-white/20 hover:border-white/40 transition-colors"
>
<span className="text-sm">REGENERATE</span>
</button>
<button
onClick={downloadImage}
className="flex-1 p-3 border border-white bg-white text-black hover:bg-white/90 flex items-center justify-center gap-2"
>
<Download className="w-4 h-4" />
<span className="text-sm">DOWNLOAD</span>
</button>
</div>
<button
onClick={reset}
className="w-full p-3 border border-white/20 hover:border-white/40 transition-colors"
>
<span className="text-sm">START OVER</span>
</button>
</motion.div>
)}
</div>
</div>
{/* Footer */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="mt-24 pt-8 border-t border-white/10 text-center"
>
<p className="text-xs text-white/40">CHILDHOOD PHOTO RECREATION</p>
</motion.div>
</div>
</div>
)
}