diff --git a/.playwright-mcp/audio-tixy-1758965935196-0.wav b/.playwright-mcp/audio-tixy-1758965935196-0.wav deleted file mode 100644 index 865aef4..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935196-0.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935197-1.wav b/.playwright-mcp/audio-tixy-1758965935197-1.wav deleted file mode 100644 index c3335e1..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935197-1.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935198-2.wav b/.playwright-mcp/audio-tixy-1758965935198-2.wav deleted file mode 100644 index a1e28a3..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935198-2.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935198-3.wav b/.playwright-mcp/audio-tixy-1758965935198-3.wav deleted file mode 100644 index 3628c6c..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935198-3.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935199-4.wav b/.playwright-mcp/audio-tixy-1758965935199-4.wav deleted file mode 100644 index fab35a5..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935199-4.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935199-5.wav b/.playwright-mcp/audio-tixy-1758965935199-5.wav deleted file mode 100644 index 52c17ad..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935199-5.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935199-6.wav b/.playwright-mcp/audio-tixy-1758965935199-6.wav deleted file mode 100644 index 1c11799..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935199-6.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935199-7.wav b/.playwright-mcp/audio-tixy-1758965935199-7.wav deleted file mode 100644 index 1edec59..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935199-7.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935200-10.wav b/.playwright-mcp/audio-tixy-1758965935200-10.wav deleted file mode 100644 index 1c7e2c8..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935200-10.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935200-11.wav b/.playwright-mcp/audio-tixy-1758965935200-11.wav deleted file mode 100644 index 6938cf5..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935200-11.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935200-12.wav b/.playwright-mcp/audio-tixy-1758965935200-12.wav deleted file mode 100644 index 014efce..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935200-12.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935200-8.wav b/.playwright-mcp/audio-tixy-1758965935200-8.wav deleted file mode 100644 index 9c6635b..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935200-8.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935200-9.wav b/.playwright-mcp/audio-tixy-1758965935200-9.wav deleted file mode 100644 index 11924bf..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935200-9.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935201-13.wav b/.playwright-mcp/audio-tixy-1758965935201-13.wav deleted file mode 100644 index b837ccf..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935201-13.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935201-14.wav b/.playwright-mcp/audio-tixy-1758965935201-14.wav deleted file mode 100644 index 384a021..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935201-14.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935202-15.wav b/.playwright-mcp/audio-tixy-1758965935202-15.wav deleted file mode 100644 index dc1edb9..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935202-15.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935202-16.wav b/.playwright-mcp/audio-tixy-1758965935202-16.wav deleted file mode 100644 index 80eea69..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935202-16.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935202-17.wav b/.playwright-mcp/audio-tixy-1758965935202-17.wav deleted file mode 100644 index 8d50f86..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935202-17.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935202-18.wav b/.playwright-mcp/audio-tixy-1758965935202-18.wav deleted file mode 100644 index f81550a..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935202-18.wav and /dev/null differ diff --git a/.playwright-mcp/audio-tixy-1758965935203-19.wav b/.playwright-mcp/audio-tixy-1758965935203-19.wav deleted file mode 100644 index 4cbaa56..0000000 Binary files a/.playwright-mcp/audio-tixy-1758965935203-19.wav and /dev/null differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8c13957 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CoolSoup is a React + TypeScript + Vite application that generates visual patterns and converts them to audio through spectral synthesis. The app features multiple image generators (Tixy expressions, geometric tiles, external APIs) and an advanced audio synthesis engine that treats images as spectrograms. + +## Development Commands + +- `pnpm dev` - Start development server with hot reload +- `pnpm build` - Build for production (TypeScript compilation + Vite build) +- `pnpm lint` - Run ESLint on the codebase +- `pnpm preview` - Preview production build locally + +## Architecture Overview + +### Core Application Structure +- **State Management**: Uses Nanostores for reactive state with persistent atoms +- **UI Layout**: Two-panel design - main content area (generator + image grid) and collapsible audio panel +- **Generator Pattern**: Pluggable generator system where each generator implements a common interface + +### Main Store (`src/stores/index.ts`) +Central state management with these key atoms: +- `appSettings` - Generator selection, grid size, colors +- `generatedImages` - Array of generated images with metadata +- `selectedImage` - Current image for audio synthesis +- `synthesisParams` - Audio synthesis configuration +- `panelOpen` - Audio panel visibility + +### Generator System +Four built-in generators located in `src/generators/`: +- **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables +- **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves +- **Picsum** (`picsum.ts`) - External random images API +- **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API + +Each generator returns `GeneratedImage[]` with: +- `id` - Unique identifier +- `canvas` - HTMLCanvasElement +- `imageData` - ImageData for synthesis +- `generator` - Generator type +- `params` - Generation parameters + +### Spectral Synthesis Engine (`src/spectral-synthesis/`) +Advanced image-to-audio synthesis library: +- **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class +- **Types** (`core/types.ts`) - SynthesisParams and related interfaces +- **Audio Export** (`audio/export.ts`) - WAV file generation and download +- **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection + +Key features: +- Mel-scale frequency mapping for perceptual accuracy +- Spectral peak detection to reduce noise +- Temporal smoothing for coherent audio trajectories +- Auto-detection of image type (spectrogram vs diagram) +- Configurable synthesis parameters (duration, frequency range, resolution) + +### Audio Export System (`src/audio-export/`) +Batch audio processing capabilities: +- Single image export with custom parameters +- Batch export of all generated images +- ZIP file generation for batch downloads +- Progress tracking for long operations + +## Key Implementation Details + +### Image Generation Flow +1. User selects generator and parameters in `GeneratorSelector` +2. `App.tsx` calls appropriate generator function +3. Generator returns array of `GeneratedImage` objects +4. Images displayed in `ImageGrid` component +5. User can click image to select for audio synthesis + +### Audio Synthesis Flow +1. User selects image from grid (sets `selectedImage`) +2. `AudioPanel` provides synthesis parameter controls +3. Synthesis triggered via spectral-synthesis library +4. Audio can be played directly or exported as WAV +5. Batch export processes all generated images + +### Component Architecture +- **App.tsx** - Main layout and generation orchestration +- **GeneratorSelector** - Generator picker with settings +- **ImageGrid** - Grid display of generated images +- **AudioPanel** - Audio synthesis controls and export +- **AudioControls** - Playback controls for generated audio + +## Generator Module Structure +Both tixy-generator and waveform-generator modules are designed as standalone packages: + +**Tixy Generator**: +- `README.md` - Documentation and usage examples +- `core/types.ts` - Type definitions +- `index.ts` - Main exports +- Generator-specific files (evaluator, patterns, etc.) + +**Waveform Generator**: +- `README.md` - Documentation and usage examples +- `core/types.ts` - Interfaces and configuration +- `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic) +- `core/generator.ts` - Control point generation and randomness strategies +- `renderer/canvas.ts` - Canvas rendering with smooth anti-aliased curves +- `index.ts` - Main exports and convenience functions + +## Development Notes +- Uses Rolldown Vite for improved performance +- Tailwind CSS for styling (no rounded corners per user preference) +- ESLint configured for React + TypeScript +- All generators support time-based animation parameters +- Image synthesis supports real-time parameter adjustment \ No newline at end of file diff --git a/index.html b/index.html index 17b6ee5..f360582 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,11 @@ - + - coolsoup + + + CoolSoup
diff --git a/package.json b/package.json index d2bb0a9..a5c186e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "@nanostores/persistent": "^1.1.0", "@nanostores/react": "^1.0.0", + "geopattern": "^1.2.3", + "jszip": "^3.10.1", "nanostores": "^1.0.1", "react": "^19.1.1", "react-dom": "^19.1.1" @@ -19,6 +21,7 @@ "devDependencies": { "@eslint/js": "^9.36.0", "@tailwindcss/postcss": "^4.1.13", + "@types/jszip": "^3.4.1", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8588ae3..05b0ad8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@nanostores/react': specifier: ^1.0.0 version: 1.0.0(nanostores@1.0.1)(react@19.1.1) + geopattern: + specifier: ^1.2.3 + version: 1.2.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 nanostores: specifier: ^1.0.1 version: 1.0.1 @@ -30,6 +36,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.13 version: 4.1.13 + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 '@types/react': specifier: ^19.1.13 version: 19.1.14 @@ -475,6 +484,10 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: @@ -629,6 +642,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -720,6 +736,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@1.2.1: + resolution: {integrity: sha512-2/JwIYRpMBDSjbQjUUppNSrmc719crhFaWIdT+TRSVA8gE+6HEobQWqJ6VkPt/H8twS7h/0WWs7veh8wmp98Ng==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -776,6 +795,9 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + geopattern@1.2.3: + resolution: {integrity: sha512-UzrR9D0xUrXx71ROZTKbTg1isWdcfcYAXsJrqtvhDkQV2JCsRyqws6TuqUdkBKncg5CPevb0vyXXUJrZpjGFXw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -810,6 +832,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -818,6 +843,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -830,6 +858,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -863,6 +894,9 @@ packages: engines: {node: '>=6'} hasBin: true + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -870,6 +904,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -1004,6 +1041,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1038,6 +1078,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1058,6 +1101,9 @@ packages: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1114,6 +1160,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -1126,6 +1175,9 @@ packages: engines: {node: '>=10'} hasBin: true + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1138,6 +1190,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1199,6 +1254,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1611,6 +1669,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.1 + '@types/react-dom@19.1.9(@types/react@19.1.14)': dependencies: '@types/react': 19.1.14 @@ -1801,6 +1863,8 @@ snapshots: convert-source-map@2.0.0: {} + core-util-is@1.0.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1905,6 +1969,8 @@ snapshots: esutils@2.0.3: {} + extend@1.2.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -1954,6 +2020,10 @@ snapshots: gensync@1.0.0-beta.2: {} + geopattern@1.2.3: + dependencies: + extend: 1.2.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -1976,6 +2046,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -1983,6 +2055,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -1991,6 +2065,8 @@ snapshots: is-number@7.0.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} jiti@2.6.0: {} @@ -2011,6 +2087,13 @@ snapshots: json5@2.2.3: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2020,6 +2103,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-darwin-arm64@1.30.1: optional: true @@ -2129,6 +2216,8 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -2153,6 +2242,8 @@ snapshots: prelude-ls@1.2.1: {} + process-nextick-args@2.0.1: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2166,6 +2257,16 @@ snapshots: react@19.1.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + resolve-from@4.0.0: {} reusify@1.1.0: {} @@ -2208,12 +2309,16 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + scheduler@0.26.0: {} semver@6.3.1: {} semver@7.7.2: {} + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2222,6 +2327,10 @@ snapshots: source-map-js@1.2.1: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -2283,6 +2392,8 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..152b140 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8c6c410..46ba4ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,27 @@ import { useStore } from '@nanostores/react' -import { appSettings, generatedImages, isGenerating } from './stores' +import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores' import { generateTixyImages } from './generators/tixy' import { generatePicsumImages } from './generators/picsum' import { generateArtInstituteImages } from './generators/art-institute' -import { generateGeometricTilesImages } from './generators/geometric-tiles' +import { generateWaveformImages } from './generators/waveform' +import { generatePartialsImages } from './generators/partials' +import { generateSlidesImages } from './generators/slides' +import { generateShapesImages } from './generators/shapes' +import { generateBandsImages } from './generators/bands' +import { generateDustImages } from './generators/dust' +import { generateGeopatternImages } from './generators/geopattern' +import { generateHarmonicsImages } from './generators/harmonics' import GeneratorSelector from './components/GeneratorSelector' import ImageGrid from './components/ImageGrid' import AudioPanel from './components/AudioPanel' +import PhotoGrid from './components/PhotoGrid' +import WebcamGrid from './components/WebcamGrid' +import HelpPopup from './components/HelpPopup' function App() { const settings = useStore(appSettings) const generating = useStore(isGenerating) + const helpOpen = useStore(helpPopupOpen) const handleGenerate = async () => { isGenerating.set(true) @@ -24,8 +35,22 @@ function App() { newImages = await generatePicsumImages(settings.gridSize, 512) } else if (settings.selectedGenerator === 'art-institute') { newImages = await generateArtInstituteImages(settings.gridSize, 512) - } else if (settings.selectedGenerator === 'geometric-tiles') { - newImages = generateGeometricTilesImages(settings.gridSize, 256) + } else if (settings.selectedGenerator === 'waveform') { + newImages = generateWaveformImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'partials') { + newImages = generatePartialsImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'slides') { + newImages = generateSlidesImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'shapes') { + newImages = generateShapesImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'bands') { + newImages = generateBandsImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'dust') { + newImages = generateDustImages(settings.gridSize, 2048) + } else if (settings.selectedGenerator === 'geopattern') { + newImages = await generateGeopatternImages(settings.gridSize, 512) + } else if (settings.selectedGenerator === 'harmonics') { + newImages = generateHarmonicsImages(settings.gridSize, 2048) } else { newImages = [] } @@ -39,12 +64,22 @@ function App() { } return ( -
-
+
+
- + {settings.selectedGenerator === 'from-photo' ? ( + + ) : settings.selectedGenerator === 'webcam' ? ( + + ) : ( + + )}
+ helpPopupOpen.set(false)} + />
) } diff --git a/src/audio-export/index.ts b/src/audio-export/index.ts new file mode 100644 index 0000000..776dabc --- /dev/null +++ b/src/audio-export/index.ts @@ -0,0 +1,239 @@ +import JSZip from 'jszip' +import { createWAVBuffer } from '../spectral-synthesis/audio/export' +import { synthesizeFromImage } from '../spectral-synthesis' +import type { GeneratedImage, GeneratorType } from '../stores' +import type { SynthesisParams } from '../spectral-synthesis' + +export interface ExportOptions { + includeGeneratorInName?: boolean + includeTimestamp?: boolean + customPrefix?: string +} + +export interface BatchExportOptions extends ExportOptions { + zipFileName?: string + createZip?: boolean +} + +export interface ExportResult { + success: boolean + filename?: string + error?: string +} + +export interface BatchExportResult { + success: boolean + totalFiles: number + successfulFiles: number + zipFilename?: string + errors: string[] +} + +/** + * Generate filename for audio export + */ +function generateFilename( + imageId: string, + generator: GeneratorType, + options: ExportOptions = {} +): string { + const parts: string[] = [] + + if (options.customPrefix) { + parts.push(options.customPrefix) + } else { + parts.push('audio') + } + + if (options.includeGeneratorInName) { + parts.push(generator) + } + + parts.push(imageId.slice(-8)) + + if (options.includeTimestamp) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) + parts.push(timestamp) + } + + return `${parts.join('-')}.wav` +} + +/** + * Generate filename for ZIP export + */ +function generateZipFilename(options: BatchExportOptions = {}): string { + if (options.zipFileName) { + return options.zipFileName.endsWith('.zip') ? options.zipFileName : `${options.zipFileName}.zip` + } + + const parts = ['audio-batch'] + + if (options.includeTimestamp) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) + parts.push(timestamp) + } + + return `${parts.join('-')}.zip` +} + +/** + * Download a file using browser download mechanism + */ +function downloadFile(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} + +/** + * Export single audio file + */ +export function exportSingleAudio( + image: GeneratedImage, + synthesisParams: SynthesisParams, + options: ExportOptions = {} +): ExportResult { + try { + const audioData = synthesizeFromImage(image.imageData, synthesisParams) + const buffer = createWAVBuffer(audioData, synthesisParams.sampleRate) + const blob = new Blob([buffer], { type: 'audio/wav' }) + + const filename = generateFilename(image.id, image.generator, { + ...options, + includeGeneratorInName: true + }) + + downloadFile(blob, filename) + + return { + success: true, + filename + } + } catch (error) { + console.error('Error exporting single audio:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + } + } +} + +/** + * Export multiple audio files as individual downloads or ZIP + */ +export async function exportBatchAudio( + images: GeneratedImage[], + synthesisParams: SynthesisParams, + options: BatchExportOptions = {} +): Promise { + const errors: string[] = [] + let successfulFiles = 0 + const createZip = options.createZip !== false + + if (!createZip) { + for (const image of images) { + try { + const result = exportSingleAudio(image, synthesisParams, options) + if (result.success) { + successfulFiles++ + } else { + errors.push(`Failed to export ${image.id}: ${result.error}`) + } + + await new Promise(resolve => setTimeout(resolve, 500)) + } catch (error) { + errors.push(`Failed to export ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + + return { + success: errors.length === 0, + totalFiles: images.length, + successfulFiles, + errors + } + } + + const zip = new JSZip() + + for (const image of images) { + try { + const audioData = synthesizeFromImage(image.imageData, synthesisParams) + const buffer = createWAVBuffer(audioData, synthesisParams.sampleRate) + + const filename = generateFilename(image.id, image.generator, { + ...options, + includeGeneratorInName: true + }) + + zip.file(filename, buffer) + successfulFiles++ + } catch (error) { + console.error(`Error processing image ${image.id}:`, error) + errors.push(`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + + if (successfulFiles === 0) { + return { + success: false, + totalFiles: images.length, + successfulFiles: 0, + errors: [...errors, 'No files were successfully processed'] + } + } + + try { + const zipBlob = await zip.generateAsync({ type: 'blob' }) + const zipFilename = generateZipFilename(options) + + downloadFile(zipBlob, zipFilename) + + return { + success: true, + totalFiles: images.length, + successfulFiles, + zipFilename, + errors + } + } catch (error) { + console.error('Error creating ZIP file:', error) + return { + success: false, + totalFiles: images.length, + successfulFiles, + errors: [...errors, `Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`] + } + } +} + +/** + * Convenience function for quick single export with generator name + */ +export function quickExportSingle( + image: GeneratedImage, + synthesisParams: SynthesisParams +): ExportResult { + return exportSingleAudio(image, synthesisParams, { + includeGeneratorInName: true, + includeTimestamp: false + }) +} + +/** + * Convenience function for quick batch export as ZIP + */ +export async function quickExportBatch( + images: GeneratedImage[], + synthesisParams: SynthesisParams +): Promise { + return exportBatchAudio(images, synthesisParams, { + includeGeneratorInName: true, + includeTimestamp: true, + createZip: true + }) +} \ No newline at end of file diff --git a/src/components/AudioControls.tsx b/src/components/AudioControls.tsx new file mode 100644 index 0000000..c7b7e7a --- /dev/null +++ b/src/components/AudioControls.tsx @@ -0,0 +1,113 @@ +import { useState, useRef } from 'react' + +interface AudioControlsProps { + isPlaying: boolean + volume: number + onPlay: () => void + onPause: () => void + onStop: () => void + onVolumeChange: (volume: number) => void + disabled?: boolean +} + +export default function AudioControls({ + isPlaying, + volume, + onPlay, + onPause, + onStop, + onVolumeChange, + disabled = false +}: AudioControlsProps) { + const [isDragging, setIsDragging] = useState(false) + + const handleVolumeChange = (e: React.ChangeEvent) => { + onVolumeChange(Number(e.target.value)) + } + + return ( +
+
+ + + +
+ +
+ + + + +
+ +