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.
160 lines
5.2 KiB
TypeScript
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>
|
|
)
|
|
} |