Files
coolsoup/src/components/PhotoDropZone.tsx
Raphaël Forment 623082ce3b Initial CoolSoup implementation
CoolSoup is a React + TypeScript + Vite application that generates visual patterns and converts them to audio through spectral synthesis. Features multiple image generators (Tixy expressions, geometric tiles, external APIs) and an advanced audio synthesis engine that treats images as spectrograms.
2025-09-29 14:44:48 +02:00

160 lines
5.2 KiB
TypeScript

import { useState, useCallback } from 'react'
import { useStore } from '@nanostores/react'
import { generateFromPhotoImage } from '../generators/from-photo'
import { generatedImages, selectedImage } from '../stores'
import type { GeneratedImage } from '../stores'
interface PhotoDropZoneProps {
size: number
}
export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
const [isDragOver, setIsDragOver] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState<string | null>(null)
const images = useStore(generatedImages)
const selected = useStore(selectedImage)
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(true)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}, [])
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
setError(null)
const files = Array.from(e.dataTransfer.files)
const imageFiles = files.filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) {
setError('Please drop an image file (PNG, JPG, GIF, etc.)')
return
}
if (imageFiles.length > 1) {
setError('Please drop only one image at a time')
return
}
const file = imageFiles[0]
try {
setIsProcessing(true)
const processedImage = await generateFromPhotoImage(file, size)
// Set the single processed image and automatically select it
generatedImages.set([processedImage])
selectedImage.set(processedImage)
console.log('Photo processed successfully:', processedImage)
} catch (error) {
console.error('Error processing image:', error)
setError('Failed to process the image. Please try again.')
} finally {
setIsProcessing(false)
}
}, [size])
const handleFileInput = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return
const file = files[0]
if (!file.type.startsWith('image/')) {
setError('Please select an image file (PNG, JPG, GIF, etc.)')
return
}
try {
setIsProcessing(true)
setError(null)
const processedImage = await generateFromPhotoImage(file, size)
// Set the single processed image and automatically select it
generatedImages.set([processedImage])
selectedImage.set(processedImage)
console.log('Photo processed successfully:', processedImage)
} catch (error) {
console.error('Error processing image:', error)
setError('Failed to process the image. Please try again.')
} finally {
setIsProcessing(false)
}
}, [size])
const processedImage = images.length > 0 ? images[0] : null
return (
<div className="flex-1 flex flex-col items-center justify-center p-8">
{processedImage ? (
// Show the processed image with drag/drop overlay
<div className="w-full max-w-2xl flex flex-col items-center">
<div
className={`w-96 h-96 border border-gray-600 relative ${
isDragOver ? 'border-white bg-gray-800 bg-opacity-50' : ''
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<canvas
ref={(canvas) => {
if (canvas && processedImage.canvas) {
const ctx = canvas.getContext('2d')!
canvas.width = processedImage.canvas.width
canvas.height = processedImage.canvas.height
canvas.style.width = '100%'
canvas.style.height = '100%'
ctx.imageSmoothingEnabled = true
ctx.drawImage(processedImage.canvas, 0, 0)
}
}}
className="w-full h-full block"
/>
{isDragOver && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="text-white text-lg">Drop to replace</div>
</div>
)}
</div>
</div>
) : (
// Show the drop zone
<div
className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${
isDragOver
? 'border-white bg-gray-800'
: 'border-gray-600 hover:border-gray-400'
} ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isProcessing ? (
<div className="text-center">
<div className="text-white text-xl mb-2">Processing image...</div>
<div className="text-gray-400">Converting to grayscale and enhancing contrast</div>
</div>
) : (
<div className="text-white text-xl mb-2">Drop your photo here</div>
)}
</div>
)}
{error && (
<div className="mt-4 text-red-400 text-center">
{error}
</div>
)}
</div>
)
}