Cleaning the codebase
This commit is contained in:
74
.gitignore
vendored
74
.gitignore
vendored
@@ -1,5 +1,24 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
*.local
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs/
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
@@ -7,18 +26,59 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
# Package manager files
|
||||||
dist
|
package-lock.json
|
||||||
dist-ssr
|
yarn.lock
|
||||||
*.local
|
.yarn/
|
||||||
|
.yarnrc.yml
|
||||||
|
|
||||||
|
# Testing and coverage
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
.nyc_output/
|
||||||
|
.coverage/
|
||||||
|
junit.xml
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.npm/
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
!.vscode/settings.json
|
||||||
.DS_Store
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|||||||
26
.prettierignore
Normal file
26
.prettierignore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
18
CLAUDE.md
18
CLAUDE.md
@@ -16,12 +16,15 @@ CoolSoup is a React + TypeScript + Vite application that generates visual patter
|
|||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
### Core Application Structure
|
### Core Application Structure
|
||||||
|
|
||||||
- **State Management**: Uses Nanostores for reactive state with persistent atoms
|
- **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
|
- **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
|
- **Generator Pattern**: Pluggable generator system where each generator implements a common interface
|
||||||
|
|
||||||
### Main Store (`src/stores/index.ts`)
|
### Main Store (`src/stores/index.ts`)
|
||||||
|
|
||||||
Central state management with these key atoms:
|
Central state management with these key atoms:
|
||||||
|
|
||||||
- `appSettings` - Generator selection, grid size, colors
|
- `appSettings` - Generator selection, grid size, colors
|
||||||
- `generatedImages` - Array of generated images with metadata
|
- `generatedImages` - Array of generated images with metadata
|
||||||
- `selectedImage` - Current image for audio synthesis
|
- `selectedImage` - Current image for audio synthesis
|
||||||
@@ -29,13 +32,16 @@ Central state management with these key atoms:
|
|||||||
- `panelOpen` - Audio panel visibility
|
- `panelOpen` - Audio panel visibility
|
||||||
|
|
||||||
### Generator System
|
### Generator System
|
||||||
|
|
||||||
Four built-in generators located in `src/generators/`:
|
Four built-in generators located in `src/generators/`:
|
||||||
|
|
||||||
- **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables
|
- **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables
|
||||||
- **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves
|
- **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves
|
||||||
- **Picsum** (`picsum.ts`) - External random images API
|
- **Picsum** (`picsum.ts`) - External random images API
|
||||||
- **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API
|
- **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API
|
||||||
|
|
||||||
Each generator returns `GeneratedImage[]` with:
|
Each generator returns `GeneratedImage[]` with:
|
||||||
|
|
||||||
- `id` - Unique identifier
|
- `id` - Unique identifier
|
||||||
- `canvas` - HTMLCanvasElement
|
- `canvas` - HTMLCanvasElement
|
||||||
- `imageData` - ImageData for synthesis
|
- `imageData` - ImageData for synthesis
|
||||||
@@ -43,13 +49,16 @@ Each generator returns `GeneratedImage[]` with:
|
|||||||
- `params` - Generation parameters
|
- `params` - Generation parameters
|
||||||
|
|
||||||
### Spectral Synthesis Engine (`src/spectral-synthesis/`)
|
### Spectral Synthesis Engine (`src/spectral-synthesis/`)
|
||||||
|
|
||||||
Advanced image-to-audio synthesis library:
|
Advanced image-to-audio synthesis library:
|
||||||
|
|
||||||
- **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class
|
- **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class
|
||||||
- **Types** (`core/types.ts`) - SynthesisParams and related interfaces
|
- **Types** (`core/types.ts`) - SynthesisParams and related interfaces
|
||||||
- **Audio Export** (`audio/export.ts`) - WAV file generation and download
|
- **Audio Export** (`audio/export.ts`) - WAV file generation and download
|
||||||
- **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection
|
- **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
|
|
||||||
- Mel-scale frequency mapping for perceptual accuracy
|
- Mel-scale frequency mapping for perceptual accuracy
|
||||||
- Spectral peak detection to reduce noise
|
- Spectral peak detection to reduce noise
|
||||||
- Temporal smoothing for coherent audio trajectories
|
- Temporal smoothing for coherent audio trajectories
|
||||||
@@ -57,7 +66,9 @@ Key features:
|
|||||||
- Configurable synthesis parameters (duration, frequency range, resolution)
|
- Configurable synthesis parameters (duration, frequency range, resolution)
|
||||||
|
|
||||||
### Audio Export System (`src/audio-export/`)
|
### Audio Export System (`src/audio-export/`)
|
||||||
|
|
||||||
Batch audio processing capabilities:
|
Batch audio processing capabilities:
|
||||||
|
|
||||||
- Single image export with custom parameters
|
- Single image export with custom parameters
|
||||||
- Batch export of all generated images
|
- Batch export of all generated images
|
||||||
- ZIP file generation for batch downloads
|
- ZIP file generation for batch downloads
|
||||||
@@ -66,6 +77,7 @@ Batch audio processing capabilities:
|
|||||||
## Key Implementation Details
|
## Key Implementation Details
|
||||||
|
|
||||||
### Image Generation Flow
|
### Image Generation Flow
|
||||||
|
|
||||||
1. User selects generator and parameters in `GeneratorSelector`
|
1. User selects generator and parameters in `GeneratorSelector`
|
||||||
2. `App.tsx` calls appropriate generator function
|
2. `App.tsx` calls appropriate generator function
|
||||||
3. Generator returns array of `GeneratedImage` objects
|
3. Generator returns array of `GeneratedImage` objects
|
||||||
@@ -73,6 +85,7 @@ Batch audio processing capabilities:
|
|||||||
5. User can click image to select for audio synthesis
|
5. User can click image to select for audio synthesis
|
||||||
|
|
||||||
### Audio Synthesis Flow
|
### Audio Synthesis Flow
|
||||||
|
|
||||||
1. User selects image from grid (sets `selectedImage`)
|
1. User selects image from grid (sets `selectedImage`)
|
||||||
2. `AudioPanel` provides synthesis parameter controls
|
2. `AudioPanel` provides synthesis parameter controls
|
||||||
3. Synthesis triggered via spectral-synthesis library
|
3. Synthesis triggered via spectral-synthesis library
|
||||||
@@ -80,6 +93,7 @@ Batch audio processing capabilities:
|
|||||||
5. Batch export processes all generated images
|
5. Batch export processes all generated images
|
||||||
|
|
||||||
### Component Architecture
|
### Component Architecture
|
||||||
|
|
||||||
- **App.tsx** - Main layout and generation orchestration
|
- **App.tsx** - Main layout and generation orchestration
|
||||||
- **GeneratorSelector** - Generator picker with settings
|
- **GeneratorSelector** - Generator picker with settings
|
||||||
- **ImageGrid** - Grid display of generated images
|
- **ImageGrid** - Grid display of generated images
|
||||||
@@ -87,15 +101,18 @@ Batch audio processing capabilities:
|
|||||||
- **AudioControls** - Playback controls for generated audio
|
- **AudioControls** - Playback controls for generated audio
|
||||||
|
|
||||||
## Generator Module Structure
|
## Generator Module Structure
|
||||||
|
|
||||||
Both tixy-generator and waveform-generator modules are designed as standalone packages:
|
Both tixy-generator and waveform-generator modules are designed as standalone packages:
|
||||||
|
|
||||||
**Tixy Generator**:
|
**Tixy Generator**:
|
||||||
|
|
||||||
- `README.md` - Documentation and usage examples
|
- `README.md` - Documentation and usage examples
|
||||||
- `core/types.ts` - Type definitions
|
- `core/types.ts` - Type definitions
|
||||||
- `index.ts` - Main exports
|
- `index.ts` - Main exports
|
||||||
- Generator-specific files (evaluator, patterns, etc.)
|
- Generator-specific files (evaluator, patterns, etc.)
|
||||||
|
|
||||||
**Waveform Generator**:
|
**Waveform Generator**:
|
||||||
|
|
||||||
- `README.md` - Documentation and usage examples
|
- `README.md` - Documentation and usage examples
|
||||||
- `core/types.ts` - Interfaces and configuration
|
- `core/types.ts` - Interfaces and configuration
|
||||||
- `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic)
|
- `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic)
|
||||||
@@ -104,6 +121,7 @@ Both tixy-generator and waveform-generator modules are designed as standalone pa
|
|||||||
- `index.ts` - Main exports and convenience functions
|
- `index.ts` - Main exports and convenience functions
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
- Uses Rolldown Vite for improved performance
|
- Uses Rolldown Vite for improved performance
|
||||||
- Tailwind CSS for styling (no rounded corners per user preference)
|
- Tailwind CSS for styling (no rounded corners per user preference)
|
||||||
- ESLint configured for React + TypeScript
|
- ESLint configured for React + TypeScript
|
||||||
|
|||||||
3197
package-lock.json
generated
3197
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"build:vite": "vite build",
|
||||||
|
"typecheck": "tsc -b",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -31,6 +35,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.44.0",
|
"typescript-eslint": "^8.44.0",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -66,6 +66,9 @@ importers:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.5.6
|
specifier: ^8.5.6
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.6.2
|
||||||
|
version: 3.6.2
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.13
|
specifier: ^4.1.13
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
@@ -1078,6 +1081,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prettier@3.6.2:
|
||||||
|
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
@@ -2242,6 +2250,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier@3.6.2: {}
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores'
|
import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores'
|
||||||
|
import type { GeneratedImage } from './stores'
|
||||||
import { generateTixyImages } from './generators/tixy'
|
import { generateTixyImages } from './generators/tixy'
|
||||||
import { generatePicsumImages } from './generators/picsum'
|
import { generatePicsumImages } from './generators/picsum'
|
||||||
import { generateArtInstituteImages } from './generators/art-institute'
|
import { generateArtInstituteImages } from './generators/art-institute'
|
||||||
@@ -27,7 +28,7 @@ function App() {
|
|||||||
isGenerating.set(true)
|
isGenerating.set(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let newImages
|
let newImages: GeneratedImage[]
|
||||||
|
|
||||||
if (settings.selectedGenerator === 'tixy') {
|
if (settings.selectedGenerator === 'tixy') {
|
||||||
newImages = generateTixyImages(settings.gridSize, 64)
|
newImages = generateTixyImages(settings.gridSize, 64)
|
||||||
@@ -76,10 +77,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AudioPanel />
|
<AudioPanel />
|
||||||
<HelpPopup
|
<HelpPopup isOpen={helpOpen} onClose={() => helpPopupOpen.set(false)} />
|
||||||
isOpen={helpOpen}
|
|
||||||
onClose={() => helpPopupOpen.set(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,20 +104,20 @@ export function exportSingleAudio(
|
|||||||
|
|
||||||
const filename = generateFilename(image.id, image.generator, {
|
const filename = generateFilename(image.id, image.generator, {
|
||||||
...options,
|
...options,
|
||||||
includeGeneratorInName: true
|
includeGeneratorInName: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
downloadFile(blob, filename)
|
downloadFile(blob, filename)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
filename
|
filename,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting single audio:', error)
|
console.error('Error exporting single audio:', error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,9 @@ export async function exportBatchAudio(
|
|||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(`Failed to export ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
errors.push(
|
||||||
|
`Failed to export ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +156,7 @@ export async function exportBatchAudio(
|
|||||||
success: errors.length === 0,
|
success: errors.length === 0,
|
||||||
totalFiles: images.length,
|
totalFiles: images.length,
|
||||||
successfulFiles,
|
successfulFiles,
|
||||||
errors
|
errors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,14 +169,16 @@ export async function exportBatchAudio(
|
|||||||
|
|
||||||
const filename = generateFilename(image.id, image.generator, {
|
const filename = generateFilename(image.id, image.generator, {
|
||||||
...options,
|
...options,
|
||||||
includeGeneratorInName: true
|
includeGeneratorInName: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
zip.file(filename, buffer)
|
zip.file(filename, buffer)
|
||||||
successfulFiles++
|
successfulFiles++
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing image ${image.id}:`, error)
|
console.error(`Error processing image ${image.id}:`, error)
|
||||||
errors.push(`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
errors.push(
|
||||||
|
`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +187,7 @@ export async function exportBatchAudio(
|
|||||||
success: false,
|
success: false,
|
||||||
totalFiles: images.length,
|
totalFiles: images.length,
|
||||||
successfulFiles: 0,
|
successfulFiles: 0,
|
||||||
errors: [...errors, 'No files were successfully processed']
|
errors: [...errors, 'No files were successfully processed'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +202,7 @@ export async function exportBatchAudio(
|
|||||||
totalFiles: images.length,
|
totalFiles: images.length,
|
||||||
successfulFiles,
|
successfulFiles,
|
||||||
zipFilename,
|
zipFilename,
|
||||||
errors
|
errors,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating ZIP file:', error)
|
console.error('Error creating ZIP file:', error)
|
||||||
@@ -206,7 +210,10 @@ export async function exportBatchAudio(
|
|||||||
success: false,
|
success: false,
|
||||||
totalFiles: images.length,
|
totalFiles: images.length,
|
||||||
successfulFiles,
|
successfulFiles,
|
||||||
errors: [...errors, `Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`]
|
errors: [
|
||||||
|
...errors,
|
||||||
|
`Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +227,7 @@ export function quickExportSingle(
|
|||||||
): ExportResult {
|
): ExportResult {
|
||||||
return exportSingleAudio(image, synthesisParams, {
|
return exportSingleAudio(image, synthesisParams, {
|
||||||
includeGeneratorInName: true,
|
includeGeneratorInName: true,
|
||||||
includeTimestamp: false
|
includeTimestamp: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +241,6 @@ export async function quickExportBatch(
|
|||||||
return exportBatchAudio(images, synthesisParams, {
|
return exportBatchAudio(images, synthesisParams, {
|
||||||
includeGeneratorInName: true,
|
includeGeneratorInName: true,
|
||||||
includeTimestamp: true,
|
includeTimestamp: true,
|
||||||
createZip: true
|
createZip: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState, useRef } from 'react'
|
|
||||||
|
|
||||||
interface AudioControlsProps {
|
interface AudioControlsProps {
|
||||||
isPlaying: boolean
|
isPlaying: boolean
|
||||||
@@ -17,9 +16,8 @@ export default function AudioControls({
|
|||||||
onPause,
|
onPause,
|
||||||
onStop,
|
onStop,
|
||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
disabled = false
|
disabled = false,
|
||||||
}: AudioControlsProps) {
|
}: AudioControlsProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
|
||||||
|
|
||||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onVolumeChange(Number(e.target.value))
|
onVolumeChange(Number(e.target.value))
|
||||||
@@ -58,7 +56,13 @@ export default function AudioControls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="text-gray-400">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="text-gray-400"
|
||||||
|
>
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
@@ -74,8 +78,9 @@ export default function AudioControls({
|
|||||||
className="w-full h-2 bg-black border border-white appearance-none cursor-pointer volume-slider disabled:cursor-not-allowed"
|
className="w-full h-2 bg-black border border-white appearance-none cursor-pointer volume-slider disabled:cursor-not-allowed"
|
||||||
title={`Volume: ${Math.round(volume * 100)}%`}
|
title={`Volume: ${Math.round(volume * 100)}%`}
|
||||||
/>
|
/>
|
||||||
<style dangerouslySetInnerHTML={{
|
<style
|
||||||
__html: `
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
.volume-slider::-webkit-slider-thumb {
|
.volume-slider::-webkit-slider-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -100,13 +105,12 @@ export default function AudioControls({
|
|||||||
.volume-slider::-moz-range-thumb:hover {
|
.volume-slider::-moz-range-thumb:hover {
|
||||||
background: ${disabled ? '#6b7280' : '#e5e7eb'};
|
background: ${disabled ? '#6b7280' : '#e5e7eb'};
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-gray-400 w-10 text-right">
|
<span className="text-xs text-gray-400 w-10 text-right">{Math.round(volume * 100)}%</span>
|
||||||
{Math.round(volume * 100)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { selectedImage, panelOpen, generatedImages, synthesisParams } from '../stores'
|
import { selectedImage, generatedImages, synthesisParams } from '../stores'
|
||||||
import { synthesizeFromImage, playAudio, createAudioPlayer, type WindowType, type AudioPlayer } from '../spectral-synthesis'
|
import {
|
||||||
|
synthesizeFromImage,
|
||||||
|
createAudioPlayer,
|
||||||
|
type AudioPlayer,
|
||||||
|
} from '../spectral-synthesis'
|
||||||
import { quickExportSingle, quickExportBatch } from '../audio-export'
|
import { quickExportSingle, quickExportBatch } from '../audio-export'
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import AudioControls from './AudioControls'
|
import AudioControls from './AudioControls'
|
||||||
@@ -14,14 +18,8 @@ export default function AudioPanel() {
|
|||||||
const [volume, setVolume] = useState(0.7)
|
const [volume, setVolume] = useState(0.7)
|
||||||
const audioPlayerRef = useRef<AudioPlayer | null>(null)
|
const audioPlayerRef = useRef<AudioPlayer | null>(null)
|
||||||
|
|
||||||
const handleClearSelection = () => {
|
|
||||||
selectedImage.set(null)
|
|
||||||
if (audioPlayerRef.current) {
|
|
||||||
audioPlayerRef.current.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateParam = <K extends keyof typeof params>(key: K, value: typeof params[K]) => {
|
const updateParam = <K extends keyof typeof params>(key: K, value: (typeof params)[K]) => {
|
||||||
synthesisParams.set({ ...params, [key]: value })
|
synthesisParams.set({ ...params, [key]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,18 +80,6 @@ export default function AudioPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlaySingle = async () => {
|
|
||||||
if (!selected) return
|
|
||||||
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
const audio = synthesizeFromImage(selected.imageData, params)
|
|
||||||
await playAudio(audio, params.sampleRate)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error playing audio:', error)
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -131,7 +117,6 @@ export default function AudioPanel() {
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error('Batch export failed:', result.errors)
|
console.error('Batch export failed:', result.errors)
|
||||||
} else {
|
} else {
|
||||||
console.log(`Exported ${result.successfulFiles}/${result.totalFiles} files to ${result.zipFilename}`)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating all audio:', error)
|
console.error('Error generating all audio:', error)
|
||||||
@@ -147,7 +132,7 @@ export default function AudioPanel() {
|
|||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-24 h-24 border border-gray-600">
|
<div className="w-24 h-24 border border-gray-600">
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && selected.canvas) {
|
if (canvas && selected.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = selected.canvas.width
|
canvas.width = selected.canvas.width
|
||||||
@@ -186,9 +171,7 @@ export default function AudioPanel() {
|
|||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
{/* Synthesis Mode Selection - Top Priority */}
|
{/* Synthesis Mode Selection - Top Priority */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-gray-400 mb-1">Synthesis Mode</label>
|
||||||
Synthesis Mode
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => updateParam('synthesisMode', 'direct')}
|
onClick={() => updateParam('synthesisMode', 'direct')}
|
||||||
@@ -216,265 +199,20 @@ export default function AudioPanel() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Duration: {params.duration}s
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="30"
|
|
||||||
step="1"
|
|
||||||
value={params.duration}
|
|
||||||
onChange={(e) => updateParam('duration', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom Mode Only Parameters */}
|
|
||||||
{params.synthesisMode === 'custom' && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Max Partials: {params.maxPartials}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="10"
|
|
||||||
max="500"
|
|
||||||
step="10"
|
|
||||||
value={params.maxPartials}
|
|
||||||
onChange={(e) => updateParam('maxPartials', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Controls audio complexity vs performance</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Frequency Density: {params.frequencyResolution}x
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="10"
|
|
||||||
step="1"
|
|
||||||
value={params.frequencyResolution}
|
|
||||||
onChange={(e) => updateParam('frequencyResolution', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Higher values create broader, richer frequency bands</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Direct Mode Only Parameters */}
|
|
||||||
{(params.synthesisMode || 'direct') === 'direct' && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
FFT Size: {params.fftSize || 2048}
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-4 gap-1">
|
|
||||||
{[1024, 2048, 4096, 8192].map(size => (
|
|
||||||
<button
|
|
||||||
key={size}
|
|
||||||
onClick={() => updateParam('fftSize', size)}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
(params.fftSize || 2048) === size
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{size}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Higher = better frequency resolution</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="0.9"
|
|
||||||
step="0.125"
|
|
||||||
value={params.frameOverlap || 0.75}
|
|
||||||
onChange={(e) => updateParam('frameOverlap', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Higher = smoother temporal resolution</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-xs text-gray-400">Disable normalization: can be very loud</label>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('disableNormalization', !(params.disableNormalization ?? false))}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.disableNormalization === true
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{params.disableNormalization === true ? 'ON' : 'OFF'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Min Frequency: {params.minFreq}Hz
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="20"
|
|
||||||
max="200"
|
|
||||||
step="10"
|
|
||||||
value={params.minFreq}
|
|
||||||
onChange={(e) => updateParam('minFreq', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Max Frequency: {params.maxFreq}Hz
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1000"
|
|
||||||
max="20000"
|
|
||||||
step="500"
|
|
||||||
value={params.maxFreq}
|
|
||||||
onChange={(e) => updateParam('maxFreq', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom Mode Only Parameters */}
|
|
||||||
{params.synthesisMode === 'custom' && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Amplitude Threshold: {params.amplitudeThreshold}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.001"
|
|
||||||
max="0.1"
|
|
||||||
step="0.001"
|
|
||||||
value={params.amplitudeThreshold}
|
|
||||||
onChange={(e) => updateParam('amplitudeThreshold', Number(e.target.value))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Window Type
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-4 gap-1">
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('windowType', 'rectangular')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.windowType === 'rectangular'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Rect
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('windowType', 'hann')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.windowType === 'hann'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Hann
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('windowType', 'hamming')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.windowType === 'hamming'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Hamming
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('windowType', 'blackman')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.windowType === 'blackman'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Blackman
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Reduces clicking/popping between time frames</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Color Inversion - Available for both modes */}
|
|
||||||
<div className="border-t border-gray-700 pt-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
Color
|
Duration: {params.duration}s
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('invert', false)}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
!params.invert
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Normal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('invert', true)}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.invert
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Inverted
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contrast - Available for both modes */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
|
||||||
Contrast: {(params.contrast || 2.2).toFixed(1)}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="5"
|
max="30"
|
||||||
step="0.1"
|
step="1"
|
||||||
value={params.contrast || 2.2}
|
value={params.duration}
|
||||||
onChange={(e) => updateParam('contrast', Number(e.target.value))}
|
onChange={e => updateParam('duration', Number(e.target.value))}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Mode Only Parameters */}
|
{/* Custom Mode Only Parameters */}
|
||||||
@@ -482,89 +220,345 @@ export default function AudioPanel() {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
Frequency Mapping
|
Max Partials: {params.maxPartials}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-4 gap-1">
|
<input
|
||||||
<button
|
type="range"
|
||||||
onClick={() => updateParam('frequencyMapping', 'mel')}
|
min="10"
|
||||||
className={`px-2 py-1 text-xs border ${
|
max="500"
|
||||||
(params.frequencyMapping || 'linear') === 'mel'
|
step="10"
|
||||||
? 'bg-white text-black border-white'
|
value={params.maxPartials}
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
onChange={e => updateParam('maxPartials', Number(e.target.value))}
|
||||||
}`}
|
className="w-full"
|
||||||
>
|
/>
|
||||||
Mel
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
</button>
|
Controls audio complexity vs performance
|
||||||
<button
|
</p>
|
||||||
onClick={() => updateParam('frequencyMapping', 'linear')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
(params.frequencyMapping || 'linear') === 'linear'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Linear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('frequencyMapping', 'bark')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.frequencyMapping === 'bark'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Bark
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => updateParam('frequencyMapping', 'log')}
|
|
||||||
className={`px-2 py-1 text-xs border ${
|
|
||||||
params.frequencyMapping === 'log'
|
|
||||||
? 'bg-white text-black border-white'
|
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Log
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
Spectral Density: {params.spectralDensity || 3}
|
Frequency Density: {params.frequencyResolution}x
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="7"
|
max="10"
|
||||||
step="1"
|
step="1"
|
||||||
value={params.spectralDensity || 3}
|
value={params.frequencyResolution}
|
||||||
onChange={(e) => updateParam('spectralDensity', Number(e.target.value))}
|
onChange={e => updateParam('frequencyResolution', Number(e.target.value))}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Tones per frequency peak (richer = higher)</p>
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Higher values create broader, richer frequency bands
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Direct Mode Only Parameters */}
|
||||||
|
{(params.synthesisMode || 'direct') === 'direct' && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
FFT Size: {params.fftSize || 2048}
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-4 gap-1">
|
||||||
|
{[1024, 2048, 4096, 8192].map(size => (
|
||||||
|
<button
|
||||||
|
key={size}
|
||||||
|
onClick={() => updateParam('fftSize', size)}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
(params.fftSize || 2048) === size
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{size}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Higher = better frequency resolution
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="0.9"
|
||||||
|
step="0.125"
|
||||||
|
value={params.frameOverlap || 0.75}
|
||||||
|
onChange={e => updateParam('frameOverlap', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Higher = smoother temporal resolution
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-xs text-gray-400">
|
||||||
|
Disable normalization: can be very loud
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
updateParam('disableNormalization', !(params.disableNormalization ?? false))
|
||||||
|
}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.disableNormalization === true
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{params.disableNormalization === true ? 'ON' : 'OFF'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Min Frequency: {params.minFreq}Hz
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="20"
|
||||||
|
max="200"
|
||||||
|
step="10"
|
||||||
|
value={params.minFreq}
|
||||||
|
onChange={e => updateParam('minFreq', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Max Frequency: {params.maxFreq}Hz
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1000"
|
||||||
|
max="20000"
|
||||||
|
step="500"
|
||||||
|
value={params.maxFreq}
|
||||||
|
onChange={e => updateParam('maxFreq', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Mode Only Parameters */}
|
||||||
|
{params.synthesisMode === 'custom' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Amplitude Threshold: {params.amplitudeThreshold}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.001"
|
||||||
|
max="0.1"
|
||||||
|
step="0.001"
|
||||||
|
value={params.amplitudeThreshold}
|
||||||
|
onChange={e => updateParam('amplitudeThreshold', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
|
<label className="block text-xs text-gray-400 mb-1">Window Type</label>
|
||||||
|
<div className="grid grid-cols-4 gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('windowType', 'rectangular')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.windowType === 'rectangular'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Rect
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('windowType', 'hann')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.windowType === 'hann'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Hann
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('windowType', 'hamming')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.windowType === 'hamming'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Hamming
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('windowType', 'blackman')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.windowType === 'blackman'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Blackman
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Reduces clicking/popping between time frames
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Color Inversion - Available for both modes */}
|
||||||
|
<div className="border-t border-gray-700 pt-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Color</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)}
|
onClick={() => updateParam('invert', false)}
|
||||||
className={`px-2 py-1 text-xs border ${
|
className={`px-2 py-1 text-xs border ${
|
||||||
params.usePerceptualWeighting !== false
|
!params.invert
|
||||||
? 'bg-white text-black border-white'
|
? 'bg-white text-black border-white'
|
||||||
: 'bg-black text-white border-gray-600 hover:border-white'
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
|
Normal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('invert', true)}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.invert
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Inverted
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">Squared RGB sum for better brightness perception</p>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{/* Contrast - Available for both modes */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Contrast: {(params.contrast || 2.2).toFixed(1)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
step="0.1"
|
||||||
|
value={params.contrast || 2.2}
|
||||||
|
onChange={e => updateParam('contrast', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Mode Only Parameters */}
|
||||||
|
{params.synthesisMode === 'custom' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Frequency Mapping</label>
|
||||||
|
<div className="grid grid-cols-4 gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('frequencyMapping', 'mel')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
(params.frequencyMapping || 'linear') === 'mel'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Mel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('frequencyMapping', 'linear')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
(params.frequencyMapping || 'linear') === 'linear'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Linear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('frequencyMapping', 'bark')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.frequencyMapping === 'bark'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Bark
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => updateParam('frequencyMapping', 'log')}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.frequencyMapping === 'log'
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Log
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
Spectral Density: {params.spectralDensity || 3}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="7"
|
||||||
|
step="1"
|
||||||
|
value={params.spectralDensity || 3}
|
||||||
|
onChange={e => updateParam('spectralDensity', Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Tones per frequency peak (richer = higher)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)
|
||||||
|
}
|
||||||
|
className={`px-2 py-1 text-xs border ${
|
||||||
|
params.usePerceptualWeighting !== false
|
||||||
|
? 'bg-white text-black border-white'
|
||||||
|
: 'bg-black text-white border-gray-600 hover:border-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Squared RGB sum for better brightness perception
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audio Controls - Below Parameters */}
|
{/* Audio Controls - Below Parameters */}
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ interface GeneratorSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function GeneratorSelector({ onGenerate, isGenerating }: GeneratorSelectorProps) {
|
export default function GeneratorSelector({ onGenerate, isGenerating }: GeneratorSelectorProps) {
|
||||||
console.log('GeneratorSelector rendering with props:', { onGenerate, isGenerating })
|
|
||||||
|
|
||||||
const settings = useStore(appSettings)
|
const settings = useStore(appSettings)
|
||||||
console.log('GeneratorSelector settings:', settings)
|
|
||||||
|
|
||||||
const handleGeneratorChange = (generator: GeneratorType) => {
|
const handleGeneratorChange = (generator: GeneratorType) => {
|
||||||
console.log('Changing generator to:', generator)
|
|
||||||
appSettings.set({ ...settings, selectedGenerator: generator })
|
appSettings.set({ ...settings, selectedGenerator: generator })
|
||||||
|
|
||||||
// Clear the grid when switching to photo or webcam modes to show drag/drop zones
|
// Clear the grid when switching to photo or webcam modes to show drag/drop zones
|
||||||
@@ -25,8 +21,6 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleGenerateClick = () => {
|
const handleGenerateClick = () => {
|
||||||
console.log('Generate button clicked in GeneratorSelector!')
|
|
||||||
console.log('onGenerate function:', onGenerate)
|
|
||||||
onGenerate()
|
onGenerate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +37,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
|||||||
{ id: 'from-photo' as const, name: 'Photo', description: 'Upload your image' },
|
{ id: 'from-photo' as const, name: 'Photo', description: 'Upload your image' },
|
||||||
{ id: 'webcam' as const, name: 'Webcam', description: 'Capture from camera' },
|
{ id: 'webcam' as const, name: 'Webcam', description: 'Capture from camera' },
|
||||||
{ id: 'art-institute' as const, name: 'Artworks', description: 'Famous artworks' },
|
{ id: 'art-institute' as const, name: 'Artworks', description: 'Famous artworks' },
|
||||||
{ id: 'picsum' as const, name: 'Picsum', description: 'Random photos' }
|
{ id: 'picsum' as const, name: 'Picsum', description: 'Random photos' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Automatically split generators into two rows
|
// Automatically split generators into two rows
|
||||||
@@ -55,7 +49,10 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
|||||||
const handleKeyPress = (event: KeyboardEvent) => {
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
if (event.key.toLowerCase() === 'g' && !isGenerating) {
|
if (event.key.toLowerCase() === 'g' && !isGenerating) {
|
||||||
// Don't trigger if user is typing in an input field
|
// Don't trigger if user is typing in an input field
|
||||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
if (
|
||||||
|
event.target instanceof HTMLInputElement ||
|
||||||
|
event.target instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -81,22 +78,23 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center h-10">
|
<div className="flex items-center justify-center h-10">
|
||||||
{settings.selectedGenerator !== 'from-photo' && settings.selectedGenerator !== 'webcam' && (
|
{settings.selectedGenerator !== 'from-photo' &&
|
||||||
<button
|
settings.selectedGenerator !== 'webcam' && (
|
||||||
onClick={handleGenerateClick}
|
<button
|
||||||
disabled={isGenerating}
|
onClick={handleGenerateClick}
|
||||||
className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed"
|
disabled={isGenerating}
|
||||||
>
|
className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||||
<div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div>
|
>
|
||||||
</button>
|
<div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div>
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second container: 85% width - Generator modes in two lines */}
|
{/* Second container: 85% width - Generator modes in two lines */}
|
||||||
<div className="w-[85%] flex flex-col space-y-2">
|
<div className="w-[85%] flex flex-col space-y-2">
|
||||||
<div className="flex items-center space-x-4 h-10">
|
<div className="flex items-center space-x-4 h-10">
|
||||||
{firstRowGenerators.map((generator) => (
|
{firstRowGenerators.map(generator => (
|
||||||
<Tooltip key={generator.id} content={generator.description}>
|
<Tooltip key={generator.id} content={generator.description}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleGeneratorChange(generator.id)}
|
onClick={() => handleGeneratorChange(generator.id)}
|
||||||
@@ -112,7 +110,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 h-10">
|
<div className="flex items-center space-x-4 h-10">
|
||||||
{secondRowGenerators.map((generator) => (
|
{secondRowGenerators.map(generator => (
|
||||||
<Tooltip key={generator.id} content={generator.description}>
|
<Tooltip key={generator.id} content={generator.description}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleGeneratorChange(generator.id)}
|
onClick={() => handleGeneratorChange(generator.id)}
|
||||||
|
|||||||
@@ -28,21 +28,21 @@ export default function HelpPopup({ isOpen, onClose }: HelpPopupProps) {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bg-black border border-white p-8 max-w-md w-full mx-4"
|
className="bg-black border border-white p-8 max-w-md w-full mx-4"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-6">
|
<div className="flex justify-between items-start mb-6">
|
||||||
<h2 className="text-xl font-bold text-white">About CoolSoup</h2>
|
<h2 className="text-xl font-bold text-white">About CoolSoup</h2>
|
||||||
<button
|
<button onClick={onClose} className="text-white hover:text-gray-300 text-xl">
|
||||||
onClick={onClose}
|
|
||||||
className="text-white hover:text-gray-300 text-xl"
|
|
||||||
>
|
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-white space-y-4">
|
<div className="text-white space-y-4">
|
||||||
<p>
|
<p>
|
||||||
CoolSoup generates visual patterns and converts them to audio through spectral/additive synthesis. Create images using mathematical expressions, waveforms, geometric patterns, or upload your own photos, then transform them into sound by treating the image as a spectrogram.
|
CoolSoup generates visual patterns and converts them to audio through spectral/additive
|
||||||
|
synthesis. Create images using mathematical expressions, waveforms, geometric patterns,
|
||||||
|
or upload your own photos, then transform them into sound by treating the image as a
|
||||||
|
spectrogram.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-600">
|
<div className="pt-4 border-t border-gray-600">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function ImageGrid() {
|
|||||||
const rect = event.currentTarget.getBoundingClientRect()
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
setLocalMousePosition({
|
setLocalMousePosition({
|
||||||
x: (event.clientX - rect.left) / rect.width,
|
x: (event.clientX - rect.left) / rect.width,
|
||||||
y: (event.clientY - rect.top) / rect.height
|
y: (event.clientY - rect.top) / rect.height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function ImageGrid() {
|
|||||||
const rect = event.currentTarget.getBoundingClientRect()
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
setLocalMousePosition({
|
setLocalMousePosition({
|
||||||
x: (event.clientX - rect.left) / rect.width,
|
x: (event.clientX - rect.left) / rect.width,
|
||||||
y: (event.clientY - rect.top) / rect.height
|
y: (event.clientY - rect.top) / rect.height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,10 @@ export default function ImageGrid() {
|
|||||||
<div className="text-lg mb-2">Generating images...</div>
|
<div className="text-lg mb-2">Generating images...</div>
|
||||||
<div className="text-sm">Please wait while we fetch your images</div>
|
<div className="text-sm">Please wait while we fetch your images</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="animate-pulse h-8 w-8 bg-white mx-auto" style={{ imageRendering: 'pixelated' }}></div>
|
<div
|
||||||
|
className="animate-pulse h-8 w-8 bg-white mx-auto"
|
||||||
|
style={{ imageRendering: 'pixelated' }}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,21 +72,19 @@ export default function ImageGrid() {
|
|||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<div className="h-full overflow-y-auto p-6">
|
<div className="h-full overflow-y-auto p-6">
|
||||||
<div className="grid grid-cols-5 gap-3">
|
<div className="grid grid-cols-5 gap-3">
|
||||||
{images.map((image) => (
|
{images.map(image => (
|
||||||
<button
|
<button
|
||||||
key={image.id}
|
key={image.id}
|
||||||
onClick={() => handleImageClick(image)}
|
onClick={() => handleImageClick(image)}
|
||||||
onMouseEnter={(e) => handleMouseEnter(image, e)}
|
onMouseEnter={e => handleMouseEnter(image, e)}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
className={`aspect-square border-2 transition-colors ${
|
className={`aspect-square border-2 transition-colors ${
|
||||||
selected?.id === image.id
|
selected?.id === image.id ? 'border-white' : 'border-gray-700 hover:border-gray-500'
|
||||||
? 'border-white'
|
|
||||||
: 'border-gray-700 hover:border-gray-500'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && image.canvas) {
|
if (canvas && image.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = image.canvas.width
|
canvas.width = image.canvas.width
|
||||||
@@ -119,7 +120,7 @@ export default function ImageGrid() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && hoveredImage.canvas) {
|
if (canvas && hoveredImage.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = 200
|
canvas.width = 200
|
||||||
@@ -143,11 +144,7 @@ export default function ImageGrid() {
|
|||||||
ctx.imageSmoothingEnabled = true
|
ctx.imageSmoothingEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(sourceCanvas, cropX, cropY, cropSize, cropSize, 0, 0, 200, 200)
|
||||||
sourceCanvas,
|
|
||||||
cropX, cropY, cropSize, cropSize,
|
|
||||||
0, 0, 200, 200
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full h-full block"
|
className="w-full h-full block"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useCallback } from 'react'
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { generateFromPhotoImage } from '../generators/from-photo'
|
import { generateFromPhotoImage } from '../generators/from-photo'
|
||||||
import { generatedImages, selectedImage } from '../stores'
|
import { generatedImages, selectedImage } from '../stores'
|
||||||
import type { GeneratedImage } from '../stores'
|
|
||||||
|
|
||||||
interface PhotoDropZoneProps {
|
interface PhotoDropZoneProps {
|
||||||
size: number
|
size: number
|
||||||
@@ -13,7 +12,6 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
|||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const images = useStore(generatedImages)
|
const images = useStore(generatedImages)
|
||||||
const selected = useStore(selectedImage)
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -25,71 +23,45 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
|||||||
setIsDragOver(false)
|
setIsDragOver(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
const handleDrop = useCallback(
|
||||||
e.preventDefault()
|
async (e: React.DragEvent) => {
|
||||||
setIsDragOver(false)
|
e.preventDefault()
|
||||||
setError(null)
|
setIsDragOver(false)
|
||||||
|
|
||||||
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)
|
setError(null)
|
||||||
const processedImage = await generateFromPhotoImage(file, size)
|
|
||||||
|
|
||||||
// Set the single processed image and automatically select it
|
const files = Array.from(e.dataTransfer.files)
|
||||||
generatedImages.set([processedImage])
|
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
||||||
selectedImage.set(processedImage)
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing image:', error)
|
||||||
|
setError('Failed to process the image. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[size]
|
||||||
|
)
|
||||||
|
|
||||||
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
|
const processedImage = images.length > 0 ? images[0] : null
|
||||||
|
|
||||||
@@ -107,7 +79,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && processedImage.canvas) {
|
if (canvas && processedImage.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = processedImage.canvas.width
|
canvas.width = processedImage.canvas.width
|
||||||
@@ -131,9 +103,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
|||||||
// Show the drop zone
|
// Show the drop zone
|
||||||
<div
|
<div
|
||||||
className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${
|
className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${
|
||||||
isDragOver
|
isDragOver ? 'border-white bg-gray-800' : 'border-gray-600 hover:border-gray-400'
|
||||||
? 'border-white bg-gray-800'
|
|
||||||
: 'border-gray-600 hover:border-gray-400'
|
|
||||||
} ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}
|
} ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
@@ -150,11 +120,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && <div className="mt-4 text-red-400 text-center">{error}</div>}
|
||||||
<div className="mt-4 text-red-400 text-center">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -30,24 +30,27 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
|
|||||||
setIsDragOver(false)
|
setIsDragOver(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
const handleDrop = useCallback(
|
||||||
e.preventDefault()
|
async (e: React.DragEvent) => {
|
||||||
setIsDragOver(false)
|
e.preventDefault()
|
||||||
|
setIsDragOver(false)
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files)
|
const files = Array.from(e.dataTransfer.files)
|
||||||
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
||||||
|
|
||||||
if (imageFiles.length === 0) return
|
if (imageFiles.length === 0) return
|
||||||
|
|
||||||
const file = imageFiles[0]
|
const file = imageFiles[0]
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDrop(file, index)
|
await onDrop(file, index)
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
}
|
}
|
||||||
}, [index, onDrop])
|
},
|
||||||
|
[index, onDrop]
|
||||||
|
)
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (image) {
|
if (image) {
|
||||||
@@ -72,7 +75,7 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
|
|||||||
</div>
|
</div>
|
||||||
) : image ? (
|
) : image ? (
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && image.canvas) {
|
if (canvas && image.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = image.canvas.width
|
canvas.width = image.canvas.width
|
||||||
@@ -102,26 +105,28 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
|
|||||||
const images = useStore(generatedImages)
|
const images = useStore(generatedImages)
|
||||||
const selected = useStore(selectedImage)
|
const selected = useStore(selectedImage)
|
||||||
|
|
||||||
const handleDrop = useCallback(async (file: File, index: number) => {
|
const handleDrop = useCallback(
|
||||||
try {
|
async (file: File, index: number) => {
|
||||||
const processedImage = await generateFromPhotoImage(file, size)
|
try {
|
||||||
|
const processedImage = await generateFromPhotoImage(file, size)
|
||||||
|
|
||||||
// Create new images array with the processed image at the specified index
|
// Create new images array with the processed image at the specified index
|
||||||
const newImages = [...images]
|
const newImages = [...images]
|
||||||
newImages[index] = processedImage
|
newImages[index] = processedImage
|
||||||
|
|
||||||
// Filter out any null/undefined values and update the store
|
// Filter out any null/undefined values and update the store
|
||||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||||
generatedImages.set(filteredImages)
|
generatedImages.set(filteredImages)
|
||||||
|
|
||||||
// Auto-select the newly processed image
|
// Auto-select the newly processed image
|
||||||
selectedImage.set(processedImage)
|
selectedImage.set(processedImage)
|
||||||
|
|
||||||
console.log('Photo processed and added to grid at index:', index)
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('Error processing photo:', error)
|
||||||
console.error('Error processing photo:', error)
|
}
|
||||||
}
|
},
|
||||||
}, [images, size])
|
[images, size]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSelect = useCallback((image: GeneratedImage) => {
|
const handleSelect = useCallback((image: GeneratedImage) => {
|
||||||
selectedImage.set(image)
|
selectedImage.set(image)
|
||||||
@@ -146,9 +151,7 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<div className="flex-1 overflow-auto p-6">
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
|
||||||
{gridSquares}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode, useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
content: string
|
content: string
|
||||||
@@ -13,7 +14,7 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
|
|||||||
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
|
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
|
||||||
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
|
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
|
||||||
left: 'right-full top-1/2 transform -translate-y-1/2 mr-2',
|
left: 'right-full top-1/2 transform -translate-y-1/2 mr-2',
|
||||||
right: 'left-full top-1/2 transform -translate-y-1/2 ml-2'
|
right: 'left-full top-1/2 transform -translate-y-1/2 ml-2',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +25,9 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<div className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}>
|
<div
|
||||||
|
className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { generateFromWebcamImage } from '../generators/webcam'
|
import { generateFromWebcamImage } from '../generators/webcam'
|
||||||
import { WebcamProcessor } from '../generators/webcam-generator'
|
import { WebcamProcessor } from '../generators/webcam-generator'
|
||||||
@@ -37,7 +37,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
|
|||||||
onClick={handleImageClick}
|
onClick={handleImageClick}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={(canvas) => {
|
ref={canvas => {
|
||||||
if (canvas && image.canvas) {
|
if (canvas && image.canvas) {
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
canvas.width = image.canvas.width
|
canvas.width = image.canvas.width
|
||||||
@@ -60,9 +60,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
|
|||||||
{isCapturing ? (
|
{isCapturing ? (
|
||||||
<div className="text-xs">Capturing...</div>
|
<div className="text-xs">Capturing...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-center">
|
<div className="text-xs text-center">Capture</div>
|
||||||
Capture
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -100,37 +98,39 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
|
|||||||
}
|
}
|
||||||
}, [processor])
|
}, [processor])
|
||||||
|
|
||||||
const handleCapture = useCallback(async (index: number) => {
|
const handleCapture = useCallback(
|
||||||
if (!cameraInitialized) {
|
async (index: number) => {
|
||||||
console.error('Camera not initialized')
|
if (!cameraInitialized) {
|
||||||
return
|
console.error('Camera not initialized')
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsCapturing(true)
|
setIsCapturing(true)
|
||||||
setCapturingIndex(index)
|
setCapturingIndex(index)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const capturedImage = await generateFromWebcamImage(processor, size)
|
const capturedImage = await generateFromWebcamImage(processor, size)
|
||||||
|
|
||||||
// Create new images array with the captured image at the specified index
|
// Create new images array with the captured image at the specified index
|
||||||
const newImages = [...images]
|
const newImages = [...images]
|
||||||
newImages[index] = capturedImage
|
newImages[index] = capturedImage
|
||||||
|
|
||||||
// Filter out any null/undefined values and update the store
|
// Filter out any null/undefined values and update the store
|
||||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||||
generatedImages.set(filteredImages)
|
generatedImages.set(filteredImages)
|
||||||
|
|
||||||
// Auto-select the newly captured image
|
// Auto-select the newly captured image
|
||||||
selectedImage.set(capturedImage)
|
selectedImage.set(capturedImage)
|
||||||
|
|
||||||
console.log('Photo captured and added to grid at index:', index)
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('Error capturing photo:', error)
|
||||||
console.error('Error capturing photo:', error)
|
} finally {
|
||||||
} finally {
|
setIsCapturing(false)
|
||||||
setIsCapturing(false)
|
setCapturingIndex(null)
|
||||||
setCapturingIndex(null)
|
}
|
||||||
}
|
},
|
||||||
}, [images, size, cameraInitialized, processor])
|
[images, size, cameraInitialized, processor]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSelect = useCallback((image: GeneratedImage) => {
|
const handleSelect = useCallback((image: GeneratedImage) => {
|
||||||
selectedImage.set(image)
|
selectedImage.set(image)
|
||||||
@@ -163,9 +163,7 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
|
||||||
{gridSquares}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,32 @@
|
|||||||
import type { GeneratedImage } from '../stores'
|
import type { GeneratedImage } from '../stores'
|
||||||
|
|
||||||
const searchTerms = [
|
const searchTerms = [
|
||||||
'painting', 'sculpture', 'drawing', 'pottery', 'portrait', 'landscape',
|
'painting',
|
||||||
'impressionist', 'modern', 'contemporary', 'abstract', 'still life',
|
'sculpture',
|
||||||
'figure', 'nature', 'urban', 'color', 'light', 'texture', 'pattern',
|
'drawing',
|
||||||
'architecture', 'street', 'woman', 'man', 'cat', 'dog', 'flower', 'tree'
|
'pottery',
|
||||||
|
'portrait',
|
||||||
|
'landscape',
|
||||||
|
'impressionist',
|
||||||
|
'modern',
|
||||||
|
'contemporary',
|
||||||
|
'abstract',
|
||||||
|
'still life',
|
||||||
|
'figure',
|
||||||
|
'nature',
|
||||||
|
'urban',
|
||||||
|
'color',
|
||||||
|
'light',
|
||||||
|
'texture',
|
||||||
|
'pattern',
|
||||||
|
'architecture',
|
||||||
|
'street',
|
||||||
|
'woman',
|
||||||
|
'man',
|
||||||
|
'cat',
|
||||||
|
'dog',
|
||||||
|
'flower',
|
||||||
|
'tree',
|
||||||
]
|
]
|
||||||
|
|
||||||
interface ArticArtwork {
|
interface ArticArtwork {
|
||||||
@@ -27,7 +49,10 @@ interface ArticSearchResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateArtInstituteImages(count: number, size: number): Promise<GeneratedImage[]> {
|
export async function generateArtInstituteImages(
|
||||||
|
count: number,
|
||||||
|
size: number
|
||||||
|
): Promise<GeneratedImage[]> {
|
||||||
const images: GeneratedImage[] = []
|
const images: GeneratedImage[] = []
|
||||||
const maxRetries = count * 3 // Try 3x more objects to account for failures
|
const maxRetries = count * 3 // Try 3x more objects to account for failures
|
||||||
|
|
||||||
@@ -63,7 +88,9 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter artworks that have images
|
// Filter artworks that have images
|
||||||
const artworksWithImages = searchData.data.filter(artwork => artwork.image_id && artwork.is_public_domain)
|
const artworksWithImages = searchData.data.filter(
|
||||||
|
artwork => artwork.image_id && artwork.is_public_domain
|
||||||
|
)
|
||||||
|
|
||||||
if (artworksWithImages.length === 0) {
|
if (artworksWithImages.length === 0) {
|
||||||
throw new Error('No artworks with images found')
|
throw new Error('No artworks with images found')
|
||||||
@@ -74,7 +101,7 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
|
|||||||
// Construct IIIF image URL - 512px width, auto height
|
// Construct IIIF image URL - 512px width, auto height
|
||||||
const imageUrl = `https://www.artic.edu/iiif/2/${artwork.image_id}/full/512,/0/default.jpg`
|
const imageUrl = `https://www.artic.edu/iiif/2/${artwork.image_id}/full/512,/0/default.jpg`
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.crossOrigin = 'anonymous'
|
img.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
@@ -122,8 +149,8 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
|
|||||||
date: artwork.date_display || 'Unknown Date',
|
date: artwork.date_display || 'Unknown Date',
|
||||||
medium: artwork.medium_display || 'Unknown Medium',
|
medium: artwork.medium_display || 'Unknown Medium',
|
||||||
department: artwork.department_title || 'Unknown Department',
|
department: artwork.department_title || 'Unknown Department',
|
||||||
searchTerm
|
searchTerm,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing Art Institute image:', error)
|
console.error('Error processing Art Institute image:', error)
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
|
|||||||
const x = i * sectionWidth
|
const x = i * sectionWidth
|
||||||
const yValue = Math.random() // 0.0 to 1.0
|
const yValue = Math.random() // 0.0 to 1.0
|
||||||
// Convert y value where 0.0 is bottom and 1.0 is top
|
// Convert y value where 0.0 is bottom and 1.0 is top
|
||||||
const y = canvasHeight - (yValue * canvasHeight)
|
const y = canvasHeight - yValue * canvasHeight
|
||||||
|
|
||||||
bands.push({
|
bands.push({
|
||||||
x,
|
x,
|
||||||
width: sectionWidth,
|
width: sectionWidth,
|
||||||
y
|
y,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
|
|||||||
return {
|
return {
|
||||||
divisions,
|
divisions,
|
||||||
bands,
|
bands,
|
||||||
opacity: 0.8 // Slight transparency for superimposition
|
opacity: 0.8, // Slight transparency for superimposition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ export function generateBandsImages(count: number, size: number): GeneratedImage
|
|||||||
bands: layer.bands.map(band => ({
|
bands: layer.bands.map(band => ({
|
||||||
x: band.x,
|
x: band.x,
|
||||||
width: band.width,
|
width: band.width,
|
||||||
y: band.y
|
y: band.y,
|
||||||
})),
|
})),
|
||||||
opacity: layer.opacity
|
opacity: layer.opacity,
|
||||||
})),
|
})),
|
||||||
strokeHeight,
|
strokeHeight,
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -16,36 +16,50 @@ const greyLevels = [
|
|||||||
'#333333', // Almost black
|
'#333333', // Almost black
|
||||||
'#222222', // Nearly black
|
'#222222', // Nearly black
|
||||||
'#111111', // Extremely dark
|
'#111111', // Extremely dark
|
||||||
'#0f0f0f' // Almost black (minimal amplitude)
|
'#0f0f0f', // Almost black (minimal amplitude)
|
||||||
]
|
]
|
||||||
|
|
||||||
interface DustParams {
|
interface DustParams {
|
||||||
pointCount: number
|
pointCount: number
|
||||||
distributionType: 'uniform' | 'clustered' | 'scattered' | 'ring' | 'spiral' | 'grid' | 'noise' | 'radial' | 'perlin'
|
distributionType:
|
||||||
|
| 'uniform'
|
||||||
|
| 'clustered'
|
||||||
|
| 'scattered'
|
||||||
|
| 'ring'
|
||||||
|
| 'spiral'
|
||||||
|
| 'grid'
|
||||||
|
| 'noise'
|
||||||
|
| 'radial'
|
||||||
|
| 'perlin'
|
||||||
concentration: number // 0-1, how concentrated the distribution is
|
concentration: number // 0-1, how concentrated the distribution is
|
||||||
clusterCount?: number // for clustered distribution
|
clusterCount?: number // for clustered distribution
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateUniformDistribution(count: number, size: number): Array<{x: number, y: number}> {
|
function generateUniformDistribution(count: number, size: number): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
points.push({
|
points.push({
|
||||||
x: Math.random() * size,
|
x: Math.random() * size,
|
||||||
y: Math.random() * size
|
y: Math.random() * size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateClusteredDistribution(count: number, size: number, clusterCount: number, concentration: number): Array<{x: number, y: number}> {
|
function generateClusteredDistribution(
|
||||||
const points = []
|
count: number,
|
||||||
const clusters = []
|
size: number,
|
||||||
|
clusterCount: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
|
const points: Array<{ x: number; y: number }> = []
|
||||||
|
const clusters: Array<{ x: number; y: number }> = []
|
||||||
|
|
||||||
// Generate cluster centers
|
// Generate cluster centers
|
||||||
for (let i = 0; i < clusterCount; i++) {
|
for (let i = 0; i < clusterCount; i++) {
|
||||||
clusters.push({
|
clusters.push({
|
||||||
x: Math.random() * size,
|
x: Math.random() * size,
|
||||||
y: Math.random() * size
|
y: Math.random() * size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +78,7 @@ function generateClusteredDistribution(count: number, size: number, clusterCount
|
|||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
x: Math.max(0, Math.min(size, cluster.x + Math.cos(angle) * distance)),
|
x: Math.max(0, Math.min(size, cluster.x + Math.cos(angle) * distance)),
|
||||||
y: Math.max(0, Math.min(size, cluster.y + Math.sin(angle) * distance))
|
y: Math.max(0, Math.min(size, cluster.y + Math.sin(angle) * distance)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -72,23 +86,28 @@ function generateClusteredDistribution(count: number, size: number, clusterCount
|
|||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateScatteredDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generateScatteredDistribution(
|
||||||
const points = []
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
|
const points: Array<{ x: number; y: number }> = []
|
||||||
const exclusionRadius = size * concentration * 0.1 // minimum distance between points
|
const exclusionRadius = size * concentration * 0.1 // minimum distance between points
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
let point: {x: number, y: number}
|
let point: { x: number; y: number }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
point = {
|
point = {
|
||||||
x: Math.random() * size,
|
x: Math.random() * size,
|
||||||
y: Math.random() * size
|
y: Math.random() * size,
|
||||||
}
|
}
|
||||||
attempts++
|
attempts++
|
||||||
} while (attempts < 50 && points.some(p =>
|
} while (
|
||||||
Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius
|
attempts < 50 &&
|
||||||
))
|
points.some(p => Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius)
|
||||||
|
)
|
||||||
|
|
||||||
points.push(point)
|
points.push(point)
|
||||||
}
|
}
|
||||||
@@ -96,7 +115,11 @@ function generateScatteredDistribution(count: number, size: number, concentratio
|
|||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRingDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generateRingDistribution(
|
||||||
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const centerX = size / 2
|
const centerX = size / 2
|
||||||
const centerY = size / 2
|
const centerY = size / 2
|
||||||
@@ -110,14 +133,18 @@ function generateRingDistribution(count: number, size: number, concentration: nu
|
|||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
x: centerX + Math.cos(angle) * finalRadius,
|
x: centerX + Math.cos(angle) * finalRadius,
|
||||||
y: centerY + Math.sin(angle) * finalRadius
|
y: centerY + Math.sin(angle) * finalRadius,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSpiralDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generateSpiralDistribution(
|
||||||
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const centerX = size / 2
|
const centerX = size / 2
|
||||||
const centerY = size / 2
|
const centerY = size / 2
|
||||||
@@ -131,14 +158,18 @@ function generateSpiralDistribution(count: number, size: number, concentration:
|
|||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
x: centerX + Math.cos(t) * radius + noise,
|
x: centerX + Math.cos(t) * radius + noise,
|
||||||
y: centerY + Math.sin(t) * radius + noise
|
y: centerY + Math.sin(t) * radius + noise,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateGridDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generateGridDistribution(
|
||||||
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const gridSize = Math.ceil(Math.sqrt(count))
|
const gridSize = Math.ceil(Math.sqrt(count))
|
||||||
const cellSize = size / gridSize
|
const cellSize = size / gridSize
|
||||||
@@ -156,15 +187,18 @@ function generateGridDistribution(count: number, size: number, concentration: nu
|
|||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
x: Math.max(0, Math.min(size, baseX + jitterX)),
|
x: Math.max(0, Math.min(size, baseX + jitterX)),
|
||||||
y: Math.max(0, Math.min(size, baseY + jitterY))
|
y: Math.max(0, Math.min(size, baseY + jitterY)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateNoiseDistribution(
|
||||||
function generateNoiseDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const gridSize = 64
|
const gridSize = 64
|
||||||
const cellSize = size / gridSize
|
const cellSize = size / gridSize
|
||||||
@@ -177,7 +211,7 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
|
|||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
let point: {x: number, y: number}
|
let point: { x: number; y: number }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const x = Math.random() * size
|
const x = Math.random() * size
|
||||||
@@ -204,7 +238,11 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
|
|||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRadialDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generateRadialDistribution(
|
||||||
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const centerX = size / 2
|
const centerX = size / 2
|
||||||
const centerY = size / 2
|
const centerY = size / 2
|
||||||
@@ -221,14 +259,18 @@ function generateRadialDistribution(count: number, size: number, concentration:
|
|||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
x: centerX + Math.cos(angle) * finalRadius,
|
x: centerX + Math.cos(angle) * finalRadius,
|
||||||
y: centerY + Math.sin(angle) * finalRadius
|
y: centerY + Math.sin(angle) * finalRadius,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePerlinLikeDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
function generatePerlinLikeDistribution(
|
||||||
|
count: number,
|
||||||
|
size: number,
|
||||||
|
concentration: number
|
||||||
|
): Array<{ x: number; y: number }> {
|
||||||
const points = []
|
const points = []
|
||||||
const scale = 0.01 + concentration * 0.05
|
const scale = 0.01 + concentration * 0.05
|
||||||
|
|
||||||
@@ -243,7 +285,7 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
|
|||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
let point: {x: number, y: number}
|
let point: { x: number; y: number }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const x = Math.random() * size
|
const x = Math.random() * size
|
||||||
@@ -270,7 +312,17 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
|
|||||||
export function generateDustImages(count: number, size: number): GeneratedImage[] {
|
export function generateDustImages(count: number, size: number): GeneratedImage[] {
|
||||||
const images: GeneratedImage[] = []
|
const images: GeneratedImage[] = []
|
||||||
|
|
||||||
const distributions = ['uniform', 'clustered', 'scattered', 'ring', 'spiral', 'grid', 'noise', 'radial', 'perlin'] as const
|
const distributions = [
|
||||||
|
'uniform',
|
||||||
|
'clustered',
|
||||||
|
'scattered',
|
||||||
|
'ring',
|
||||||
|
'spiral',
|
||||||
|
'grid',
|
||||||
|
'noise',
|
||||||
|
'radial',
|
||||||
|
'perlin',
|
||||||
|
] as const
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -295,11 +347,11 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
|
|||||||
pointCount,
|
pointCount,
|
||||||
distributionType,
|
distributionType,
|
||||||
concentration,
|
concentration,
|
||||||
clusterCount
|
clusterCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate points based on distribution type
|
// Generate points based on distribution type
|
||||||
let points: Array<{x: number, y: number}>
|
let points: Array<{ x: number; y: number }>
|
||||||
|
|
||||||
switch (distributionType) {
|
switch (distributionType) {
|
||||||
case 'uniform':
|
case 'uniform':
|
||||||
@@ -350,8 +402,8 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
|
|||||||
...params,
|
...params,
|
||||||
pointColor,
|
pointColor,
|
||||||
actualPointCount: points.length,
|
actualPointCount: points.length,
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function processPhoto(file: File, config: PhotoProcessingConfig): Promise
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
originalFile: file,
|
originalFile: file,
|
||||||
config
|
config,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
@@ -55,7 +55,7 @@ function calculateImageDimensions(
|
|||||||
imgWidth: number,
|
imgWidth: number,
|
||||||
imgHeight: number,
|
imgHeight: number,
|
||||||
targetSize: number
|
targetSize: number
|
||||||
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
|
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
|
||||||
const aspectRatio = imgWidth / imgHeight
|
const aspectRatio = imgWidth / imgHeight
|
||||||
|
|
||||||
let drawWidth: number
|
let drawWidth: number
|
||||||
@@ -96,7 +96,7 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set R, G, B to the same grayscale value
|
// Set R, G, B to the same grayscale value
|
||||||
data[i] = final // Red
|
data[i] = final // Red
|
||||||
data[i + 1] = final // Green
|
data[i + 1] = final // Green
|
||||||
data[i + 2] = final // Blue
|
data[i + 2] = final // Blue
|
||||||
// Alpha (data[i + 3]) remains unchanged
|
// Alpha (data[i + 3]) remains unchanged
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
|||||||
const result = await processPhoto(file, {
|
const result = await processPhoto(file, {
|
||||||
targetSize: size,
|
targetSize: size,
|
||||||
contrastEnhancement: true,
|
contrastEnhancement: true,
|
||||||
grayscaleConversion: true
|
grayscaleConversion: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const image: GeneratedImage = {
|
const image: GeneratedImage = {
|
||||||
@@ -21,8 +21,8 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
|||||||
processedAt: new Date().toISOString(),
|
processedAt: new Date().toISOString(),
|
||||||
size,
|
size,
|
||||||
contrastEnhanced: true,
|
contrastEnhanced: true,
|
||||||
grayscaleConverted: true
|
grayscaleConverted: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return image
|
return image
|
||||||
@@ -46,8 +46,8 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
|||||||
params: {
|
params: {
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallbackImage
|
return fallbackImage
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const PATTERN_TYPES = [
|
|||||||
'tessellation',
|
'tessellation',
|
||||||
'nestedSquares',
|
'nestedSquares',
|
||||||
'mosaicSquares',
|
'mosaicSquares',
|
||||||
'chevrons'
|
'chevrons',
|
||||||
]
|
]
|
||||||
|
|
||||||
// Grayscale base colors for variety
|
// Grayscale base colors for variety
|
||||||
@@ -47,7 +47,7 @@ const GRAYSCALE_COLORS = [
|
|||||||
'#cccccc', // Light grey
|
'#cccccc', // Light grey
|
||||||
'#dddddd', // Very light grey
|
'#dddddd', // Very light grey
|
||||||
'#eeeeee', // Almost white
|
'#eeeeee', // Almost white
|
||||||
'#ffffff' // Pure white
|
'#ffffff', // Pure white
|
||||||
]
|
]
|
||||||
|
|
||||||
function generateRandomSeed(): string {
|
function generateRandomSeed(): string {
|
||||||
@@ -60,7 +60,10 @@ function generateRandomSeed(): string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCanvasElement, imageData: ImageData }> {
|
function svgToCanvas(
|
||||||
|
svgString: string,
|
||||||
|
size: number
|
||||||
|
): Promise<{ canvas: HTMLCanvasElement; imageData: ImageData }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
@@ -106,7 +109,7 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
|
|||||||
final = Math.max(0, Math.min(255, final))
|
final = Math.max(0, Math.min(255, final))
|
||||||
|
|
||||||
// Set R, G, B to the same enhanced grayscale value
|
// Set R, G, B to the same enhanced grayscale value
|
||||||
data[i] = final // Red
|
data[i] = final // Red
|
||||||
data[i + 1] = final // Green
|
data[i + 1] = final // Green
|
||||||
data[i + 2] = final // Blue
|
data[i + 2] = final // Blue
|
||||||
// Alpha (data[i + 3]) remains unchanged
|
// Alpha (data[i + 3]) remains unchanged
|
||||||
@@ -130,7 +133,10 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateGeopatternImages(count: number, size: number): Promise<GeneratedImage[]> {
|
export async function generateGeopatternImages(
|
||||||
|
count: number,
|
||||||
|
size: number
|
||||||
|
): Promise<GeneratedImage[]> {
|
||||||
const images: GeneratedImage[] = []
|
const images: GeneratedImage[] = []
|
||||||
|
|
||||||
// Load the geopattern library dynamically
|
// Load the geopattern library dynamically
|
||||||
@@ -150,7 +156,7 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
|||||||
// Generate the geopattern
|
// Generate the geopattern
|
||||||
const pattern = geopatternLib.generate(seed, {
|
const pattern = geopatternLib.generate(seed, {
|
||||||
generator: patternType,
|
generator: patternType,
|
||||||
baseColor: baseColor
|
baseColor: baseColor,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get SVG string
|
// Get SVG string
|
||||||
@@ -169,8 +175,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
|||||||
seed,
|
seed,
|
||||||
baseColor,
|
baseColor,
|
||||||
originalSvg: svgString,
|
originalSvg: svgString,
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
@@ -196,8 +202,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
|||||||
seed: 'error',
|
seed: 'error',
|
||||||
baseColor: '#000000',
|
baseColor: '#000000',
|
||||||
size,
|
size,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(fallbackImage)
|
images.push(fallbackImage)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const greyLevels = [
|
|||||||
'#888888', // Very dark grey
|
'#888888', // Very dark grey
|
||||||
'#777777', // Darker grey
|
'#777777', // Darker grey
|
||||||
'#666666', // Even darker grey
|
'#666666', // Even darker grey
|
||||||
'#555555' // Much darker grey
|
'#555555', // Much darker grey
|
||||||
]
|
]
|
||||||
|
|
||||||
interface HarmonicSeries {
|
interface HarmonicSeries {
|
||||||
@@ -54,7 +54,12 @@ function calculateHarmonicAmplitude(harmonicNumber: number, timbre: string): num
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
|
function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
|
||||||
const timbres: Array<'bright' | 'warm' | 'dark' | 'metallic'> = ['bright', 'warm', 'dark', 'metallic']
|
const timbres: Array<'bright' | 'warm' | 'dark' | 'metallic'> = [
|
||||||
|
'bright',
|
||||||
|
'warm',
|
||||||
|
'dark',
|
||||||
|
'metallic',
|
||||||
|
]
|
||||||
const timbre = timbres[Math.floor(Math.random() * timbres.length)]
|
const timbre = timbres[Math.floor(Math.random() * timbres.length)]
|
||||||
|
|
||||||
// Fundamental frequency position (in lower 2/3 of canvas for musical range)
|
// Fundamental frequency position (in lower 2/3 of canvas for musical range)
|
||||||
@@ -66,7 +71,7 @@ function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
|
|||||||
|
|
||||||
for (let i = 1; i <= maxHarmonics; i++) {
|
for (let i = 1; i <= maxHarmonics; i++) {
|
||||||
// Higher harmonics = higher frequencies = lower Y positions
|
// Higher harmonics = higher frequencies = lower Y positions
|
||||||
const yPosition = fundamental - (fundamental * (i - 1) / maxHarmonics)
|
const yPosition = fundamental - (fundamental * (i - 1)) / maxHarmonics
|
||||||
|
|
||||||
// Stop if we go above the canvas
|
// Stop if we go above the canvas
|
||||||
if (yPosition <= 0) break
|
if (yPosition <= 0) break
|
||||||
@@ -78,14 +83,14 @@ function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
|
|||||||
frequency: i,
|
frequency: i,
|
||||||
yPosition,
|
yPosition,
|
||||||
amplitude,
|
amplitude,
|
||||||
thickness
|
thickness,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fundamental,
|
fundamental,
|
||||||
harmonics,
|
harmonics,
|
||||||
timbre
|
timbre,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +142,10 @@ export function generateHarmonicsImages(count: number, size: number): GeneratedI
|
|||||||
frequency: h.frequency,
|
frequency: h.frequency,
|
||||||
yPosition: h.yPosition,
|
yPosition: h.yPosition,
|
||||||
amplitude: h.amplitude,
|
amplitude: h.amplitude,
|
||||||
thickness: h.thickness
|
thickness: h.thickness,
|
||||||
})),
|
})),
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const greyLevels = [
|
|||||||
'#bbbbbb', // Medium grey
|
'#bbbbbb', // Medium grey
|
||||||
'#aaaaaa', // Medium-dark grey
|
'#aaaaaa', // Medium-dark grey
|
||||||
'#999999', // Dark grey
|
'#999999', // Dark grey
|
||||||
'#888888' // Very dark grey (lower amplitude)
|
'#888888', // Very dark grey (lower amplitude)
|
||||||
]
|
]
|
||||||
|
|
||||||
export function generatePartialsImages(count: number, size: number): GeneratedImage[] {
|
export function generatePartialsImages(count: number, size: number): GeneratedImage[] {
|
||||||
@@ -30,7 +30,7 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
|
|||||||
ctx.fillRect(0, 0, size, size)
|
ctx.fillRect(0, 0, size, size)
|
||||||
|
|
||||||
// Generate line positions, ensuring they don't overlap
|
// Generate line positions, ensuring they don't overlap
|
||||||
const linePositions: Array<{y: number, thickness: number}> = []
|
const linePositions: Array<{ y: number; thickness: number }> = []
|
||||||
const minSpacing = size / (lineCount + 1)
|
const minSpacing = size / (lineCount + 1)
|
||||||
|
|
||||||
for (let j = 0; j < lineCount; j++) {
|
for (let j = 0; j < lineCount; j++) {
|
||||||
@@ -59,8 +59,8 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
|
|||||||
lineCount,
|
lineCount,
|
||||||
linePositions: linePositions.map(l => ({ y: l.y, thickness: l.thickness })),
|
linePositions: linePositions.map(l => ({ y: l.y, thickness: l.thickness })),
|
||||||
lineColor,
|
lineColor,
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -22,56 +22,59 @@ const highQualityIds = [
|
|||||||
1, 2, 3, 5, 6, 8, 9, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
1, 2, 3, 5, 6, 8, 9, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||||
35, 36, 37, 39, 40, 42, 43, 44, 47, 48, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
35, 36, 37, 39, 40, 42, 43, 44, 47, 48, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
||||||
64, 65, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
|
64, 65, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
|
||||||
91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116,
|
91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116, 119,
|
||||||
119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138,
|
120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138,
|
||||||
139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158,
|
139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,
|
||||||
159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178,
|
158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
|
||||||
179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198,
|
177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
|
||||||
199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
|
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214,
|
||||||
219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,
|
215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
|
||||||
239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258,
|
234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252,
|
||||||
259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278,
|
253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
|
||||||
279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298,
|
272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290,
|
||||||
299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318,
|
291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309,
|
||||||
319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
|
310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328,
|
||||||
339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358,
|
329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347,
|
||||||
359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
|
348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366,
|
||||||
379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398,
|
367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385,
|
||||||
399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
|
386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404,
|
||||||
419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438,
|
405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423,
|
||||||
439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458,
|
424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442,
|
||||||
459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478,
|
443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461,
|
||||||
479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498,
|
462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480,
|
||||||
499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
|
481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499,
|
||||||
519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538,
|
500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
|
||||||
539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558,
|
519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537,
|
||||||
559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578,
|
538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556,
|
||||||
579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598,
|
557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575,
|
||||||
599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618,
|
576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594,
|
||||||
619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638,
|
595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613,
|
||||||
639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658,
|
614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632,
|
||||||
659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678,
|
633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651,
|
||||||
679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698,
|
652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670,
|
||||||
699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718,
|
671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
|
||||||
719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738,
|
690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708,
|
||||||
739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758,
|
709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727,
|
||||||
759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778,
|
728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746,
|
||||||
779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798,
|
747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765,
|
||||||
799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818,
|
766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784,
|
||||||
819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838,
|
785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803,
|
||||||
839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858,
|
804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822,
|
||||||
859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878,
|
823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841,
|
||||||
879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898,
|
842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860,
|
||||||
899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918,
|
861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879,
|
||||||
919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938,
|
880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898,
|
||||||
939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958,
|
899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917,
|
||||||
959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978,
|
918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936,
|
||||||
979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998,
|
937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955,
|
||||||
999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016,
|
956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974,
|
||||||
1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034,
|
975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993,
|
||||||
1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052,
|
994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010,
|
||||||
1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070,
|
1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026,
|
||||||
1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084
|
1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042,
|
||||||
|
1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058,
|
||||||
|
1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074,
|
||||||
|
1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Keep track of recently used IDs to avoid immediate repeats
|
// Keep track of recently used IDs to avoid immediate repeats
|
||||||
@@ -97,7 +100,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
|
|||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.crossOrigin = 'anonymous'
|
img.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
try {
|
try {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@@ -114,7 +117,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
generator: 'picsum',
|
generator: 'picsum',
|
||||||
params: { url, imageId }
|
params: { url, imageId },
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing Picsum image:', error)
|
console.error('Error processing Picsum image:', error)
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
import type { GeneratedImage } from '../stores'
|
import type { GeneratedImage } from '../stores'
|
||||||
|
|
||||||
type ShapeType = 'circle' | 'square' | 'triangle' | 'diamond' | 'pentagon' | 'hexagon' |
|
type ShapeType =
|
||||||
'star' | 'cross' | 'ellipse' | 'rect' | 'octagon' | 'arrow' | 'ring'
|
| 'circle'
|
||||||
|
| 'square'
|
||||||
|
| 'triangle'
|
||||||
|
| 'diamond'
|
||||||
|
| 'pentagon'
|
||||||
|
| 'hexagon'
|
||||||
|
| 'star'
|
||||||
|
| 'cross'
|
||||||
|
| 'ellipse'
|
||||||
|
| 'rect'
|
||||||
|
| 'octagon'
|
||||||
|
| 'arrow'
|
||||||
|
| 'ring'
|
||||||
|
|
||||||
type FillStyle = 'solid' | 'outline'
|
type FillStyle = 'solid' | 'outline'
|
||||||
|
|
||||||
const greyLevels = [
|
const greyLevels = [
|
||||||
'#ffffff', '#eeeeee', '#dddddd', '#cccccc',
|
'#ffffff',
|
||||||
'#bbbbbb', '#aaaaaa', '#999999', '#888888', '#777777'
|
'#eeeeee',
|
||||||
|
'#dddddd',
|
||||||
|
'#cccccc',
|
||||||
|
'#bbbbbb',
|
||||||
|
'#aaaaaa',
|
||||||
|
'#999999',
|
||||||
|
'#888888',
|
||||||
|
'#777777',
|
||||||
]
|
]
|
||||||
|
|
||||||
interface Shape {
|
interface Shape {
|
||||||
@@ -172,8 +191,8 @@ function drawPolygon(ctx: CanvasRenderingContext2D, shape: Shape, sides: number)
|
|||||||
|
|
||||||
for (let i = 0; i < sides; i++) {
|
for (let i = 0; i < sides; i++) {
|
||||||
const angle = (i * 2 * Math.PI) / sides
|
const angle = (i * 2 * Math.PI) / sides
|
||||||
const px = Math.cos(angle) * shape.size / 2
|
const px = (Math.cos(angle) * shape.size) / 2
|
||||||
const py = Math.sin(angle) * shape.size / 2
|
const py = (Math.sin(angle) * shape.size) / 2
|
||||||
|
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
ctx.moveTo(px, py)
|
ctx.moveTo(px, py)
|
||||||
@@ -234,8 +253,19 @@ function drawShape(ctx: CanvasRenderingContext2D, shape: Shape) {
|
|||||||
export function generateShapesImages(count: number, size: number): GeneratedImage[] {
|
export function generateShapesImages(count: number, size: number): GeneratedImage[] {
|
||||||
const images: GeneratedImage[] = []
|
const images: GeneratedImage[] = []
|
||||||
const shapeTypes: ShapeType[] = [
|
const shapeTypes: ShapeType[] = [
|
||||||
'circle', 'square', 'triangle', 'diamond', 'pentagon', 'hexagon',
|
'circle',
|
||||||
'star', 'cross', 'ellipse', 'rect', 'octagon', 'arrow', 'ring'
|
'square',
|
||||||
|
'triangle',
|
||||||
|
'diamond',
|
||||||
|
'pentagon',
|
||||||
|
'hexagon',
|
||||||
|
'star',
|
||||||
|
'cross',
|
||||||
|
'ellipse',
|
||||||
|
'rect',
|
||||||
|
'octagon',
|
||||||
|
'arrow',
|
||||||
|
'ring',
|
||||||
]
|
]
|
||||||
const fillStyles: FillStyle[] = ['solid', 'outline']
|
const fillStyles: FillStyle[] = ['solid', 'outline']
|
||||||
|
|
||||||
@@ -313,7 +343,7 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
|
|||||||
rotation: Math.random() * Math.PI * 2,
|
rotation: Math.random() * Math.PI * 2,
|
||||||
color,
|
color,
|
||||||
fillStyle,
|
fillStyle,
|
||||||
strokeWidth: Math.floor(Math.random() * 4) + 1 // 1-4px stroke
|
strokeWidth: Math.floor(Math.random() * 4) + 1, // 1-4px stroke
|
||||||
}
|
}
|
||||||
|
|
||||||
shapes.push(shape)
|
shapes.push(shape)
|
||||||
@@ -342,10 +372,10 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
|
|||||||
rotation: s.rotation,
|
rotation: s.rotation,
|
||||||
color: s.color,
|
color: s.color,
|
||||||
fillStyle: s.fillStyle,
|
fillStyle: s.fillStyle,
|
||||||
strokeWidth: s.strokeWidth
|
strokeWidth: s.strokeWidth,
|
||||||
})),
|
})),
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -11,10 +11,18 @@ const greyLevels = [
|
|||||||
'#888888', // Very dark grey (lower amplitude)
|
'#888888', // Very dark grey (lower amplitude)
|
||||||
'#777777', // Darker grey
|
'#777777', // Darker grey
|
||||||
'#666666', // Even darker grey
|
'#666666', // Even darker grey
|
||||||
'#555555' // Very dark grey
|
'#555555', // Very dark grey
|
||||||
]
|
]
|
||||||
|
|
||||||
type StrokeType = 'linear' | 'logarithmic' | 'exponential' | 'cubic' | 'sine' | 'bounce' | 'elastic' | 'zigzag'
|
type StrokeType =
|
||||||
|
| 'linear'
|
||||||
|
| 'logarithmic'
|
||||||
|
| 'exponential'
|
||||||
|
| 'cubic'
|
||||||
|
| 'sine'
|
||||||
|
| 'bounce'
|
||||||
|
| 'elastic'
|
||||||
|
| 'zigzag'
|
||||||
|
|
||||||
interface LinePoint {
|
interface LinePoint {
|
||||||
x: number
|
x: number
|
||||||
@@ -28,7 +36,12 @@ interface SlideLine {
|
|||||||
strokeType: StrokeType
|
strokeType: StrokeType
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateStrokePath(startY: number, endY: number, width: number, strokeType: StrokeType): LinePoint[] {
|
function generateStrokePath(
|
||||||
|
startY: number,
|
||||||
|
endY: number,
|
||||||
|
width: number,
|
||||||
|
strokeType: StrokeType
|
||||||
|
): LinePoint[] {
|
||||||
const points: LinePoint[] = []
|
const points: LinePoint[] = []
|
||||||
const steps = width
|
const steps = width
|
||||||
|
|
||||||
@@ -42,10 +55,10 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
|
|||||||
y = startY + (endY - startY) * t
|
y = startY + (endY - startY) * t
|
||||||
break
|
break
|
||||||
case 'logarithmic':
|
case 'logarithmic':
|
||||||
y = startY + (endY - startY) * Math.log(1 + t * (Math.E - 1)) / Math.log(Math.E)
|
y = startY + ((endY - startY) * Math.log(1 + t * (Math.E - 1))) / Math.log(Math.E)
|
||||||
break
|
break
|
||||||
case 'exponential':
|
case 'exponential':
|
||||||
y = startY + (endY - startY) * (Math.exp(t) - 1) / (Math.exp(1) - 1)
|
y = startY + ((endY - startY) * (Math.exp(t) - 1)) / (Math.exp(1) - 1)
|
||||||
break
|
break
|
||||||
case 'cubic':
|
case 'cubic':
|
||||||
const easedT = t * t * (3 - 2 * t) // smooth step
|
const easedT = t * t * (3 - 2 * t) // smooth step
|
||||||
@@ -57,24 +70,28 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
|
|||||||
break
|
break
|
||||||
case 'bounce':
|
case 'bounce':
|
||||||
let bounceT = t
|
let bounceT = t
|
||||||
if (bounceT < 1/2.75) {
|
if (bounceT < 1 / 2.75) {
|
||||||
bounceT = 7.5625 * bounceT * bounceT
|
bounceT = 7.5625 * bounceT * bounceT
|
||||||
} else if (bounceT < 2/2.75) {
|
} else if (bounceT < 2 / 2.75) {
|
||||||
bounceT = 7.5625 * (bounceT - 1.5/2.75) * (bounceT - 1.5/2.75) + 0.75
|
bounceT = 7.5625 * (bounceT - 1.5 / 2.75) * (bounceT - 1.5 / 2.75) + 0.75
|
||||||
} else if (bounceT < 2.5/2.75) {
|
} else if (bounceT < 2.5 / 2.75) {
|
||||||
bounceT = 7.5625 * (bounceT - 2.25/2.75) * (bounceT - 2.25/2.75) + 0.9375
|
bounceT = 7.5625 * (bounceT - 2.25 / 2.75) * (bounceT - 2.25 / 2.75) + 0.9375
|
||||||
} else {
|
} else {
|
||||||
bounceT = 7.5625 * (bounceT - 2.625/2.75) * (bounceT - 2.625/2.75) + 0.984375
|
bounceT = 7.5625 * (bounceT - 2.625 / 2.75) * (bounceT - 2.625 / 2.75) + 0.984375
|
||||||
}
|
}
|
||||||
y = startY + (endY - startY) * bounceT
|
y = startY + (endY - startY) * bounceT
|
||||||
break
|
break
|
||||||
case 'elastic':
|
case 'elastic':
|
||||||
const elasticT = t === 0 ? 0 : t === 1 ? 1 :
|
const elasticT =
|
||||||
Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
|
t === 0
|
||||||
|
? 0
|
||||||
|
: t === 1
|
||||||
|
? 1
|
||||||
|
: Math.pow(2, -10 * t) * Math.sin(((t * 10 - 0.75) * (2 * Math.PI)) / 3) + 1
|
||||||
y = startY + (endY - startY) * elasticT
|
y = startY + (endY - startY) * elasticT
|
||||||
break
|
break
|
||||||
case 'zigzag':
|
case 'zigzag':
|
||||||
const zigzagT = Math.abs((t * 6) % 2 - 1) // creates zigzag pattern
|
const zigzagT = Math.abs(((t * 6) % 2) - 1) // creates zigzag pattern
|
||||||
y = startY + (endY - startY) * zigzagT
|
y = startY + (endY - startY) * zigzagT
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@@ -107,7 +124,16 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
|
|||||||
|
|
||||||
// Generate slides with more variety
|
// Generate slides with more variety
|
||||||
const slides: (SlideLine & { color: string })[] = []
|
const slides: (SlideLine & { color: string })[] = []
|
||||||
const strokeTypes: StrokeType[] = ['linear', 'logarithmic', 'exponential', 'cubic', 'sine', 'bounce', 'elastic', 'zigzag']
|
const strokeTypes: StrokeType[] = [
|
||||||
|
'linear',
|
||||||
|
'logarithmic',
|
||||||
|
'exponential',
|
||||||
|
'cubic',
|
||||||
|
'sine',
|
||||||
|
'bounce',
|
||||||
|
'elastic',
|
||||||
|
'zigzag',
|
||||||
|
]
|
||||||
|
|
||||||
for (let j = 0; j < lineCount; j++) {
|
for (let j = 0; j < lineCount; j++) {
|
||||||
// More varied positioning - some can go edge to edge
|
// More varied positioning - some can go edge to edge
|
||||||
@@ -153,11 +179,11 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
|
|||||||
startY: s.startY,
|
startY: s.startY,
|
||||||
endY: s.endY,
|
endY: s.endY,
|
||||||
thickness: s.thickness,
|
thickness: s.thickness,
|
||||||
strokeType: s.strokeType
|
strokeType: s.strokeType,
|
||||||
})),
|
})),
|
||||||
colors: slides.map(s => s.color),
|
colors: slides.map(s => s.color),
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ A standalone module for generating Tixy-like shader patterns in JavaScript/TypeS
|
|||||||
## What is Tixy?
|
## What is Tixy?
|
||||||
|
|
||||||
Tixy is a minimalist programming language designed by Martin Kleppe for creating visual patterns using 4 variables:
|
Tixy is a minimalist programming language designed by Martin Kleppe for creating visual patterns using 4 variables:
|
||||||
|
|
||||||
- `t` - time
|
- `t` - time
|
||||||
- `i` - index (pixel index in the grid)
|
- `i` - index (pixel index in the grid)
|
||||||
- `x` - x coordinate
|
- `x` - x coordinate
|
||||||
@@ -24,7 +25,7 @@ const result = renderTixyToCanvas(expression, {
|
|||||||
height: 64,
|
height: 64,
|
||||||
time: 0,
|
time: 0,
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
foregroundColor: '#ffffff'
|
foregroundColor: '#ffffff',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add to DOM
|
// Add to DOM
|
||||||
|
|||||||
@@ -2,9 +2,28 @@ import type { TixyFunction, TixyExpression } from './types'
|
|||||||
import { TixyFormulaGenerator } from './formula-generator'
|
import { TixyFormulaGenerator } from './formula-generator'
|
||||||
|
|
||||||
const MATH_METHODS = [
|
const MATH_METHODS = [
|
||||||
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor',
|
'abs',
|
||||||
'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan',
|
'acos',
|
||||||
'sinh', 'cosh', 'tanh', 'sign'
|
'asin',
|
||||||
|
'atan',
|
||||||
|
'atan2',
|
||||||
|
'ceil',
|
||||||
|
'cos',
|
||||||
|
'exp',
|
||||||
|
'floor',
|
||||||
|
'log',
|
||||||
|
'max',
|
||||||
|
'min',
|
||||||
|
'pow',
|
||||||
|
'random',
|
||||||
|
'round',
|
||||||
|
'sin',
|
||||||
|
'sqrt',
|
||||||
|
'tan',
|
||||||
|
'sinh',
|
||||||
|
'cosh',
|
||||||
|
'tanh',
|
||||||
|
'sign',
|
||||||
]
|
]
|
||||||
|
|
||||||
export function compileTixyExpression(code: string): TixyExpression {
|
export function compileTixyExpression(code: string): TixyExpression {
|
||||||
@@ -22,7 +41,7 @@ export function compileTixyExpression(code: string): TixyExpression {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
code: processedCode,
|
code: processedCode,
|
||||||
compiled
|
compiled,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to compile Tixy expression: ${error}`)
|
throw new Error(`Failed to compile Tixy expression: ${error}`)
|
||||||
@@ -48,11 +67,11 @@ export function evaluateTixyExpression(
|
|||||||
const formulaGenerator = new TixyFormulaGenerator()
|
const formulaGenerator = new TixyFormulaGenerator()
|
||||||
|
|
||||||
// Generate expressions dynamically
|
// Generate expressions dynamically
|
||||||
export function generateTixyExpression(): { expression: string, description: string } {
|
export function generateTixyExpression(): { expression: string; description: string } {
|
||||||
const result = formulaGenerator.generateFormula()
|
const result = formulaGenerator.generateFormula()
|
||||||
return {
|
return {
|
||||||
expression: result.expression,
|
expression: result.expression,
|
||||||
description: result.description
|
description: result.description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +80,19 @@ export function getExampleExpressions(): Record<string, string> {
|
|||||||
const expressions: Record<string, string> = {}
|
const expressions: Record<string, string> = {}
|
||||||
|
|
||||||
// Generate expressions for each theme
|
// Generate expressions for each theme
|
||||||
const themes = ['organic', 'geometric', 'interference', 'chaotic', 'minimalist', 'psychedelic', 'bitwise'] as const
|
const themes = [
|
||||||
|
'organic',
|
||||||
|
'geometric',
|
||||||
|
'interference',
|
||||||
|
'chaotic',
|
||||||
|
'minimalist',
|
||||||
|
'psychedelic',
|
||||||
|
'bitwise',
|
||||||
|
] as const
|
||||||
|
|
||||||
themes.forEach(theme => {
|
themes.forEach(theme => {
|
||||||
for (let i = 0; i < 5; i++) { // Generate 5 expressions per theme
|
for (let i = 0; i < 5; i++) {
|
||||||
|
// Generate 5 expressions per theme
|
||||||
const result = formulaGenerator.generateFormula(theme)
|
const result = formulaGenerator.generateFormula(theme)
|
||||||
expressions[result.expression] = result.description
|
expressions[result.expression] = result.description
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
export type Theme = 'organic' | 'geometric' | 'interference' | 'chaotic' | 'minimalist' | 'psychedelic' | 'bitwise'
|
export type Theme =
|
||||||
export type CoordinateSpace = 'cartesian' | 'polar' | 'log_polar' | 'hyperbolic' | 'wave_distort' | 'spiral'
|
| 'organic'
|
||||||
|
| 'geometric'
|
||||||
|
| 'interference'
|
||||||
|
| 'chaotic'
|
||||||
|
| 'minimalist'
|
||||||
|
| 'psychedelic'
|
||||||
|
| 'bitwise'
|
||||||
|
export type CoordinateSpace =
|
||||||
|
| 'cartesian'
|
||||||
|
| 'polar'
|
||||||
|
| 'log_polar'
|
||||||
|
| 'hyperbolic'
|
||||||
|
| 'wave_distort'
|
||||||
|
| 'spiral'
|
||||||
|
|
||||||
interface PatternConfig {
|
interface PatternConfig {
|
||||||
weight: number
|
weight: number
|
||||||
@@ -45,28 +58,28 @@ const COORDINATE_TRANSFORMS = {
|
|||||||
|
|
||||||
polar: (x: string, y: string) => ({
|
polar: (x: string, y: string) => ({
|
||||||
x: `sqrt((${x}-8)**2+(${y}-8)**2)`, // r
|
x: `sqrt((${x}-8)**2+(${y}-8)**2)`, // r
|
||||||
y: `atan2(${y}-8,${x}-8)` // θ
|
y: `atan2(${y}-8,${x}-8)`, // θ
|
||||||
}),
|
}),
|
||||||
|
|
||||||
log_polar: (x: string, y: string) => ({
|
log_polar: (x: string, y: string) => ({
|
||||||
x: `log(sqrt((${x}-8)**2+(${y}-8)**2)+1)`, // log(r)
|
x: `log(sqrt((${x}-8)**2+(${y}-8)**2)+1)`, // log(r)
|
||||||
y: `atan2(${y}-8,${x}-8)` // θ
|
y: `atan2(${y}-8,${x}-8)`, // θ
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hyperbolic: (x: string, y: string) => ({
|
hyperbolic: (x: string, y: string) => ({
|
||||||
x: `(${x}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`,
|
x: `(${x}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`,
|
||||||
y: `(${y}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`
|
y: `(${y}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
wave_distort: (x: string, y: string, freq = 0.5, amp = 2) => ({
|
wave_distort: (x: string, y: string, freq = 0.5, amp = 2) => ({
|
||||||
x: `${x}+sin(${y}*${freq})*${amp}`,
|
x: `${x}+sin(${y}*${freq})*${amp}`,
|
||||||
y: `${y}+cos(${x}*${freq})*${amp}`
|
y: `${y}+cos(${x}*${freq})*${amp}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
spiral: (x: string, y: string, tightness = 0.3) => ({
|
spiral: (x: string, y: string, tightness = 0.3) => ({
|
||||||
x: `sqrt((${x}-8)**2+(${y}-8)**2)*cos(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`,
|
x: `sqrt((${x}-8)**2+(${y}-8)**2)*cos(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`,
|
||||||
y: `sqrt((${x}-8)**2+(${y}-8)**2)*sin(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`
|
y: `sqrt((${x}-8)**2+(${y}-8)**2)*sin(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`,
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern building blocks with parameterization
|
// Pattern building blocks with parameterization
|
||||||
@@ -74,19 +87,27 @@ const PATTERN_GENERATORS = {
|
|||||||
// Wave patterns
|
// Wave patterns
|
||||||
sine_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
|
sine_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
|
||||||
switch (axis) {
|
switch (axis) {
|
||||||
case 'x': return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
case 'x':
|
||||||
case 'y': return `sin(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
case 'xy': return `sin((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
case 'y':
|
||||||
case 'radial': return `sin(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
return `sin(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
|
case 'xy':
|
||||||
|
return `sin((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
|
case 'radial':
|
||||||
|
return `sin(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cos_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
|
cos_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
|
||||||
switch (axis) {
|
switch (axis) {
|
||||||
case 'x': return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
case 'x':
|
||||||
case 'y': return `cos(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
case 'xy': return `cos((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
case 'y':
|
||||||
case 'radial': return `cos(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
return `cos(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
|
case 'xy':
|
||||||
|
return `cos((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
|
case 'radial':
|
||||||
|
return `cos(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -100,11 +121,11 @@ const PATTERN_GENERATORS = {
|
|||||||
|
|
||||||
// Grid patterns
|
// Grid patterns
|
||||||
grid: (sizeX: number, sizeY: number, phase: number) =>
|
grid: (sizeX: number, sizeY: number, phase: number) =>
|
||||||
`sin((x%${sizeX.toFixed(1)})*${(2*Math.PI/sizeX).toFixed(2)}+t*${phase.toFixed(2)})*cos((y%${sizeY.toFixed(1)})*${(2*Math.PI/sizeY).toFixed(2)}+t*${phase.toFixed(2)})`,
|
`sin((x%${sizeX.toFixed(1)})*${((2 * Math.PI) / sizeX).toFixed(2)}+t*${phase.toFixed(2)})*cos((y%${sizeY.toFixed(1)})*${((2 * Math.PI) / sizeY).toFixed(2)}+t*${phase.toFixed(2)})`,
|
||||||
|
|
||||||
// Noise patterns
|
// Noise patterns
|
||||||
noise: (scaleX: number, scaleY: number, evolution: number) =>
|
noise: (scaleX: number, scaleY: number, evolution: number) =>
|
||||||
`sin(x*${scaleX.toFixed(2)}+y*${scaleY.toFixed(2)}+t*${evolution.toFixed(2)})*cos(x*${(scaleX*1.618).toFixed(2)}+y*${(scaleY*0.618).toFixed(2)}+t*${(evolution*1.414).toFixed(2)})`,
|
`sin(x*${scaleX.toFixed(2)}+y*${scaleY.toFixed(2)}+t*${evolution.toFixed(2)})*cos(x*${(scaleX * 1.618).toFixed(2)}+y*${(scaleY * 0.618).toFixed(2)}+t*${(evolution * 1.414).toFixed(2)})`,
|
||||||
|
|
||||||
// Geometric shapes
|
// Geometric shapes
|
||||||
checkerboard: (size: number, phase: number) =>
|
checkerboard: (size: number, phase: number) =>
|
||||||
@@ -148,9 +169,12 @@ const PATTERN_GENERATORS = {
|
|||||||
|
|
||||||
modular_arithmetic: (modX: number, modY: number, operation: 'add' | 'mult' | 'xor') => {
|
modular_arithmetic: (modX: number, modY: number, operation: 'add' | 'mult' | 'xor') => {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'add': return `((floor(x)%${Math.floor(modX)})+(floor(y)%${Math.floor(modY)}))%16/8`
|
case 'add':
|
||||||
case 'mult': return `((floor(x)%${Math.floor(modX)})*(floor(y)%${Math.floor(modY)}))%16/8`
|
return `((floor(x)%${Math.floor(modX)})+(floor(y)%${Math.floor(modY)}))%16/8`
|
||||||
case 'xor': return `((floor(x)%${Math.floor(modX)})^(floor(y)%${Math.floor(modY)}))%16/8`
|
case 'mult':
|
||||||
|
return `((floor(x)%${Math.floor(modX)})*(floor(y)%${Math.floor(modY)}))%16/8`
|
||||||
|
case 'xor':
|
||||||
|
return `((floor(x)%${Math.floor(modX)})^(floor(y)%${Math.floor(modY)}))%16/8`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -168,16 +192,16 @@ const PATTERN_GENERATORS = {
|
|||||||
`sin(${a.toFixed(2)}*x+${b.toFixed(2)}*y+t)*cos(${c.toFixed(2)}*x*y+t*2)`,
|
`sin(${a.toFixed(2)}*x+${b.toFixed(2)}*y+t)*cos(${c.toFixed(2)}*x*y+t*2)`,
|
||||||
|
|
||||||
voronoi: (seed1: number, seed2: number, scale: number) =>
|
voronoi: (seed1: number, seed2: number, scale: number) =>
|
||||||
`min(sqrt((x-${seed1.toFixed(1)})**2+(y-${seed2.toFixed(1)})**2),sqrt((x-${(16-seed1).toFixed(1)})**2+(y-${(16-seed2).toFixed(1)})**2))*${scale.toFixed(2)}`,
|
`min(sqrt((x-${seed1.toFixed(1)})**2+(y-${seed2.toFixed(1)})**2),sqrt((x-${(16 - seed1).toFixed(1)})**2+(y-${(16 - seed2).toFixed(1)})**2))*${scale.toFixed(2)}`,
|
||||||
|
|
||||||
reaction_diffusion: (diffusion: number, reaction: number) =>
|
reaction_diffusion: (diffusion: number, reaction: number) =>
|
||||||
`tanh((sin(x*${diffusion.toFixed(2)}+t)*cos(y*${diffusion.toFixed(2)}+t)-${reaction.toFixed(2)})*4)`,
|
`tanh((sin(x*${diffusion.toFixed(2)}+t)*cos(y*${diffusion.toFixed(2)}+t)-${reaction.toFixed(2)})*4)`,
|
||||||
|
|
||||||
fractal_noise: (octaves: number, persistence: number) =>
|
fractal_noise: (octaves: number, persistence: number) =>
|
||||||
`sin(x*${octaves.toFixed(1)}+t)*${persistence.toFixed(2)}+sin(x*${(octaves*2).toFixed(1)}+t)*${(persistence*0.5).toFixed(2)}+sin(x*${(octaves*4).toFixed(1)}+t)*${(persistence*0.25).toFixed(2)}`,
|
`sin(x*${octaves.toFixed(1)}+t)*${persistence.toFixed(2)}+sin(x*${(octaves * 2).toFixed(1)}+t)*${(persistence * 0.5).toFixed(2)}+sin(x*${(octaves * 4).toFixed(1)}+t)*${(persistence * 0.25).toFixed(2)}`,
|
||||||
|
|
||||||
musical_harmony: (fundamental: number, overtone: number) =>
|
musical_harmony: (fundamental: number, overtone: number) =>
|
||||||
`sin(x*${fundamental.toFixed(2)}+t)+sin(x*${(fundamental*overtone).toFixed(2)}+t*${PHI.toFixed(3)})*0.5`,
|
`sin(x*${fundamental.toFixed(2)}+t)+sin(x*${(fundamental * overtone).toFixed(2)}+t*${PHI.toFixed(3)})*0.5`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme configurations with weighted pattern preferences
|
// Theme configurations with weighted pattern preferences
|
||||||
@@ -188,10 +212,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
|||||||
fractal_noise: { weight: 3, params: { octaves: [0.5, 2], persistence: [0.3, 0.8] } },
|
fractal_noise: { weight: 3, params: { octaves: [0.5, 2], persistence: [0.3, 0.8] } },
|
||||||
noise: { weight: 2, params: { scaleX: [0.1, 0.5], scaleY: [0.1, 0.5], evolution: [0.5, 2] } },
|
noise: { weight: 2, params: { scaleX: [0.1, 0.5], scaleY: [0.1, 0.5], evolution: [0.5, 2] } },
|
||||||
sine_wave: { weight: 2, params: { freq: [0.1, 0.4], phase: [0.2, 1], axis: ['x', 'y'] } },
|
sine_wave: { weight: 2, params: { freq: [0.1, 0.4], phase: [0.2, 1], axis: ['x', 'y'] } },
|
||||||
am_modulation: { weight: 1, params: { carrierFreq: [0.2, 0.6], modFreq: [0.1, 0.4], depth: [0.3, 0.8] } }
|
am_modulation: {
|
||||||
|
weight: 1,
|
||||||
|
params: { carrierFreq: [0.2, 0.6], modFreq: [0.1, 0.4], depth: [0.3, 0.8] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
combinators: ['*', '+', 'max', 'min'],
|
combinators: ['*', '+', 'max', 'min'],
|
||||||
complexity: [2, 3]
|
complexity: [2, 3],
|
||||||
},
|
},
|
||||||
|
|
||||||
geometric: {
|
geometric: {
|
||||||
@@ -199,21 +226,27 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
|||||||
voronoi: { weight: 3, params: { seed1: [2, 14], seed2: [2, 14], scale: [0.3, 1] } },
|
voronoi: { weight: 3, params: { seed1: [2, 14], seed2: [2, 14], scale: [0.3, 1] } },
|
||||||
grid: { weight: 3, params: { sizeX: [2, 8], sizeY: [2, 8], phase: [0.5, 2] } },
|
grid: { weight: 3, params: { sizeX: [2, 8], sizeY: [2, 8], phase: [0.5, 2] } },
|
||||||
checkerboard: { weight: 2, params: { size: [1, 4], phase: [0.1, 1] } },
|
checkerboard: { weight: 2, params: { size: [1, 4], phase: [0.1, 1] } },
|
||||||
diamond: { weight: 1, params: { centerX: [7, 9], centerY: [7, 9], size: [0.5, 2], speed: [0.5, 2] } }
|
diamond: {
|
||||||
|
weight: 1,
|
||||||
|
params: { centerX: [7, 9], centerY: [7, 9], size: [0.5, 2], speed: [0.5, 2] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
combinators: ['*', '+', 'floor', 'sign', 'min'],
|
combinators: ['*', '+', 'floor', 'sign', 'min'],
|
||||||
complexity: [1, 2]
|
complexity: [1, 2],
|
||||||
},
|
},
|
||||||
|
|
||||||
interference: {
|
interference: {
|
||||||
patterns: {
|
patterns: {
|
||||||
interference: { weight: 4, params: { freq1: [0.2, 0.8], freq2: [0.2, 0.8], phase1: [0.5, 2], phase2: [0.5, 2] } },
|
interference: {
|
||||||
|
weight: 4,
|
||||||
|
params: { freq1: [0.2, 0.8], freq2: [0.2, 0.8], phase1: [0.5, 2], phase2: [0.5, 2] },
|
||||||
|
},
|
||||||
sine_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
|
sine_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
|
||||||
cos_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
|
cos_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
|
||||||
grid: { weight: 2, params: { sizeX: [3, 8], sizeY: [3, 8], phase: [0.5, 2] } }
|
grid: { weight: 2, params: { sizeX: [3, 8], sizeY: [3, 8], phase: [0.5, 2] } },
|
||||||
},
|
},
|
||||||
combinators: ['+', '-', '*', 'max'],
|
combinators: ['+', '-', '*', 'max'],
|
||||||
complexity: [2, 3]
|
complexity: [2, 3],
|
||||||
},
|
},
|
||||||
|
|
||||||
chaotic: {
|
chaotic: {
|
||||||
@@ -222,10 +255,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
|||||||
mandelbrot_like: { weight: 2, params: { scale: [0.1, 0.5], iterations: [2, 5] } },
|
mandelbrot_like: { weight: 2, params: { scale: [0.1, 0.5], iterations: [2, 5] } },
|
||||||
fractal_noise: { weight: 2, params: { octaves: [1, 4], persistence: [0.2, 0.8] } },
|
fractal_noise: { weight: 2, params: { octaves: [1, 4], persistence: [0.2, 0.8] } },
|
||||||
xor_pattern: { weight: 2, params: { maskX: [7, 31], maskY: [7, 31], timeShift: [1, 4] } },
|
xor_pattern: { weight: 2, params: { maskX: [7, 31], maskY: [7, 31], timeShift: [1, 4] } },
|
||||||
interference: { weight: 1, params: { freq1: [0.5, 2], freq2: [0.5, 2], phase1: [2, 6], phase2: [2, 6] } }
|
interference: {
|
||||||
|
weight: 1,
|
||||||
|
params: { freq1: [0.5, 2], freq2: [0.5, 2], phase1: [2, 6], phase2: [2, 6] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
combinators: ['*', '+', 'tan', 'pow', '%', '^'],
|
combinators: ['*', '+', 'tan', 'pow', '%', '^'],
|
||||||
complexity: [2, 3]
|
complexity: [2, 3],
|
||||||
},
|
},
|
||||||
|
|
||||||
minimalist: {
|
minimalist: {
|
||||||
@@ -233,44 +269,62 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
|||||||
checkerboard: { weight: 4, params: { size: [3, 8], phase: [0.1, 0.5] } },
|
checkerboard: { weight: 4, params: { size: [3, 8], phase: [0.1, 0.5] } },
|
||||||
grid: { weight: 3, params: { sizeX: [4, 12], sizeY: [4, 12], phase: [0.2, 0.8] } },
|
grid: { weight: 3, params: { sizeX: [4, 12], sizeY: [4, 12], phase: [0.2, 0.8] } },
|
||||||
binary_maze: { weight: 2, params: { complexity: [3, 7], timeEvolution: [0.1, 0.5] } },
|
binary_maze: { weight: 2, params: { complexity: [3, 7], timeEvolution: [0.1, 0.5] } },
|
||||||
sine_wave: { weight: 1, params: { freq: [0.1, 0.3], phase: [0.2, 0.8], axis: ['x', 'y'] } }
|
sine_wave: { weight: 1, params: { freq: [0.1, 0.3], phase: [0.2, 0.8], axis: ['x', 'y'] } },
|
||||||
},
|
},
|
||||||
combinators: ['sign', 'floor', 'abs', '&'],
|
combinators: ['sign', 'floor', 'abs', '&'],
|
||||||
complexity: [1, 2]
|
complexity: [1, 2],
|
||||||
},
|
},
|
||||||
|
|
||||||
psychedelic: {
|
psychedelic: {
|
||||||
patterns: {
|
patterns: {
|
||||||
musical_harmony: { weight: 3, params: { fundamental: [0.5, 2], overtone: [1.5, 4] } },
|
musical_harmony: { weight: 3, params: { fundamental: [0.5, 2], overtone: [1.5, 4] } },
|
||||||
interference: { weight: 2, params: { freq1: [0.8, 2.5], freq2: [0.8, 2.5], phase1: [2, 8], phase2: [2, 8] } },
|
interference: {
|
||||||
|
weight: 2,
|
||||||
|
params: { freq1: [0.8, 2.5], freq2: [0.8, 2.5], phase1: [2, 8], phase2: [2, 8] },
|
||||||
|
},
|
||||||
bit_rotation: { weight: 2, params: { frequency: [0.8, 2], timeSpeed: [2, 6] } },
|
bit_rotation: { weight: 2, params: { frequency: [0.8, 2], timeSpeed: [2, 6] } },
|
||||||
strange_attractor: { weight: 2, params: { a: [1, 3], b: [0.5, 2], c: [0.3, 1.5] } },
|
strange_attractor: { weight: 2, params: { a: [1, 3], b: [0.5, 2], c: [0.3, 1.5] } },
|
||||||
spiral: { weight: 1, params: { centerX: [4, 12], centerY: [4, 12], tightness: [3, 12], rotation: [3, 10] } }
|
spiral: {
|
||||||
|
weight: 1,
|
||||||
|
params: { centerX: [4, 12], centerY: [4, 12], tightness: [3, 12], rotation: [3, 10] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
combinators: ['*', '+', 'tan', 'cos', 'sin', '^'],
|
combinators: ['*', '+', 'tan', 'cos', 'sin', '^'],
|
||||||
complexity: [3, 4]
|
complexity: [3, 4],
|
||||||
},
|
},
|
||||||
|
|
||||||
bitwise: {
|
bitwise: {
|
||||||
patterns: {
|
patterns: {
|
||||||
xor_pattern: { weight: 3, params: { maskX: [3, 31], maskY: [3, 31], timeShift: [0.1, 2] } },
|
xor_pattern: { weight: 3, params: { maskX: [3, 31], maskY: [3, 31], timeShift: [0.1, 2] } },
|
||||||
and_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } },
|
and_pattern: {
|
||||||
|
weight: 2,
|
||||||
|
params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] },
|
||||||
|
},
|
||||||
or_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } },
|
or_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } },
|
||||||
bit_shift: { weight: 2, params: { direction: ['left', 'right'], amount: [1, 4], modulator: [0.5, 2] } },
|
bit_shift: {
|
||||||
|
weight: 2,
|
||||||
|
params: { direction: ['left', 'right'], amount: [1, 4], modulator: [0.5, 2] },
|
||||||
|
},
|
||||||
binary_cellular: { weight: 2, params: { rule: [7, 31], evolution: [0.2, 1.5] } },
|
binary_cellular: { weight: 2, params: { rule: [7, 31], evolution: [0.2, 1.5] } },
|
||||||
bit_rotation: { weight: 1, params: { frequency: [0.3, 1.5], timeSpeed: [0.5, 3] } },
|
bit_rotation: { weight: 1, params: { frequency: [0.3, 1.5], timeSpeed: [0.5, 3] } },
|
||||||
modular_arithmetic: { weight: 2, params: { modX: [3, 16], modY: [3, 16], operation: ['add', 'mult', 'xor'] } },
|
modular_arithmetic: {
|
||||||
|
weight: 2,
|
||||||
|
params: { modX: [3, 16], modY: [3, 16], operation: ['add', 'mult', 'xor'] },
|
||||||
|
},
|
||||||
sierpinski: { weight: 1, params: { scale: [0.3, 1.2], timeShift: [0.1, 0.8] } },
|
sierpinski: { weight: 1, params: { scale: [0.3, 1.2], timeShift: [0.1, 0.8] } },
|
||||||
bit_noise: { weight: 1, params: { density: [0.5, 2], evolution: [0.3, 2] } },
|
bit_noise: { weight: 1, params: { density: [0.5, 2], evolution: [0.3, 2] } },
|
||||||
binary_maze: { weight: 1, params: { complexity: [3, 15], timeEvolution: [0.1, 1] } }
|
binary_maze: { weight: 1, params: { complexity: [3, 15], timeEvolution: [0.1, 1] } },
|
||||||
},
|
},
|
||||||
combinators: ['^', '&', '|', '+', '*', 'sign', 'floor'],
|
combinators: ['^', '&', '|', '+', '*', 'sign', 'floor'],
|
||||||
complexity: [1, 3]
|
complexity: [1, 3],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TixyFormulaGenerator {
|
export class TixyFormulaGenerator {
|
||||||
generateFormula(theme?: Theme, varietyConfig?: VarietyConfig): { expression: string, theme: Theme, description: string } {
|
generateFormula(
|
||||||
|
theme?: Theme,
|
||||||
|
varietyConfig?: VarietyConfig
|
||||||
|
): { expression: string; theme: Theme; description: string } {
|
||||||
// Enhanced variety system - randomly decide enhancement features
|
// Enhanced variety system - randomly decide enhancement features
|
||||||
const useCoordinateTransform = !varietyConfig || random() < 0.4
|
const useCoordinateTransform = !varietyConfig || random() < 0.4
|
||||||
const useThemeHybrid = !varietyConfig || random() < 0.3
|
const useThemeHybrid = !varietyConfig || random() < 0.3
|
||||||
@@ -304,15 +358,20 @@ export class TixyFormulaGenerator {
|
|||||||
// Coordinate transformation selection
|
// Coordinate transformation selection
|
||||||
let coordinateSpace: CoordinateSpace = 'cartesian'
|
let coordinateSpace: CoordinateSpace = 'cartesian'
|
||||||
if (useCoordinateTransform) {
|
if (useCoordinateTransform) {
|
||||||
coordinateSpace = choice(['cartesian', 'polar', 'log_polar', 'hyperbolic', 'wave_distort', 'spiral'])
|
coordinateSpace = choice([
|
||||||
|
'cartesian',
|
||||||
|
'polar',
|
||||||
|
'log_polar',
|
||||||
|
'hyperbolic',
|
||||||
|
'wave_distort',
|
||||||
|
'spiral',
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate base patterns with enhanced variety
|
// Generate base patterns with enhanced variety
|
||||||
for (let i = 0; i < numPatterns; i++) {
|
for (let i = 0; i < numPatterns; i++) {
|
||||||
const patternType = weightedChoice(
|
const patternType = weightedChoice(
|
||||||
Object.fromEntries(
|
Object.fromEntries(Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight]))
|
||||||
Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight])
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const patternConfig = config.patterns[patternType]
|
const patternConfig = config.patterns[patternType]
|
||||||
@@ -334,9 +393,10 @@ export class TixyFormulaGenerator {
|
|||||||
// Combine patterns with advanced techniques
|
// Combine patterns with advanced techniques
|
||||||
const expression = this.combinePatterns(patterns, config.combinators)
|
const expression = this.combinePatterns(patterns, config.combinators)
|
||||||
|
|
||||||
const themeDesc = hybridThemes.length > 0
|
const themeDesc =
|
||||||
? `${selectedTheme}+${hybridThemes.slice(1).join('+')}`
|
hybridThemes.length > 0
|
||||||
: selectedTheme
|
? `${selectedTheme}+${hybridThemes.slice(1).join('+')}`
|
||||||
|
: selectedTheme
|
||||||
|
|
||||||
const transformDesc = coordinateSpace !== 'cartesian' ? `[${coordinateSpace}]` : ''
|
const transformDesc = coordinateSpace !== 'cartesian' ? `[${coordinateSpace}]` : ''
|
||||||
const couplingDesc = useParameterCoupling ? '[coupled]' : ''
|
const couplingDesc = useParameterCoupling ? '[coupled]' : ''
|
||||||
@@ -344,11 +404,13 @@ export class TixyFormulaGenerator {
|
|||||||
return {
|
return {
|
||||||
expression,
|
expression,
|
||||||
theme: selectedTheme,
|
theme: selectedTheme,
|
||||||
description: `${themeDesc}${transformDesc}${couplingDesc}: ${descriptions.join(' + ')}`
|
description: `${themeDesc}${transformDesc}${couplingDesc}: ${descriptions.join(' + ')}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateParams(paramConfig: Record<string, [number, number] | string[]>): Record<string, any> {
|
private generateParams(
|
||||||
|
paramConfig: Record<string, [number, number] | string[]>
|
||||||
|
): Record<string, any> {
|
||||||
const params: Record<string, any> = {}
|
const params: Record<string, any> = {}
|
||||||
|
|
||||||
for (const [key, range] of Object.entries(paramConfig)) {
|
for (const [key, range] of Object.entries(paramConfig)) {
|
||||||
@@ -369,9 +431,19 @@ export class TixyFormulaGenerator {
|
|||||||
case 'cos_wave':
|
case 'cos_wave':
|
||||||
return PATTERN_GENERATORS.cos_wave(params.freq, params.phase, params.axis)
|
return PATTERN_GENERATORS.cos_wave(params.freq, params.phase, params.axis)
|
||||||
case 'ripple':
|
case 'ripple':
|
||||||
return PATTERN_GENERATORS.ripple(params.centerX, params.centerY, params.frequency, params.speed)
|
return PATTERN_GENERATORS.ripple(
|
||||||
|
params.centerX,
|
||||||
|
params.centerY,
|
||||||
|
params.frequency,
|
||||||
|
params.speed
|
||||||
|
)
|
||||||
case 'spiral':
|
case 'spiral':
|
||||||
return PATTERN_GENERATORS.spiral(params.centerX, params.centerY, params.tightness, params.rotation)
|
return PATTERN_GENERATORS.spiral(
|
||||||
|
params.centerX,
|
||||||
|
params.centerY,
|
||||||
|
params.tightness,
|
||||||
|
params.rotation
|
||||||
|
)
|
||||||
case 'grid':
|
case 'grid':
|
||||||
return PATTERN_GENERATORS.grid(params.sizeX, params.sizeY, params.phase)
|
return PATTERN_GENERATORS.grid(params.sizeX, params.sizeY, params.phase)
|
||||||
case 'noise':
|
case 'noise':
|
||||||
@@ -381,7 +453,12 @@ export class TixyFormulaGenerator {
|
|||||||
case 'diamond':
|
case 'diamond':
|
||||||
return PATTERN_GENERATORS.diamond(params.centerX, params.centerY, params.size, params.speed)
|
return PATTERN_GENERATORS.diamond(params.centerX, params.centerY, params.size, params.speed)
|
||||||
case 'interference':
|
case 'interference':
|
||||||
return PATTERN_GENERATORS.interference(params.freq1, params.freq2, params.phase1, params.phase2)
|
return PATTERN_GENERATORS.interference(
|
||||||
|
params.freq1,
|
||||||
|
params.freq2,
|
||||||
|
params.phase1,
|
||||||
|
params.phase2
|
||||||
|
)
|
||||||
case 'mandelbrot_like':
|
case 'mandelbrot_like':
|
||||||
return PATTERN_GENERATORS.mandelbrot_like(params.scale, params.iterations)
|
return PATTERN_GENERATORS.mandelbrot_like(params.scale, params.iterations)
|
||||||
case 'am_modulation':
|
case 'am_modulation':
|
||||||
@@ -431,7 +508,7 @@ export class TixyFormulaGenerator {
|
|||||||
const hybridConfig: ThemeConfig = {
|
const hybridConfig: ThemeConfig = {
|
||||||
patterns: { ...primaryConfig.patterns },
|
patterns: { ...primaryConfig.patterns },
|
||||||
combinators: [...primaryConfig.combinators],
|
combinators: [...primaryConfig.combinators],
|
||||||
complexity: primaryConfig.complexity
|
complexity: primaryConfig.complexity,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge patterns from hybrid themes with reduced weights
|
// Merge patterns from hybrid themes with reduced weights
|
||||||
@@ -441,7 +518,7 @@ export class TixyFormulaGenerator {
|
|||||||
if (!hybridConfig.patterns[patternName]) {
|
if (!hybridConfig.patterns[patternName]) {
|
||||||
hybridConfig.patterns[patternName] = {
|
hybridConfig.patterns[patternName] = {
|
||||||
weight: Math.round(config.weight * 0.5), // Reduced weight for hybrid patterns
|
weight: Math.round(config.weight * 0.5), // Reduced weight for hybrid patterns
|
||||||
params: config.params
|
params: config.params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -457,11 +534,14 @@ export class TixyFormulaGenerator {
|
|||||||
return hybridConfig
|
return hybridConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateCoupledParams(paramConfig: Record<string, [number, number] | string[]>, index: number): Record<string, any> {
|
private generateCoupledParams(
|
||||||
|
paramConfig: Record<string, [number, number] | string[]>,
|
||||||
|
index: number
|
||||||
|
): Record<string, any> {
|
||||||
const params: Record<string, any> = {}
|
const params: Record<string, any> = {}
|
||||||
|
|
||||||
// Use mathematical constants and relationships for coupling
|
// Use mathematical constants and relationships for coupling
|
||||||
const couplingFactors = [1, PHI, PI/4, E/2, Math.sqrt(2), Math.sqrt(3)]
|
const couplingFactors = [1, PHI, PI / 4, E / 2, Math.sqrt(2), Math.sqrt(3)]
|
||||||
const baseFactor = couplingFactors[index % couplingFactors.length]
|
const baseFactor = couplingFactors[index % couplingFactors.length]
|
||||||
|
|
||||||
for (const [key, range] of Object.entries(paramConfig)) {
|
for (const [key, range] of Object.entries(paramConfig)) {
|
||||||
@@ -508,9 +588,7 @@ export class TixyFormulaGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace x and y in the pattern with transformed coordinates
|
// Replace x and y in the pattern with transformed coordinates
|
||||||
return pattern
|
return pattern.replace(/\bx\b/g, `(${transformedX})`).replace(/\by\b/g, `(${transformedY})`)
|
||||||
.replace(/\bx\b/g, `(${transformedX})`)
|
|
||||||
.replace(/\by\b/g, `(${transformedY})`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private combinePatterns(patterns: string[], combinators: string[]): string {
|
private combinePatterns(patterns: string[], combinators: string[]): string {
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
export { compileTixyExpression, evaluateTixyExpression, EXAMPLE_EXPRESSIONS, generateTixyExpression } from './core/evaluator'
|
export {
|
||||||
|
compileTixyExpression,
|
||||||
|
evaluateTixyExpression,
|
||||||
|
EXAMPLE_EXPRESSIONS,
|
||||||
|
generateTixyExpression,
|
||||||
|
} from './core/evaluator'
|
||||||
export { renderTixyToCanvas, renderTixyToImageData } from './renderer/canvas'
|
export { renderTixyToCanvas, renderTixyToImageData } from './renderer/canvas'
|
||||||
export type { TixyParams, TixyFunction, TixyExpression, TixyRenderOptions, TixyResult } from './core/types'
|
export type {
|
||||||
|
TixyParams,
|
||||||
|
TixyFunction,
|
||||||
|
TixyExpression,
|
||||||
|
TixyRenderOptions,
|
||||||
|
TixyResult,
|
||||||
|
} from './core/types'
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function renderTixyToCanvas(
|
|||||||
backgroundColor = '#000000',
|
backgroundColor = '#000000',
|
||||||
foregroundColor = '#ffffff',
|
foregroundColor = '#ffffff',
|
||||||
threshold = 0.3,
|
threshold = 0.3,
|
||||||
pixelSize = 1
|
pixelSize = 1,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@@ -31,13 +31,14 @@ export function renderTixyToCanvas(
|
|||||||
const i = y * width + x
|
const i = y * width + x
|
||||||
const value = evaluateTixyExpression(expression, time, i, x, y)
|
const value = evaluateTixyExpression(expression, time, i, x, y)
|
||||||
|
|
||||||
const color = Math.abs(value) > threshold
|
const color =
|
||||||
? {
|
Math.abs(value) > threshold
|
||||||
r: Math.round(fgColor.r * (Math.sign(value) > 0 ? 1 : 0.8)),
|
? {
|
||||||
g: Math.round(fgColor.g * (Math.sign(value) > 0 ? 1 : 0.8)),
|
r: Math.round(fgColor.r * (Math.sign(value) > 0 ? 1 : 0.8)),
|
||||||
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8))
|
g: Math.round(fgColor.g * (Math.sign(value) > 0 ? 1 : 0.8)),
|
||||||
}
|
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8)),
|
||||||
: bgColor
|
}
|
||||||
|
: bgColor
|
||||||
|
|
||||||
for (let py = 0; py < pixelSize; py++) {
|
for (let py = 0; py < pixelSize; py++) {
|
||||||
for (let px = 0; px < pixelSize; px++) {
|
for (let px = 0; px < pixelSize; px++) {
|
||||||
@@ -58,7 +59,7 @@ export function renderTixyToCanvas(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
imageData,
|
imageData,
|
||||||
canvas
|
canvas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +72,11 @@ export function renderTixyToImageData(
|
|||||||
|
|
||||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return result ? {
|
return result
|
||||||
r: parseInt(result[1], 16),
|
? {
|
||||||
g: parseInt(result[2], 16),
|
r: parseInt(result[1], 16),
|
||||||
b: parseInt(result[3], 16)
|
g: parseInt(result[2], 16),
|
||||||
} : { r: 0, g: 0, b: 0 }
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: { r: 0, g: 0, b: 0 }
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ const colorPalettes = [
|
|||||||
{ bg: '#1a1a1a', fg: '#ffffff' },
|
{ bg: '#1a1a1a', fg: '#ffffff' },
|
||||||
{ bg: '#333333', fg: '#ffffff' },
|
{ bg: '#333333', fg: '#ffffff' },
|
||||||
{ bg: '#000000', fg: '#666666' },
|
{ bg: '#000000', fg: '#666666' },
|
||||||
{ bg: '#222222', fg: '#dddddd' }
|
{ bg: '#222222', fg: '#dddddd' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function generateTixyImages(count: number, size: number): GeneratedImage[] {
|
export function generateTixyImages(count: number, size: number): GeneratedImage[] {
|
||||||
@@ -38,7 +38,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
|
|||||||
time,
|
time,
|
||||||
backgroundColor: palette.bg,
|
backgroundColor: palette.bg,
|
||||||
foregroundColor: palette.fg,
|
foregroundColor: palette.fg,
|
||||||
pixelSize: 4
|
pixelSize: 4,
|
||||||
})
|
})
|
||||||
|
|
||||||
const image = {
|
const image = {
|
||||||
@@ -46,7 +46,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
|
|||||||
canvas: result.canvas,
|
canvas: result.canvas,
|
||||||
imageData: result.imageData,
|
imageData: result.imageData,
|
||||||
generator: 'tixy' as const,
|
generator: 'tixy' as const,
|
||||||
params: { expression, time, colors: palette }
|
params: { expression, time, colors: palette },
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const customWaveform = generateWaveform({
|
|||||||
splits: 32,
|
splits: 32,
|
||||||
interpolation: 'cubic',
|
interpolation: 'cubic',
|
||||||
randomness: 'smooth',
|
randomness: 'smooth',
|
||||||
lineWidth: 3
|
lineWidth: 3,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -33,27 +33,30 @@ const customWaveform = generateWaveform({
|
|||||||
### Main Functions
|
### Main Functions
|
||||||
|
|
||||||
#### `generateWaveform(config?)`
|
#### `generateWaveform(config?)`
|
||||||
|
|
||||||
Generate a waveform with optional configuration override.
|
Generate a waveform with optional configuration override.
|
||||||
|
|
||||||
#### `generateRandomWaveform()`
|
#### `generateRandomWaveform()`
|
||||||
|
|
||||||
Generate a waveform with randomized parameters for maximum variety.
|
Generate a waveform with randomized parameters for maximum variety.
|
||||||
|
|
||||||
#### `generateWaveformBatch(count)`
|
#### `generateWaveformBatch(count)`
|
||||||
|
|
||||||
Generate multiple random waveforms efficiently.
|
Generate multiple random waveforms efficiently.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface WaveformConfig {
|
interface WaveformConfig {
|
||||||
width: number // Canvas width (default: 256)
|
width: number // Canvas width (default: 256)
|
||||||
height: number // Canvas height (default: 256)
|
height: number // Canvas height (default: 256)
|
||||||
splits: number // Number of control points (8-64)
|
splits: number // Number of control points (8-64)
|
||||||
interpolation: InterpolationType // Curve type
|
interpolation: InterpolationType // Curve type
|
||||||
randomness: RandomnessStrategy // Distribution strategy
|
randomness: RandomnessStrategy // Distribution strategy
|
||||||
lineWidth: number // Stroke width (1-4)
|
lineWidth: number // Stroke width (1-4)
|
||||||
backgroundColor: string // Background color
|
backgroundColor: string // Background color
|
||||||
lineColor: string // Line color
|
lineColor: string // Line color
|
||||||
smoothness: number // Curve smoothness (0-1)
|
smoothness: number // Curve smoothness (0-1)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,30 +98,33 @@ waveform-generator/
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Basic Waveform
|
### Basic Waveform
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const simple = generateWaveform({
|
const simple = generateWaveform({
|
||||||
splits: 16,
|
splits: 16,
|
||||||
interpolation: 'linear'
|
interpolation: 'linear',
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Smooth Organic Curves
|
### Smooth Organic Curves
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const organic = generateWaveform({
|
const organic = generateWaveform({
|
||||||
splits: 24,
|
splits: 24,
|
||||||
interpolation: 'cubic',
|
interpolation: 'cubic',
|
||||||
randomness: 'smooth',
|
randomness: 'smooth',
|
||||||
smoothness: 0.7
|
smoothness: 0.7,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sharp Electronic Waveform
|
### Sharp Electronic Waveform
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const electronic = generateWaveform({
|
const electronic = generateWaveform({
|
||||||
splits: 32,
|
splits: 32,
|
||||||
interpolation: 'exponential',
|
interpolation: 'exponential',
|
||||||
randomness: 'uniform',
|
randomness: 'uniform',
|
||||||
lineWidth: 1
|
lineWidth: 1,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ControlPoint, WaveformConfig, RandomnessStrategy } from './types'
|
import type { ControlPoint, WaveformConfig, RandomnessStrategy, InterpolationType } from './types'
|
||||||
import { smoothControlPoints, applyTension } from './interpolation'
|
import { smoothControlPoints, applyTension } from './interpolation'
|
||||||
|
|
||||||
// Generate random control points based on configuration
|
// Generate random control points based on configuration
|
||||||
@@ -81,7 +81,8 @@ function generateRandomY(
|
|||||||
|
|
||||||
// Generate gaussian-distributed random numbers using Box-Muller transform
|
// Generate gaussian-distributed random numbers using Box-Muller transform
|
||||||
function gaussianRandom(mean: number = 0, stdDev: number = 1): number {
|
function gaussianRandom(mean: number = 0, stdDev: number = 1): number {
|
||||||
let u = 0, v = 0
|
let u = 0,
|
||||||
|
v = 0
|
||||||
while (u === 0) u = Math.random() // Converting [0,1) to (0,1)
|
while (u === 0) u = Math.random() // Converting [0,1) to (0,1)
|
||||||
while (v === 0) v = Math.random()
|
while (v === 0) v = Math.random()
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ export function generateWaveformVariation(baseConfig: WaveformConfig): WaveformC
|
|||||||
// Pure interpolation types (60% of patterns)
|
// Pure interpolation types (60% of patterns)
|
||||||
{ type: 'pure', weight: 0.6 },
|
{ type: 'pure', weight: 0.6 },
|
||||||
// Blended/mixed interpolation (40% of patterns)
|
// Blended/mixed interpolation (40% of patterns)
|
||||||
{ type: 'blended', weight: 0.4 }
|
{ type: 'blended', weight: 0.4 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const strategyType = weightedRandomChoice(strategies)
|
const strategyType = weightedRandomChoice(strategies)
|
||||||
@@ -124,7 +125,7 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
|
|||||||
{ interpolation: 'linear' as const, weight: 0.3 },
|
{ interpolation: 'linear' as const, weight: 0.3 },
|
||||||
{ interpolation: 'exponential' as const, weight: 0.25 },
|
{ interpolation: 'exponential' as const, weight: 0.25 },
|
||||||
{ interpolation: 'logarithmic' as const, weight: 0.25 },
|
{ interpolation: 'logarithmic' as const, weight: 0.25 },
|
||||||
{ interpolation: 'cubic' as const, weight: 0.2 }
|
{ interpolation: 'cubic' as const, weight: 0.2 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const interpolationType = weightedRandomChoice(pureTypes)
|
const interpolationType = weightedRandomChoice(pureTypes)
|
||||||
@@ -132,10 +133,10 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
|
|||||||
return {
|
return {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
splits: randomChoice([8, 12, 16, 20, 24, 32]),
|
splits: randomChoice([8, 12, 16, 20, 24, 32]),
|
||||||
interpolation: interpolationType,
|
interpolation: interpolationType as InterpolationType,
|
||||||
randomness: randomChoice(['uniform', 'gaussian', 'smooth'] as const),
|
randomness: randomChoice(['uniform', 'gaussian', 'smooth'] as const),
|
||||||
smoothness: randomChoice([0.2, 0.4, 0.6, 0.8]),
|
smoothness: randomChoice([0.2, 0.4, 0.6, 0.8]),
|
||||||
lineWidth: 2
|
lineWidth: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,29 +151,29 @@ function generateBlendedInterpolationVariation(baseConfig: WaveformConfig): Wave
|
|||||||
interpolation: 'cubic' as const,
|
interpolation: 'cubic' as const,
|
||||||
randomness: 'gaussian' as const,
|
randomness: 'gaussian' as const,
|
||||||
smoothness: 0.8,
|
smoothness: 0.8,
|
||||||
splits: 16
|
splits: 16,
|
||||||
},
|
},
|
||||||
// Low smoothness + uniform = sharp chaotic lines
|
// Low smoothness + uniform = sharp chaotic lines
|
||||||
{
|
{
|
||||||
interpolation: 'linear' as const,
|
interpolation: 'linear' as const,
|
||||||
randomness: 'uniform' as const,
|
randomness: 'uniform' as const,
|
||||||
smoothness: 0.1,
|
smoothness: 0.1,
|
||||||
splits: 32
|
splits: 32,
|
||||||
},
|
},
|
||||||
// Exponential + smooth = flowing acceleration curves
|
// Exponential + smooth = flowing acceleration curves
|
||||||
{
|
{
|
||||||
interpolation: 'exponential' as const,
|
interpolation: 'exponential' as const,
|
||||||
randomness: 'smooth' as const,
|
randomness: 'smooth' as const,
|
||||||
smoothness: 0.6,
|
smoothness: 0.6,
|
||||||
splits: 20
|
splits: 20,
|
||||||
},
|
},
|
||||||
// Logarithmic + gaussian = natural decay curves
|
// Logarithmic + gaussian = natural decay curves
|
||||||
{
|
{
|
||||||
interpolation: 'logarithmic' as const,
|
interpolation: 'logarithmic' as const,
|
||||||
randomness: 'gaussian' as const,
|
randomness: 'gaussian' as const,
|
||||||
smoothness: 0.5,
|
smoothness: 0.5,
|
||||||
splits: 24
|
splits: 24,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const config = randomChoice(blendedConfigs)
|
const config = randomChoice(blendedConfigs)
|
||||||
@@ -180,7 +181,7 @@ function generateBlendedInterpolationVariation(baseConfig: WaveformConfig): Wave
|
|||||||
return {
|
return {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
...config,
|
...config,
|
||||||
lineWidth: 2
|
lineWidth: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ function randomChoice<T>(array: readonly T[]): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Weighted random selection utility
|
// Weighted random selection utility
|
||||||
function weightedRandomChoice<T extends { weight: number }>(choices: T[]): T[keyof T] {
|
function weightedRandomChoice<T extends { weight: number; type?: any; interpolation?: any }>(choices: T[]): any {
|
||||||
const totalWeight = choices.reduce((sum, choice) => sum + choice.weight, 0)
|
const totalWeight = choices.reduce((sum, choice) => sum + choice.weight, 0)
|
||||||
let random = Math.random() * totalWeight
|
let random = Math.random() * totalWeight
|
||||||
|
|
||||||
@@ -209,5 +210,7 @@ export function generateMultipleWaveforms(
|
|||||||
count: number,
|
count: number,
|
||||||
baseConfig: WaveformConfig
|
baseConfig: WaveformConfig
|
||||||
): WaveformConfig[] {
|
): WaveformConfig[] {
|
||||||
return Array(count).fill(null).map(() => generateWaveformVariation(baseConfig))
|
return Array(count)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => generateWaveformVariation(baseConfig))
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,6 @@ export function interpolate(
|
|||||||
type: InterpolationType,
|
type: InterpolationType,
|
||||||
smoothness: number = 0.5
|
smoothness: number = 0.5
|
||||||
): number {
|
): number {
|
||||||
const dx = p2.x - p1.x
|
|
||||||
const dy = p2.y - p1.y
|
const dy = p2.y - p1.y
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -93,10 +92,7 @@ export function smoothControlPoints(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate curve tension for natural-looking waveforms
|
// Calculate curve tension for natural-looking waveforms
|
||||||
export function applyTension(
|
export function applyTension(points: ControlPoint[], tension: number = 0.5): ControlPoint[] {
|
||||||
points: ControlPoint[],
|
|
||||||
tension: number = 0.5
|
|
||||||
): ControlPoint[] {
|
|
||||||
if (points.length <= 2) return points
|
if (points.length <= 2) return points
|
||||||
|
|
||||||
const tensioned = [...points]
|
const tensioned = [...points]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const DEFAULT_WAVEFORM_CONFIG: WaveformConfig = {
|
|||||||
lineWidth: 0.5, // Very thin line for precise frequency definition
|
lineWidth: 0.5, // Very thin line for precise frequency definition
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
lineColor: '#ffffff',
|
lineColor: '#ffffff',
|
||||||
smoothness: 0.5
|
smoothness: 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common split counts for variety
|
// Common split counts for variety
|
||||||
@@ -48,5 +48,5 @@ export const INTERPOLATION_WEIGHTS = {
|
|||||||
linear: 0.3,
|
linear: 0.3,
|
||||||
exponential: 0.25,
|
exponential: 0.25,
|
||||||
logarithmic: 0.25,
|
logarithmic: 0.25,
|
||||||
cubic: 0.2
|
cubic: 0.2,
|
||||||
} as const
|
} as const
|
||||||
@@ -6,35 +6,31 @@ export type {
|
|||||||
RandomnessStrategy,
|
RandomnessStrategy,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
WaveformConfig,
|
WaveformConfig,
|
||||||
WaveformResult
|
WaveformResult,
|
||||||
} from './core/types'
|
} from './core/types'
|
||||||
|
|
||||||
export {
|
export { DEFAULT_WAVEFORM_CONFIG, SPLIT_COUNTS, INTERPOLATION_WEIGHTS } from './core/types'
|
||||||
DEFAULT_WAVEFORM_CONFIG,
|
|
||||||
SPLIT_COUNTS,
|
|
||||||
INTERPOLATION_WEIGHTS
|
|
||||||
} from './core/types'
|
|
||||||
|
|
||||||
// Interpolation functions
|
// Interpolation functions
|
||||||
export {
|
export {
|
||||||
interpolate,
|
interpolate,
|
||||||
generateCurvePoints,
|
generateCurvePoints,
|
||||||
smoothControlPoints,
|
smoothControlPoints,
|
||||||
applyTension
|
applyTension,
|
||||||
} from './core/interpolation'
|
} from './core/interpolation'
|
||||||
|
|
||||||
// Generation logic
|
// Generation logic
|
||||||
export {
|
export {
|
||||||
generateControlPoints,
|
generateControlPoints,
|
||||||
generateWaveformVariation,
|
generateWaveformVariation,
|
||||||
generateMultipleWaveforms
|
generateMultipleWaveforms,
|
||||||
} from './core/generator'
|
} from './core/generator'
|
||||||
|
|
||||||
// Canvas rendering
|
// Canvas rendering
|
||||||
export {
|
export {
|
||||||
renderWaveformToCanvas,
|
renderWaveformToCanvas,
|
||||||
renderWaveformWithBezier,
|
renderWaveformWithBezier,
|
||||||
renderMultipleWaveforms
|
renderMultipleWaveforms,
|
||||||
} from './renderer/canvas'
|
} from './renderer/canvas'
|
||||||
|
|
||||||
// Main convenience function for generating complete waveforms
|
// Main convenience function for generating complete waveforms
|
||||||
@@ -56,5 +52,7 @@ export function generateRandomWaveform(): WaveformResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateWaveformBatch(count: number): WaveformResult[] {
|
export function generateWaveformBatch(count: number): WaveformResult[] {
|
||||||
return Array(count).fill(null).map(() => generateRandomWaveform())
|
return Array(count)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => generateRandomWaveform())
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export function renderWaveformToCanvas(
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
controlPoints,
|
controlPoints,
|
||||||
config
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export function renderWaveformWithBezier(
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
controlPoints,
|
controlPoints,
|
||||||
config
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +178,7 @@ export function renderMultipleWaveforms(
|
|||||||
ctx.lineCap = 'round'
|
ctx.lineCap = 'round'
|
||||||
ctx.lineJoin = 'round'
|
ctx.lineJoin = 'round'
|
||||||
|
|
||||||
const curvePoints = generateCurvePoints(
|
const curvePoints = generateCurvePoints(points, config.interpolation, config.smoothness, 6)
|
||||||
points,
|
|
||||||
config.interpolation,
|
|
||||||
config.smoothness,
|
|
||||||
6
|
|
||||||
)
|
|
||||||
|
|
||||||
drawSmoothCurve(ctx, curvePoints)
|
drawSmoothCurve(ctx, curvePoints)
|
||||||
})
|
})
|
||||||
@@ -194,6 +189,6 @@ export function renderMultipleWaveforms(
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
controlPoints: waveformData[0]?.points || [],
|
controlPoints: waveformData[0]?.points || [],
|
||||||
config: firstConfig || waveformData[0]?.config
|
config: firstConfig || waveformData[0]?.config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { generateRandomWaveform, generateWaveformVariation, DEFAULT_WAVEFORM_CONFIG } from './waveform-generator'
|
import {
|
||||||
|
generateRandomWaveform,
|
||||||
|
} from './waveform-generator'
|
||||||
import type { GeneratedImage } from '../stores'
|
import type { GeneratedImage } from '../stores'
|
||||||
|
|
||||||
// Generate multiple random waveform images
|
// Generate multiple random waveform images
|
||||||
@@ -40,8 +42,8 @@ export function generateWaveformImages(count: number, size: number = 256): Gener
|
|||||||
randomness: waveform.config.randomness,
|
randomness: waveform.config.randomness,
|
||||||
smoothness: waveform.config.smoothness,
|
smoothness: waveform.config.smoothness,
|
||||||
lineWidth: waveform.config.lineWidth,
|
lineWidth: waveform.config.lineWidth,
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
@@ -91,8 +93,8 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
|
|||||||
smoothness: waveform.config.smoothness,
|
smoothness: waveform.config.smoothness,
|
||||||
lineWidth: waveform.config.lineWidth,
|
lineWidth: waveform.config.lineWidth,
|
||||||
style: 'varied',
|
style: 'varied',
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(image)
|
images.push(image)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export class WebcamProcessor {
|
|||||||
video: {
|
video: {
|
||||||
width: { ideal: 1280 },
|
width: { ideal: 1280 },
|
||||||
height: { ideal: 720 },
|
height: { ideal: 720 },
|
||||||
facingMode: 'user'
|
facingMode: 'user',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.video = document.createElement('video')
|
this.video = document.createElement('video')
|
||||||
@@ -66,7 +66,7 @@ export class WebcamProcessor {
|
|||||||
canvas,
|
canvas,
|
||||||
imageData,
|
imageData,
|
||||||
capturedAt: new Date(),
|
capturedAt: new Date(),
|
||||||
config
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export class WebcamProcessor {
|
|||||||
videoWidth: number,
|
videoWidth: number,
|
||||||
videoHeight: number,
|
videoHeight: number,
|
||||||
targetSize: number
|
targetSize: number
|
||||||
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
|
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
|
||||||
const aspectRatio = videoWidth / videoHeight
|
const aspectRatio = videoWidth / videoHeight
|
||||||
|
|
||||||
let drawWidth: number
|
let drawWidth: number
|
||||||
@@ -114,7 +114,11 @@ export class WebcamProcessor {
|
|||||||
return { drawWidth, drawHeight, offsetX, offsetY }
|
return { drawWidth, drawHeight, offsetX, offsetY }
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhanceContrast: boolean) {
|
private convertToGrayscale(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
size: number,
|
||||||
|
enhanceContrast: boolean
|
||||||
|
) {
|
||||||
const imageData = ctx.getImageData(0, 0, size, size)
|
const imageData = ctx.getImageData(0, 0, size, size)
|
||||||
const data = imageData.data
|
const data = imageData.data
|
||||||
|
|
||||||
@@ -133,7 +137,7 @@ export class WebcamProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set R, G, B to the same grayscale value
|
// Set R, G, B to the same grayscale value
|
||||||
data[i] = final // Red
|
data[i] = final // Red
|
||||||
data[i + 1] = final // Green
|
data[i + 1] = final // Green
|
||||||
data[i + 2] = final // Blue
|
data[i + 2] = final // Blue
|
||||||
// Alpha (data[i + 3]) remains unchanged
|
// Alpha (data[i + 3]) remains unchanged
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function generateFromWebcamImage(
|
|||||||
const result = await processor.capturePhoto({
|
const result = await processor.capturePhoto({
|
||||||
targetSize: size,
|
targetSize: size,
|
||||||
contrastEnhancement: true,
|
contrastEnhancement: true,
|
||||||
grayscaleConversion: true
|
grayscaleConversion: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const image: GeneratedImage = {
|
const image: GeneratedImage = {
|
||||||
@@ -21,8 +21,8 @@ export async function generateFromWebcamImage(
|
|||||||
capturedAt: result.capturedAt.toISOString(),
|
capturedAt: result.capturedAt.toISOString(),
|
||||||
size,
|
size,
|
||||||
contrastEnhanced: true,
|
contrastEnhanced: true,
|
||||||
grayscaleConverted: true
|
grayscaleConverted: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return image
|
return image
|
||||||
@@ -45,8 +45,8 @@ export async function generateFromWebcamImage(
|
|||||||
generator: 'webcam',
|
generator: 'webcam',
|
||||||
params: {
|
params: {
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
size
|
size,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallbackImage
|
return fallbackImage
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import App from './App.tsx'
|
|||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const audioData = synthesizeFromImage(imageData, {
|
|||||||
duration: 10,
|
duration: 10,
|
||||||
minFreq: 100,
|
minFreq: 100,
|
||||||
maxFreq: 10000,
|
maxFreq: 10000,
|
||||||
maxPartials: 200
|
maxPartials: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Export as WAV
|
// Export as WAV
|
||||||
@@ -35,6 +35,7 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
|
|||||||
### Main Functions
|
### Main Functions
|
||||||
|
|
||||||
#### `synthesizeFromImage(imageData, params?)`
|
#### `synthesizeFromImage(imageData, params?)`
|
||||||
|
|
||||||
- **imageData**: `ImageData` - Canvas image data
|
- **imageData**: `ImageData` - Canvas image data
|
||||||
- **params**: `Partial<SynthesisParams>` - Optional parameters
|
- **params**: `Partial<SynthesisParams>` - Optional parameters
|
||||||
- **Returns**: `Float32Array` - Audio samples
|
- **Returns**: `Float32Array` - Audio samples
|
||||||
@@ -42,16 +43,17 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
|
|||||||
### Types
|
### Types
|
||||||
|
|
||||||
#### `SynthesisParams`
|
#### `SynthesisParams`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface SynthesisParams {
|
interface SynthesisParams {
|
||||||
duration: number // Audio duration in seconds
|
duration: number // Audio duration in seconds
|
||||||
minFreq: number // Minimum frequency in Hz
|
minFreq: number // Minimum frequency in Hz
|
||||||
maxFreq: number // Maximum frequency in Hz
|
maxFreq: number // Maximum frequency in Hz
|
||||||
sampleRate: number // Sample rate in Hz
|
sampleRate: number // Sample rate in Hz
|
||||||
frequencyResolution: number // Frequency bin downsampling
|
frequencyResolution: number // Frequency bin downsampling
|
||||||
timeResolution: number // Time slice downsampling
|
timeResolution: number // Time slice downsampling
|
||||||
amplitudeThreshold: number // Minimum amplitude threshold
|
amplitudeThreshold: number // Minimum amplitude threshold
|
||||||
maxPartials: number // Maximum simultaneous partials
|
maxPartials: number // Maximum simultaneous partials
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ spectral-synthesis/
|
|||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Synthesis
|
### Basic Synthesis
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
@@ -89,12 +92,13 @@ const audio = synthesizeFromImage(imageData)
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Advanced Usage
|
### Advanced Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ImageToAudioSynthesizer } from './spectral-synthesis'
|
import { ImageToAudioSynthesizer } from './spectral-synthesis'
|
||||||
|
|
||||||
const synthesizer = new ImageToAudioSynthesizer({
|
const synthesizer = new ImageToAudioSynthesizer({
|
||||||
duration: 5,
|
duration: 5,
|
||||||
maxPartials: 150
|
maxPartials: 150,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = synthesizer.synthesize(imageData)
|
const result = synthesizer.synthesize(imageData)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function createWAVBuffer(audioData: Float32Array, sampleRate: number): Ar
|
|||||||
let offset = 44
|
let offset = 44
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const sample = Math.max(-1, Math.min(1, audioData[i]))
|
const sample = Math.max(-1, Math.min(1, audioData[i]))
|
||||||
view.setInt16(offset, sample * 0x7FFF, true)
|
view.setInt16(offset, sample * 0x7fff, true)
|
||||||
offset += 2
|
offset += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,9 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
|
|||||||
gainNode.connect(audioContext.destination)
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
if (audioContext.sampleRate !== sampleRate) {
|
if (audioContext.sampleRate !== sampleRate) {
|
||||||
console.warn(`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`)
|
console.warn(
|
||||||
|
`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +104,8 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
|
|||||||
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
// Resume from pause is not supported with AudioBufferSource
|
// Resume from pause is not supported with AudioBufferSource
|
||||||
// We need to restart from the beginning
|
// We need to restart from the beginning (pausedAt was ${pausedAt}s)
|
||||||
|
void pausedAt // Track for future resume feature
|
||||||
isPaused = false
|
isPaused = false
|
||||||
pausedAt = 0
|
pausedAt = 0
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,7 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
|
|||||||
|
|
||||||
onStateChange(callback: (isPlaying: boolean) => void) {
|
onStateChange(callback: (isPlaying: boolean) => void) {
|
||||||
stateCallback = callback
|
stateCallback = callback
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +177,7 @@ export async function playAudio(audioData: Float32Array, sampleRate: number): Pr
|
|||||||
const player = createAudioPlayer(audioData, sampleRate)
|
const player = createAudioPlayer(audioData, sampleRate)
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
player.onStateChange((isPlaying) => {
|
player.onStateChange(isPlaying => {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
generateSpectralDensity,
|
generateSpectralDensity,
|
||||||
mapFrequency,
|
mapFrequency,
|
||||||
mapFrequencyLinear,
|
mapFrequencyLinear,
|
||||||
normalizeAudioGlobal
|
normalizeAudioGlobal,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +78,7 @@ export class ImageToAudioSynthesizer {
|
|||||||
disableNormalization: false,
|
disableNormalization: false,
|
||||||
disableContrast: false,
|
disableContrast: false,
|
||||||
exactBinMapping: false,
|
exactBinMapping: false,
|
||||||
...params
|
...params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ export class ImageToAudioSynthesizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom synthesis mode - sophisticated audio processing
|
* Custom synthesis mode - sophisticated audio processing
|
||||||
*/
|
*/
|
||||||
@@ -112,7 +111,7 @@ export class ImageToAudioSynthesizer {
|
|||||||
spectralDensity,
|
spectralDensity,
|
||||||
usePerceptualWeighting,
|
usePerceptualWeighting,
|
||||||
frequencyMapping,
|
frequencyMapping,
|
||||||
invert = false
|
invert = false,
|
||||||
} = this.params
|
} = this.params
|
||||||
|
|
||||||
// Calculate synthesis parameters
|
// Calculate synthesis parameters
|
||||||
@@ -136,7 +135,6 @@ export class ImageToAudioSynthesizer {
|
|||||||
const previousAmplitudes = new Float32Array(effectiveHeight)
|
const previousAmplitudes = new Float32Array(effectiveHeight)
|
||||||
const smoothingFactor = 0.2 // Reduced for sharper transients
|
const smoothingFactor = 0.2 // Reduced for sharper transients
|
||||||
|
|
||||||
|
|
||||||
// Process each time slice
|
// Process each time slice
|
||||||
for (let col = 0; col < effectiveWidth; col++) {
|
for (let col = 0; col < effectiveWidth; col++) {
|
||||||
const sourceCol = col
|
const sourceCol = col
|
||||||
@@ -160,9 +158,12 @@ export class ImageToAudioSynthesizer {
|
|||||||
return normalizedDb
|
return normalizedDb
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// Detect spectral peaks
|
// Detect spectral peaks
|
||||||
const peaks = detectSpectralPeaks(processedSpectrum, Math.min(amplitudeThreshold, 0.01), false)
|
const peaks = detectSpectralPeaks(
|
||||||
|
processedSpectrum,
|
||||||
|
Math.min(amplitudeThreshold, 0.01),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
// Generate partials from peaks with spectral density
|
// Generate partials from peaks with spectral density
|
||||||
const partials: SpectralPeak[] = []
|
const partials: SpectralPeak[] = []
|
||||||
@@ -176,14 +177,21 @@ export class ImageToAudioSynthesizer {
|
|||||||
} else if (frequencyMapping === 'linear') {
|
} else if (frequencyMapping === 'linear') {
|
||||||
frequency = mapFrequencyLinear(peakRow, effectiveHeight, minFreq, maxFreq)
|
frequency = mapFrequencyLinear(peakRow, effectiveHeight, minFreq, maxFreq)
|
||||||
} else {
|
} else {
|
||||||
frequency = mapFrequency(peakRow, effectiveHeight, minFreq, maxFreq, frequencyMapping || 'mel')
|
frequency = mapFrequency(
|
||||||
|
peakRow,
|
||||||
|
effectiveHeight,
|
||||||
|
minFreq,
|
||||||
|
maxFreq,
|
||||||
|
frequencyMapping || 'mel'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let amplitude = processedSpectrum[peakRow]
|
let amplitude = processedSpectrum[peakRow]
|
||||||
|
|
||||||
// Apply temporal smoothing
|
// Apply temporal smoothing
|
||||||
if (col > 0) {
|
if (col > 0) {
|
||||||
amplitude = smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude
|
amplitude =
|
||||||
|
smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude
|
||||||
}
|
}
|
||||||
previousAmplitudes[peakRow] = amplitude
|
previousAmplitudes[peakRow] = amplitude
|
||||||
|
|
||||||
@@ -236,7 +244,7 @@ export class ImageToAudioSynthesizer {
|
|||||||
return {
|
return {
|
||||||
audio: normalizedAudio,
|
audio: normalizedAudio,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
duration
|
duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +264,7 @@ export class ImageToAudioSynthesizer {
|
|||||||
disableNormalization = false,
|
disableNormalization = false,
|
||||||
disableContrast = false,
|
disableContrast = false,
|
||||||
exactBinMapping = true,
|
exactBinMapping = true,
|
||||||
invert = false
|
invert = false,
|
||||||
} = this.params
|
} = this.params
|
||||||
|
|
||||||
const totalSamples = Math.floor(duration * sampleRate)
|
const totalSamples = Math.floor(duration * sampleRate)
|
||||||
@@ -282,7 +290,6 @@ export class ImageToAudioSynthesizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Map image rows to these exact bins
|
// Map image rows to these exact bins
|
||||||
console.log(`Ultra-precise mode: Using ${freqBins.length} exact FFT bins from ${minFreq}Hz to ${maxFreq}Hz`)
|
|
||||||
} else {
|
} else {
|
||||||
// Linear frequency mapping
|
// Linear frequency mapping
|
||||||
freqBins = []
|
freqBins = []
|
||||||
@@ -339,16 +346,16 @@ export class ImageToAudioSynthesizer {
|
|||||||
const contrast = this.params.contrast || 1.0
|
const contrast = this.params.contrast || 1.0
|
||||||
// Fast power optimization for common cases
|
// Fast power optimization for common cases
|
||||||
if (contrast === 1.0) {
|
if (contrast === 1.0) {
|
||||||
amplitude = intensity // No contrast
|
amplitude = intensity // No contrast
|
||||||
} else if (contrast === 2.0) {
|
} else if (contrast === 2.0) {
|
||||||
amplitude = intensity * intensity // Square is much faster than Math.pow
|
amplitude = intensity * intensity // Square is much faster than Math.pow
|
||||||
} else if (contrast === 0.5) {
|
} else if (contrast === 0.5) {
|
||||||
amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow
|
amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow
|
||||||
} else if (contrast === 3.0) {
|
} else if (contrast === 3.0) {
|
||||||
amplitude = intensity * intensity * intensity // Cube
|
amplitude = intensity * intensity * intensity // Cube
|
||||||
} else if (contrast === 4.0) {
|
} else if (contrast === 4.0) {
|
||||||
const sq = intensity * intensity
|
const sq = intensity * intensity
|
||||||
amplitude = sq * sq // Fourth power
|
amplitude = sq * sq // Fourth power
|
||||||
} else {
|
} else {
|
||||||
// Fast power approximation for arbitrary values
|
// Fast power approximation for arbitrary values
|
||||||
// Uses bit manipulation + lookup for ~10x speedup over Math.pow
|
// Uses bit manipulation + lookup for ~10x speedup over Math.pow
|
||||||
@@ -380,8 +387,8 @@ export class ImageToAudioSynthesizer {
|
|||||||
|
|
||||||
// Phase increment method - mathematically identical but much faster
|
// Phase increment method - mathematically identical but much faster
|
||||||
// Eliminates array lookups and multiplications in tight loop
|
// Eliminates array lookups and multiplications in tight loop
|
||||||
let phase = freqCoeff * startSample / sampleRate // Initial phase
|
let phase = (freqCoeff * startSample) / sampleRate // Initial phase
|
||||||
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
|
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
|
||||||
for (let i = 0; i < frameLength; i++) {
|
for (let i = 0; i < frameLength; i++) {
|
||||||
columnSpectrum[i] += amplitude * Math.sin(phase)
|
columnSpectrum[i] += amplitude * Math.sin(phase)
|
||||||
phase += phaseIncrement
|
phase += phaseIncrement
|
||||||
@@ -415,7 +422,7 @@ export class ImageToAudioSynthesizer {
|
|||||||
return {
|
return {
|
||||||
audio,
|
audio,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
duration
|
duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +477,7 @@ export function createDirectParams(overrides: Partial<SynthesisParams> = {}): Sy
|
|||||||
disableNormalization: false,
|
disableNormalization: false,
|
||||||
disableContrast: false,
|
disableContrast: false,
|
||||||
exactBinMapping: false,
|
exactBinMapping: false,
|
||||||
...overrides
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +505,7 @@ export function createCustomParams(overrides: Partial<SynthesisParams> = {}): Sy
|
|||||||
disableNormalization: false,
|
disableNormalization: false,
|
||||||
disableContrast: false,
|
disableContrast: false,
|
||||||
exactBinMapping: false,
|
exactBinMapping: false,
|
||||||
...overrides
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ export function barkToHz(bark: number): number {
|
|||||||
let freq = 1000 // Initial guess
|
let freq = 1000 // Initial guess
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const barkEst = hzToBark(freq)
|
const barkEst = hzToBark(freq)
|
||||||
const derivative = 13 * 0.00076 / (1 + Math.pow(0.00076 * freq, 2)) +
|
const derivative =
|
||||||
3.5 * 2 * (freq / 7500) * (1 / 7500) / (1 + Math.pow(freq / 7500, 4))
|
(13 * 0.00076) / (1 + Math.pow(0.00076 * freq, 2)) +
|
||||||
|
(3.5 * 2 * (freq / 7500) * (1 / 7500)) / (1 + Math.pow(freq / 7500, 4))
|
||||||
freq = freq - (barkEst - bark) / derivative
|
freq = freq - (barkEst - bark) / derivative
|
||||||
if (Math.abs(hzToBark(freq) - bark) < 0.001) break
|
if (Math.abs(hzToBark(freq) - bark) < 0.001) break
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,11 @@ export function applyAmplitudeCurve(amplitude: number, curve: string, gamma: num
|
|||||||
/**
|
/**
|
||||||
* Apply soft thresholding using tanh function
|
* Apply soft thresholding using tanh function
|
||||||
*/
|
*/
|
||||||
export function applySoftThreshold(amplitude: number, threshold: number, softness: number = 0.1): number {
|
export function applySoftThreshold(
|
||||||
|
amplitude: number,
|
||||||
|
threshold: number,
|
||||||
|
softness: number = 0.1
|
||||||
|
): number {
|
||||||
if (threshold <= 0) return amplitude
|
if (threshold <= 0) return amplitude
|
||||||
|
|
||||||
const ratio = amplitude / threshold
|
const ratio = amplitude / threshold
|
||||||
@@ -76,7 +81,13 @@ export function applySoftThreshold(amplitude: number, threshold: number, softnes
|
|||||||
/**
|
/**
|
||||||
* Map frequency using specified scale
|
* Map frequency using specified scale
|
||||||
*/
|
*/
|
||||||
export function mapFrequency(row: number, totalRows: number, minFreq: number, maxFreq: number, scale: string): number {
|
export function mapFrequency(
|
||||||
|
row: number,
|
||||||
|
totalRows: number,
|
||||||
|
minFreq: number,
|
||||||
|
maxFreq: number,
|
||||||
|
scale: string
|
||||||
|
): number {
|
||||||
const normalizedRow = row / (totalRows - 1)
|
const normalizedRow = row / (totalRows - 1)
|
||||||
|
|
||||||
switch (scale) {
|
switch (scale) {
|
||||||
@@ -109,7 +120,11 @@ export function mapFrequency(row: number, totalRows: number, minFreq: number, ma
|
|||||||
/**
|
/**
|
||||||
* Detect spectral peaks in amplitude spectrum with optional smoothing
|
* Detect spectral peaks in amplitude spectrum with optional smoothing
|
||||||
*/
|
*/
|
||||||
export function detectSpectralPeaks(spectrum: number[], threshold: number = 0.01, useSmoothing: boolean = false): number[] {
|
export function detectSpectralPeaks(
|
||||||
|
spectrum: number[],
|
||||||
|
threshold: number = 0.01,
|
||||||
|
useSmoothing: boolean = false
|
||||||
|
): number[] {
|
||||||
if (useSmoothing) {
|
if (useSmoothing) {
|
||||||
return detectSmoothSpectralPeaks(spectrum, threshold)
|
return detectSmoothSpectralPeaks(spectrum, threshold)
|
||||||
}
|
}
|
||||||
@@ -126,9 +141,7 @@ export function detectSpectralPeaks(spectrum: number[], threshold: number = 0.01
|
|||||||
// Fallback: use local maxima with lower threshold if no peaks found
|
// Fallback: use local maxima with lower threshold if no peaks found
|
||||||
if (peaks.length === 0) {
|
if (peaks.length === 0) {
|
||||||
for (let i = 1; i < spectrum.length - 1; i++) {
|
for (let i = 1; i < spectrum.length - 1; i++) {
|
||||||
if (spectrum[i] > spectrum[i - 1] &&
|
if (spectrum[i] > spectrum[i - 1] && spectrum[i] > spectrum[i + 1] && spectrum[i] > 0.001) {
|
||||||
spectrum[i] > spectrum[i + 1] &&
|
|
||||||
spectrum[i] > 0.001) {
|
|
||||||
peaks.push(i)
|
peaks.push(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,12 +161,13 @@ export function detectSmoothSpectralPeaks(spectrum: number[], threshold: number
|
|||||||
for (let i = 2; i < smoothedSpectrum.length - 2; i++) {
|
for (let i = 2; i < smoothedSpectrum.length - 2; i++) {
|
||||||
const current = smoothedSpectrum[i]
|
const current = smoothedSpectrum[i]
|
||||||
|
|
||||||
if (current > threshold &&
|
if (
|
||||||
current > smoothedSpectrum[i - 1] &&
|
current > threshold &&
|
||||||
current > smoothedSpectrum[i + 1] &&
|
current > smoothedSpectrum[i - 1] &&
|
||||||
current > smoothedSpectrum[i - 2] &&
|
current > smoothedSpectrum[i + 1] &&
|
||||||
current > smoothedSpectrum[i + 2]) {
|
current > smoothedSpectrum[i - 2] &&
|
||||||
|
current > smoothedSpectrum[i + 2]
|
||||||
|
) {
|
||||||
// Find the exact peak position with sub-bin accuracy using parabolic interpolation
|
// Find the exact peak position with sub-bin accuracy using parabolic interpolation
|
||||||
const y1 = smoothedSpectrum[i - 1]
|
const y1 = smoothedSpectrum[i - 1]
|
||||||
const y2 = smoothedSpectrum[i]
|
const y2 = smoothedSpectrum[i]
|
||||||
@@ -199,7 +213,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
|
|||||||
let sum = 0
|
let sum = 0
|
||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
for (let j = Math.max(0, i - halfWindow); j <= Math.min(spectrum.length - 1, i + halfWindow); j++) {
|
for (
|
||||||
|
let j = Math.max(0, i - halfWindow);
|
||||||
|
j <= Math.min(spectrum.length - 1, i + halfWindow);
|
||||||
|
j++
|
||||||
|
) {
|
||||||
sum += spectrum[j]
|
sum += spectrum[j]
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -213,7 +231,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
|
|||||||
/**
|
/**
|
||||||
* Apply perceptual amplitude weighting with contrast control
|
* Apply perceptual amplitude weighting with contrast control
|
||||||
*/
|
*/
|
||||||
export function perceptualAmplitudeWeighting(freq: number, amplitude: number, contrast: number = 2.2): number {
|
export function perceptualAmplitudeWeighting(
|
||||||
|
freq: number,
|
||||||
|
amplitude: number,
|
||||||
|
contrast: number = 2.2
|
||||||
|
): number {
|
||||||
// Apply contrast curve first (like LeviBorodenko's approach)
|
// Apply contrast curve first (like LeviBorodenko's approach)
|
||||||
const contrastedAmplitude = Math.pow(amplitude, contrast)
|
const contrastedAmplitude = Math.pow(amplitude, contrast)
|
||||||
|
|
||||||
@@ -223,8 +245,6 @@ export function perceptualAmplitudeWeighting(freq: number, amplitude: number, co
|
|||||||
return contrastedAmplitude * weight
|
return contrastedAmplitude * weight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate spectral density by creating multiple tones per frequency bin
|
* Generate spectral density by creating multiple tones per frequency bin
|
||||||
* Inspired by LeviBorodenko's multi-tone approach
|
* Inspired by LeviBorodenko's multi-tone approach
|
||||||
@@ -239,13 +259,13 @@ export function generateSpectralDensity(
|
|||||||
const toneSpacing = bandwidth / numTones
|
const toneSpacing = bandwidth / numTones
|
||||||
|
|
||||||
for (let i = 0; i < numTones; i++) {
|
for (let i = 0; i < numTones; i++) {
|
||||||
const freq = centerFreq + (i - numTones/2) * toneSpacing
|
const freq = centerFreq + (i - numTones / 2) * toneSpacing
|
||||||
const toneAmplitude = amplitude * (1 - Math.abs(i - numTones/2) / numTones * 0.3) // Slight amplitude variation
|
const toneAmplitude = amplitude * (1 - (Math.abs(i - numTones / 2) / numTones) * 0.3) // Slight amplitude variation
|
||||||
|
|
||||||
peaks.push({
|
peaks.push({
|
||||||
frequency: freq,
|
frequency: freq,
|
||||||
amplitude: toneAmplitude,
|
amplitude: toneAmplitude,
|
||||||
phase: 0
|
phase: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +348,8 @@ export function analyzeImageBrightness(imageData: ImageData): {
|
|||||||
const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness
|
const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness
|
||||||
|
|
||||||
// Calculate contrast (standard deviation)
|
// Calculate contrast (standard deviation)
|
||||||
const variance = brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length
|
const variance =
|
||||||
|
brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length
|
||||||
const contrast = Math.sqrt(variance)
|
const contrast = Math.sqrt(variance)
|
||||||
|
|
||||||
// Make recommendation
|
// Make recommendation
|
||||||
@@ -346,7 +367,7 @@ export function analyzeImageBrightness(imageData: ImageData): {
|
|||||||
medianBrightness,
|
medianBrightness,
|
||||||
edgeBrightness: avgEdgeBrightness,
|
edgeBrightness: avgEdgeBrightness,
|
||||||
contrast,
|
contrast,
|
||||||
recommendation
|
recommendation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,19 +387,19 @@ export function generateWindow(length: number, windowType: string): Float32Array
|
|||||||
switch (windowType) {
|
switch (windowType) {
|
||||||
case 'hann':
|
case 'hann':
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (length - 1)))
|
window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (length - 1)))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'hamming':
|
case 'hamming':
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
window[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (length - 1))
|
window[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (length - 1))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'blackman':
|
case 'blackman':
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const factor = 2 * Math.PI * i / (length - 1)
|
const factor = (2 * Math.PI * i) / (length - 1)
|
||||||
window[i] = 0.42 - 0.5 * Math.cos(factor) + 0.08 * Math.cos(2 * factor)
|
window[i] = 0.42 - 0.5 * Math.cos(factor) + 0.08 * Math.cos(2 * factor)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -450,7 +471,12 @@ export function extractSpectrum(
|
|||||||
/**
|
/**
|
||||||
* Alternative linear frequency mapping inspired by alexadam's approach
|
* Alternative linear frequency mapping inspired by alexadam's approach
|
||||||
*/
|
*/
|
||||||
export function mapFrequencyLinear(row: number, totalRows: number, minFreq: number, maxFreq: number): number {
|
export function mapFrequencyLinear(
|
||||||
|
row: number,
|
||||||
|
totalRows: number,
|
||||||
|
minFreq: number,
|
||||||
|
maxFreq: number
|
||||||
|
): number {
|
||||||
// Direct linear mapping from top to bottom (high freq at top)
|
// Direct linear mapping from top to bottom (high freq at top)
|
||||||
const normalizedRow = row / (totalRows - 1)
|
const normalizedRow = row / (totalRows - 1)
|
||||||
return maxFreq - normalizedRow * (maxFreq - minFreq)
|
return maxFreq - normalizedRow * (maxFreq - minFreq)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export {
|
|||||||
createDirectParams,
|
createDirectParams,
|
||||||
createCustomParams,
|
createCustomParams,
|
||||||
synthesizeDirect,
|
synthesizeDirect,
|
||||||
synthesizeCustom
|
synthesizeCustom,
|
||||||
} from './core/synthesizer'
|
} from './core/synthesizer'
|
||||||
export type { SynthesisParams, SpectralPeak, SynthesisResult, WindowType } from './core/types'
|
export type { SynthesisParams, SpectralPeak, SynthesisResult, WindowType } from './core/types'
|
||||||
|
|
||||||
@@ -25,14 +25,9 @@ export {
|
|||||||
mapFrequency,
|
mapFrequency,
|
||||||
mapFrequencyLinear,
|
mapFrequencyLinear,
|
||||||
normalizeAudioGlobal,
|
normalizeAudioGlobal,
|
||||||
generateSpectralDensity
|
generateSpectralDensity,
|
||||||
} from './core/utils'
|
} from './core/utils'
|
||||||
|
|
||||||
// Audio export
|
// Audio export
|
||||||
export {
|
export { createWAVBuffer, downloadWAV, playAudio, createAudioPlayer } from './audio/export'
|
||||||
createWAVBuffer,
|
|
||||||
downloadWAV,
|
|
||||||
playAudio,
|
|
||||||
createAudioPlayer
|
|
||||||
} from './audio/export'
|
|
||||||
export type { AudioPlayer } from './audio/export'
|
export type { AudioPlayer } from './audio/export'
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
import { atom } from 'nanostores'
|
import { atom } from 'nanostores'
|
||||||
import type { SynthesisParams } from '../spectral-synthesis'
|
import type { SynthesisParams } from '../spectral-synthesis'
|
||||||
|
|
||||||
export type GeneratorType = 'tixy' | 'picsum' | 'art-institute' | 'waveform' | 'partials' | 'slides' | 'shapes' | 'bands' | 'dust' | 'geopattern' | 'from-photo' | 'webcam' | 'harmonics'
|
export type GeneratorType =
|
||||||
|
| 'tixy'
|
||||||
|
| 'picsum'
|
||||||
|
| 'art-institute'
|
||||||
|
| 'waveform'
|
||||||
|
| 'partials'
|
||||||
|
| 'slides'
|
||||||
|
| 'shapes'
|
||||||
|
| 'bands'
|
||||||
|
| 'dust'
|
||||||
|
| 'geopattern'
|
||||||
|
| 'from-photo'
|
||||||
|
| 'webcam'
|
||||||
|
| 'harmonics'
|
||||||
|
|
||||||
export interface GeneratedImage {
|
export interface GeneratedImage {
|
||||||
id: string
|
id: string
|
||||||
@@ -22,7 +35,7 @@ export const appSettings = atom<AppSettings>({
|
|||||||
selectedGenerator: 'tixy',
|
selectedGenerator: 'tixy',
|
||||||
gridSize: 25,
|
gridSize: 25,
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
foregroundColor: '#ffffff'
|
foregroundColor: '#ffffff',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const generatedImages = atom<GeneratedImage[]>([])
|
export const generatedImages = atom<GeneratedImage[]>([])
|
||||||
@@ -54,5 +67,5 @@ export const synthesisParams = atom<SynthesisParams>({
|
|||||||
frameOverlap: 0.75,
|
frameOverlap: 0.75,
|
||||||
disableNormalization: false,
|
disableNormalization: false,
|
||||||
disableContrast: false,
|
disableContrast: false,
|
||||||
exactBinMapping: false
|
exactBinMapping: false,
|
||||||
})
|
})
|
||||||
17
src/types/geopattern.d.ts
vendored
Normal file
17
src/types/geopattern.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
declare module 'geopattern' {
|
||||||
|
interface GeoPattern {
|
||||||
|
toDataUri(): string
|
||||||
|
toString(): string
|
||||||
|
toSvg(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeoPatternOptions {
|
||||||
|
color?: string
|
||||||
|
baseColor?: string
|
||||||
|
generator?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate(input: string, options?: GeoPatternOptions): GeoPattern
|
||||||
|
|
||||||
|
export = generate
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,41 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
// Generate source maps for production debugging
|
||||||
|
sourcemap: false,
|
||||||
|
// Optimize chunk size threshold
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
// Enable minification (use default for rolldown)
|
||||||
|
minify: true,
|
||||||
|
// Rollup options for advanced bundling
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// Manual chunk splitting for better caching
|
||||||
|
manualChunks: (id) => {
|
||||||
|
if (id.includes('react') || id.includes('react-dom')) {
|
||||||
|
return 'react'
|
||||||
|
}
|
||||||
|
if (id.includes('nanostores')) {
|
||||||
|
return 'stores'
|
||||||
|
}
|
||||||
|
if (id.includes('jszip') || id.includes('geopattern')) {
|
||||||
|
return 'vendor'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Optimize chunk file names
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
entryFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Target modern browsers for better optimization
|
||||||
|
target: 'esnext',
|
||||||
|
// Enable CSS code splitting
|
||||||
|
cssCodeSplit: true,
|
||||||
|
},
|
||||||
|
// Optimize dependency pre-bundling
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['react', 'react-dom', 'nanostores', '@nanostores/react'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user